feat: add inbound hold lifecycle

This commit is contained in:
2026-09-07 11:20:10 +08:00
parent e52ecb9315
commit 0c8f6fe2b3
48 changed files with 1280 additions and 16 deletions
@@ -0,0 +1,85 @@
-- 平台入库单扣货独立写权限;查看入库单不会自动获得扣货能力。
USE baiyue_iam;
SET NAMES utf8mb4;
DROP PROCEDURE IF EXISTS migrate_inbound_hold_permission;
DELIMITER $$
CREATE PROCEDURE migrate_inbound_hold_permission()
BEGIN
IF EXISTS (
SELECT 1
FROM iam_permission
WHERE (id = 5102032 AND (tenant_id <> 0
OR app_code <> 'PLATFORM'
OR permission_type <> 'BUTTON'
OR permission_code <> 'platform:firstleg:inbound:hold'))
OR (permission_code = 'platform:firstleg:inbound:hold'
AND (id <> 5102032 OR tenant_id <> 0 OR app_code <> 'PLATFORM'))
) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='入库单扣货权限初始化失败:权限ID、编码或类型冲突';
END IF;
INSERT INTO iam_permission
(id, tenant_id, app_code, permission_type, parent_id, permission_code,
permission_name, route_path, http_method, sort_no, status, built_in, deleted)
VALUES
(5102032, 0, 'PLATFORM', 'BUTTON', 5102030, 'platform:firstleg:inbound:hold',
'扣货与解除扣货', NULL, 'POST', 20, 'ACTIVE', 1, 0)
ON DUPLICATE KEY UPDATE
permission_name=VALUES(permission_name), parent_id=VALUES(parent_id),
route_path=VALUES(route_path), http_method=VALUES(http_method),
sort_no=VALUES(sort_no), status='ACTIVE', built_in=1, deleted=0;
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
SELECT 0, role.id, permission.id, CURRENT_TIMESTAMP(3)
FROM iam_role role
JOIN iam_permission permission
ON permission.tenant_id=0
AND permission.app_code='PLATFORM'
AND permission.permission_code='platform:firstleg:inbound:hold'
WHERE role.tenant_id=0
AND role.app_code='PLATFORM'
AND role.role_code='PLATFORM_ADMIN'
AND role.status='ACTIVE'
AND role.deleted=0
AND permission.status='ACTIVE'
AND permission.deleted=0;
IF (SELECT COUNT(*)
FROM iam_permission
WHERE id=5102032
AND tenant_id=0
AND app_code='PLATFORM'
AND permission_type='BUTTON'
AND parent_id=5102030
AND permission_code='platform:firstleg:inbound:hold'
AND http_method='POST'
AND status='ACTIVE'
AND built_in=1
AND deleted=0) <> 1 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
AND permission.tenant_id=grant_row.tenant_id
WHERE role.tenant_id=0
AND role.app_code='PLATFORM'
AND role.role_code='PLATFORM_ADMIN'
AND role.status='ACTIVE'
AND role.deleted=0
AND permission.permission_code='platform:firstleg:inbound:hold'
AND permission.status='ACTIVE'
AND permission.deleted=0) <> 1 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='入库单扣货权限初始化失败:平台超级管理员授权不完整';
END IF;
END$$
DELIMITER ;
CALL migrate_inbound_hold_permission();
DROP PROCEDURE IF EXISTS migrate_inbound_hold_permission;
+1 -1
View File
@@ -124,7 +124,7 @@ paths:
get:
tags: [IAM Resources / IAM 资源]
summary: 查询当前用户有效权限
description: 返回当前有效平台成员权限编码;只要求有效 PLATFORM JWT,不依赖 `platform:role:list`。资金账户充值审核与手工调账分别返回 `platform:finance:recharge:review`、`platform:finance:customer-account:adjust`,并与 `platform:finance:customer-account: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`。列表权限包含详情和只读辅助接口,写操作独立授权。
description: 返回当前有效平台成员权限编码;只要求有效 PLATFORM JWT,不依赖 `platform:role:list`。入库单扣货与解除扣货返回独立写权限 `platform:firstleg:inbound:hold`,不会由 `platform:firstleg:inbound:list` 查看权限自动获得。资金账户充值审核与手工调账分别返回 `platform:finance:recharge:review`、`platform:finance:customer-account:adjust`,并与 `platform:finance:customer-account: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: 当前平台成员权限查询成功
@@ -46,6 +46,8 @@ class IamOpenApiContractTest {
.contains("merchant:finance:recharge:create")
.contains("platform:finance:recharge:review")
.contains("platform:finance:customer-account:adjust")
.contains("platform:firstleg:inbound:hold")
.contains("不会由 `platform:firstleg:inbound:list` 查看权限自动获得")
.contains("两者独立授权")
.contains("列表权限包含详情只读")
.contains("PermissionCodesApiResponse:")
@@ -0,0 +1,67 @@
package com.baiyue.iam.repository;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import java.nio.charset.StandardCharsets;
import static org.assertj.core.api.Assertions.assertThat;
class InboundHoldPermissionSqlContractTest {
@Test
void registersHoldAsASeparateWritePermissionUnderInboundList() throws Exception {
String sql = sql();
assertThat(sql).contains(
"(5102032, 0, 'PLATFORM', 'BUTTON', 5102030, 'platform:firstleg:inbound:hold'",
"'扣货与解除扣货', NULL, 'POST'",
"permission_type='BUTTON'",
"parent_id=5102030",
"http_method='POST'");
assertThat(sql).doesNotContain(
"permission_code IN ('platform:firstleg:inbound:list', 'platform:firstleg:inbound:hold')",
"permission_code IN ('platform:firstleg:inbound:view', 'platform:firstleg:inbound:hold')");
}
@Test
void failsClosedWhenTheFixedIdentityIsOccupied() throws Exception {
assertThat(sql()).contains(
"migrate_inbound_hold_permission",
"id = 5102032",
"permission_type <> 'BUTTON'",
"permission_code = 'platform:firstleg:inbound:hold'",
"SIGNAL SQLSTATE '45000'",
"权限ID、编码或类型冲突");
}
@Test
void grantsHoldOnlyToPlatformAdminByDefault() throws Exception {
String sql = sql();
int grantStart = sql.indexOf("INSERT IGNORE INTO iam_role_permission");
int grantEnd = sql.indexOf(';', grantStart);
String grant = sql.substring(grantStart, grantEnd);
assertThat(grant).contains(
"role.app_code='PLATFORM'",
"role.role_code='PLATFORM_ADMIN'",
"permission.permission_code='platform:firstleg:inbound:hold'");
assertThat(grant).doesNotContain(
"ORDER_OPERATOR",
"WAREHOUSE_OPERATOR",
"WAREHOUSE_TALLY",
"PLATFORM_CUSTOM_ADMIN");
}
@Test
void assertsTheResourceAndDefaultGrant() throws Exception {
assertThat(sql()).contains(
"入库单扣货权限初始化失败:权限资源不完整",
"入库单扣货权限初始化失败:平台超级管理员授权不完整",
") <> 1 THEN");
}
private static String sql() throws Exception {
return new ClassPathResource("db/inbound-hold-permission.sql")
.getContentAsString(StandardCharsets.UTF_8);
}
}
@@ -139,6 +139,22 @@ class AuthorizationSnapshotServiceTest {
"platform:finance:customer-account:adjust");
}
@Test
void exposesInboundHoldIndependentlyFromReadPermissions() {
when(mapper.countActiveIdentity(51L, 0L, 19L)).thenReturn(1);
when(mapper.findRoles(51L, 0L, 19L, "PLATFORM")).thenReturn(List.of("PLATFORM_ADMIN"));
when(mapper.findPermissions(51L, 0L, 19L, "PLATFORM")).thenReturn(List.of(
"platform:firstleg:inbound:list",
"platform:firstleg:inbound:hold"));
var permissions = service.currentPermissions(new AuthenticatedUser(51L, 0L, 0L, 19L,
"platform-admin", "PLATFORM", "PLATFORM", java.util.Set.of()), "PLATFORM");
assertThat(permissions).containsExactlyInAnyOrder(
"platform:firstleg:inbound:list",
"platform:firstleg:inbound:hold");
}
@Test
void rejectsIncompleteCurrentIdentityAsBusinessError() {
assertThatThrownBy(() -> service