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
@@ -67,7 +67,11 @@ VALUES
(5107221,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:create','创建车辆主数据',NULL,'POST',921,'ACTIVE',1,0),
(5107222,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:edit','编辑车辆主数据',NULL,'PUT',922,'ACTIVE',1,0),
(5107223,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:status','变更车辆状态',NULL,'PATCH',923,'ACTIVE',1,0),
(5107224,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:export','导出车辆主数据',NULL,'GET',924,'ACTIVE',1,0)
(5107224,0,'PLATFORM','BUTTON',5107220,'platform:overseas-master-data:vehicle:export','导出车辆主数据',NULL,'GET',924,'ACTIVE',1,0),
(5306100,0,'MERCHANT','MENU',0,'merchant:cross-border:procurement','海外采购','/cross-border/procurement',NULL,70,'ACTIVE',1,0),
(5306110,0,'MERCHANT','MENU',0,'merchant:cross-border:domestic-trucking','美国本土卡','/cross-border/domestic-trucking',NULL,80,'ACTIVE',1,0),
(5306120,0,'MERCHANT','MENU',0,'merchant:cross-border:pallet-shipping','美本土散板','/cross-border/pallet-shipping',NULL,90,'ACTIVE',1,0),
(5306130,0,'MERCHANT','MENU',0,'merchant:warehouse:inbound:list','海外仓入库订单','/orders/inbound',NULL,50,'ACTIVE',1,0)
ON DUPLICATE KEY UPDATE permission_name=VALUES(permission_name), route_path=VALUES(route_path),
http_method=VALUES(http_method), sort_no=VALUES(sort_no), status='ACTIVE', built_in=1, deleted=0;
@@ -203,6 +207,52 @@ WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
AND UPPER(role.role_code)='PLATFORM_CUSTOM_ADMIN'
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
-- 客户默认管理员获得当前客户海外业务入口;其他客户角色按企业实际分工授权。
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
SELECT 0, role.id, permission.id, NOW(3)
FROM iam_role role
JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='MERCHANT'
AND permission.permission_code IN (
'merchant:cross-border:procurement',
'merchant:cross-border:domestic-trucking',
'merchant:cross-border:pallet-shipping',
'merchant:warehouse:inbound:list'
)
WHERE role.tenant_id=0 AND role.app_code='MERCHANT' AND role.role_code='MERCHANT_OWNER'
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
DROP PROCEDURE IF EXISTS assert_overseas_merchant_permissions;
DELIMITER $$
CREATE PROCEDURE assert_overseas_merchant_permissions()
BEGIN
IF (SELECT COUNT(*) FROM iam_permission
WHERE tenant_id=0 AND app_code='MERCHANT' AND status='ACTIVE' AND deleted=0
AND permission_code IN (
'merchant:cross-border:procurement',
'merchant:cross-border:domestic-trucking',
'merchant:cross-border:pallet-shipping',
'merchant:warehouse:inbound:list')) <> 4 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外客户权限初始化失败:权限资源不完整';
END IF;
IF (SELECT COUNT(*) FROM iam_role_permission grant_row
JOIN iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
JOIN iam_permission permission ON permission.id=grant_row.permission_id
WHERE role.tenant_id=0 AND role.app_code='MERCHANT' AND role.role_code='MERCHANT_OWNER'
AND role.status='ACTIVE' AND role.deleted=0
AND permission.tenant_id=0 AND permission.app_code='MERCHANT'
AND permission.status='ACTIVE' AND permission.deleted=0
AND permission.permission_code IN (
'merchant:cross-border:procurement',
'merchant:cross-border:domestic-trucking',
'merchant:cross-border:pallet-shipping',
'merchant:warehouse:inbound:list')) <> 4 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外客户权限初始化失败:客户默认管理员授权不完整';
END IF;
END$$
DELIMITER ;
CALL assert_overseas_merchant_permissions();
DROP PROCEDURE assert_overseas_merchant_permissions;
-- Tail-leg order actions are available to the current platform administrator role.
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
SELECT 0, role.id, permission.id, NOW(3)
+2 -2
View File
@@ -124,7 +124,7 @@ paths:
get:
tags: [IAM Resources / IAM 资源]
summary: 查询当前用户有效权限
description: 返回当前有效平台成员权限编码;只要求有效 PLATFORM JWT,不依赖 `platform:role:list`。五期海外仓返回 `platform:overseas-warehouse:dispatch:list`、`platform:overseas-warehouse:dispatch:edit`、`platform:overseas-warehouse:dispatch:cancel`、`platform:overseas-warehouse:dispatch:transfer-delivery`、`platform:overseas-warehouse:outbound:list`、`platform:overseas-warehouse:outbound:create`、`platform:overseas-warehouse:outbound:cancel`、`platform:overseas-warehouse:delivery:list`、`platform:overseas-warehouse:delivery:return`、`platform:overseas-warehouse:delivery:status`、`platform:overseas-warehouse:exception-order:list`、`platform:overseas-warehouse:exception-order:create`、`platform:overseas-warehouse:customer-visit:list`、`platform:overseas-warehouse:customer-visit:edit`;列表权限包含详情和只读辅助接口,写操作独立授权。
description: 返回当前有效平台成员权限编码;只要求有效 PLATFORM JWT,不依赖 `platform:role:list`。海外仓返回 `platform:overseas-warehouse:dispatch:list`、`platform:overseas-warehouse:dispatch:edit`、`platform:overseas-warehouse:dispatch:cancel`、`platform:overseas-warehouse:dispatch:transfer-delivery`、`platform:overseas-warehouse:outbound:list`、`platform:overseas-warehouse:outbound:create`、`platform:overseas-warehouse:outbound:cancel`、`platform:overseas-warehouse:delivery:list`、`platform:overseas-warehouse:delivery:return`、`platform:overseas-warehouse:delivery:status`、`platform:overseas-warehouse:exception-order:list`、`platform:overseas-warehouse:exception-order:create`、`platform:overseas-warehouse:customer-visit:list`、`platform:overseas-warehouse:customer-visit:edit`;海外 PDA 盘点、移库、换标和现场查询统一返回 `lastleg:pda:operate`,且仅限 `OVERSEAS_WAREHOUSE_PDA_OPERATOR`。列表权限包含详情和只读辅助接口,写操作独立授权。
responses:
'200':
description: 当前平台成员权限查询成功
@@ -326,7 +326,7 @@ paths:
get:
tags: [IAM Resources / IAM 资源]
summary: 查询当前用户有效权限
description: 返回当前有效客户成员权限编码。五期客户派送返回 `merchant:warehouse:delivery:list` 和 `merchant:warehouse:delivery:create`;列表权限包含详情只读,创建权限独立授权。
description: 返回当前有效客户成员权限编码。海外采购、本土卡、本土散板和海外入库订单分别返回 `merchant:cross-border:procurement`、`merchant:cross-border:domestic-trucking`、`merchant:cross-border:pallet-shipping`、`merchant:warehouse:inbound:list`;客户派送返回 `merchant:warehouse:delivery:list` 和 `merchant:warehouse:delivery:create`。列表权限包含详情只读,创建权限独立授权。
responses:
'200':
description: 当前商户成员权限查询成功
@@ -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