feat: complete overseas customer and PDA workflows

This commit is contained in:
2026-09-04 12:59:37 +08:00
parent a274d029bb
commit 54cb9ceade
60 changed files with 1598 additions and 277 deletions
@@ -36,6 +36,12 @@ class IamOpenApiContractTest {
.contains("platform:overseas-warehouse:outbound:cancel")
.contains("merchant:warehouse:delivery:list")
.contains("merchant:warehouse:delivery:create")
.contains("merchant:cross-border:procurement")
.contains("merchant:cross-border:domestic-trucking")
.contains("merchant:cross-border:pallet-shipping")
.contains("merchant:warehouse:inbound:list")
.contains("lastleg:pda:operate")
.contains("OVERSEAS_WAREHOUSE_PDA_OPERATOR")
.contains("列表权限包含详情只读")
.contains("PermissionCodesApiResponse:")
.contains("TenantPageApiResponse:")
@@ -0,0 +1,109 @@
package com.baiyue.iam.repository;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;
import static org.assertj.core.api.Assertions.assertThat;
class OverseasCustomerPermissionSqlContractTest {
private static final Pattern MERCHANT_CODE = Pattern.compile("merchant:[a-z0-9:-]+");
private static final Set<String> CUSTOMER_OVERSEAS_PERMISSIONS = Set.of(
"merchant:cross-border:procurement",
"merchant:cross-border:domestic-trucking",
"merchant:cross-border:pallet-shipping",
"merchant:warehouse:inbound:list");
@Test
void declaresTheFrozenCustomerOverseasPermissionCatalog() throws Exception {
String sql = sql();
assertThat(merchantCodes(sql)).containsExactlyInAnyOrderElementsOf(CUSTOMER_OVERSEAS_PERMISSIONS);
for (String permission : CUSTOMER_OVERSEAS_PERMISSIONS) {
assertThat(sql).contains("0,'MERCHANT','MENU'", "'" + permission + "'");
}
assertThat(sql).contains(
"'merchant:cross-border:procurement','海外采购','/cross-border/procurement'",
"'merchant:cross-border:domestic-trucking','美国本土卡','/cross-border/domestic-trucking'",
"'merchant:cross-border:pallet-shipping','美本土散板','/cross-border/pallet-shipping'",
"'merchant:warehouse:inbound:list','海外仓入库订单','/orders/inbound'");
}
@Test
void grantsAllFourPermissionsOnlyToTheDefaultCustomerAdministrator() throws Exception {
String sql = sql();
int comment = sql.indexOf("-- 客户默认管理员获得当前客户海外业务入口");
int grantStart = sql.indexOf("INSERT IGNORE INTO iam_role_permission", comment);
int grantEnd = sql.indexOf(';', grantStart);
assertThat(comment).isGreaterThanOrEqualTo(0);
assertThat(grantStart).isGreaterThan(comment);
assertThat(grantEnd).isGreaterThan(grantStart);
String grant = sql.substring(grantStart, grantEnd);
assertThat(merchantCodes(grant)).containsExactlyInAnyOrderElementsOf(CUSTOMER_OVERSEAS_PERMISSIONS);
assertThat(grant).contains(
"role.app_code='MERCHANT'",
"role.role_code='MERCHANT_OWNER'",
"permission.app_code='MERCHANT'");
assertThat(grant).doesNotContain("PLATFORM_ADMIN", "PLATFORM_CUSTOM_ADMIN");
}
@Test
void assertsCatalogAndOwnerGrantCompleteness() throws Exception {
String sql = sql();
assertThat(sql).contains(
"assert_overseas_merchant_permissions",
"海外客户权限初始化失败:权限资源不完整",
"海外客户权限初始化失败:客户默认管理员授权不完整",
"permission.app_code='MERCHANT'",
"role.role_code='MERCHANT_OWNER'",
")) <> 4 THEN");
}
@Test
void keepsAllOverseasPdaOperationsBehindTheSingleExistingPermission() throws Exception {
String sql = sql();
Set<String> pdaCodes = new LinkedHashSet<>();
Pattern.compile("lastleg:pda:[a-z0-9:-]+")
.matcher(sql).results().map(result -> result.group()).forEach(pdaCodes::add);
assertThat(pdaCodes).containsExactly("lastleg:pda:operate");
assertThat(sql).contains(
"role.id=4000005",
"role.role_code='OVERSEAS_WAREHOUSE_PDA_OPERATOR'",
"存在非海外仓 PDA 作业员授权");
assertThat(sql).doesNotContain(
"lastleg:pda:stocktake",
"lastleg:pda:transfer",
"lastleg:pda:relabel",
"lastleg:pda:query");
}
@Test
void matchesTheFrozenLastlegPermissionConstants() throws Exception {
Path constants = Path.of(
"lastleg-service/src/main/java/com/baiyue/lastleg/security/OverseasPermissions.java");
if (!Files.exists(constants)) constants = Path.of("..").resolve(constants);
assertThat(merchantCodes(Files.readString(constants, StandardCharsets.UTF_8)))
.containsAll(CUSTOMER_OVERSEAS_PERMISSIONS);
}
private static Set<String> merchantCodes(String source) {
Set<String> values = new LinkedHashSet<>();
MERCHANT_CODE.matcher(source).results().map(result -> result.group()).forEach(values::add);
return values;
}
private static String sql() throws Exception {
return new ClassPathResource("db/lastleg-permissions.sql")
.getContentAsString(StandardCharsets.UTF_8);
}
}
@@ -138,17 +138,17 @@ class Phase5DeliveryPermissionSqlContractTest {
void domesticUnifiedEntryInlinesTheCompleteModuleSqlAfterPhase4Permissions() throws Exception {
String moduleSql = sql().trim();
String unifiedSql = repositoryFile("deploy/sql/头程数据库统一增量-MySQL8.sql");
int phase4 = unifiedSql.indexOf("-- 四期海外权限统一目录");
int phase5 = unifiedSql.indexOf("-- 五期海外排车、出库与派送权限(来源:iam/db/phase5-delivery-permissions.sql)");
int bodyStart = unifiedSql.indexOf("USE baiyue_iam;", phase5);
int overseasCatalog = unifiedSql.indexOf("-- 海外权限统一目录");
int outboundDelivery = unifiedSql.indexOf("-- 海外排车、出库与派送权限(来源:iam/db/phase5-delivery-permissions.sql)");
int bodyStart = unifiedSql.indexOf("USE baiyue_iam;", outboundDelivery);
int nextSection = unifiedSql.indexOf("-- IAM 客户提交即成功", bodyStart);
assertThat(phase4).isGreaterThanOrEqualTo(0);
assertThat(phase5).isGreaterThan(phase4);
assertThat(bodyStart).isGreaterThan(phase5);
assertThat(overseasCatalog).isGreaterThanOrEqualTo(0);
assertThat(outboundDelivery).isGreaterThan(overseasCatalog);
assertThat(bodyStart).isGreaterThan(outboundDelivery);
assertThat(nextSection).isGreaterThan(bodyStart);
assertThat(unifiedSql.substring(bodyStart, nextSection).trim()).isEqualTo(moduleSql);
assertThat(codes(unifiedSql.substring(phase5, nextSection)))
assertThat(codes(unifiedSql.substring(outboundDelivery, nextSection)))
.containsExactlyInAnyOrderElementsOf(REQUIRED);
}
@@ -157,7 +157,7 @@ class Phase5DeliveryPermissionSqlContractTest {
String readme = repositoryFile("deploy/sql/README.md");
assertThat(readme).contains(
"五期海外排车、出库与派送权限",
"海外排车、出库与派送权限",
"测试和部署只执行固定入口,不再单独执行模块脚本",
"排车列表/编辑/取消/转派送",
"异常订单列表/创建和客户回访列表/编辑十四项权限",
@@ -90,6 +90,26 @@ class AuthorizationSnapshotServiceTest {
"merchant:warehouse:delivery:list");
}
@Test
void exposesCustomerOverseasBusinessPermissionsFromCurrentRoleAssignments() {
when(mapper.countActiveIdentity(42L, 7L, 9L)).thenReturn(1);
when(mapper.findRoles(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of("MERCHANT_OWNER"));
when(mapper.findPermissions(42L, 7L, 9L, "MERCHANT")).thenReturn(List.of(
"merchant:cross-border:procurement",
"merchant:cross-border:domestic-trucking",
"merchant:cross-border:pallet-shipping",
"merchant:warehouse:inbound:list"));
var result = service.currentPermissions(new AuthenticatedUser(42L, 7L, 7L, 9L,
"owner", "MERCHANT", "MERCHANT", java.util.Set.of()), "MERCHANT");
assertThat(result).containsExactlyInAnyOrder(
"merchant:cross-border:procurement",
"merchant:cross-border:domestic-trucking",
"merchant:cross-border:pallet-shipping",
"merchant:warehouse:inbound:list");
}
@Test
void rejectsIncompleteCurrentIdentityAsBusinessError() {
assertThatThrownBy(() -> service