优化
This commit is contained in:
@@ -55,7 +55,7 @@
|
||||
|---|---|
|
||||
| 公共依赖版本 | `platform-bom/pom.xml`;统一管理公共组件版本,不放业务实现 |
|
||||
| 通用响应、分页、异常、基础实体与导出契约 | `common-core/`;从 `ApiResponse.java`、`PageResult.java`、`BizException.java`、`ExportContracts.java` 定位 |
|
||||
| Web 异常、请求关联 ID、Long ID 序列化 | `common-web/`;从 `GlobalExceptionHandler.java`、`CorrelationIdFilter.java`、`LongIdJacksonConfiguration.java` 定位 |
|
||||
| Web 异常、请求关联/Trace、同步出站传播、Long ID 序列化 | `common-web/`;从 `GlobalExceptionHandler.java`、`CorrelationIdFilter.java`、`TraceParent.java`、`FeignTraceRequestInterceptor.java`、`RestClientTraceInterceptor.java`、`LongIdJacksonConfiguration.java` 定位 |
|
||||
| JWT、JWKS、会话校验与认证用户 | `common-security/`;从 `JwtAuthenticationFilter.java`、`JwtClaimsMapper.java`、`AccessTokenSessionValidator.java` 定位 |
|
||||
| Springdoc/OpenAPI 自动配置 | `common-openapi/`;从 `BaiyueOpenApiAutoConfiguration.java`、`OpenApiProperties.java` 定位 |
|
||||
| 业务审计注解、脱敏、Outbox 与投递 | `common-audit/`;从 `BusinessAudit.java`、`BusinessAuditAspect.java`、`SensitiveDataSanitizer.java`、`AuditOutboxPublisher.java` 定位 |
|
||||
@@ -83,7 +83,7 @@
|
||||
- 数据库只访问所属服务库。模块表结构位于 `<module>/src/main/resources/db/`;当前开发阶段直接更新最新模块基线和 `deploy/sql/头程数据库统一增量-MySQL8.sql`,不为历史开发数据新增兼容迁移。已有名称含 `migration` 的脚本仅作现有结构来源和开发追溯,部署说明见 `deploy/sql/README.md`。
|
||||
- 通用配置在 `config/nacos/{local,cn}/`;服务日志、上传文件、`target/`、`reports/` 不属于源码。
|
||||
- Java 签名变更的最低编译门禁是 `mvn -pl <module> -am test-compile`,它必须在部署前完成并覆盖测试源码;有行为变化时继续运行 `mvn -pl <module> -am test`。`-DskipTests` 可跳过测试执行但仍须保留测试编译,`-Dmaven.test.skip=true` 会跳过测试编译,不得作为签名变更的交付验证。全量验证仍使用 `mvn -DskipTests validate`、`mvn test`;不要在本地启动 Java/Nacos/数据库,除非用户明确要求。
|
||||
- `AGENTS.md` 索引维护完成后运行 `./scripts/agents-index-contract.ps1`;脚本只读源码目录和指定指引,验证根快速表唯一性、部署单元、Client active 路由、海外 Gateway 覆盖、Lastleg 海外安全投影映射、模块覆盖、关键入口与排除目录,不访问 HTTP、数据库或部署环境。
|
||||
- `AGENTS.md` 索引维护完成后运行 `./scripts/agents-index-contract.ps1`;脚本只读源码目录和指定指引,验证根快速表唯一性、部署单元、Client active 路由、四期范围漂移、订舱关联候选与柜量语义、海外 Gateway 覆盖、PDA 运行配置、Lastleg 海外安全投影、同步 Trace、模块覆盖、关键入口与排除目录,不访问 HTTP、数据库或部署环境。
|
||||
- 单服务镜像构建/部署从 `Jenkinsfile.service`、`deploy/docker/Dockerfile`、`deploy/scripts/deploy-built-service.sh` 进入;新镜像启动失败或健康检查失败时只允许回滚到同服务先前镜像,并保持任务失败,离线合同见 `scripts/deploy-built-service-rollback.test.mjs`。实际部署仍须当前对话明确授权。
|
||||
|
||||
## 全量 HTTP 验收脚本路由
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- Phase-one local administrator accounts.
|
||||
-- Phase-one local platform administrator account. Merchant accounts are created through onboarding.
|
||||
-- Development-only initial password: ChangeMe123!
|
||||
-- The BCrypt hash is intentionally stored instead of plaintext.
|
||||
USE baiyue_auth;
|
||||
@@ -10,9 +10,7 @@ INSERT INTO auth_account (
|
||||
operator_tenant_id, default_membership_id, default_app_code, deleted
|
||||
) VALUES
|
||||
(1000001, 'platform_admin', '$2a$12$AMZJjd88leYPLI9uKvjZiew2CTQXI32BXoHFX.FrCi28/IeZ7mZ96',
|
||||
'平台系统管理员', 'ACTIVE', 0, 1, 0, 0, 0, 3000001, 'PLATFORM', 0),
|
||||
(1000002, 'merchant_admin', '$2a$12$AMZJjd88leYPLI9uKvjZiew2CTQXI32BXoHFX.FrCi28/IeZ7mZ96',
|
||||
'IT 测试客户管理员', 'ACTIVE', 0, 1, 0, 10001, 10001, 3000002, 'MERCHANT', 0)
|
||||
'平台系统管理员', 'ACTIVE', 0, 1, 0, 0, 0, 3000001, 'PLATFORM', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
password_hash = VALUES(password_hash),
|
||||
display_name = VALUES(display_name),
|
||||
|
||||
+4
-3
@@ -11,7 +11,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InitialAccountSeedSqlContractTest {
|
||||
@Test
|
||||
void initializesOnlyThePlatformAndItTestAdministratorAccounts() throws Exception {
|
||||
void initializesOnlyThePlatformAdministratorAccount() throws Exception {
|
||||
assertAccountSeed(new ClassPathResource("db/phase1-admin-accounts.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8));
|
||||
assertAccountSeed(Files.readString(unifiedSql(), StandardCharsets.UTF_8));
|
||||
@@ -21,9 +21,10 @@ class InitialAccountSeedSqlContractTest {
|
||||
assertThat(sql)
|
||||
.contains("(1000001, 'platform_admin'")
|
||||
.contains("'平台系统管理员', 'ACTIVE'")
|
||||
.contains("(1000002, 'merchant_admin'")
|
||||
.contains("'IT 测试客户管理员', 'ACTIVE'")
|
||||
.contains("password_change_required")
|
||||
.doesNotContain("merchant_admin")
|
||||
.doesNotContain("IT_TEST")
|
||||
.doesNotContain("(1000002, 'merchant_admin'")
|
||||
.doesNotContain("一期演示企业管理员");
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
## 唯一数据库初始化与发布入口
|
||||
|
||||
- 国内区域固定入口:`头程数据库统一增量-MySQL8.sql`。文件名保留历史兼容,内容维护为当前完整数据库基线和存量环境兼容升级,不再按日期拆分发布脚本。
|
||||
- 海外区域固定增量入口:`海外区域统一增量-MySQL8.sql`;空库先分别执行 `lastleg-service/src/main/resources/db/lastleg.sql`与 `warehouse-overseas-service/src/main/resources/db/warehouse.sql` 基线,再执行该入口。固定入口仅升级 `baiyue_lastleg`与 `baiyue_lastlag_warehouse`;IAM 安全事件 Inbox 和可恢复投影已并入 `baiyue_lastleg.ll_security_*`,Redis 只作可重建镜像。
|
||||
- 海外区域固定增量入口:`海外区域统一增量-MySQL8.sql`;空库先分别执行 `lastleg-service/src/main/resources/db/lastleg.sql`与 `warehouse-overseas-service/src/main/resources/db/warehouse.sql` 基线,再执行该入口。固定入口会创建并校验独立的 `baiyue_event_bridge_overseas`,再升级 `baiyue_lastleg`与 `baiyue_lastlag_warehouse`;IAM 安全事件 Inbox 和可恢复投影已并入 `baiyue_lastleg.ll_security_*`,Redis 只作可重建镜像。
|
||||
- Lastleg 基线和海外固定增量仅确定性初始化国内固定 `platform_admin`(`authUserId=1000001`、`tenantId=0`)的 USER/AUTHORIZATION version=0 安全投影;其他既有开发账号不建立兼容表或长期补投入口。
|
||||
- 国内、海外 Compose 部署默认设置 `SPRING_SQL_INIT_MODE=never`,执行任一 SQL 均需用户单独授权。
|
||||
- 该脚本汇总一期、二期及已实现三期出仓能力的数据库基线、存量升级、IAM 权限资源和最终结构校验,适用于 MySQL 8.0。
|
||||
- 正式上线前的数据库变更直接维护该文件中的当前基线、存量兼容处理和最终校验,不再创建按日期拆分的发布 SQL;正式上线后的迁移策略在生产发布规范中另行冻结。
|
||||
- 当前测试环境尚未上线,船期价格表相关表、字段和索引只维护在现有初始化建表定义及本固定入口中,不向 `phase1-order-extension.sql`、`phase2-fba-pricing-migration.sql` 或新的增量脚本追加同一结构。
|
||||
- 模块 `src/main/resources/db/` 下的 SQL 用于开发溯源;`deploy/sql/` 下已有日期文件全部冻结为历史记录,不再单独部署,也不得与固定入口重复执行。
|
||||
- 固定入口不包含 `auth/phase1-admin-accounts.sql` 和 `warehouse/warehouse-seed.sql`;仅在开发/测试基线账号缺失时创建最小登录账号,重复执行不覆盖现有密码。脚本不写入演示订单;正式平台公共仓库主数据已内联到固定入口。
|
||||
- 固定入口不包含 `auth/phase1-admin-accounts.sql` 和 `warehouse/warehouse-seed.sql`;仅在开发/测试基线账号缺失时创建平台管理员,重复执行不覆盖现有密码。脚本不创建固定客户、客户账号或演示订单;客户必须通过正式建档/注册流程创建。正式平台公共仓库主数据已内联到固定入口。
|
||||
- `warehouse/phase2-pallet-location-schema-migration.sql` 的有效结构已被当前 `phase2-pallet-location-schema.sql` 和后续共享溢出位迁移完整覆盖,因此不重复内联,避免每次部署无条件删除并重建同名唯一索引。
|
||||
- 脚本会创建 9 个独立数据库:`baiyue_auth`、`baiyue_iam`、`baiyue_order`、`baiyue_warehouse`、`baiyue_firstleg`、`baiyue_file`、`baiyue_audit`、`baiyue_event_bridge`、`baiyue_finance`。Gateway、Print、Integration Service 无数据库。
|
||||
- IAM 消息中心基线已内联到固定入口,包括 5 张 `iam_notification_*` 表、任务/事件幂等约束、平台消息管理权限、企业成员收件箱权限及默认角色授权;模块溯源脚本为 `iam/src/main/resources/db/notification-center.sql`。
|
||||
|
||||
+43
-143
@@ -278,7 +278,7 @@ PREPARE auth_password_change_required_stmt FROM @auth_password_change_required_s
|
||||
EXECUTE auth_password_change_required_stmt;
|
||||
DEALLOCATE PREPARE auth_password_change_required_stmt;
|
||||
|
||||
-- 开发/测试环境最小登录基线:一个平台系统管理员和一个 IT 测试客户管理员。
|
||||
-- 开发/测试环境最小登录基线只保留平台系统管理员;客户必须走正式建档/注册流程创建。
|
||||
-- 缺失账号首次创建时写入开发基线 BCrypt 哈希;重复执行统一增量绝不覆盖现有密码。
|
||||
INSERT INTO auth_account (
|
||||
id, username, password_hash, display_name, account_status,
|
||||
@@ -286,9 +286,7 @@ INSERT INTO auth_account (
|
||||
operator_tenant_id, default_membership_id, default_app_code, deleted
|
||||
) VALUES
|
||||
(1000001, 'platform_admin', '$2a$12$AMZJjd88leYPLI9uKvjZiew2CTQXI32BXoHFX.FrCi28/IeZ7mZ96',
|
||||
'平台系统管理员', 'ACTIVE', 0, 1, 0, 0, 0, 3000001, 'PLATFORM', 0),
|
||||
(1000002, 'merchant_admin', '$2a$12$AMZJjd88leYPLI9uKvjZiew2CTQXI32BXoHFX.FrCi28/IeZ7mZ96',
|
||||
'IT 测试客户管理员', 'ACTIVE', 0, 1, 0, 10001, 10001, 3000002, 'MERCHANT', 0)
|
||||
'平台系统管理员', 'ACTIVE', 0, 1, 0, 0, 0, 3000001, 'PLATFORM', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
display_name = VALUES(display_name),
|
||||
account_status = 'ACTIVE',
|
||||
@@ -1194,10 +1192,7 @@ INSERT INTO iam_tenant (
|
||||
approved_at, approved_by, created_by, deleted, version
|
||||
) VALUES
|
||||
(0, 'PLATFORM', '虫虫出海平台', 'PLATFORM', 'ACTIVE', '虫虫出海平台',
|
||||
NULL, '平台系统管理员', NULL, NULL, NOW(3), 1000001, 1000001, 0, 0),
|
||||
(10001, 'IT_TEST', 'IT 测试客户', 'MERCHANT', 'ACTIVE', 'IT 测试客户',
|
||||
'91310000ITTEST0001', 'IT 测试客户管理员', '13800000000', 'it-test@example.com',
|
||||
NOW(3), 1000001, 1000001, 0, 0)
|
||||
NULL, '平台系统管理员', NULL, NULL, NOW(3), 1000001, 1000001, 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
tenant_code = VALUES(tenant_code), tenant_name = VALUES(tenant_name), tenant_type = VALUES(tenant_type),
|
||||
status = 'ACTIVE', legal_name = VALUES(legal_name),
|
||||
@@ -1206,28 +1201,10 @@ ON DUPLICATE KEY UPDATE
|
||||
contact_email = VALUES(contact_email), approved_at = COALESCE(approved_at, NOW(3)),
|
||||
approved_by = VALUES(approved_by), deleted = 0;
|
||||
|
||||
-- The built-in IT customer must be immediately usable for order acceptance tests.
|
||||
-- These approved metadata rows intentionally have no document URL: initialization must not
|
||||
-- point at qualification files that do not exist in File service storage.
|
||||
INSERT INTO iam_tenant_qualification (
|
||||
id, tenant_id, qualification_type, certificate_no, document_url,
|
||||
valid_from, valid_to, status, review_comment, deleted
|
||||
) VALUES
|
||||
(2100001, 10001, 'BUSINESS_LICENSE', '91310000ITTEST0001', NULL,
|
||||
NULL, NULL, 'APPROVED', '开发测试初始化资质,无文件附件', 0),
|
||||
(2100002, 10001, 'LEGAL_PERSON_ID', NULL, NULL,
|
||||
NULL, NULL, 'APPROVED', '开发测试初始化资质,无文件附件', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
tenant_id = VALUES(tenant_id), qualification_type = VALUES(qualification_type),
|
||||
certificate_no = VALUES(certificate_no), status = 'APPROVED',
|
||||
review_comment = VALUES(review_comment), deleted = 0;
|
||||
|
||||
INSERT INTO iam_user (
|
||||
id, auth_user_id, user_type, display_name, phone, email, status, deleted, version
|
||||
) VALUES
|
||||
(2000001, 1000001, 'INTERNAL', '平台系统管理员', NULL, NULL, 'ACTIVE', 0, 0),
|
||||
(2000002, 1000002, 'ENTERPRISE', 'IT 测试客户管理员', '13800000000',
|
||||
'it-test@example.com', 'ACTIVE', 0, 0)
|
||||
(2000001, 1000001, 'INTERNAL', '平台系统管理员', NULL, NULL, 'ACTIVE', 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
display_name = VALUES(display_name), phone = VALUES(phone), email = VALUES(email),
|
||||
status = 'ACTIVE', deleted = 0;
|
||||
@@ -1235,8 +1212,7 @@ ON DUPLICATE KEY UPDATE
|
||||
INSERT INTO iam_membership (
|
||||
id, tenant_id, user_id, member_type, member_no, status, joined_at, deleted, version
|
||||
) VALUES
|
||||
(3000001, 0, 2000001, 'EMPLOYEE', 'PLATFORM-ADMIN-001', 'ACTIVE', NOW(3), 0, 0),
|
||||
(3000002, 10001, 2000002, 'OWNER', 'MERCHANT-OWNER-001', 'ACTIVE', NOW(3), 0, 0)
|
||||
(3000001, 0, 2000001, 'EMPLOYEE', 'PLATFORM-ADMIN-001', 'ACTIVE', NOW(3), 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
member_type = VALUES(member_type), member_no = VALUES(member_no),
|
||||
status = 'ACTIVE', joined_at = COALESCE(joined_at, NOW(3)), left_at = NULL, deleted = 0;
|
||||
@@ -1246,9 +1222,7 @@ INSERT INTO iam_role (
|
||||
) VALUES
|
||||
(4000001, 0, 'PLATFORM', 'PLATFORM_ADMIN', '平台超级管理员', 'SYSTEM', 'ACTIVE', '一期平台全部菜单和按钮', 0, 0),
|
||||
(4000002, 0, 'PLATFORM', 'CUSTOMER_REVIEWER', '企业审核员', 'SYSTEM', 'ACTIVE', '企业资料与入驻审核', 0, 0),
|
||||
(4000003, 0, 'PLATFORM', 'ORDER_OPERATOR', '平台订单运营', 'SYSTEM', 'ACTIVE', '头程订单审核与履约处理', 0, 0),
|
||||
(4000101, 10001, 'MERCHANT', 'MERCHANT_OWNER', '企业管理员', 'SYSTEM', 'ACTIVE', '本企业一期全部菜单和按钮', 0, 0),
|
||||
(4000102, 10001, 'MERCHANT', 'MERCHANT_ORDER_MANAGER', '企业订单管理员', 'SYSTEM', 'ACTIVE', '本企业头程订单操作', 0, 0)
|
||||
(4000003, 0, 'PLATFORM', 'ORDER_OPERATOR', '平台订单运营', 'SYSTEM', 'ACTIVE', '头程订单审核与履约处理', 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
role_name = VALUES(role_name), role_type = VALUES(role_type), status = 'ACTIVE',
|
||||
description = VALUES(description), deleted = 0;
|
||||
@@ -1335,39 +1309,8 @@ SELECT 4000003, id, 0 FROM iam_permission
|
||||
WHERE id IN (5102019, 5102024, 5102025)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
-- Merchant: level-one menus, level-two menus and buttons.
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, deleted)
|
||||
VALUES
|
||||
(5200000, 10001, 'MERCHANT', 'MENU', 0, 'merchant:enterprise', '企业管理', '/enterprise', NULL, 10, 'ACTIVE', 0),
|
||||
(5201000, 10001, 'MERCHANT', 'MENU', 0, 'merchant:order', '头程订单', '/first-leg-orders', NULL, 20, 'ACTIVE', 0),
|
||||
(5202000, 10001, 'MERCHANT', 'MENU', 0, 'merchant:master-data', '基础资料', '/master-data', NULL, 30, 'ACTIVE', 0),
|
||||
(5200010, 10001, 'MERCHANT', 'MENU', 5200000, 'merchant:profile:view', '企业资料', '/enterprise/profile', NULL, 10, 'ACTIVE', 0),
|
||||
(5200020, 10001, 'MERCHANT', 'MENU', 5200000, 'merchant:member:list', '企业成员', '/enterprise/members', NULL, 20, 'ACTIVE', 0),
|
||||
(5200030, 10001, 'MERCHANT', 'MENU', 5200000, 'merchant:role:list', '角色管理', '/enterprise/roles', NULL, 30, 'ACTIVE', 0),
|
||||
(5201010, 10001, 'MERCHANT', 'MENU', 5201000, 'merchant:order:list', '我的订单', '/first-leg-orders/list', NULL, 10, 'ACTIVE', 0),
|
||||
(5201020, 10001, 'MERCHANT', 'MENU', 5201000, 'merchant:order:create-page', '新建订单', '/first-leg-orders/create', NULL, 20, 'ACTIVE', 0),
|
||||
(5202010, 10001, 'MERCHANT', 'MENU', 5202000, 'merchant:warehouse:list', '收货仓选择', '/master-data/warehouses', NULL, 10, 'ACTIVE', 0),
|
||||
(5202020, 10001, 'MERCHANT', 'MENU', 5202000, 'merchant:counterparty:list', '往来单位', '/master-data/counterparties', NULL, 20, 'ACTIVE', 0),
|
||||
(5200011, 10001, 'MERCHANT', 'BUTTON', 5200010, 'merchant:profile:edit', '编辑企业资料', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5200012, 10001, 'MERCHANT', 'BUTTON', 5200010, 'merchant:qualification:upload', '上传企业资质', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5200021, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:create', '新增成员', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5200022, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:grant-role', '分配角色', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5200031, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:create', '新增角色', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5200032, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:grant', '角色授权', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5201011, 10001, 'MERCHANT', 'BUTTON', 5201010, 'merchant:order:view', '查看订单', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5201012, 10001, 'MERCHANT', 'BUTTON', 5201010, 'merchant:order:cancel', '取消订单', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5201021, 10001, 'MERCHANT', 'BUTTON', 5201020, 'merchant:order:create', '创建草稿', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5201022, 10001, 'MERCHANT', 'BUTTON', 5201020, 'merchant:order:edit', '编辑草稿', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5201023, 10001, 'MERCHANT', 'BUTTON', 5201020, 'merchant:order:submit', '提交订单', NULL, NULL, 30, 'ACTIVE', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_type = VALUES(permission_type), parent_id = VALUES(parent_id),
|
||||
permission_name = VALUES(permission_name), route_path = VALUES(route_path),
|
||||
http_method = NULL, sort_no = VALUES(sort_no), status = 'ACTIVE', deleted = 0;
|
||||
|
||||
INSERT INTO iam_membership_role (membership_id, role_id, tenant_id) VALUES
|
||||
(3000001, 4000001, 0),
|
||||
(3000002, 4000101, 10001)
|
||||
(3000001, 4000001, 0)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
-- Full administrator roles receive all phase-one permissions in their application scope.
|
||||
@@ -1376,11 +1319,6 @@ SELECT 4000001, id, 0 FROM iam_permission
|
||||
WHERE app_code = 'PLATFORM' AND tenant_id = 0 AND permission_type IN ('MENU', 'BUTTON')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000101, id, 10001 FROM iam_permission
|
||||
WHERE app_code = 'MERCHANT' AND tenant_id = 10001 AND permission_type IN ('MENU', 'BUTTON')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
-- Limited platform and merchant operational roles.
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000002, id, 0 FROM iam_permission
|
||||
@@ -1396,17 +1334,10 @@ SELECT 4000003, id, 0 FROM iam_permission
|
||||
WHERE permission_code LIKE 'platform:order%' OR permission_code LIKE 'platform:firstleg%'
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000102, id, 10001 FROM iam_permission
|
||||
WHERE permission_code LIKE 'merchant:order%' OR permission_code IN ('merchant:master-data', 'merchant:warehouse:list')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_data_scope (id, role_id, tenant_id, scope_type, scope_expression) VALUES
|
||||
(6000001, 4000001, 0, 'TENANT', NULL),
|
||||
(6000002, 4000002, 0, 'TENANT', NULL),
|
||||
(6000003, 4000003, 0, 'TENANT', NULL),
|
||||
(6000101, 4000101, 10001, 'TENANT', NULL),
|
||||
(6000102, 4000102, 10001, 'TENANT', NULL)
|
||||
(6000003, 4000003, 0, 'TENANT', NULL)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id), scope_type = VALUES(scope_type), scope_expression = NULL;
|
||||
|
||||
-- ============================================================================
|
||||
@@ -1455,17 +1386,34 @@ INSERT INTO iam_role (
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE status='ACTIVE', deleted=0;
|
||||
|
||||
-- Clone the一期 merchant menu/button template into the global scope.
|
||||
-- Global merchant permission template used by every formally onboarded customer.
|
||||
INSERT INTO iam_permission (
|
||||
id, tenant_id, app_code, permission_type, parent_id, permission_code,
|
||||
permission_name, route_path, http_method, sort_no, status, deleted
|
||||
)
|
||||
SELECT id + 100000, 0, app_code, permission_type,
|
||||
CASE WHEN parent_id = 0 THEN 0 ELSE parent_id + 100000 END,
|
||||
permission_code, permission_name, route_path, NULL, sort_no, 'ACTIVE', 0
|
||||
FROM iam_permission
|
||||
WHERE tenant_id = 10001 AND app_code = 'MERCHANT'
|
||||
AND permission_type IN ('MENU', 'BUTTON')
|
||||
VALUES
|
||||
(5300000, 0, 'MERCHANT', 'MENU', 0, 'merchant:enterprise', '企业管理', '/enterprise', NULL, 10, 'ACTIVE', 0),
|
||||
(5301000, 0, 'MERCHANT', 'MENU', 0, 'merchant:order', '头程订单', '/first-leg-orders', NULL, 20, 'ACTIVE', 0),
|
||||
(5302000, 0, 'MERCHANT', 'MENU', 0, 'merchant:master-data', '基础资料', '/master-data', NULL, 30, 'ACTIVE', 0),
|
||||
(5300010, 0, 'MERCHANT', 'MENU', 5300000, 'merchant:profile:view', '企业资料', '/enterprise/profile', NULL, 10, 'ACTIVE', 0),
|
||||
(5300020, 0, 'MERCHANT', 'MENU', 5300000, 'merchant:member:list', '企业成员', '/enterprise/members', NULL, 20, 'ACTIVE', 0),
|
||||
(5300030, 0, 'MERCHANT', 'MENU', 5300000, 'merchant:role:list', '角色管理', '/enterprise/roles', NULL, 30, 'ACTIVE', 0),
|
||||
(5301010, 0, 'MERCHANT', 'MENU', 5301000, 'merchant:order:list', '我的订单', '/first-leg-orders/list', NULL, 10, 'ACTIVE', 0),
|
||||
(5301020, 0, 'MERCHANT', 'MENU', 5301000, 'merchant:order:create-page', '新建订单', '/first-leg-orders/create', NULL, 20, 'ACTIVE', 0),
|
||||
(5302010, 0, 'MERCHANT', 'MENU', 5302000, 'merchant:warehouse:list', '收货仓选择', '/master-data/warehouses', NULL, 10, 'ACTIVE', 0),
|
||||
(5302020, 0, 'MERCHANT', 'MENU', 5302000, 'merchant:counterparty:list', '往来单位', '/master-data/counterparties', NULL, 20, 'ACTIVE', 0),
|
||||
(5300011, 0, 'MERCHANT', 'BUTTON', 5300010, 'merchant:profile:edit', '编辑企业资料', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5300012, 0, 'MERCHANT', 'BUTTON', 5300010, 'merchant:qualification:upload', '上传企业资质', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5300021, 0, 'MERCHANT', 'BUTTON', 5300020, 'merchant:member:create', '新增成员', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5300022, 0, 'MERCHANT', 'BUTTON', 5300020, 'merchant:member:grant-role', '分配成员角色', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5300031, 0, 'MERCHANT', 'BUTTON', 5300030, 'merchant:role:create', '新增角色', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5300032, 0, 'MERCHANT', 'BUTTON', 5300030, 'merchant:role:grant', '角色授权', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5301011, 0, 'MERCHANT', 'BUTTON', 5301010, 'merchant:order:view', '查看订单', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5301012, 0, 'MERCHANT', 'BUTTON', 5301010, 'merchant:order:cancel', '取消订单', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5301021, 0, 'MERCHANT', 'BUTTON', 5301020, 'merchant:order:create', '创建草稿', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5301022, 0, 'MERCHANT', 'BUTTON', 5301020, 'merchant:order:edit', '编辑草稿', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5301023, 0, 'MERCHANT', 'BUTTON', 5301020, 'merchant:order:submit', '提交订单', NULL, NULL, 30, 'ACTIVE', 0),
|
||||
(5301024, 0, 'MERCHANT', 'BUTTON', 5301020, 'merchant:order:quick-create', '快速下单', NULL, NULL, 40, 'ACTIVE', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_name=VALUES(permission_name), route_path=VALUES(route_path),
|
||||
parent_id=VALUES(parent_id), sort_no=VALUES(sort_no), status='ACTIVE', deleted=0;
|
||||
@@ -1507,20 +1455,11 @@ DEALLOCATE PREPARE member_no_index_stmt;
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, sort_no, status, deleted)
|
||||
VALUES
|
||||
(5200023, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:edit', '编辑成员', 30, 'ACTIVE', 0),
|
||||
(5200024, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:status', '启停成员', 40, 'ACTIVE', 0),
|
||||
(5200025, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:reset-password', '重置成员密码', 50, 'ACTIVE', 0),
|
||||
(5300023, 0, 'MERCHANT', 'BUTTON', 5300020, 'merchant:member:edit', '编辑成员', 30, 'ACTIVE', 0),
|
||||
(5300024, 0, 'MERCHANT', 'BUTTON', 5300020, 'merchant:member:status', '启停成员', 40, 'ACTIVE', 0),
|
||||
(5300025, 0, 'MERCHANT', 'BUTTON', 5300020, 'merchant:member:reset-password', '重置成员密码', 50, 'ACTIVE', 0)
|
||||
ON DUPLICATE KEY UPDATE permission_name = VALUES(permission_name), status = VALUES(status), deleted = 0;
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000101, id, 10001 FROM iam_permission
|
||||
WHERE tenant_id = 10001 AND permission_code IN
|
||||
('merchant:member:edit', 'merchant:member:status', 'merchant:member:reset-password')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000100, id, 0 FROM iam_permission
|
||||
WHERE tenant_id = 0 AND permission_code IN
|
||||
@@ -1548,9 +1487,6 @@ VALUES
|
||||
(5101013, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:status', '启停用户', 30, 'ACTIVE', 0),
|
||||
(5101014, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:reset-password', '重置密码', 40, 'ACTIVE', 0),
|
||||
(5101015, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:grant-role', '分配内部用户角色', 50, 'ACTIVE', 0),
|
||||
(5200033, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:edit', '编辑角色', 30, 'ACTIVE', 0),
|
||||
(5200034, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:status', '启停角色', 40, 'ACTIVE', 0),
|
||||
(5200035, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:delete', '删除角色', 50, 'ACTIVE', 0),
|
||||
(5300033, 0, 'MERCHANT', 'BUTTON', 5300030, 'merchant:role:edit', '编辑角色', 30, 'ACTIVE', 0),
|
||||
(5300034, 0, 'MERCHANT', 'BUTTON', 5300030, 'merchant:role:status', '启停角色', 40, 'ACTIVE', 0),
|
||||
(5300035, 0, 'MERCHANT', 'BUTTON', 5300030, 'merchant:role:delete', '删除角色', 50, 'ACTIVE', 0)
|
||||
@@ -1569,12 +1505,6 @@ WHERE tenant_id = 0 AND app_code = 'PLATFORM'
|
||||
'platform:user:grant-role')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000101, id, 10001 FROM iam_permission
|
||||
WHERE tenant_id = 10001 AND app_code = 'MERCHANT'
|
||||
AND permission_code IN ('merchant:role:edit', 'merchant:role:status', 'merchant:role:delete')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000100, id, 0 FROM iam_permission
|
||||
WHERE tenant_id = 0 AND app_code = 'MERCHANT'
|
||||
@@ -4286,23 +4216,9 @@ SET @unified_ddl = IF(
|
||||
PREPARE unified_stmt FROM @unified_ddl; EXECUTE unified_stmt; DEALLOCATE PREPARE unified_stmt;
|
||||
|
||||
-- ============================================================================
|
||||
-- 重新同步一期商户管理员全局模板,包含一期后续追加的菜单和按钮
|
||||
-- 重新授权一期商户管理员全局模板,包含一期后续追加的菜单和按钮
|
||||
-- ============================================================================
|
||||
USE baiyue_iam;
|
||||
INSERT INTO iam_permission (
|
||||
id, tenant_id, app_code, permission_type, parent_id, permission_code,
|
||||
permission_name, route_path, http_method, sort_no, status, deleted
|
||||
)
|
||||
SELECT id + 100000, 0, app_code, permission_type,
|
||||
CASE WHEN parent_id = 0 THEN 0 ELSE parent_id + 100000 END,
|
||||
permission_code, permission_name, route_path, NULL, sort_no, 'ACTIVE', 0
|
||||
FROM iam_permission
|
||||
WHERE tenant_id = 10001 AND app_code = 'MERCHANT'
|
||||
AND permission_type IN ('MENU', 'BUTTON')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_name = VALUES(permission_name), route_path = VALUES(route_path),
|
||||
parent_id = VALUES(parent_id), sort_no = VALUES(sort_no), status = 'ACTIVE', deleted = 0;
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT owner_role.id, permission.id, 0
|
||||
FROM iam_role owner_role
|
||||
@@ -15350,10 +15266,9 @@ VALUES
|
||||
(5105019, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:cost:create', '登记成本', NULL, 'POST', 100, 'ACTIVE', 0),
|
||||
(5105020, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:customer-terms:view', '查看客户账期', NULL, 'GET', 110, 'ACTIVE', 0),
|
||||
(5105021, 0, 'PLATFORM', 'BUTTON', 5105000, 'platform:finance:customer-terms:update', '修改客户账期', NULL, 'PUT', 120, 'ACTIVE', 0),
|
||||
(5205000, 10001, 'MERCHANT', 'MENU', 5200000, 'merchant:finance:statement', '财务账单', '/finance/statements', NULL, 40, 'ACTIVE', 0),
|
||||
(5205010, 10001, 'MERCHANT', 'BUTTON', 5205000, 'merchant:finance:statement:list', '查看账单', NULL, 'GET', 10, 'ACTIVE', 0),
|
||||
(5205011, 10001, 'MERCHANT', 'BUTTON', 5205000, 'merchant:finance:statement:view', '查看账单详情及收款', NULL, 'GET', 20, 'ACTIVE', 0),
|
||||
(5205012, 10001, 'MERCHANT', 'BUTTON', 5205000, 'merchant:finance:statement:confirm', '确认客户对账单', NULL, 'POST', 30, 'ACTIVE', 0),
|
||||
(5305000, 0, 'MERCHANT', 'MENU', 5300000, 'merchant:finance:statement', '财务账单', '/finance/statements', NULL, 40, 'ACTIVE', 0),
|
||||
(5305010, 0, 'MERCHANT', 'BUTTON', 5305000, 'merchant:finance:statement:list', '查看账单', NULL, 'GET', 10, 'ACTIVE', 0),
|
||||
(5305011, 0, 'MERCHANT', 'BUTTON', 5305000, 'merchant:finance:statement:view', '查看账单详情及收款', NULL, 'GET', 20, 'ACTIVE', 0),
|
||||
(5305012, 0, 'MERCHANT', 'BUTTON', 5305000, 'merchant:finance:statement:confirm', '确认客户对账单', NULL, 'POST', 30, 'ACTIVE', 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), status='ACTIVE', deleted=0;
|
||||
|
||||
@@ -15486,11 +15401,6 @@ WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code IN (
|
||||
'platform:finance:settlement-ap:create', 'platform:finance:payment:create',
|
||||
'platform:finance:settlement:void')
|
||||
ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id);
|
||||
INSERT INTO baiyue_iam.iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000101, id, 10001 FROM baiyue_iam.iam_permission
|
||||
WHERE tenant_id=10001 AND app_code='MERCHANT' AND (permission_code='merchant:finance:statement' OR permission_code LIKE 'merchant:finance:%')
|
||||
ON DUPLICATE KEY UPDATE tenant_id=VALUES(tenant_id);
|
||||
|
||||
-- 开发期 list-only 收口:先移除角色关联并停用旧只读资源,再执行最终 Finance 断言。
|
||||
DELETE rp FROM baiyue_iam.iam_role_permission rp
|
||||
JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id
|
||||
@@ -15956,22 +15866,18 @@ SELECT 0, r.id, p.id, CURRENT_TIMESTAMP(3) FROM iam_role r JOIN iam_permission p
|
||||
WHERE r.tenant_id=0 AND r.app_code='PLATFORM' AND r.role_code='PLATFORM_ADMIN' AND p.permission_code IN ('platform:finance:customer-account:list','platform:finance:customer-arap:list','platform:finance:receivable-statistics:list','platform:finance:customer-statement:list','platform:finance:settlement-ar:list','platform:finance:settlement-ap:list','platform:finance:customer-account:export','platform:finance:customer-arap:export','platform:finance:receivable-statistics:export','platform:finance:customer-statement:export','platform:finance:settlement-ar:export','platform:finance:settlement-ap:export','platform:download:list');
|
||||
UPDATE iam_permission p JOIN (SELECT 'platform:finance:fee-source:review' code,5105041 parent_id UNION ALL SELECT 'platform:finance:fee-source:edit',5105041 UNION ALL SELECT 'platform:finance:reconciliation:create',5105042 UNION ALL SELECT 'platform:finance:reconciliation:review',5105042 UNION ALL SELECT 'platform:finance:reconciliation:issue',5105042 UNION ALL SELECT 'platform:finance:reconciliation:adjust',5105042 UNION ALL SELECT 'platform:finance:reconciliation:void',5105042 UNION ALL SELECT 'platform:finance:settlement-ar:create',5105043 UNION ALL SELECT 'platform:finance:settlement-ap:create',5105044 UNION ALL SELECT 'platform:finance:payment:create',5105044) mapping ON mapping.code=p.permission_code SET p.parent_id=mapping.parent_id WHERE p.tenant_id=0 AND p.app_code='PLATFORM' AND p.permission_type='BUTTON';
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 10001, r.id, p.id, CURRENT_TIMESTAMP(3) FROM iam_role r JOIN iam_permission p ON p.tenant_id=10001 AND p.app_code='MERCHANT'
|
||||
WHERE r.tenant_id=10001 AND r.app_code='MERCHANT' AND r.role_code IN ('MERCHANT_OWNER','MERCHANT_ADMIN')
|
||||
AND p.permission_code='merchant:finance:statement:confirm';
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, r.id, p.id, CURRENT_TIMESTAMP(3) FROM iam_role r JOIN iam_permission p ON p.tenant_id=0 AND p.app_code='MERCHANT'
|
||||
WHERE r.tenant_id=0 AND r.app_code='MERCHANT' AND r.role_code='MERCHANT_OWNER'
|
||||
AND r.status='ACTIVE' AND r.deleted=0
|
||||
AND p.permission_code='merchant:finance:statement:confirm'
|
||||
AND p.permission_code IN ('merchant:finance:statement','merchant:finance:statement:list',
|
||||
'merchant:finance:statement:view','merchant:finance:statement:confirm')
|
||||
AND p.status='ACTIVE' AND p.deleted=0;
|
||||
|
||||
INSERT INTO baiyue_iam.iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, deleted)
|
||||
VALUES
|
||||
(5201024, 10001, 'MERCHANT', 'BUTTON', 5201020, 'merchant:order:quick-create', '快速下单', NULL, NULL, 40, 'ACTIVE', 0)
|
||||
(5301024, 0, 'MERCHANT', 'BUTTON', 5301020, 'merchant:order:quick-create', '快速下单', NULL, NULL, 40, 'ACTIVE', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_type = VALUES(permission_type), parent_id = VALUES(parent_id), permission_name = VALUES(permission_name),
|
||||
route_path = VALUES(route_path), sort_no = VALUES(sort_no), status = 'ACTIVE', deleted = 0;
|
||||
@@ -16187,11 +16093,8 @@ BEGIN
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 32 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台 Finance 权限不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=10001 AND app_code='MERCHANT' AND permission_code IN ('merchant:finance:statement:list','merchant:finance:statement:view','merchant:finance:statement:confirm') AND status='ACTIVE' AND deleted=0) <> 3 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:客户 Finance 查询与确认权限不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='MERCHANT' AND permission_code='merchant:finance:statement:confirm' AND status='ACTIVE' AND deleted=0) <> 1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:客户全局对账单确认权限不完整';
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id=0 AND app_code='MERCHANT' AND permission_code IN ('merchant:finance:statement','merchant:finance:statement:list','merchant:finance:statement:view','merchant:finance:statement:confirm') AND status='ACTIVE' AND deleted=0) <> 4 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:客户全局 Finance 权限模板不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000001 AND rp.tenant_id=0 AND (p.permission_code='platform:finance' OR p.permission_code LIKE 'platform:finance:%')) <> 33 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台管理员 Finance 授权不完整';
|
||||
@@ -16199,11 +16102,8 @@ BEGIN
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000001 AND rp.tenant_id=0 AND p.status='ACTIVE' AND p.deleted=0 AND p.permission_code IN ('platform:download:list','platform:finance:customer-account:export','platform:finance:customer-arap:export','platform:finance:receivable-statistics:export','platform:finance:customer-statement:export','platform:finance:settlement-ar:export','platform:finance:settlement-ap:export')) <> 7 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:平台管理员 Finance 导出和下载中心授权不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE rp.role_id=4000101 AND rp.tenant_id=10001 AND (p.permission_code='merchant:finance:statement' OR p.permission_code LIKE 'merchant:finance:%')) <> 4 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:客户管理员 Finance 查询与确认授权不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_role r ON r.id=rp.role_id AND r.tenant_id=rp.tenant_id JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE r.tenant_id=0 AND r.app_code='MERCHANT' AND r.role_code='MERCHANT_OWNER' AND r.status='ACTIVE' AND r.deleted=0 AND p.permission_code='merchant:finance:statement:confirm' AND p.status='ACTIVE' AND p.deleted=0) <> 1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:客户全局管理员缺少对账单确认权限';
|
||||
IF (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission rp JOIN baiyue_iam.iam_role r ON r.id=rp.role_id AND r.tenant_id=rp.tenant_id JOIN baiyue_iam.iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE r.id=4000100 AND r.tenant_id=0 AND r.app_code='MERCHANT' AND r.role_code='MERCHANT_OWNER' AND r.status='ACTIVE' AND r.deleted=0 AND p.permission_code IN ('merchant:finance:statement','merchant:finance:statement:list','merchant:finance:statement:view','merchant:finance:statement:confirm') AND p.status='ACTIVE' AND p.deleted=0) <> 4 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='统一迁移失败:客户全局管理员 Finance 模板授权不完整';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
+2
-1
@@ -32,7 +32,7 @@
|
||||
| PDA 拣货与装柜 | `controller/PlatformLoadingPdaController.java`、`loading/controller/PlatformLoadingDeliveryController.java` |
|
||||
| 装柜完成跨区事件 | `service/LoadingCompletedOutboxService.java`、`LoadingCompletedOutboxRelay.java`、`repository/LoadingCompletedOutboxRepository.java`、`mapper/LoadingCartonManifestMapper.java`、`client/EventBridgeClient.java`;从 `fl_inbound_order.destination_type` 冻结 V2 `orders[].destinationType=FBA|OVERSEAS`,OVERSEAS 按最终实装与稳定箱码冻结,FBA 可仅冻结箱数;真实生产验收 `src/test/java/com/baiyue/firstleg/service/TaillegRealProducerAcceptanceTest.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
||||
| 海外卸柜完成事件回写 | 内部入口 `controller/InternalOverseasContainerEventController.java`、消费者 `service/OverseasContainerEventConsumer.java`、Inbox `entity/ContainerEventInboxEntity.java` 与 `mapper/ContainerEventInboxMapper.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
||||
| 三期海运订舱与出仓前船期资料 | `oceanbooking/controller/PlatformOceanBookingController.java`、`oceanbooking/service/OceanBookingService.java`、`portschedule/controller/PlatformPortScheduleController.java`;订舱契约见 `src/main/resources/openapi/ocean-bookings-v1.yaml`,表结构见 `src/main/resources/db/phase3-ocean-booking-schema.sql` |
|
||||
| 三期海运订舱、关联候选与出仓前船期资料 | `oceanbooking/controller/PlatformOceanBookingController.java`、`oceanbooking/service/OceanBookingService.java`、`oceanbooking/mapper/OceanBookingMapper.java`、`portschedule/controller/PlatformPortScheduleController.java`;关联候选入口 `/api/platform/v1/ocean-bookings/link-candidates`,订舱契约见 `src/main/resources/openapi/ocean-bookings-v1.yaml`,表结构见 `src/main/resources/db/phase3-ocean-booking-schema.sql` |
|
||||
| 四期国内出仓后船节点定时查询 | `outbound/dispatch/service/OutboundDispatchService.java` 在确认出仓事务内幂等创建现有 `CarrierScheduleQuery`;编排与租约 `portschedule/service/CarrierScheduleQueryScheduler.java`,任务/十节点事实/异常事实/不可变 attempt `CarrierScheduleQueryService.java`,平台查询与人工补录入口 `PlatformCarrierScheduleQueryController.java` 的 `/{queryId}/manual-nodes`;停止只由实际 `EMPTY_RETURNED` 节点触发,不提供无节点停止入口;外部船司/轨迹调用经 `integration-service/`;标准见 `../../docs/四期/船节点标准化契约.md` |
|
||||
| 船节点跨区交接事件 | `portschedule/service/CarrierScheduleNodeOutboxService.java`;实际节点落库后发布 `FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED` V1,复用 `LoadingCompletedOutboxRepository`/Relay 投 Event Bridge;海外消费见 `../lastleg-service/src/main/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumer.java` |
|
||||
| Firstleg 业务单据编号 | `service/FirstlegDocumentNumberGenerator.java`;当前支持 `OB`、`TA`、`CB`、`CAB`、`LP` 前缀,按北京时间每日独立编号;具体业务归属以各调用服务为准 |
|
||||
@@ -56,6 +56,7 @@
|
||||
- 订舱状态固定为 `DRAFT → RELEASED`:草稿允许资料不完整并可替换或删除,放舱时校验完整持久化聚合,放舱后永久只读。创建草稿和放舱都只维护订舱聚合、三类参与方及柜型需求,不创建真实 `containerNo`、货柜行或分配关系;真实货柜只在后续提柜关联时占用已放舱柜型需求。
|
||||
- 订舱柜型需求至少一项、最多 20 项,数量必须为正整数,容量和限重必须匹配平台柜型规格。起运仓必须是启用的国内中转仓;物流渠道必须来自该仓当前 ACTIVE 价格版本,服务端保存渠道名称快照,渠道失效时失败关闭。
|
||||
- 订舱号统一使用 `OB-yyMMdd-####`,按北京时间创建日每日独立递增且单日最多 9999;不得以实体 ID 拼接生成。放舱不要求真实柜号,也不得为满足放舱校验伪造柜号。
|
||||
- 订舱列表 `containerCount` 固定为该订舱全部柜型需求 `quantity` 之和,与真实柜关联/解除无关;关联候选只返回 `RELEASED`、剩余数量大于 0 且 `warehouseId + channelCode + containerType` 精确匹配的需求,由服务端集合查询 ACTIVE 分配量。前端不得扇出详情计算或用已关联柜数覆盖该字段,最终关联命令仍须重验目标货柜可修改性。
|
||||
- 四期船节点任务从确认柜子离开国内仓库后启用,必须绑定真实订舱和柜次;十个标准节点、预计/实际时间、异常事实、供应商缺失节点和还柜停止规则统一以 `../../docs/四期/船节点标准化契约.md` 为准。`CarrierScheduleQuery` 的正常业务查询槽位固定为 `Asia/Shanghai` 每日 09:00、12:30、16:00;每分钟任务只扫描已到期 `next_allowed_at`,不是每分钟请求船司。成功、无变化、需人工或永久失败进入下一固定槽位;仅技术性 `RETRYABLE_FAILURE` 在当前槽内按15/30分钟退避,含首次最多3次且不得跨下一槽,实际 `EMPTY_RETURNED` 后停止。错过的正常槽位直接顺延,不在服务恢复时补跑;单轮按受控批次持续领取并受最大任务数保护。租约、调度尝试和内部停止入口不得暴露为平台页面 API,外部失败不得推进业务节点,不得在三期另建查询任务。
|
||||
- 海外卸柜完成事件只接受 `OVERSEAS -> CN` 的 `OVERSEAS_CONTAINER_UNLOADED` V1,并通过内部服务令牌进入。消费者必须校验信封、Payload 哈希、来源货柜和时间顺序,以 event ID + Payload 哈希幂等,使用事件聚合版本阻止乱序覆盖;预计报空时间固定按卸柜照片上传时间加两天计算,实际报空时间可为空。
|
||||
- Firstleg 只拥有业务费用及履约事实;Finance 拥有费用入池、账期、账单、收款、成本分摊结果和财务状态。修改费用来源版本、发布条件、柜次归属或体积快照时同步检查 `../finance/src/main/java/com/baiyue/finance/client/FirstlegFeeSourceClient.java`、同目录 `FirstlegCostSnapshotClient.java` 及双方契约测试,不在 Firstleg 复制财务状态机。
|
||||
|
||||
@@ -29,7 +29,9 @@ VALUES
|
||||
(5105053,0,'PLATFORM','BUTTON',5105043,'platform:finance:settlement-ar:export','导出应收结算单',NULL,'POST',90,'ACTIVE',1,0),
|
||||
(5105054,0,'PLATFORM','BUTTON',5105044,'platform:finance:settlement-ap:export','导出应付结算单',NULL,'POST',90,'ACTIVE',1,0),
|
||||
(5105055,0,'PLATFORM','BUTTON',5105045,'platform:finance:receivable-statistics:export','导出应收款统计表',NULL,'POST',90,'ACTIVE',1,0),
|
||||
(5205012,10001,'MERCHANT','BUTTON',5205000,'merchant:finance:statement:confirm','确认客户对账单',NULL,'POST',30,'ACTIVE',1,0),
|
||||
(5305000,0,'MERCHANT','MENU',5300000,'merchant:finance:statement','财务账单','/finance/statements',NULL,40,'ACTIVE',1,0),
|
||||
(5305010,0,'MERCHANT','BUTTON',5305000,'merchant:finance:statement:list','查看账单',NULL,'GET',10,'ACTIVE',1,0),
|
||||
(5305011,0,'MERCHANT','BUTTON',5305000,'merchant:finance:statement:view','查看账单详情及收款',NULL,'GET',20,'ACTIVE',1,0),
|
||||
(5305012,0,'MERCHANT','BUTTON',5305000,'merchant:finance:statement:confirm','确认客户对账单',NULL,'POST',30,'ACTIVE',1,0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_type=VALUES(permission_type), parent_id=VALUES(parent_id),
|
||||
@@ -101,18 +103,12 @@ UPDATE iam_permission p
|
||||
SET p.parent_id=mapping.parent_id
|
||||
WHERE p.tenant_id=0 AND p.app_code='PLATFORM' AND p.permission_type='BUTTON';
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 10001, r.id, p.id, CURRENT_TIMESTAMP(3)
|
||||
FROM iam_role r
|
||||
JOIN iam_permission p ON p.tenant_id=10001 AND p.app_code='MERCHANT'
|
||||
WHERE r.tenant_id=10001 AND r.app_code='MERCHANT' AND r.role_code IN ('MERCHANT_OWNER','MERCHANT_ADMIN')
|
||||
AND p.permission_code='merchant:finance:statement:confirm';
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
||||
SELECT 0, r.id, p.id, CURRENT_TIMESTAMP(3)
|
||||
FROM iam_role r
|
||||
JOIN iam_permission p ON p.tenant_id=0 AND p.app_code='MERCHANT'
|
||||
WHERE r.tenant_id=0 AND r.app_code='MERCHANT' AND r.role_code='MERCHANT_OWNER'
|
||||
AND r.status='ACTIVE' AND r.deleted=0
|
||||
AND p.permission_code='merchant:finance:statement:confirm'
|
||||
AND p.permission_code IN ('merchant:finance:statement','merchant:finance:statement:list',
|
||||
'merchant:finance:statement:view','merchant:finance:statement:confirm')
|
||||
AND p.status='ACTIVE' AND p.deleted=0;
|
||||
|
||||
@@ -26,10 +26,7 @@ INSERT INTO iam_tenant (
|
||||
approved_at, approved_by, created_by, deleted, version
|
||||
) VALUES
|
||||
(0, 'PLATFORM', '虫虫出海平台', 'PLATFORM', 'ACTIVE', '虫虫出海平台',
|
||||
NULL, '平台系统管理员', NULL, NULL, NOW(3), 1000001, 1000001, 0, 0),
|
||||
(10001, 'IT_TEST', 'IT 测试客户', 'MERCHANT', 'ACTIVE', 'IT 测试客户',
|
||||
'91310000ITTEST0001', 'IT 测试客户管理员', '13800000000', 'it-test@example.com',
|
||||
NOW(3), 1000001, 1000001, 0, 0)
|
||||
NULL, '平台系统管理员', NULL, NULL, NOW(3), 1000001, 1000001, 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
tenant_code = VALUES(tenant_code), tenant_name = VALUES(tenant_name), tenant_type = VALUES(tenant_type),
|
||||
status = 'ACTIVE', legal_name = VALUES(legal_name),
|
||||
@@ -38,28 +35,10 @@ ON DUPLICATE KEY UPDATE
|
||||
contact_email = VALUES(contact_email), approved_at = COALESCE(approved_at, NOW(3)),
|
||||
approved_by = VALUES(approved_by), deleted = 0;
|
||||
|
||||
-- The built-in IT customer must be immediately usable for order acceptance tests.
|
||||
-- These approved metadata rows intentionally have no document URL: initialization must not
|
||||
-- point at qualification files that do not exist in File service storage.
|
||||
INSERT INTO iam_tenant_qualification (
|
||||
id, tenant_id, qualification_type, certificate_no, document_url,
|
||||
valid_from, valid_to, status, review_comment, deleted
|
||||
) VALUES
|
||||
(2100001, 10001, 'BUSINESS_LICENSE', '91310000ITTEST0001', NULL,
|
||||
NULL, NULL, 'APPROVED', '开发测试初始化资质,无文件附件', 0),
|
||||
(2100002, 10001, 'LEGAL_PERSON_ID', NULL, NULL,
|
||||
NULL, NULL, 'APPROVED', '开发测试初始化资质,无文件附件', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
tenant_id = VALUES(tenant_id), qualification_type = VALUES(qualification_type),
|
||||
certificate_no = VALUES(certificate_no), status = 'APPROVED',
|
||||
review_comment = VALUES(review_comment), deleted = 0;
|
||||
|
||||
INSERT INTO iam_user (
|
||||
id, auth_user_id, user_type, display_name, phone, email, status, deleted, version
|
||||
) VALUES
|
||||
(2000001, 1000001, 'INTERNAL', '平台系统管理员', NULL, NULL, 'ACTIVE', 0, 0),
|
||||
(2000002, 1000002, 'ENTERPRISE', 'IT 测试客户管理员', '13800000000',
|
||||
'it-test@example.com', 'ACTIVE', 0, 0)
|
||||
(2000001, 1000001, 'INTERNAL', '平台系统管理员', NULL, NULL, 'ACTIVE', 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
display_name = VALUES(display_name), phone = VALUES(phone), email = VALUES(email),
|
||||
status = 'ACTIVE', deleted = 0;
|
||||
@@ -67,8 +46,7 @@ ON DUPLICATE KEY UPDATE
|
||||
INSERT INTO iam_membership (
|
||||
id, tenant_id, user_id, member_type, member_no, status, joined_at, deleted, version
|
||||
) VALUES
|
||||
(3000001, 0, 2000001, 'EMPLOYEE', 'PLATFORM-ADMIN-001', 'ACTIVE', NOW(3), 0, 0),
|
||||
(3000002, 10001, 2000002, 'OWNER', 'MERCHANT-OWNER-001', 'ACTIVE', NOW(3), 0, 0)
|
||||
(3000001, 0, 2000001, 'EMPLOYEE', 'PLATFORM-ADMIN-001', 'ACTIVE', NOW(3), 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
member_type = VALUES(member_type), member_no = VALUES(member_no),
|
||||
status = 'ACTIVE', joined_at = COALESCE(joined_at, NOW(3)), left_at = NULL, deleted = 0;
|
||||
@@ -78,9 +56,7 @@ INSERT INTO iam_role (
|
||||
) VALUES
|
||||
(4000001, 0, 'PLATFORM', 'PLATFORM_ADMIN', '平台超级管理员', 'SYSTEM', 'ACTIVE', '一期平台全部菜单和按钮', 0, 0),
|
||||
(4000002, 0, 'PLATFORM', 'CUSTOMER_REVIEWER', '企业审核员', 'SYSTEM', 'ACTIVE', '企业资料与入驻审核', 0, 0),
|
||||
(4000003, 0, 'PLATFORM', 'ORDER_OPERATOR', '平台订单运营', 'SYSTEM', 'ACTIVE', '头程订单审核与履约处理', 0, 0),
|
||||
(4000101, 10001, 'MERCHANT', 'MERCHANT_OWNER', '企业管理员', 'SYSTEM', 'ACTIVE', '本企业一期全部菜单和按钮', 0, 0),
|
||||
(4000102, 10001, 'MERCHANT', 'MERCHANT_ORDER_MANAGER', '企业订单管理员', 'SYSTEM', 'ACTIVE', '本企业头程订单操作', 0, 0)
|
||||
(4000003, 0, 'PLATFORM', 'ORDER_OPERATOR', '平台订单运营', 'SYSTEM', 'ACTIVE', '头程订单审核与履约处理', 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
role_name = VALUES(role_name), role_type = VALUES(role_type), status = 'ACTIVE',
|
||||
description = VALUES(description), deleted = 0;
|
||||
@@ -167,40 +143,8 @@ SELECT 4000003, id, 0 FROM iam_permission
|
||||
WHERE id IN (5102019, 5102024, 5102025)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
-- Merchant: level-one menus, level-two menus and buttons.
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, route_path, http_method, sort_no, status, deleted)
|
||||
VALUES
|
||||
(5200000, 10001, 'MERCHANT', 'MENU', 0, 'merchant:enterprise', '企业管理', '/enterprise', NULL, 10, 'ACTIVE', 0),
|
||||
(5201000, 10001, 'MERCHANT', 'MENU', 0, 'merchant:order', '头程订单', '/first-leg-orders', NULL, 20, 'ACTIVE', 0),
|
||||
(5202000, 10001, 'MERCHANT', 'MENU', 0, 'merchant:master-data', '基础资料', '/master-data', NULL, 30, 'ACTIVE', 0),
|
||||
(5200010, 10001, 'MERCHANT', 'MENU', 5200000, 'merchant:profile:view', '企业资料', '/enterprise/profile', NULL, 10, 'ACTIVE', 0),
|
||||
(5200020, 10001, 'MERCHANT', 'MENU', 5200000, 'merchant:member:list', '企业成员', '/enterprise/members', NULL, 20, 'ACTIVE', 0),
|
||||
(5200030, 10001, 'MERCHANT', 'MENU', 5200000, 'merchant:role:list', '角色管理', '/enterprise/roles', NULL, 30, 'ACTIVE', 0),
|
||||
(5201010, 10001, 'MERCHANT', 'MENU', 5201000, 'merchant:order:list', '我的订单', '/first-leg-orders/list', NULL, 10, 'ACTIVE', 0),
|
||||
(5201020, 10001, 'MERCHANT', 'MENU', 5201000, 'merchant:order:create-page', '新建订单', '/first-leg-orders/create', NULL, 20, 'ACTIVE', 0),
|
||||
(5202010, 10001, 'MERCHANT', 'MENU', 5202000, 'merchant:warehouse:list', '收货仓选择', '/master-data/warehouses', NULL, 10, 'ACTIVE', 0),
|
||||
(5202020, 10001, 'MERCHANT', 'MENU', 5202000, 'merchant:counterparty:list', '往来单位', '/master-data/counterparties', NULL, 20, 'ACTIVE', 0),
|
||||
(5200011, 10001, 'MERCHANT', 'BUTTON', 5200010, 'merchant:profile:edit', '编辑企业资料', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5200012, 10001, 'MERCHANT', 'BUTTON', 5200010, 'merchant:qualification:upload', '上传企业资质', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5200021, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:create', '新增成员', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5200022, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:grant-role', '分配角色', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5200031, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:create', '新增角色', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5200032, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:grant', '角色授权', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5201011, 10001, 'MERCHANT', 'BUTTON', 5201010, 'merchant:order:view', '查看订单', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5201012, 10001, 'MERCHANT', 'BUTTON', 5201010, 'merchant:order:cancel', '取消订单', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5201021, 10001, 'MERCHANT', 'BUTTON', 5201020, 'merchant:order:create', '创建草稿', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5201022, 10001, 'MERCHANT', 'BUTTON', 5201020, 'merchant:order:edit', '编辑草稿', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5201023, 10001, 'MERCHANT', 'BUTTON', 5201020, 'merchant:order:submit', '提交订单', NULL, NULL, 30, 'ACTIVE', 0),
|
||||
(5201024, 10001, 'MERCHANT', 'BUTTON', 5201020, 'merchant:order:quick-create', '快速下单', NULL, NULL, 40, 'ACTIVE', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_type = VALUES(permission_type), parent_id = VALUES(parent_id),
|
||||
permission_name = VALUES(permission_name), route_path = VALUES(route_path),
|
||||
http_method = NULL, sort_no = VALUES(sort_no), status = 'ACTIVE', deleted = 0;
|
||||
|
||||
INSERT INTO iam_membership_role (membership_id, role_id, tenant_id) VALUES
|
||||
(3000001, 4000001, 0),
|
||||
(3000002, 4000101, 10001)
|
||||
(3000001, 4000001, 0)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
-- Full administrator roles receive all phase-one permissions in their application scope.
|
||||
@@ -209,11 +153,6 @@ SELECT 4000001, id, 0 FROM iam_permission
|
||||
WHERE app_code = 'PLATFORM' AND tenant_id = 0 AND permission_type IN ('MENU', 'BUTTON')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000101, id, 10001 FROM iam_permission
|
||||
WHERE app_code = 'MERCHANT' AND tenant_id = 10001 AND permission_type IN ('MENU', 'BUTTON')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
-- Limited platform and merchant operational roles.
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000002, id, 0 FROM iam_permission
|
||||
@@ -229,15 +168,8 @@ SELECT 4000003, id, 0 FROM iam_permission
|
||||
WHERE permission_code LIKE 'platform:order%' OR permission_code LIKE 'platform:firstleg%'
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000102, id, 10001 FROM iam_permission
|
||||
WHERE permission_code LIKE 'merchant:order%' OR permission_code IN ('merchant:master-data', 'merchant:warehouse:list')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_data_scope (id, role_id, tenant_id, scope_type, scope_expression) VALUES
|
||||
(6000001, 4000001, 0, 'TENANT', NULL),
|
||||
(6000002, 4000002, 0, 'TENANT', NULL),
|
||||
(6000003, 4000003, 0, 'TENANT', NULL),
|
||||
(6000101, 4000101, 10001, 'TENANT', NULL),
|
||||
(6000102, 4000102, 10001, 'TENANT', NULL)
|
||||
(6000003, 4000003, 0, 'TENANT', NULL)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id), scope_type = VALUES(scope_type), scope_expression = NULL;
|
||||
|
||||
@@ -17,20 +17,11 @@ DEALLOCATE PREPARE member_no_index_stmt;
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name, sort_no, status, deleted)
|
||||
VALUES
|
||||
(5200023, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:edit', '编辑成员', 30, 'ACTIVE', 0),
|
||||
(5200024, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:status', '启停成员', 40, 'ACTIVE', 0),
|
||||
(5200025, 10001, 'MERCHANT', 'BUTTON', 5200020, 'merchant:member:reset-password', '重置成员密码', 50, 'ACTIVE', 0),
|
||||
(5300023, 0, 'MERCHANT', 'BUTTON', 5300020, 'merchant:member:edit', '编辑成员', 30, 'ACTIVE', 0),
|
||||
(5300024, 0, 'MERCHANT', 'BUTTON', 5300020, 'merchant:member:status', '启停成员', 40, 'ACTIVE', 0),
|
||||
(5300025, 0, 'MERCHANT', 'BUTTON', 5300020, 'merchant:member:reset-password', '重置成员密码', 50, 'ACTIVE', 0)
|
||||
ON DUPLICATE KEY UPDATE permission_name = VALUES(permission_name), status = VALUES(status), deleted = 0;
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000101, id, 10001 FROM iam_permission
|
||||
WHERE tenant_id = 10001 AND permission_code IN
|
||||
('merchant:member:edit', 'merchant:member:status', 'merchant:member:reset-password')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000100, id, 0 FROM iam_permission
|
||||
WHERE tenant_id = 0 AND permission_code IN
|
||||
|
||||
@@ -41,17 +41,34 @@ INSERT INTO iam_role (
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE status='ACTIVE', deleted=0;
|
||||
|
||||
-- Clone the一期 merchant menu/button template into the global scope.
|
||||
-- Global merchant permission template used by every formally onboarded customer.
|
||||
INSERT INTO iam_permission (
|
||||
id, tenant_id, app_code, permission_type, parent_id, permission_code,
|
||||
permission_name, route_path, http_method, sort_no, status, deleted
|
||||
)
|
||||
SELECT id + 100000, 0, app_code, permission_type,
|
||||
CASE WHEN parent_id = 0 THEN 0 ELSE parent_id + 100000 END,
|
||||
permission_code, permission_name, route_path, NULL, sort_no, 'ACTIVE', 0
|
||||
FROM iam_permission
|
||||
WHERE tenant_id = 10001 AND app_code = 'MERCHANT'
|
||||
AND permission_type IN ('MENU', 'BUTTON')
|
||||
VALUES
|
||||
(5300000, 0, 'MERCHANT', 'MENU', 0, 'merchant:enterprise', '企业管理', '/enterprise', NULL, 10, 'ACTIVE', 0),
|
||||
(5301000, 0, 'MERCHANT', 'MENU', 0, 'merchant:order', '头程订单', '/first-leg-orders', NULL, 20, 'ACTIVE', 0),
|
||||
(5302000, 0, 'MERCHANT', 'MENU', 0, 'merchant:master-data', '基础资料', '/master-data', NULL, 30, 'ACTIVE', 0),
|
||||
(5300010, 0, 'MERCHANT', 'MENU', 5300000, 'merchant:profile:view', '企业资料', '/enterprise/profile', NULL, 10, 'ACTIVE', 0),
|
||||
(5300020, 0, 'MERCHANT', 'MENU', 5300000, 'merchant:member:list', '企业成员', '/enterprise/members', NULL, 20, 'ACTIVE', 0),
|
||||
(5300030, 0, 'MERCHANT', 'MENU', 5300000, 'merchant:role:list', '角色管理', '/enterprise/roles', NULL, 30, 'ACTIVE', 0),
|
||||
(5301010, 0, 'MERCHANT', 'MENU', 5301000, 'merchant:order:list', '我的订单', '/first-leg-orders/list', NULL, 10, 'ACTIVE', 0),
|
||||
(5301020, 0, 'MERCHANT', 'MENU', 5301000, 'merchant:order:create-page', '新建订单', '/first-leg-orders/create', NULL, 20, 'ACTIVE', 0),
|
||||
(5302010, 0, 'MERCHANT', 'MENU', 5302000, 'merchant:warehouse:list', '收货仓选择', '/master-data/warehouses', NULL, 10, 'ACTIVE', 0),
|
||||
(5302020, 0, 'MERCHANT', 'MENU', 5302000, 'merchant:counterparty:list', '往来单位', '/master-data/counterparties', NULL, 20, 'ACTIVE', 0),
|
||||
(5300011, 0, 'MERCHANT', 'BUTTON', 5300010, 'merchant:profile:edit', '编辑企业资料', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5300012, 0, 'MERCHANT', 'BUTTON', 5300010, 'merchant:qualification:upload', '上传企业资质', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5300021, 0, 'MERCHANT', 'BUTTON', 5300020, 'merchant:member:create', '新增成员', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5300022, 0, 'MERCHANT', 'BUTTON', 5300020, 'merchant:member:grant-role', '分配成员角色', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5300031, 0, 'MERCHANT', 'BUTTON', 5300030, 'merchant:role:create', '新增角色', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5300032, 0, 'MERCHANT', 'BUTTON', 5300030, 'merchant:role:grant', '角色授权', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5301011, 0, 'MERCHANT', 'BUTTON', 5301010, 'merchant:order:view', '查看订单', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5301012, 0, 'MERCHANT', 'BUTTON', 5301010, 'merchant:order:cancel', '取消订单', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5301021, 0, 'MERCHANT', 'BUTTON', 5301020, 'merchant:order:create', '创建草稿', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5301022, 0, 'MERCHANT', 'BUTTON', 5301020, 'merchant:order:edit', '编辑草稿', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5301023, 0, 'MERCHANT', 'BUTTON', 5301020, 'merchant:order:submit', '提交订单', NULL, NULL, 30, 'ACTIVE', 0),
|
||||
(5301024, 0, 'MERCHANT', 'BUTTON', 5301020, 'merchant:order:quick-create', '快速下单', NULL, NULL, 40, 'ACTIVE', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_name=VALUES(permission_name), route_path=VALUES(route_path),
|
||||
parent_id=VALUES(parent_id), sort_no=VALUES(sort_no), status='ACTIVE', deleted=0;
|
||||
|
||||
@@ -19,9 +19,6 @@ VALUES
|
||||
(5101013, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:status', '启停用户', 30, 'ACTIVE', 0),
|
||||
(5101014, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:reset-password', '重置密码', 40, 'ACTIVE', 0),
|
||||
(5101015, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:grant-role', '分配内部用户角色', 50, 'ACTIVE', 0),
|
||||
(5200033, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:edit', '编辑角色', 30, 'ACTIVE', 0),
|
||||
(5200034, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:status', '启停角色', 40, 'ACTIVE', 0),
|
||||
(5200035, 10001, 'MERCHANT', 'BUTTON', 5200030, 'merchant:role:delete', '删除角色', 50, 'ACTIVE', 0),
|
||||
(5300033, 0, 'MERCHANT', 'BUTTON', 5300030, 'merchant:role:edit', '编辑角色', 30, 'ACTIVE', 0),
|
||||
(5300034, 0, 'MERCHANT', 'BUTTON', 5300030, 'merchant:role:status', '启停角色', 40, 'ACTIVE', 0),
|
||||
(5300035, 0, 'MERCHANT', 'BUTTON', 5300030, 'merchant:role:delete', '删除角色', 50, 'ACTIVE', 0)
|
||||
@@ -40,12 +37,6 @@ WHERE tenant_id = 0 AND app_code = 'PLATFORM'
|
||||
'platform:user:grant-role')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000101, id, 10001 FROM iam_permission
|
||||
WHERE tenant_id = 10001 AND app_code = 'MERCHANT'
|
||||
AND permission_code IN ('merchant:role:edit', 'merchant:role:status', 'merchant:role:delete')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000100, id, 0 FROM iam_permission
|
||||
WHERE tenant_id = 0 AND app_code = 'MERCHANT'
|
||||
|
||||
+5
-2
@@ -31,6 +31,9 @@ class AdminPrototypeMenuPermissionSqlContractTest {
|
||||
assertThat(sql).contains("(" + (5105050 + index) + ",0,'PLATFORM','BUTTON'," + (5105040 + index) + ",'" + FINANCE_EXPORTS[index] + "'");
|
||||
}
|
||||
assertThat(sql)
|
||||
.contains("(5305000,0,'MERCHANT','MENU',5300000,'merchant:finance:statement'")
|
||||
.contains("(5305010,0,'MERCHANT','BUTTON',5305000,'merchant:finance:statement:list'")
|
||||
.contains("(5305011,0,'MERCHANT','BUTTON',5305000,'merchant:finance:statement:view'")
|
||||
.contains("(5305012,0,'MERCHANT','BUTTON',5305000,'merchant:finance:statement:confirm'")
|
||||
.contains("r.tenant_id=0 AND r.app_code='MERCHANT' AND r.role_code='MERCHANT_OWNER'")
|
||||
.contains("'platform:finance:fee-source:review' code, 5105041 parent_id")
|
||||
@@ -60,8 +63,8 @@ class AdminPrototypeMenuPermissionSqlContractTest {
|
||||
.contains("permission_code LIKE 'platform:finance:%' AND status='ACTIVE' AND deleted=0) <> 32")
|
||||
.contains("(p.permission_code='platform:finance' OR p.permission_code LIKE 'platform:finance:%')) <> 33")
|
||||
.contains("Finance 导出和下载中心授权不完整")
|
||||
.contains("merchant:finance:statement:confirm")
|
||||
.contains("客户全局管理员缺少对账单确认权限")
|
||||
.contains("客户全局 Finance 权限模板不完整")
|
||||
.contains("客户全局管理员 Finance 模板授权不完整")
|
||||
.satisfies(content -> assertThat(content.indexOf("(5105050,0,'PLATFORM','BUTTON'"))
|
||||
.isLessThan(content.indexOf("CREATE PROCEDURE assert_finance_permissions()")));
|
||||
}
|
||||
|
||||
+25
-8
@@ -11,25 +11,42 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InitialIdentitySeedSqlContractTest {
|
||||
@Test
|
||||
void initializesPlatformAndItTestCustomerIdentitiesWithStableIds() throws Exception {
|
||||
assertIdentitySeed(new ClassPathResource("db/phase1-admin-rbac.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8));
|
||||
assertIdentitySeed(Files.readString(unifiedSql(), StandardCharsets.UTF_8));
|
||||
void initializesOnlyPlatformIdentityAndGlobalMerchantTemplates() throws Exception {
|
||||
String identitySeed = new ClassPathResource("db/phase1-admin-rbac.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
String templateSeed = new ClassPathResource("db/phase1-onboarding.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
String unified = Files.readString(unifiedSql(), StandardCharsets.UTF_8);
|
||||
assertPlatformIdentitySeed(identitySeed);
|
||||
assertPlatformIdentitySeed(unified);
|
||||
assertGlobalMerchantTemplate(templateSeed);
|
||||
assertGlobalMerchantTemplate(unified);
|
||||
}
|
||||
|
||||
private static void assertIdentitySeed(String sql) {
|
||||
private static void assertPlatformIdentitySeed(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("(0, 'PLATFORM', '虫虫出海平台'")
|
||||
.contains("(10001, 'IT_TEST', 'IT 测试客户'")
|
||||
.contains("(2000001, 1000001, 'INTERNAL', '平台系统管理员'")
|
||||
.contains("(2000002, 1000002, 'ENTERPRISE', 'IT 测试客户管理员'")
|
||||
.contains("(3000001, 0, 2000001, 'EMPLOYEE'")
|
||||
.contains("(3000002, 10001, 2000002, 'OWNER'")
|
||||
.contains("tenant_code = VALUES(tenant_code)")
|
||||
.doesNotContain("merchant_admin")
|
||||
.doesNotContain("IT_TEST")
|
||||
.doesNotContain("(10001,")
|
||||
.doesNotContain("(2000002,")
|
||||
.doesNotContain("(3000002,")
|
||||
.doesNotContain("4000101, 10001")
|
||||
.doesNotContain("PHASE1_DEMO")
|
||||
.doesNotContain("一期演示企业");
|
||||
}
|
||||
|
||||
private static void assertGlobalMerchantTemplate(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("4000100, 0, 'MERCHANT', 'MERCHANT_OWNER'")
|
||||
.contains("5300000, 0, 'MERCHANT', 'MENU'")
|
||||
.contains("5301024, 0, 'MERCHANT', 'BUTTON'")
|
||||
.doesNotContain("WHERE tenant_id = 10001 AND app_code = 'MERCHANT'");
|
||||
}
|
||||
|
||||
private static Path unifiedSql() {
|
||||
Path path = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
return Files.exists(path) ? path : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql");
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
-- Any returned row represents a failed phase-one seed assertion.
|
||||
SELECT 'missing_auth_accounts' AS failure
|
||||
WHERE (SELECT COUNT(*) FROM baiyue_auth.auth_account
|
||||
WHERE username IN ('platform_admin', 'merchant_admin') AND account_status = 'ACTIVE') <> 2
|
||||
WHERE username = 'platform_admin' AND account_status = 'ACTIVE') <> 1
|
||||
UNION ALL
|
||||
SELECT 'missing_active_identities'
|
||||
WHERE (SELECT COUNT(*) FROM baiyue_iam.iam_user u
|
||||
JOIN baiyue_iam.iam_membership m ON m.user_id = u.id
|
||||
WHERE u.auth_user_id IN (1000001, 1000002)
|
||||
AND u.status = 'ACTIVE' AND m.status = 'ACTIVE') <> 2
|
||||
WHERE u.auth_user_id = 1000001
|
||||
AND u.status = 'ACTIVE' AND m.status = 'ACTIVE') <> 1
|
||||
UNION ALL
|
||||
SELECT 'unexpected_fixed_customer_seed'
|
||||
WHERE EXISTS (SELECT 1 FROM baiyue_iam.iam_tenant WHERE tenant_code = 'IT_TEST')
|
||||
OR EXISTS (SELECT 1 FROM baiyue_auth.auth_account WHERE username = 'merchant_admin')
|
||||
UNION ALL
|
||||
SELECT 'unexpected_permission_type'
|
||||
WHERE EXISTS (SELECT 1 FROM baiyue_iam.iam_permission
|
||||
@@ -34,10 +38,6 @@ SELECT 'platform_admin_not_fully_granted'
|
||||
WHERE (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission WHERE role_id = 4000001)
|
||||
<> (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id = 0 AND app_code = 'PLATFORM' AND permission_type IN ('MENU', 'BUTTON'))
|
||||
UNION ALL
|
||||
SELECT 'merchant_owner_not_fully_granted'
|
||||
WHERE (SELECT COUNT(*) FROM baiyue_iam.iam_role_permission WHERE role_id = 4000101)
|
||||
<> (SELECT COUNT(*) FROM baiyue_iam.iam_permission WHERE tenant_id = 10001 AND app_code = 'MERCHANT' AND permission_type IN ('MENU', 'BUTTON'))
|
||||
UNION ALL
|
||||
SELECT 'missing_global_merchant_owner_template'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM baiyue_iam.iam_role
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ node scripts/create-pending-review-orders.mjs
|
||||
| Admin/Client 本地测试与类型检查 | `scripts/frontend-full-regression.ps1` |
|
||||
| PDA 本地页面和状态测试 | `../apps/pda-uniapp/tests/*.test.mjs` |
|
||||
| 本地数据库 SQL 静态/受控验收 | `scripts/local-db-full-acceptance.ps1` |
|
||||
| AGENTS 快速索引一致性 | `scripts/agents-index-contract.ps1`;只读核对根快速表唯一性、部署单元、业务模块、Client active 路由、海外 Gateway 覆盖、PDA 注册页面、Maven 模块、Lastleg 海外安全投影映射、关键入口与排除目录 |
|
||||
| AGENTS 快速索引一致性 | `scripts/agents-index-contract.ps1`;只读核对根快速表唯一性、部署单元、业务模块、Client active 路由、四期范围漂移、订舱候选与柜量语义、海外 Gateway 覆盖、PDA 页面/运行配置/能力开关、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口与排除目录 |
|
||||
| Java 质量聚合摘要 | `node --test scripts/java-quality-summary.test.mjs`、`node scripts/java-quality-summary.mjs --output target/java-quality-summary.json`;聚合 Maven/服务拓扑、OpenAPI 覆盖、JaCoCo 和 SpotBugs 报告,默认只报告存量缺口,CI 用显式 `--require-jacoco --require-spotbugs` 检查报告完整性且当前允许失败 |
|
||||
| 单服务部署失败回滚合同 | `scripts/deploy-built-service-rollback.test.mjs`;模拟成功、健康失败和启动失败,确认仅恢复同服务先前健康镜像且部署任务仍失败 |
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import process from 'node:process'
|
||||
const CONFIGURED_MODULES = ['merchant-qualification-fixture', 'customers', 'overseas-storage-fees', 'system', 'notifications', 'master-data', 'suppliers', 'domestic-container', 'orders', 'fee-items', 'pricing', 'downloads', 'inbound-weight-ocr', 'inbound-measurement', 'inbound-fees', 'phase3-container-loading', 'phase3-container-overview', 'phase3-loading', 'phase3-loading-model-bridge', 'phase3-pda-convergence', 'phase3-evidence-gate', 'supplemental', 'supplemental-boundaries', 'ocean-bookings', 'truck-appointments', 'service-center-flow', 'phase3-loading-finalize', 'phase3-customs', 'phase3-port-schedules', 'phase3-carrier-schedule-queries', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate', 'phase3-dispatch-gate', 'phase3-waiting-for-loading', 'phase3-warehouse-rbac', 'phase3-order-splitting', 'manual-state-matrix', 'phase4-overseas-warehouse', 'phase4-carrier-pickup-ready', 'phase4-overseas-pda', 'phase4-overseas-pda-operations', 'phase4-overseas-inbound-operations', 'phase4-overseas-client-services', 'phase4-overseas-dispatch', 'phase4-cross-region-events', 'phase4-overseas-business', 'phase4-overseas-master-data', 'phase4-carrier-nodes', 'finance', 'finance-full-fixtures', 'finance-reconciliation-full', 'finance-settlement-full', 'finance-downloads', 'customer-operating-statistics']
|
||||
|
||||
const MODULE_DEPENDENCIES = new Map([
|
||||
['suppliers', ['merchant-qualification-fixture']],
|
||||
['supplemental', ['merchant-qualification-fixture']],
|
||||
['orders', ['merchant-qualification-fixture']],
|
||||
['inbound-measurement', ['orders']],
|
||||
['phase3-container-loading', ['inbound-measurement']],
|
||||
@@ -232,11 +234,20 @@ function usage() {
|
||||
--username <name> 必填,管理员账号
|
||||
--password <value> 必填,管理员密码
|
||||
--modules <names> 逗号分隔,默认 ${DEFAULT_MODULES.join(',')}
|
||||
--customer-name <name> 指定本轮隔离客户名称,默认自动生成
|
||||
--merchant-username <name> 复用既有已审核客户账号,不再创建客户
|
||||
--merchant-password <value> 复用客户账号密码,不写入报告
|
||||
--origin-warehouse-name <name> 固定国内起运仓名称
|
||||
--channel-name <name> 固定物流渠道名称
|
||||
--channel-code <code> 固定物流渠道编码
|
||||
--order-profile-mode <mode> 订单档案模式:fixed 或 randomized,默认fixed
|
||||
--wait-split-count <n> 同一客户创建的 WAIT_SPLIT 订单数,1-100,默认1
|
||||
--output <path> JSON 报告路径,默认 reports/
|
||||
--help 显示帮助
|
||||
|
||||
也可使用环境变量: ADMIN_TEST_GATEWAY, ADMIN_OVERSEAS_GATEWAY, ADMIN_TEST_USERNAME, ADMIN_TEST_PASSWORD,
|
||||
ADMIN_TEST_WEIGHT_IMAGE_PATH`)
|
||||
ADMIN_TEST_WEIGHT_IMAGE_PATH, ADMIN_TEST_CUSTOMER_NAME, MERCHANT_TEST_USERNAME,
|
||||
MERCHANT_TEST_PASSWORD, ADMIN_WAIT_SPLIT_COUNT`)
|
||||
}
|
||||
|
||||
export function parseArguments(argv) {
|
||||
@@ -255,12 +266,27 @@ export function parseArguments(argv) {
|
||||
values.set(name, next); index += 1
|
||||
}
|
||||
}
|
||||
const merchantUsername = values.get('merchant-username') || process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const merchantPassword = values.get('merchant-password') || process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
const modules = normalizeModuleOrder((values.get('modules') || DEFAULT_MODULES.join(',')).split(',').map((v) => v.trim()).filter(Boolean))
|
||||
if (merchantUsername && merchantPassword) {
|
||||
const fixtureIndex = modules.indexOf('merchant-qualification-fixture')
|
||||
if (fixtureIndex >= 0) modules.splice(fixtureIndex, 1)
|
||||
}
|
||||
return {
|
||||
gateway: (values.get('gateway') || process.env.ADMIN_TEST_GATEWAY || '').replace(/\/$/, ''),
|
||||
overseasGateway: (values.get('overseas-gateway') || process.env.ADMIN_OVERSEAS_GATEWAY || '').replace(/\/$/, ''),
|
||||
username: values.get('username') || process.env.ADMIN_TEST_USERNAME || '',
|
||||
password: values.get('password') || process.env.ADMIN_TEST_PASSWORD || '',
|
||||
modules: normalizeModuleOrder((values.get('modules') || DEFAULT_MODULES.join(',')).split(',').map((v) => v.trim()).filter(Boolean)),
|
||||
modules,
|
||||
customerName: values.get('customer-name') || process.env.ADMIN_TEST_CUSTOMER_NAME || '',
|
||||
merchantUsername,
|
||||
merchantPassword,
|
||||
originWarehouseName: values.get('origin-warehouse-name') || '',
|
||||
channelName: values.get('channel-name') || '',
|
||||
channelCode: values.get('channel-code') || '',
|
||||
orderProfileMode: values.get('order-profile-mode') || 'fixed',
|
||||
waitSplitCount: Number(values.get('wait-split-count') || process.env.ADMIN_WAIT_SPLIT_COUNT || 1),
|
||||
output: values.get('output') || '',
|
||||
}
|
||||
}
|
||||
@@ -273,6 +299,32 @@ export function validateExecutionOptions(options) {
|
||||
&& (!options.overseasGateway || options.overseasGateway.trim() === '')) {
|
||||
throw new Error('四期海外模块缺少必填执行参数: overseas-gateway')
|
||||
}
|
||||
if (!Number.isInteger(options.waitSplitCount) || options.waitSplitCount < 1 || options.waitSplitCount > 100) {
|
||||
throw new Error('wait-split-count必须是1-100的整数')
|
||||
}
|
||||
if (options.customerName && options.customerName.trim().length > 128) {
|
||||
throw new Error('customer-name不能超过128个字符')
|
||||
}
|
||||
if (Boolean(options.merchantUsername) !== Boolean(options.merchantPassword)) {
|
||||
throw new Error('merchant-username和merchant-password必须同时提供')
|
||||
}
|
||||
for (const [name, value] of [['origin-warehouse-name', options.originWarehouseName],
|
||||
['channel-name', options.channelName], ['channel-code', options.channelCode]]) {
|
||||
if (value && value.trim().length > 128) throw new Error(`${name}不能超过128个字符`)
|
||||
}
|
||||
if (!['fixed', 'randomized'].includes(options.orderProfileMode)) {
|
||||
throw new Error('order-profile-mode必须为fixed或randomized')
|
||||
}
|
||||
if (options.orderProfileMode === 'randomized'
|
||||
&& (!options.originWarehouseName || !options.channelName || !options.channelCode)) {
|
||||
throw new Error('randomized订单档案必须同时固定起运仓、渠道名称和渠道编码')
|
||||
}
|
||||
if (options.waitSplitCount > 1) {
|
||||
const fixtureModules = new Set(['merchant-qualification-fixture', 'orders', 'inbound-measurement'])
|
||||
if (options.modules.some((module) => !fixtureModules.has(module))) {
|
||||
throw new Error('批量WAIT_SPLIT只允许执行merchant-qualification-fixture、orders、inbound-measurement')
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -555,6 +607,124 @@ export class AcceptanceRun {
|
||||
this.modules.push({ name, startedAt, finishedAt: new Date().toISOString(), success: fatal === null && steps.every((s) => s.passed), passed: steps.filter((s) => s.passed).length, failed: steps.filter((s) => !s.passed).length, fatal })
|
||||
}
|
||||
|
||||
captureWaitSplitFixture(sequence) {
|
||||
const order = this.createdResources.orders || {}
|
||||
const measurement = this.createdResources.inboundMeasurement || {}
|
||||
const ready = Boolean(order.orderId && measurement.inboundId
|
||||
&& String(order.tenantId) === String(measurement.tenantId)
|
||||
&& measurement.processStatus === 'WAIT_SPLIT' && measurement.finalStatus === 'APPROVED')
|
||||
this.assert('wait-split-fixtures', `固定客户第 ${sequence} 单停在待分柜`, null, ready,
|
||||
`tenantId=${measurement.tenantId || 'missing'}, orderId=${order.orderId || 'missing'}, inboundId=${measurement.inboundId || 'missing'}, processStatus=${measurement.processStatus || 'missing'}, reviewStatus=${measurement.finalStatus || 'missing'}`,
|
||||
{ scenario: 'fixed-customer-wait-split', deploymentBlocker: true })
|
||||
if (!ready) return false
|
||||
this.createdResources.waitSplitFixtures ??= []
|
||||
this.createdResources.waitSplitFixtures.push({
|
||||
sequence, tenantId: String(measurement.tenantId), orderId: String(order.orderId),
|
||||
orderNo: order.orderNo || null, inboundId: String(measurement.inboundId),
|
||||
inboundCode: order.inboundCode || null, processStatus: measurement.processStatus,
|
||||
reviewStatus: measurement.finalStatus, skuCount: order.skuLines?.length || null,
|
||||
totalPackageCount: (order.skuLines || []).reduce((sum, item) => sum + Number(item.packageCount || 0), 0),
|
||||
palletCount: measurement.palletCount || null,
|
||||
actualGrossWeightKg: measurement.actualGrossWeightKg || null,
|
||||
actualNetWeightKg: measurement.actualNetWeightKg || null,
|
||||
measurementProfile: this.context.waitSplitVariant?.measurementProfile || null,
|
||||
receivingWarehouseId: order.receivingWarehouseId || null,
|
||||
receivingWarehouseName: order.receivingWarehouseName || null,
|
||||
destinationWarehouseId: order.destinationWarehouseId || null,
|
||||
destinationWarehouseCode: order.destinationWarehouseCode || null,
|
||||
destinationWarehouseName: order.destinationWarehouseName || null,
|
||||
channelCode: order.channelCode || null,
|
||||
channelName: order.channelName || null,
|
||||
pricingTaxMode: order.pricingTaxMode || null,
|
||||
destinationType: order.destinationType || null,
|
||||
fulfillmentType: order.fulfillmentType || null,
|
||||
pickupMethod: order.pickupMethod || null,
|
||||
taxMode: order.taxMode || null,
|
||||
containerMode: order.containerMode || null,
|
||||
customsInspection: order.customsInspection ?? null,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
waitSplitVariant(sequence) {
|
||||
const dimensionProfiles = [
|
||||
{ code: 'SMALL', first: { length: 29.1, width: 19.1, height: 18.1 }, replacement: { length: 29.5, width: 19.5, height: 18.5 }, outer: { length: 34, width: 24, height: 24 } },
|
||||
{ code: 'MEDIUM', first: { length: 39.1, width: 29.1, height: 28.1 }, replacement: { length: 39.5, width: 29.5, height: 28.5 }, outer: { length: 44, width: 34, height: 34 } },
|
||||
{ code: 'LARGE', first: { length: 49.1, width: 39.1, height: 38.1 }, replacement: { length: 49.5, width: 39.5, height: 38.5 }, outer: { length: 54, width: 44, height: 44 } },
|
||||
{ code: 'EXTRA_LARGE', first: { length: 59.1, width: 49.1, height: 48.1 }, replacement: { length: 59.5, width: 49.5, height: 48.5 }, outer: { length: 64, width: 54, height: 54 } },
|
||||
]
|
||||
const skuCount = 1 + ((sequence - 1) % 4)
|
||||
const packageCountPerSku = 1 + (Math.floor((sequence - 1) / 4) % 5)
|
||||
const palletCount = 1 + ((sequence - 1) % 3)
|
||||
const grossWeightPerPalletKg = 45 + ((sequence - 1) % 6) * 15
|
||||
return {
|
||||
sequence, skuCount, packageCountPerSku, palletCount, grossWeightPerPalletKg,
|
||||
measurementProfile: dimensionProfiles[(sequence - 1) % dimensionProfiles.length],
|
||||
}
|
||||
}
|
||||
|
||||
randomizedOrderProfile(sequence) {
|
||||
const combinations = [
|
||||
['FBA', 'STANDARD'],
|
||||
['OVERSEAS', 'STANDARD'],
|
||||
['PRIVATE', 'STANDARD'],
|
||||
['FBA', 'ONE_CLICK_DROPSHIP'],
|
||||
['OVERSEAS', 'ONE_CLICK_DROPSHIP'],
|
||||
['PRIVATE', 'ONE_CLICK_DROPSHIP'],
|
||||
]
|
||||
const [destinationType, fulfillmentType] = combinations[(sequence * 5 + 1) % combinations.length]
|
||||
const pickupMethods = ['PICKUP', 'SELF_EXPRESS', 'SELF_TRUCK']
|
||||
return {
|
||||
destinationType,
|
||||
fulfillmentType,
|
||||
pickupMethod: pickupMethods[(sequence * 7 + 2) % pickupMethods.length],
|
||||
taxMode: sequence % 2 === 0 ? 'TAX_REFUND' : 'BUY_DECLARATION',
|
||||
containerMode: sequence % 2 === 0 ? 'FULL_CONTAINER' : 'LCL',
|
||||
customsInspection: sequence % 4 === 0,
|
||||
}
|
||||
}
|
||||
|
||||
applyWaitSplitVariant(sequence) {
|
||||
const variant = this.waitSplitVariant(sequence)
|
||||
this.context.waitSplitVariant = variant
|
||||
this.context.phase3LoadingIncrementalDetailCount = variant.skuCount
|
||||
this.context.phase3LoadingIncrementalPackageCount = variant.packageCountPerSku
|
||||
const profile = this.options.orderProfileMode === 'randomized'
|
||||
? this.randomizedOrderProfile(sequence)
|
||||
: { destinationType: 'FBA', fulfillmentType: 'STANDARD', pickupMethod: 'PICKUP',
|
||||
taxMode: 'BUY_DECLARATION', containerMode: 'LCL', customsInspection: false }
|
||||
this.context.orderProfile = profile
|
||||
this.context.orderDestinationType = profile.destinationType
|
||||
this.context.orderFulfillmentType = profile.fulfillmentType
|
||||
this.context.waitSplitRoute = {
|
||||
originWarehouseName: this.options.originWarehouseName || '',
|
||||
channelName: this.options.channelName || '',
|
||||
channelCode: this.options.channelCode || '',
|
||||
destinationIndex: (sequence * 17 + 11),
|
||||
taxModeIndex: sequence - 1,
|
||||
}
|
||||
}
|
||||
|
||||
async createAdditionalWaitSplitFixtures() {
|
||||
const total = this.options.waitSplitCount
|
||||
if (total <= 1) return
|
||||
const baseMarker = this.marker
|
||||
const orders = await import(new URL('./admin-test-env-modules/orders.mjs', import.meta.url))
|
||||
const measurement = await import(new URL('./admin-test-env-modules/inbound-measurement.mjs', import.meta.url))
|
||||
try {
|
||||
for (let sequence = 2; sequence <= total; sequence += 1) {
|
||||
this.marker = `${baseMarker}-WS${String(sequence).padStart(2, '0')}`
|
||||
this.applyWaitSplitVariant(sequence)
|
||||
await this.executeModule('orders', () => orders.run(this))
|
||||
if (this.modules.at(-1)?.success !== true) break
|
||||
await this.executeModule('inbound-measurement', () => measurement.run(this))
|
||||
if (this.modules.at(-1)?.success !== true || !this.captureWaitSplitFixture(sequence)) break
|
||||
}
|
||||
} finally {
|
||||
this.marker = baseMarker
|
||||
}
|
||||
}
|
||||
|
||||
async refreshSession(reason) {
|
||||
const response = await this.request('POST', '/api/auth/refresh',
|
||||
{ refreshToken: this.refreshToken }, '')
|
||||
@@ -1184,6 +1354,11 @@ export class AcceptanceRun {
|
||||
}
|
||||
|
||||
async run() {
|
||||
if (this.options.merchantUsername && this.options.merchantPassword) {
|
||||
process.env.MERCHANT_TEST_USERNAME = this.options.merchantUsername
|
||||
process.env.MERCHANT_TEST_PASSWORD = this.options.merchantPassword
|
||||
}
|
||||
if (this.options.waitSplitCount > 1) this.applyWaitSplitVariant(1)
|
||||
const loginResponse = await this.request('POST', '/api/auth/login', { username: this.options.username, password: this.options.password }, '')
|
||||
const login = this.apiSuccess('authentication', '平台管理员登录', loginResponse)
|
||||
if (!login?.accessToken) this.fatal = '平台管理员登录失败,未执行业务模块'
|
||||
@@ -1270,6 +1445,9 @@ export class AcceptanceRun {
|
||||
} finally {
|
||||
if (restoreFixture) restoreFixture()
|
||||
}
|
||||
if (module === 'inbound-measurement' && this.captureWaitSplitFixture(1)) {
|
||||
await this.createAdditionalWaitSplitFixtures()
|
||||
}
|
||||
const laterOverseasModule = this.options.modules.slice(moduleIndex + 1)
|
||||
.some((name) => name.startsWith('phase4-overseas-'))
|
||||
if (module === 'system' && laterOverseasModule) {
|
||||
@@ -1290,7 +1468,8 @@ export class AcceptanceRun {
|
||||
deploymentBlockers: this.deploymentBlockers, coverageGaps: this.coverageGaps,
|
||||
createdResources: this.createdResources,
|
||||
})
|
||||
const forbiddenValues = [this.options.password, this.token, this.refreshToken, this.overseasToken]
|
||||
const forbiddenValues = [this.options.password, this.options.merchantPassword,
|
||||
this.token, this.refreshToken, this.overseasToken]
|
||||
.filter((value) => typeof value === 'string' && value.length >= 8)
|
||||
const sensitivePatterns = [
|
||||
/"(?:password|newPassword|temporaryPassword|accessToken|refreshToken|authorization)"\s*:/i,
|
||||
|
||||
@@ -5,8 +5,8 @@ const BASE = '/api/platform/v1/fee-items'
|
||||
const BASELINE_COUNT = 111
|
||||
const DOMESTIC_COUNT = 84
|
||||
const INTERNATIONAL_COUNT = 27
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function records(value) { return Array.isArray(value?.records) ? value.records : [] }
|
||||
|
||||
@@ -7,10 +7,12 @@ import { fileURLToPath } from 'node:url'
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const source = fs.readFileSync(path.join(here, 'finance-demo-fixtures.mjs'), 'utf8')
|
||||
|
||||
test('演示数据只消费本轮固定 marker 和客户 10001', () => {
|
||||
test('演示数据只消费本轮固定 marker 和动态正式客户 fixture', () => {
|
||||
assert.match(source, /const MARKER = 'FIN-DEMO-20260812'/)
|
||||
assert.match(source, /fixture\.marker !== runMarker/)
|
||||
assert.match(source, /fixture\.customerId !== '10001'/)
|
||||
assert.match(source, /customerId !== id\(qualificationFixture\?\.tenantId\)/)
|
||||
assert.match(source, /context\.context\.merchantQualificationFixture/)
|
||||
assert.doesNotMatch(source, /customerId:\s*['"]?10001|accountId=10001/)
|
||||
assert.match(source, /const runMarker = `\$\{MARKER\}-/)
|
||||
assert.match(source, /禁止借历史来源/)
|
||||
assert.doesNotMatch(source, /DELETE|cleanup|remove/i)
|
||||
@@ -41,8 +43,8 @@ test('所有财务写操作使用 marker 幂等键并上传真实凭证', () =>
|
||||
})
|
||||
|
||||
test('额外应付审核路径先追加 feeId 再追加 accountId 查询参数', () => {
|
||||
assert.match(source, /`\$\{basePath\}\/\$\{created\.id\}\/approve\?accountId=10001`/)
|
||||
assert.doesNotMatch(source, /fee-details\?accountId=10001\/\$\{created\.id\}/)
|
||||
assert.match(source, /`\$\{basePath\}\/\$\{created\.id\}\/approve\?accountId=\$\{encodeURIComponent\(customerId\)\}`/)
|
||||
assert.doesNotMatch(source, /fee-details\?accountId=.*\/\$\{created\.id\}/)
|
||||
})
|
||||
|
||||
test('费用来源使用 operationVersion,应付供应商长整型保持字符串精度', () => {
|
||||
|
||||
@@ -30,10 +30,10 @@ function blocker(context, name, response, evidence, category) {
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForSources(context, sourceFeeIds) {
|
||||
async function waitForSources(context, customerId, sourceFeeIds) {
|
||||
let pool = []
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
const response = await context.request('GET', `${BASE}/fee-pool?customerId=10001&direction=ALL`)
|
||||
const response = await context.request('GET', `${BASE}/fee-pool?customerId=${encodeURIComponent(customerId)}&direction=ALL`)
|
||||
pool = rows(data(response)).concat(rows(data(response)?.items))
|
||||
const visible = new Set(pool.map((item) => id(item.sourceId)))
|
||||
if (sourceFeeIds.every((sourceId) => visible.has(sourceId))) return pool
|
||||
@@ -80,10 +80,11 @@ async function createExtraPayable(context, fixture, index) {
|
||||
unitPrice: 90 + index * 10, amount: 90 + index * 10, currency: 'CNY',
|
||||
supplierId, remark: `${MARKER}-AP-${index}` }
|
||||
const basePath = `/api/platform/v1/inbounds/${inboundId}/fee-details`
|
||||
const createdResponse = await write(context, 'POST', `${basePath}?accountId=10001`, body, `extra-payable-${index}`)
|
||||
const customerId = id(fixture.customerId)
|
||||
const createdResponse = await write(context, 'POST', `${basePath}?accountId=${encodeURIComponent(customerId)}`, body, `extra-payable-${index}`)
|
||||
const created = data(createdResponse)
|
||||
if (!ok(createdResponse) || !created?.id) return null
|
||||
const approveResponse = await write(context, 'POST', `${basePath}/${created.id}/approve?accountId=10001`, undefined, `extra-payable-${index}-approve`)
|
||||
const approveResponse = await write(context, 'POST', `${basePath}/${created.id}/approve?accountId=${encodeURIComponent(customerId)}`, undefined, `extra-payable-${index}-approve`)
|
||||
record(context, `创建第 ${index} 条额外应付来源`, approveResponse, ok(approveResponse),
|
||||
`sourceFeeId=${created.id}, inboundId=${inboundId}, supplierId=${supplierId}`)
|
||||
return { sourceFeeId: id(created.id), direction: 'PAYABLE', businessDate: body.businessDate,
|
||||
@@ -91,7 +92,7 @@ async function createExtraPayable(context, fixture, index) {
|
||||
}
|
||||
|
||||
async function createStatement(context, source, role, targetStatus) {
|
||||
const body = { customerId: '10001', currency: 'CNY', generationMode: 'SELECTED',
|
||||
const body = { customerId: id(source.customerId), currency: 'CNY', generationMode: 'SELECTED',
|
||||
feeSourceIds: [source.feeSourceId], periodStart: source.businessDate, periodEnd: source.businessDate,
|
||||
remark: `${MARKER}-${role}` }
|
||||
const response = await write(context, 'POST', `${BASE}/statements`, body, `statement-${role}-create`)
|
||||
@@ -120,7 +121,7 @@ async function createAr(context, statement, role) {
|
||||
|
||||
async function receive(context, fixture, settlement, amount, role) {
|
||||
const response = await write(context, 'POST', `${BASE}/receipts`, {
|
||||
customerId: 10001, receivedDate: new Date().toISOString().slice(0, 10), amount, currency: 'CNY',
|
||||
customerId: id(fixture.customerId), receivedDate: new Date().toISOString().slice(0, 10), amount, currency: 'CNY',
|
||||
paymentMethod: 'BANK_TRANSFER', voucherFileIds: [fixture.voucherFileId], remark: `${MARKER}-${role}`,
|
||||
allocations: [{ billId: Number(settlement.reconciliationStatementId), amount }],
|
||||
}, `receipt-${role}`)
|
||||
@@ -130,7 +131,7 @@ async function receive(context, fixture, settlement, amount, role) {
|
||||
|
||||
async function createAp(context, fixture, source, role) {
|
||||
const response = await write(context, 'POST', `${BASE}/payable-settlements`, {
|
||||
customerId: 10001, supplierId: id(source.supplierId), feeSourceIds: [Number(source.feeSourceId)],
|
||||
customerId: id(fixture.customerId), supplierId: id(source.supplierId), feeSourceIds: [Number(source.feeSourceId)],
|
||||
dueDate: new Date().toISOString().slice(0, 10), currency: 'CNY',
|
||||
}, `ap-${role}-${source.feeSourceId}-${source.supplierId}-create`)
|
||||
const value = data(response)
|
||||
@@ -165,8 +166,9 @@ async function recoverExistingDemo(context, reportPath) {
|
||||
const report = JSON.parse(await readFile(reportPath, 'utf8'))
|
||||
const fixture = report?.createdResources?.financeFullFixtures
|
||||
const matrix = structuredClone(report?.createdResources?.financeDemo ?? {})
|
||||
if (fixture?.marker !== matrix.runMarker || matrix.marker !== MARKER || matrix.customerId !== '10001') {
|
||||
throw new Error('恢复报告不是 FIN-DEMO-20260812 / 客户 10001 的同一轮 fixture')
|
||||
if (fixture?.marker !== matrix.runMarker || matrix.marker !== MARKER
|
||||
|| !id(matrix.customerId) || id(matrix.customerId) !== id(fixture.customerId)) {
|
||||
throw new Error('恢复报告不是 FIN-DEMO-20260812 同一轮动态客户 fixture')
|
||||
}
|
||||
context.marker = matrix.runMarker
|
||||
await verifyUnfilteredLists(context)
|
||||
@@ -189,7 +191,7 @@ async function recoverExistingDemo(context, reportPath) {
|
||||
const role = roles[index]
|
||||
const detail = await sourceDetail(context, payableIds[index])
|
||||
const source = detail.value
|
||||
if (!ok(detail.response) || source?.direction !== 'PAYABLE' || source?.customerId !== '10001'
|
||||
if (!ok(detail.response) || source?.direction !== 'PAYABLE' || id(source?.customerId) !== id(matrix.customerId)
|
||||
|| !id(source?.sourceFeeId)) {
|
||||
blocker(context, `${role} 恢复来源属于本轮 fixture`, detail.response,
|
||||
`feeSourceId=${payableIds[index]}, sourceFeeId=${source?.sourceFeeId ?? 'missing'}`,
|
||||
@@ -276,9 +278,12 @@ export async function run(context) {
|
||||
const producer = await import('./finance-full-fixtures.mjs')
|
||||
await producer.run(context)
|
||||
const fixture = context.createdResources.financeFullFixtures
|
||||
if (!fixture || fixture.marker !== runMarker || fixture.customerId !== '10001') {
|
||||
const qualificationFixture = context.context.merchantQualificationFixture
|
||||
const customerId = id(fixture?.customerId)
|
||||
if (!fixture || fixture.marker !== runMarker || !customerId
|
||||
|| customerId !== id(qualificationFixture?.tenantId)) {
|
||||
blocker(context, '取得演示企业真实 Finance fixture', null,
|
||||
'finance-full-fixtures 未返回 marker=FIN-DEMO-20260812、customerId=10001 的独立资源,禁止借历史来源',
|
||||
'finance-full-fixtures 未返回与 merchant-qualification-fixture 同客户的本轮独立资源,禁止借历史来源',
|
||||
'missing-finance-demo-fixture')
|
||||
return
|
||||
}
|
||||
@@ -289,10 +294,10 @@ export async function run(context) {
|
||||
if (value) extraPayables.push(value)
|
||||
}
|
||||
const produced = [...fixture.feeSources.map((item) => ({ ...item, sourceFeeId: id(item.feeSourceId) })), ...extraPayables]
|
||||
const pool = await waitForSources(context, produced.map((item) => item.sourceFeeId).filter(Boolean))
|
||||
const pool = await waitForSources(context, customerId, produced.map((item) => item.sourceFeeId).filter(Boolean))
|
||||
const poolBySource = new Map(pool.map((item) => [id(item.sourceId), item]))
|
||||
const sources = produced.map((source) => ({ ...source,
|
||||
feeSourceId: financeId(poolBySource.get(source.sourceFeeId)), marker: runMarker,
|
||||
feeSourceId: financeId(poolBySource.get(source.sourceFeeId)), marker: runMarker, customerId,
|
||||
supplierId: id(source.supplierId ?? fixture.supplier?.supplierId) }))
|
||||
.filter((source) => source.feeSourceId)
|
||||
const receivables = sources.filter((source) => source.direction === 'RECEIVABLE')
|
||||
@@ -304,7 +309,8 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
|
||||
const matrix = { marker: MARKER, runMarker, customerId: '10001', customerLabel: '一期演示企业 10001',
|
||||
const matrix = { marker: MARKER, runMarker, customerId,
|
||||
customerLabel: qualificationFixture?.tenantName || `动态验收客户 ${customerId}`,
|
||||
feeSources: {}, statements: {}, receivableSettlements: {}, payableSettlements: {},
|
||||
voucherFileId: id(fixture.voucherFileId), container: fixture.container, allocations: fixture.allocations }
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ test('measurement submission confirms weight evidence before submit', () => {
|
||||
})
|
||||
|
||||
test('measurement fixture derives pallet resources from confirmed image count', () => {
|
||||
assert.match(source, /palletCount: WEIGHING_IMAGE_COUNT/)
|
||||
assert.match(source, /waitSplitVariant\?\.palletCount \|\| 1/)
|
||||
assert.match(source, /palletIds: warehouseResourcesReady\.palletIds/)
|
||||
assert.match(source, /confirmedRows\.length === palletIds\.length/)
|
||||
assert.doesNotMatch(source, /Math\.ceil\(Number\(totalVolumeCbm\)/)
|
||||
assert.doesNotMatch(source, /Math\.ceil\(totalVolume \/ DEFAULT_PALLET_CAPACITY_CBM\)/)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const MODULE = 'inbound-measurement'
|
||||
const SCENARIO = 'mojia-carton-measurement'
|
||||
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
|
||||
const WEIGHING_IMAGE_COUNT = 1
|
||||
const FIXTURE_PALLET_CAPACITY_CBM = 1.2
|
||||
const WEIGHT_IMAGE = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82,
|
||||
0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84,
|
||||
@@ -44,16 +43,17 @@ async function provisionWarehouseResources(context, { inboundId, warehouseId, pa
|
||||
if (!warehouseId || !Number.isInteger(palletCount) || palletCount < 1) return null
|
||||
|
||||
const suffix = `${String(context.marker).replace(/\D/g, '').slice(-8)}${String(inboundId).slice(-6)}`
|
||||
const palletResponse = await context.request('POST', '/api/platform/v1/pallets?tenantId=0', {
|
||||
warehouseId, palletType: 'STANDARD', capacityCbm: FIXTURE_PALLET_CAPACITY_CBM,
|
||||
weightKg: 8, color: '蓝色', remark: `${context.marker}-INBOUND-${inboundId}`,
|
||||
})
|
||||
const palletId = asId(context.apiSuccess(MODULE, '创建称重托盘主数据', palletResponse,
|
||||
`warehouseId=${warehouseId}`, OPTIONS))
|
||||
if (!palletId) return null
|
||||
|
||||
const resources = []
|
||||
const palletIds = []
|
||||
for (let index = 1; index <= palletCount; index += 1) {
|
||||
const palletResponse = await context.request('POST', '/api/platform/v1/pallets?tenantId=0', {
|
||||
warehouseId, palletType: 'STANDARD', capacityCbm: FIXTURE_PALLET_CAPACITY_CBM,
|
||||
weightKg: 8, color: '蓝色', remark: `${context.marker}-INBOUND-${inboundId}-PALLET-${index}`,
|
||||
})
|
||||
const palletId = asId(context.apiSuccess(MODULE, `创建称重托盘主数据 ${index}/${palletCount}`,
|
||||
palletResponse, `warehouseId=${warehouseId}`, OPTIONS))
|
||||
if (!palletId) return null
|
||||
palletIds.push(palletId)
|
||||
const resourceSuffix = `${suffix}${String(index).padStart(2, '0')}`
|
||||
let locationId = null
|
||||
if (context.context?.phase3LoadingRequireSharedOverflow !== true) {
|
||||
@@ -75,54 +75,65 @@ async function provisionWarehouseResources(context, { inboundId, warehouseId, pa
|
||||
marker: context.marker, tenantId: '0', warehouseId: asId(warehouseId), palletCount,
|
||||
palletCapacityCbm: FIXTURE_PALLET_CAPACITY_CBM, resources,
|
||||
}
|
||||
return { palletId, palletCount, resources }
|
||||
return { palletId: palletIds[0], palletIds, palletCount, resources }
|
||||
}
|
||||
|
||||
async function createConfirmedWeightEvidence(context, { inboundId, tenantId, palletId }) {
|
||||
const uploadResponse = await context.requestMultipart('POST',
|
||||
`/api/platform/v1/files/measurements/${tenantId}/${inboundId}`, {
|
||||
content: WEIGHT_IMAGE, type: 'image/png', name: `weight-${inboundId}.png`,
|
||||
})
|
||||
const upload = context.apiSuccess(MODULE, '上传称重图片并执行 OCR', uploadResponse,
|
||||
`inboundId=${inboundId}`, OPTIONS)
|
||||
const fileId = asId(upload?.fileId)
|
||||
if (!fileId) return null
|
||||
async function createConfirmedWeightEvidence(context, {
|
||||
inboundId, tenantId, palletIds, grossWeightPerPalletKg,
|
||||
}) {
|
||||
if (!Array.isArray(palletIds) || palletIds.length < 1) return null
|
||||
const confirmedResults = []
|
||||
for (let index = 0; index < palletIds.length; index += 1) {
|
||||
const palletId = palletIds[index]
|
||||
const uploadResponse = await context.requestMultipart('POST',
|
||||
`/api/platform/v1/files/measurements/${tenantId}/${inboundId}`, {
|
||||
content: WEIGHT_IMAGE, type: 'image/png', name: `weight-${inboundId}-${index + 1}.png`,
|
||||
})
|
||||
const upload = context.apiSuccess(MODULE, `上传称重图片并执行 OCR ${index + 1}/${palletIds.length}`,
|
||||
uploadResponse, `inboundId=${inboundId}`, OPTIONS)
|
||||
const fileId = asId(upload?.fileId)
|
||||
if (!fileId) return null
|
||||
|
||||
const createResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results?${query(tenantId)}`, {
|
||||
fileId, palletId, ocrProvider: 'ADMIN_ACCEPTANCE', ocrRawText: null,
|
||||
ocrWeightValue: null, ocrWeightUnit: null, confidence: null,
|
||||
})
|
||||
const created = context.apiSuccess(MODULE, '登记称重图片与托盘快照', createResponse,
|
||||
`fileId=${fileId}, palletId=${palletId}`, OPTIONS)
|
||||
if (!created?.id) return null
|
||||
const createResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results?${query(tenantId)}`, {
|
||||
fileId, palletId, ocrProvider: 'ADMIN_ACCEPTANCE', ocrRawText: null,
|
||||
ocrWeightValue: null, ocrWeightUnit: null, confidence: null,
|
||||
})
|
||||
const created = context.apiSuccess(MODULE, `登记称重图片与托盘快照 ${index + 1}/${palletIds.length}`,
|
||||
createResponse, `fileId=${fileId}, palletId=${palletId}`, OPTIONS)
|
||||
if (!created?.id) return null
|
||||
|
||||
const confirmResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${created.id}/confirm?${query(tenantId)}`, {
|
||||
version: Number(created.version), adjustedWeightValue: 100,
|
||||
adjustedWeightUnit: 'KG', adjustmentReason: `${context.marker} 自动化人工确认称重`,
|
||||
})
|
||||
const confirmed = context.apiSuccess(MODULE, '人工确认称重图片净重', confirmResponse,
|
||||
`resultId=${created.id}, palletId=${palletId}`, OPTIONS)
|
||||
const netWeightKg = Number(confirmed?.netWeightKg)
|
||||
const grossWeightKg = Number(confirmed?.grossWeightKg)
|
||||
if (confirmed?.recognitionStatus !== 'CONFIRMED' || !Number.isFinite(netWeightKg) || netWeightKg <= 0) {
|
||||
return null
|
||||
const grossWeight = Number(grossWeightPerPalletKg) + index * 5
|
||||
const confirmResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${created.id}/confirm?${query(tenantId)}`, {
|
||||
version: Number(created.version), adjustedWeightValue: grossWeight,
|
||||
adjustedWeightUnit: 'KG', adjustmentReason: `${context.marker} 自动化人工确认称重`,
|
||||
})
|
||||
const confirmed = context.apiSuccess(MODULE, `人工确认称重图片净重 ${index + 1}/${palletIds.length}`,
|
||||
confirmResponse, `resultId=${created.id}, palletId=${palletId}`, OPTIONS)
|
||||
const netWeightKg = Number(confirmed?.netWeightKg)
|
||||
const grossWeightKg = Number(confirmed?.grossWeightKg)
|
||||
if (confirmed?.recognitionStatus !== 'CONFIRMED' || !Number.isFinite(netWeightKg) || netWeightKg <= 0) {
|
||||
return null
|
||||
}
|
||||
confirmedResults.push({ id: asId(created.id), palletId, netWeightKg, grossWeightKg })
|
||||
}
|
||||
|
||||
const workspaceResponse = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement?${query(tenantId)}`)
|
||||
const workspace = context.apiSuccess(MODULE, '回查已确认称重图片和货物总重量', workspaceResponse,
|
||||
`resultId=${created.id}`, OPTIONS)
|
||||
`resultIds=${confirmedResults.map((result) => result.id).join(',')}`, OPTIONS)
|
||||
const confirmedRows = (workspace?.weightResults || [])
|
||||
.filter((result) => result?.recognitionStatus === 'CONFIRMED')
|
||||
const persisted = confirmedRows.length === WEIGHING_IMAGE_COUNT
|
||||
&& asId(confirmedRows[0]?.id) === asId(created.id)
|
||||
&& asId(confirmedRows[0]?.palletId) === palletId
|
||||
const grossWeightKg = confirmedResults.reduce((sum, result) => sum + result.grossWeightKg, 0)
|
||||
const netWeightKg = confirmedResults.reduce((sum, result) => sum + result.netWeightKg, 0)
|
||||
const persisted = confirmedRows.length === palletIds.length
|
||||
&& confirmedResults.every((expected) => confirmedRows.some((row) =>
|
||||
asId(row?.id) === expected.id && asId(row?.palletId) === expected.palletId))
|
||||
&& closeTo(workspace?.actualGrossWeightKg, grossWeightKg, 0.001)
|
||||
&& closeTo(workspace?.actualNetWeightKg, netWeightKg, 0.001)
|
||||
context.assert(MODULE, '图片张数决定托数且净重汇总已落地', workspaceResponse, persisted,
|
||||
`confirmedImages=${confirmedRows.length}, expectedPallets=${WEIGHING_IMAGE_COUNT}, grossWeight=${grossWeightKg}, netWeight=${netWeightKg}, actualGross=${workspace?.actualGrossWeightKg ?? 'missing'}, actualNet=${workspace?.actualNetWeightKg ?? 'missing'}`,
|
||||
`confirmedImages=${confirmedRows.length}, expectedPallets=${palletIds.length}, grossWeight=${grossWeightKg}, netWeight=${netWeightKg}, actualGross=${workspace?.actualGrossWeightKg ?? 'missing'}, actualNet=${workspace?.actualNetWeightKg ?? 'missing'}`,
|
||||
OPTIONS)
|
||||
return persisted ? { count: confirmedRows.length, grossWeightKg, netWeightKg } : null
|
||||
}
|
||||
@@ -280,8 +291,8 @@ export async function run(context) {
|
||||
}
|
||||
|
||||
const scanPath = `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/carton-scans?${query(tenantId)}`
|
||||
// Keep the isolated sample inside the customer's largest configured inner box (60x50x50cm).
|
||||
const firstDimensions = { length: 59.1, width: 40.2, height: 30.3 }
|
||||
const measurementProfile = context.context?.waitSplitVariant?.measurementProfile
|
||||
const firstDimensions = measurementProfile?.first || { length: 59.1, width: 40.2, height: 30.3 }
|
||||
const firstShipmentId = `FBA-${context.marker.slice(-12)}-01`
|
||||
const firstScanResponse = await multipartRequest(context, scanPath, {
|
||||
boxCode, ...firstDimensions, scannedShipmentId: firstShipmentId,
|
||||
@@ -325,7 +336,7 @@ export async function run(context) {
|
||||
&& Number(countResult?.measurementVersion) === Number(firstScan.measurementVersion) + 1,
|
||||
`version=${countResult?.measurementVersion ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const replacement = { length: 59.5, width: 41.2, height: 31.3 }
|
||||
const replacement = measurementProfile?.replacement || { length: 59.5, width: 41.2, height: 31.3 }
|
||||
const replacementShipmentId = `FBA-${context.marker.slice(-12)}-02`
|
||||
const rescanResponse = await multipartRequest(context, scanPath, {
|
||||
boxCode, ...replacement, scannedShipmentId: replacementShipmentId,
|
||||
@@ -342,8 +353,7 @@ export async function run(context) {
|
||||
let measuredResponse = await context.request('GET', measurementPath)
|
||||
let measured = context.apiSuccess(MODULE, '回查覆盖后的尺寸与体积结果', measuredResponse, '', OPTIONS)
|
||||
let measuredItem = measured?.items?.find((item) => asId(item.id) === asId(firstScan.itemId))
|
||||
// The verified customer rule maps this scan to EXTRA_LARGE 60x50x50cm and adds 2cm per edge.
|
||||
const weightedOuter = { length: 64, width: 54, height: 54 }
|
||||
const weightedOuter = measurementProfile?.outer || { length: 64, width: 54, height: 54 }
|
||||
const unitVolume = halfUp(
|
||||
weightedOuter.length * weightedOuter.width * weightedOuter.height / 1_000_000, 3)
|
||||
const totalVolume = unitVolume * originalPackageCount
|
||||
@@ -374,7 +384,7 @@ export async function run(context) {
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/items/${firstScan.itemId}/confirmed-package-count?${query(tenantId)}`,
|
||||
{ measurementVersion: Number(measured?.version), confirmedPackageCount: packagesPerDetail })
|
||||
const resetFirstCount = context.apiSuccess(MODULE,
|
||||
'增量 fixture 将首条货物确认箱数恢复为自身两箱', resetFirstCountResponse,
|
||||
'增量 fixture 将首条货物确认箱数恢复为自身箱数', resetFirstCountResponse,
|
||||
`itemId=${asId(firstScan.itemId)}, confirmedPackageCount=${packagesPerDetail}`, OPTIONS)
|
||||
context.assert(MODULE, '增量首条货物箱数和版本更新正确', resetFirstCountResponse,
|
||||
Number(resetFirstCount?.confirmedPackageCount) === packagesPerDetail
|
||||
@@ -409,10 +419,10 @@ export async function run(context) {
|
||||
`measurementVersion=${countDetail?.measurementVersion ?? 'missing'}`, OPTIONS)
|
||||
}
|
||||
measuredResponse = await context.request('GET', measurementPath)
|
||||
measured = context.apiSuccess(MODULE, '回查增量 fixture 四条测量明细', measuredResponse, '', OPTIONS)
|
||||
measured = context.apiSuccess(MODULE, '回查增量 fixture 动态测量明细', measuredResponse, '', OPTIONS)
|
||||
measuredItem = measured?.items?.find((item) => asId(item.id) === asId(firstScan.itemId))
|
||||
const detailIds = new Set((measured?.items || []).map((item) => asId(item?.inboundDetailId)).filter(Boolean))
|
||||
context.assert(MODULE, '增量 fixture 形成四条真实独立货物测量明细', measuredResponse,
|
||||
context.assert(MODULE, '增量 fixture 形成动态真实独立货物测量明细', measuredResponse,
|
||||
detailIds.size >= incrementalDetailCount
|
||||
&& measured.items.filter((item) => detailIds.has(asId(item?.inboundDetailId))).length >= incrementalDetailCount,
|
||||
`measurementItems=${measured?.items?.length ?? 'missing'}, distinctInboundDetails=${detailIds.size}`, OPTIONS)
|
||||
@@ -482,12 +492,13 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
|
||||
const palletCount = Math.max(1, Number(context.context?.waitSplitVariant?.palletCount || 1))
|
||||
const warehouseResourcesReady = await provisionWarehouseResources(context, {
|
||||
inboundId, warehouseId: asId(summary?.warehouseId), palletCount: WEIGHING_IMAGE_COUNT,
|
||||
inboundId, warehouseId: asId(summary?.warehouseId), palletCount,
|
||||
})
|
||||
context.assert(MODULE, '测量提交前按称重图片数准备真实库位', draftResponse,
|
||||
Boolean(warehouseResourcesReady),
|
||||
`warehouseId=${asId(summary?.warehouseId) || 'missing'}, palletCount=${WEIGHING_IMAGE_COUNT}`,
|
||||
`warehouseId=${asId(summary?.warehouseId) || 'missing'}, palletCount=${palletCount}`,
|
||||
OPTIONS)
|
||||
if (!warehouseResourcesReady) {
|
||||
finishScenario(context, start)
|
||||
@@ -495,11 +506,12 @@ export async function run(context) {
|
||||
}
|
||||
|
||||
const weightEvidence = await createConfirmedWeightEvidence(context, {
|
||||
inboundId, tenantId, palletId: warehouseResourcesReady.palletId,
|
||||
inboundId, tenantId, palletIds: warehouseResourcesReady.palletIds,
|
||||
grossWeightPerPalletKg: Number(context.context?.waitSplitVariant?.grossWeightPerPalletKg || 100),
|
||||
})
|
||||
context.assert(MODULE, '测量提交前已通过正式接口确认称重图片', draftResponse,
|
||||
Boolean(weightEvidence),
|
||||
`confirmedImages=${weightEvidence?.count ?? 0}, expected=${WEIGHING_IMAGE_COUNT}`,
|
||||
`confirmedImages=${weightEvidence?.count ?? 0}, expected=${palletCount}`,
|
||||
OPTIONS)
|
||||
if (!weightEvidence) {
|
||||
finishScenario(context, start)
|
||||
|
||||
@@ -7,23 +7,23 @@ const iamSeed = readFileSync(new URL('../../iam/src/main/resources/db/phase1-adm
|
||||
const oceanBookingSchema = readFileSync(new URL('../../firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql', import.meta.url), 'utf8')
|
||||
const truckAppointmentSchema = readFileSync(new URL('../../firstleg/src/main/resources/db/phase3-truck-appointment-schema.sql', import.meta.url), 'utf8')
|
||||
|
||||
test('统一初始化 SQL 保留最小登录与客户主体', () => {
|
||||
assert.match(sql, /一个平台系统管理员和一个 IT 测试客户管理员/)
|
||||
test('统一初始化 SQL 只保留平台管理员且不创建固定客户', () => {
|
||||
assert.match(sql, /最小登录基线只保留平台系统管理员/)
|
||||
assert.equal((sql.match(/'platform_admin'/g) || []).length, 1)
|
||||
assert.equal((sql.match(/'merchant_admin'/g) || []).length, 1)
|
||||
assert.match(sql, /\(10001, 'IT_TEST', 'IT 测试客户'/)
|
||||
assert.doesNotMatch(sql, /'merchant_admin'|'IT_TEST'|'IT 测试客户'/)
|
||||
assert.doesNotMatch(iamSeed, /'IT_TEST'|'IT 测试客户'|\(10001,/)
|
||||
const accountSeed = sql.match(/INSERT INTO auth_account \([\s\S]*?deleted = 0;/)?.[0]
|
||||
assert.ok(accountSeed, 'missing auth account seed')
|
||||
assert.doesNotMatch(accountSeed, /ON DUPLICATE KEY UPDATE[\s\S]*?password_hash\s*=/,
|
||||
're-running the unified SQL must not reset an existing account password')
|
||||
})
|
||||
|
||||
test('IT 测试客户初始化后具备已审核的必填资质和下单资格', () => {
|
||||
for (const source of [sql, iamSeed]) {
|
||||
assert.match(source, /\(2100001, 10001, 'BUSINESS_LICENSE', '91310000ITTEST0001', NULL,[\s\S]*?'APPROVED'/)
|
||||
assert.match(source, /\(2100002, 10001, 'LEGAL_PERSON_ID', NULL, NULL,[\s\S]*?'APPROVED'/)
|
||||
assert.match(source, /status = 'APPROVED'/)
|
||||
}
|
||||
test('统一初始化直接提供tenant0商户角色与权限模板', () => {
|
||||
assert.match(sql, /4000100, 0, 'MERCHANT', 'MERCHANT_OWNER'/)
|
||||
assert.match(sql, /5300000, 0, 'MERCHANT', 'MENU',[\s\S]*?'merchant:enterprise'/)
|
||||
assert.match(sql, /5301024, 0, 'MERCHANT', 'BUTTON',[\s\S]*?'merchant:order:quick-create'/)
|
||||
assert.match(sql, /5305000, 0, 'MERCHANT', 'MENU',[\s\S]*?'merchant:finance:statement'/)
|
||||
assert.match(sql, /r\.id=4000100[\s\S]*?客户全局管理员 Finance 模板授权不完整/)
|
||||
})
|
||||
|
||||
test('统一初始化 SQL 提供人工验收所需公共仓储主数据', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import test from 'node:test'
|
||||
|
||||
const acceptance = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
|
||||
const fixture = readFileSync(new URL('./merchant-qualification-fixture.mjs', import.meta.url), 'utf8')
|
||||
const orders = readFileSync(new URL('./orders.mjs', import.meta.url), 'utf8')
|
||||
|
||||
test('默认全量验收先准备已审核商户再运行订单链路', () => {
|
||||
const modules = acceptance.match(/const CONFIGURED_MODULES = \[([^\n]+)\]/)?.[1] || ''
|
||||
@@ -22,5 +23,38 @@ test('商户夹具通过正式 File 和 IAM 审核流程建立下单资格', ()
|
||||
assert.match(fixture, /item\.status === 'APPROVED'/)
|
||||
assert.match(fixture, /MERCHANT_TEST_USERNAME/)
|
||||
assert.match(fixture, /MERCHANT_TEST_PASSWORD/)
|
||||
assert.match(fixture, /context\.options\?\.customerName/)
|
||||
assert.doesNotMatch(fixture, /internal\/iam|UPDATE\s+iam_|INSERT\s+INTO\s+iam_/i)
|
||||
})
|
||||
|
||||
test('批量待分柜参数固定同一客户并限制为订单测量链', () => {
|
||||
assert.match(acceptance, /--customer-name <name>/)
|
||||
assert.match(acceptance, /--merchant-username <name>/)
|
||||
assert.match(acceptance, /--origin-warehouse-name <name>/)
|
||||
assert.match(acceptance, /--channel-name <name>/)
|
||||
assert.match(acceptance, /--wait-split-count <n>/)
|
||||
assert.match(acceptance, /--order-profile-mode <mode>/)
|
||||
assert.match(acceptance, /merchant-username和merchant-password必须同时提供/)
|
||||
assert.match(acceptance, /modules\.splice\(fixtureIndex, 1\)/)
|
||||
assert.match(acceptance, /wait-split-count必须是1-100的整数/)
|
||||
assert.match(acceptance, /批量WAIT_SPLIT只允许执行merchant-qualification-fixture、orders、inbound-measurement/)
|
||||
assert.match(acceptance, /createAdditionalWaitSplitFixtures/)
|
||||
assert.match(acceptance, /measurement\.processStatus === 'WAIT_SPLIT'/)
|
||||
assert.match(acceptance, /measurement\.finalStatus === 'APPROVED'/)
|
||||
assert.match(acceptance, /waitSplitVariant\(sequence\)/)
|
||||
assert.match(acceptance, /packageCountPerSku/)
|
||||
assert.match(acceptance, /grossWeightPerPalletKg/)
|
||||
assert.match(acceptance, /randomizedOrderProfile\(sequence\)/)
|
||||
assert.match(acceptance, /\['PRIVATE', 'STANDARD'\]/)
|
||||
assert.match(acceptance, /ONE_CLICK_DROPSHIP/)
|
||||
assert.match(acceptance, /destinationWarehouseCode: order\.destinationWarehouseCode/)
|
||||
assert.match(acceptance, /channelName: order\.channelName/)
|
||||
assert.match(acceptance, /pricingTaxMode: order\.pricingTaxMode/)
|
||||
assert.match(orders, /customerId: profile\.id/)
|
||||
assert.match(orders, /customerName: companyName/)
|
||||
assert.match(orders, /\['FBA', 'OVERSEAS', 'PRIVATE'\]\.includes/)
|
||||
assert.match(orders, /destinationType === 'PRIVATE'/)
|
||||
assert.match(orders, /PRIVATE_ADDRESS/)
|
||||
assert.match(orders, /eligible\.length < 1/)
|
||||
assert.doesNotMatch(orders, /customerName: master\.partner\?\.partnerName/)
|
||||
})
|
||||
|
||||
@@ -28,10 +28,11 @@ async function waitForDimensionInitialization(context, tenantId) {
|
||||
|
||||
export async function run(context) {
|
||||
const suffix = context.marker.replace(/[^0-9A-Z]/gi, '').slice(-10).padStart(10, '0')
|
||||
const customerName = String(context.options?.customerName || '').trim()
|
||||
const phone = await context.findAvailablePhone(MODULE)
|
||||
const createResponse = await context.request('POST', '/api/platform/v1/iam/tenants', {
|
||||
tenantName: `全量验收下单客户-${suffix}`,
|
||||
legalName: `全量验收下单有限公司-${suffix}`,
|
||||
tenantName: customerName || `全量验收下单客户-${suffix}`,
|
||||
legalName: customerName ? `${customerName}有限公司` : `全量验收下单有限公司-${suffix}`,
|
||||
legalRepresentative: '全量验收法人',
|
||||
contactName: '全量验收管理员',
|
||||
contactPhone: phone,
|
||||
|
||||
@@ -2,8 +2,8 @@ import { findPricedOrderRouteAcrossOrigins } from './orders.mjs'
|
||||
|
||||
const MODULE = 'order-inbound-status-fixtures'
|
||||
const OPTIONS = { scenario: 'manual-order-inbound-status-matrix', deploymentBlocker: true }
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
|
||||
const ORDER_TARGETS = [
|
||||
['DRAFT', []],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const MODULE = 'orders'
|
||||
const CONTRACT_OPTIONS = { scenario: 'isolated-order-chain', deploymentBlocker: true }
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
const BOXES = [
|
||||
{ boxType: 'SMALL', lengthCm: 30, widthCm: 20, heightCm: 20 },
|
||||
{ boxType: 'MEDIUM', lengthCm: 40, widthCm: 30, heightCm: 30 },
|
||||
@@ -21,8 +21,8 @@ function success(response) { return Boolean(response) && response.http === 200 &
|
||||
function sleep(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)) }
|
||||
function key(context, operation) { return `${context.marker}-${operation}`.slice(0, 128) }
|
||||
export function requestedOrderDestinationType(context) {
|
||||
return String(context?.context?.orderDestinationType || 'FBA').trim().toUpperCase() === 'OVERSEAS'
|
||||
? 'OVERSEAS' : 'FBA'
|
||||
const value = String(context?.context?.orderDestinationType || 'FBA').trim().toUpperCase()
|
||||
return ['FBA', 'OVERSEAS', 'PRIVATE'].includes(value) ? value : 'FBA'
|
||||
}
|
||||
export function approvedInboundReady(orderDetail, taskResponse, tenantId, orderId) {
|
||||
const summary = orderDetail?.summary
|
||||
@@ -202,35 +202,88 @@ async function assertProductImageDownload(context, merchantToken, fileId) {
|
||||
|
||||
async function loadMasterData(context, merchantToken) {
|
||||
const destinationType = requestedOrderDestinationType(context)
|
||||
const fixedRoute = context.context?.waitSplitRoute || {}
|
||||
const domesticResponse = await request(context, 'GET',
|
||||
'/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=DOMESTIC', undefined, merchantToken)
|
||||
const domesticPage = context.apiSuccess(MODULE, 'Client 查询国内收货仓', domesticResponse)
|
||||
context.assert(MODULE, '存在可用国内收货仓', domesticResponse, records(domesticPage).length > 0,
|
||||
`count=${records(domesticPage).length}`)
|
||||
|
||||
const destinationResponse = await request(context, 'GET',
|
||||
`/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=${encodeURIComponent(destinationType)}`,
|
||||
undefined, merchantToken)
|
||||
const destinationPage = context.apiSuccess(MODULE, `Client 查询 ${destinationType} 目的仓`, destinationResponse)
|
||||
context.assert(MODULE, `存在可用 ${destinationType} 目的仓`, destinationResponse,
|
||||
records(destinationPage).length > 0, `count=${records(destinationPage).length}`)
|
||||
let destinationResponse = null
|
||||
let destinationPage = null
|
||||
if (destinationType !== 'PRIVATE') {
|
||||
destinationResponse = await request(context, 'GET',
|
||||
`/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=${encodeURIComponent(destinationType)}`,
|
||||
undefined, merchantToken)
|
||||
destinationPage = context.apiSuccess(MODULE, `Client 查询 ${destinationType} 目的仓`, destinationResponse)
|
||||
context.assert(MODULE, `存在可用 ${destinationType} 目的仓`, destinationResponse,
|
||||
records(destinationPage).length > 0, `count=${records(destinationPage).length}`)
|
||||
}
|
||||
const receivings = records(domesticPage).filter((warehouse) => warehouse?.status === 'ACTIVE')
|
||||
const destinations = records(destinationPage).filter((warehouse) => warehouse?.status === 'ACTIVE')
|
||||
if (receivings.length === 0 || destinations.length === 0) throw new Error('缺少创建测试订单所需的收货仓或目的仓')
|
||||
let destinations = records(destinationPage).filter((warehouse) => warehouse?.status === 'ACTIVE')
|
||||
if (Number(destinationPage?.total) > destinations.length) {
|
||||
const nextResponse = await request(context, 'GET',
|
||||
`/api/merchant/v1/warehouses?page=2&size=200&status=ACTIVE&warehouseType=${encodeURIComponent(destinationType)}`,
|
||||
undefined, merchantToken)
|
||||
const nextPage = context.apiSuccess(MODULE, `Client 查询 ${destinationType} 目的仓第二页`, nextResponse)
|
||||
destinations = [...destinations, ...records(nextPage).filter((warehouse) => warehouse?.status === 'ACTIVE')]
|
||||
}
|
||||
if (receivings.length === 0 || (destinationType !== 'PRIVATE' && destinations.length === 0)) {
|
||||
throw new Error('缺少创建测试订单所需的收货仓或目的仓')
|
||||
}
|
||||
|
||||
const pricingResult = await findPricedOrderRouteAcrossOrigins({
|
||||
receivings, destinations,
|
||||
loadChannels: (receiving) => request(context, 'GET',
|
||||
let pricingResult
|
||||
if (fixedRoute.originWarehouseName && (fixedRoute.channelCode || fixedRoute.channelName)) {
|
||||
const receiving = receivings.find((warehouse) => warehouse.warehouseName === fixedRoute.originWarehouseName)
|
||||
if (!receiving) throw new Error(`未找到固定起运仓: ${fixedRoute.originWarehouseName}`)
|
||||
const channelsResponse = await request(context, 'GET',
|
||||
`/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=${encodeURIComponent(receiving.id)}`,
|
||||
undefined, merchantToken),
|
||||
estimate: (body) => request(context, 'POST', '/api/merchant/v1/first-leg/pricing/estimate', body, merchantToken),
|
||||
destinationType,
|
||||
})
|
||||
undefined, merchantToken)
|
||||
const channels = context.apiSuccess(MODULE, 'Client 查询固定起运仓物流渠道', channelsResponse)
|
||||
const channel = (Array.isArray(channels) ? channels : []).find((item) =>
|
||||
(!fixedRoute.channelCode || item.channelCode === fixedRoute.channelCode)
|
||||
&& (!fixedRoute.channelName || item.channelName === fixedRoute.channelName))
|
||||
if (!channel) throw new Error(`未找到固定物流渠道: ${fixedRoute.channelCode || fixedRoute.channelName}`)
|
||||
const modes = Array.isArray(channel.pricingTaxModes) ? [...channel.pricingTaxModes] : []
|
||||
if (modes.length === 0) throw new Error('固定物流渠道没有可用报价税制')
|
||||
const shift = modes.length ? Number(fixedRoute.taxModeIndex || 0) % modes.length : 0
|
||||
const selectedChannel = { ...channel, pricingTaxModes: [...modes.slice(shift), ...modes.slice(0, shift)] }
|
||||
if (destinationType === 'PRIVATE') {
|
||||
pricingResult = { receiving, priced: { channel: selectedChannel,
|
||||
pricingTaxMode: selectedChannel.pricingTaxModes[0], destination: null },
|
||||
response: channelsResponse, attempts: 0, failures: [], originFailures: [] }
|
||||
} else {
|
||||
const detailResponse = await context.request('GET',
|
||||
`/api/platform/v1/fba-pricing/carrier-rates/price-books/${encodeURIComponent(channel.priceBookVersionId)}`)
|
||||
const detail = context.apiSuccess(MODULE, 'Admin 查询固定渠道当前价格明细', detailResponse)
|
||||
const pricedCodes = new Set((detail?.rates || [])
|
||||
.filter((rate) => rate.channelCode === channel.channelCode && rate.destinationType === destinationType)
|
||||
.map((rate) => String(rate.destinationCode || '').trim()).filter(Boolean))
|
||||
const eligible = destinations.filter((destination) => pricedCodes.has(String(destination.warehouseCode)))
|
||||
if (eligible.length < 1) throw new Error(`固定渠道没有可计价目的仓: ${destinationType}`)
|
||||
const destination = eligible[Number(fixedRoute.destinationIndex || 0) % eligible.length]
|
||||
const result = await findPricedOrderRoute({
|
||||
receiving, destinations: [destination], channels: [selectedChannel],
|
||||
estimate: (body) => request(context, 'POST', '/api/merchant/v1/first-leg/pricing/estimate', body, merchantToken),
|
||||
destinationType,
|
||||
})
|
||||
pricingResult = { receiving, ...result, originFailures: [] }
|
||||
}
|
||||
} else {
|
||||
pricingResult = await findPricedOrderRouteAcrossOrigins({
|
||||
receivings, destinations,
|
||||
loadChannels: (receiving) => request(context, 'GET',
|
||||
`/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=${encodeURIComponent(receiving.id)}`,
|
||||
undefined, merchantToken),
|
||||
estimate: (body) => request(context, 'POST', '/api/merchant/v1/first-leg/pricing/estimate', body, merchantToken),
|
||||
destinationType,
|
||||
})
|
||||
}
|
||||
const priced = pricingResult.priced
|
||||
const receiving = pricingResult.receiving
|
||||
context.addStep(MODULE, 'Client 从 ACTIVE 国内仓选择具有真实价格覆盖的渠道和目的仓', pricingResult.response, Boolean(priced),
|
||||
priced
|
||||
? `origin=${receiving.id}, priceBookVersionId=${priced.channel.priceBookVersionId}, channel=${priced.channel.channelCode}, pricingTaxMode=${priced.pricingTaxMode}, destination=${priced.destination.warehouseCode}, attempts=${pricingResult.attempts}`
|
||||
? `origin=${receiving.id}, priceBookVersionId=${priced.channel.priceBookVersionId}, channel=${priced.channel.channelCode}, pricingTaxMode=${priced.pricingTaxMode}, destination=${priced.destination?.warehouseCode || 'PRIVATE_ADDRESS'}, attempts=${pricingResult.attempts}`
|
||||
: originPricingFailureEvidence(pricingResult),
|
||||
CONTRACT_OPTIONS)
|
||||
if (!priced) throw new Error(`当前 ACTIVE 国内收货仓、渠道、报价税制和 ${destinationType} 目的仓没有可计价组合`)
|
||||
@@ -249,22 +302,39 @@ function buildPayload(context, profile, master, productImageFileId) {
|
||||
const shipmentId = `SHIP-${context.marker}`.slice(0, 64)
|
||||
const referenceId = `REF-${context.marker}`.slice(0, 64)
|
||||
const companyName = profile.tenantName || `订单验收客户-${context.marker}`
|
||||
const requestedSkuCount = Number(context.context?.waitSplitVariant?.skuCount || BOXES.length)
|
||||
const skuCount = Math.max(1, Math.min(BOXES.length, requestedSkuCount))
|
||||
const packageCountPerSku = Math.max(1,
|
||||
Number(context.context?.waitSplitVariant?.packageCountPerSku
|
||||
|| context.context?.phase3LoadingIncrementalPackageCount || 1))
|
||||
const selectedBoxes = BOXES.slice(0, skuCount)
|
||||
const orderProfile = context.context?.orderProfile || {}
|
||||
const isPrivate = master.destinationType === 'PRIVATE'
|
||||
const pickupMethod = orderProfile.pickupMethod || 'PICKUP'
|
||||
const receiver = isPrivate ? {
|
||||
companyName: `${companyName} 私人收件人`, contactName: `收件人-${context.marker}`.slice(0, 64),
|
||||
phone: '12025550188', email: 'private-receiver@example.com', countryCode: 'US',
|
||||
province: 'New York', city: 'New York', district: 'Queens',
|
||||
addressLine: `${context.marker} 88 Randomized Test Ave`, postalCode: '11354',
|
||||
} : null
|
||||
return {
|
||||
fulfillmentType: String(context.context?.orderFulfillmentType || 'STANDARD').trim().toUpperCase(),
|
||||
customerId: master.partner?.id ?? null,
|
||||
customerName: master.partner?.partnerName ?? companyName,
|
||||
customerId: profile.id,
|
||||
customerName: companyName,
|
||||
transportMode: 'SEA', tradeTerm: 'FOB',
|
||||
destinationCountryCode: master.destination.countryCode || 'US',
|
||||
destinationWarehouseId: master.destination.id,
|
||||
destinationWarehouseName: master.destination.warehouseName,
|
||||
destinationCountryCode: master.destination?.countryCode || receiver?.countryCode || 'US',
|
||||
destinationWarehouseId: master.destination?.id || null,
|
||||
destinationWarehouseName: master.destination?.warehouseName || (isPrivate ? '私人地址' : null),
|
||||
expectedShipDate: tomorrow(), customerRemark: `${context.marker} Client 创建隔离订单`,
|
||||
pickupMethod: 'PICKUP', receivingWarehouseId: master.receiving.id,
|
||||
pickupMethod, requestedPickupAt: pickupMethod === 'PICKUP'
|
||||
? new Date(Date.now() + 7 * 86_400_000).toISOString().slice(0, 19) : null,
|
||||
receivingWarehouseId: master.receiving.id,
|
||||
receivingWarehouseName: master.receiving.warehouseName,
|
||||
channelCode: master.channel.channelCode, channelName: master.channel.channelName,
|
||||
carrierId: null, carrierName: null, taxMode: 'BUY_DECLARATION',
|
||||
pricingTaxMode: master.pricingTaxMode, containerMode: 'LCL',
|
||||
customsInspection: false, destinationType: master.destinationType,
|
||||
amazonWarehouseCode: master.destinationType === 'FBA' ? master.destination.warehouseCode : null,
|
||||
carrierId: null, carrierName: null, taxMode: orderProfile.taxMode || 'BUY_DECLARATION',
|
||||
pricingTaxMode: master.pricingTaxMode, containerMode: orderProfile.containerMode || 'LCL',
|
||||
customsInspection: orderProfile.customsInspection ?? false, destinationType: master.destinationType,
|
||||
amazonWarehouseCode: master.destinationType === 'FBA' ? master.destination?.warehouseCode : null,
|
||||
shipmentId: master.destinationType === 'FBA' ? shipmentId : null,
|
||||
referenceId: master.destinationType === 'FBA' ? referenceId : null,
|
||||
sender: {
|
||||
@@ -272,11 +342,11 @@ function buildPayload(context, profile, master, productImageFileId) {
|
||||
email: 'order-acceptance@example.com', countryCode: 'CN', province: '广东省',
|
||||
city: '深圳市', district: '南山区', addressLine: `${context.marker} 测试地址`, postalCode: '518000',
|
||||
},
|
||||
receiver: null,
|
||||
cargoes: BOXES.map((box, index) => {
|
||||
receiver,
|
||||
cargoes: selectedBoxes.map((box, index) => {
|
||||
const volumeCbm = box.lengthCm * box.widthCm * box.heightCm / 1_000_000
|
||||
const unitsPerPackage = 10 + index
|
||||
const packageCount = Number(context.context?.phase3LoadingIncrementalPackageCount) === 2 ? 2 : 1
|
||||
const packageCount = packageCountPerSku
|
||||
return {
|
||||
skuNo: `SKU-${box.boxType}-${context.marker}`.slice(0, 64),
|
||||
cargoName: `${box.boxType} 测试产品`, cargoNameEn: `${box.boxType} test product`,
|
||||
@@ -300,11 +370,12 @@ function buildPayload(context, profile, master, productImageFileId) {
|
||||
}
|
||||
|
||||
function assertCargoContract(context, response, cargoes, payload) {
|
||||
const complete = Array.isArray(cargoes) && cargoes.length === BOXES.length
|
||||
&& BOXES.every((box, index) => {
|
||||
const actual = cargoes[index]; const expected = payload.cargoes[index]
|
||||
return Number(actual?.lengthCm) === box.lengthCm && Number(actual?.widthCm) === box.widthCm
|
||||
&& Number(actual?.heightCm) === box.heightCm && id(actual?.productImageFileId) === id(expected.productImageFileId)
|
||||
const expectedCargoes = payload.cargoes || []
|
||||
const complete = Array.isArray(cargoes) && cargoes.length === expectedCargoes.length
|
||||
&& expectedCargoes.every((expected, index) => {
|
||||
const actual = cargoes[index]
|
||||
return Number(actual?.lengthCm) === expected.lengthCm && Number(actual?.widthCm) === expected.widthCm
|
||||
&& Number(actual?.heightCm) === expected.heightCm && id(actual?.productImageFileId) === id(expected.productImageFileId)
|
||||
&& actual?.skuNo === expected.skuNo && actual?.cargoName === expected.cargoName
|
||||
&& actual?.cargoNameEn === expected.cargoNameEn && actual?.customsHsCode === expected.customsHsCode
|
||||
&& Number(actual?.unitsPerPackage) === expected.unitsPerPackage
|
||||
@@ -320,7 +391,7 @@ function assertCargoContract(context, response, cargoes, payload) {
|
||||
&& (actual?.referenceId ?? null) === (expected.referenceId ?? null)
|
||||
&& actual?.upcNo === expected.upcNo && actual?.cargoRemark.includes(context.marker)
|
||||
})
|
||||
context.assert(MODULE, '四箱型产品字段及图片契约完整回显', response, complete,
|
||||
context.assert(MODULE, '动态SKU产品字段及图片契约完整回显', response, complete,
|
||||
`cargoes=${Array.isArray(cargoes) ? cargoes.length : 'n/a'}, imageFileId=${payload.cargoes[0].productImageFileId}`,
|
||||
CONTRACT_OPTIONS)
|
||||
}
|
||||
@@ -405,9 +476,18 @@ export async function run(context) {
|
||||
})),
|
||||
boxTypes: BOXES.map((box) => box.boxType), inboundId: null,
|
||||
receivingWarehouseId: id(master.receiving.id),
|
||||
receivingWarehouseName: master.receiving.warehouseName,
|
||||
destinationType: master.destinationType,
|
||||
destinationWarehouseId: id(master.destination.id),
|
||||
destinationWarehouseCode: master.destination.warehouseCode,
|
||||
destinationWarehouseId: id(master.destination?.id),
|
||||
destinationWarehouseCode: master.destination?.warehouseCode || null,
|
||||
destinationWarehouseName: master.destination?.warehouseName || (master.destinationType === 'PRIVATE' ? '私人地址' : null),
|
||||
channelCode: master.channel.channelCode,
|
||||
channelName: master.channel.channelName,
|
||||
pricingTaxMode: master.pricingTaxMode,
|
||||
pickupMethod: payload.pickupMethod,
|
||||
taxMode: payload.taxMode,
|
||||
containerMode: payload.containerMode,
|
||||
customsInspection: payload.customsInspection,
|
||||
}
|
||||
|
||||
const replayResponse = await request(context, 'POST', createPath, payload, merchantToken,
|
||||
|
||||
@@ -4,8 +4,8 @@ const MODULE = 'phase3-customs'
|
||||
const OPTIONS = { scenario: 'phase3-customs-collaboration', deploymentBlocker: true }
|
||||
const PLATFORM = '/api/platform/v1/customs'
|
||||
const MERCHANT = '/api/merchant/v1/customs'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
||||
|
||||
@@ -3,8 +3,8 @@ import { createHash } from 'node:crypto'
|
||||
|
||||
const MODULE = 'phase3-loading'
|
||||
const OPTIONS = { scenario: 'phase3-split-preload-loading', deploymentBlocker: true }
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
const MATSON_SUPPLIER_NAME = 'MATSON 开发联调船司'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const MODULE = 'phase3-order-splitting'
|
||||
const SCENARIO = 'source-loaded-single-remainder-chain'
|
||||
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
const CONTAINERS = '/api/platform/v1/containers'
|
||||
const SMART = '/api/platform/v1/first-leg/smart-loading'
|
||||
const PDA = '/api/platform/v1/first-leg/pda-loading'
|
||||
|
||||
@@ -46,6 +46,10 @@ export async function run(context) {
|
||||
const module = 'supplemental'
|
||||
const scenario = 'service-health-and-auth-boundary'
|
||||
const blocker = { scenario, deploymentBlocker: true }
|
||||
const customerId = String(context.context.merchantQualificationFixture?.tenantId || '')
|
||||
context.assert(module, '客户作用域负向检查使用本轮正式客户', null, Boolean(customerId),
|
||||
`customerId=${customerId || 'missing'}`, blocker)
|
||||
if (!customerId) return
|
||||
|
||||
// Health endpoints are part of the deployed service contract. Warehouse is
|
||||
// special: a schema/readiness failure is intentionally HTTP 503, not a fake
|
||||
@@ -102,7 +106,7 @@ export async function run(context) {
|
||||
}
|
||||
for (const [name, path] of [
|
||||
['仓库非法状态', '/api/platform/v1/warehouse-admin?page=1&size=5&status=NOT_A_STATUS'],
|
||||
['仓库客户作用域', '/api/platform/v1/warehouse-admin?page=1&size=5&tenantId=10001'],
|
||||
['仓库客户作用域', `/api/platform/v1/warehouse-admin?page=1&size=5&tenantId=${encodeURIComponent(customerId)}`],
|
||||
]) {
|
||||
const response = await context.request('GET', path)
|
||||
context.addStep(module, `${name}安全失败`, response, isBusinessFailure(response),
|
||||
@@ -118,8 +122,8 @@ export async function run(context) {
|
||||
await checkPage(context, module, '托盘仓库筛选', `${palletBase}&warehouseId=${warehouseId}`, { scenario: 'warehouse-resources' })
|
||||
}
|
||||
const resourceScopeChecks = [
|
||||
['库位客户作用域', '/api/platform/v1/locations?page=1&size=5&tenantId=10001'],
|
||||
['托盘客户作用域', '/api/platform/v1/pallets?page=1&size=5&tenantId=10001'],
|
||||
['库位客户作用域', `/api/platform/v1/locations?page=1&size=5&tenantId=${encodeURIComponent(customerId)}`],
|
||||
['托盘客户作用域', `/api/platform/v1/pallets?page=1&size=5&tenantId=${encodeURIComponent(customerId)}`],
|
||||
['库位非法状态', `${locationBase}&status=ALLOCATED`],
|
||||
]
|
||||
for (const [name, path] of resourceScopeChecks) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const MODULE = 'suppliers'
|
||||
const OPTIONS = { scenario: 'platform-public-suppliers', deploymentBlocker: true }
|
||||
const BASE = '/api/platform/v1/suppliers'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
|
||||
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
||||
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
||||
|
||||
function ok(response) { return response?.http === 200 && String(response?.body?.code) === '200' }
|
||||
function code(response, expected) { return response?.http === 200 && String(response?.body?.code) === expected }
|
||||
|
||||
@@ -172,6 +172,48 @@ Assert-Contains $overseasCompose '/internal/lastleg/security/v1/events' 'Lastleg
|
||||
Assert-Contains $overseasCompose 'LASTLEG_SECURITY_SERVICE_URL' 'Lastleg 安全校验地址'
|
||||
Assert-NotContains $serviceJenkinsfile "'overseas-security-service'" '独立海外安全服务构建入口'
|
||||
|
||||
# 高变化语义必须在最近指引与服务契约中保持一致。
|
||||
Assert-Contains $adminAgents '海外出库(范围外现存代码)' 'Admin 四期范围边界'
|
||||
Assert-Contains $pdaAgents 'VITE_H5_API_BASE_URL' '国内 PDA H5 运行配置'
|
||||
Assert-Contains $overseasPdaAgents 'tests/app-icons.test.mjs' '海外 PDA 图标合同'
|
||||
Assert-Contains $backendAgents 'RestClientTraceInterceptor.java' '后端同步 Trace 入口'
|
||||
$oceanBookingOpenApi = Read-RequiredText (Join-Path $backendRoot 'firstleg/src/main/resources/openapi/ocean-bookings-v1.yaml')
|
||||
Assert-Contains $oceanBookingOpenApi 'never represents linked or ACTIVE containers' '订舱柜量语义'
|
||||
Assert-Contains $oceanBookingOpenApi '/api/platform/v1/ocean-bookings/link-candidates:' '订舱关联候选契约'
|
||||
Assert-Contains $oceanBookingOpenApi 'exact warehouseId, channelCode and containerType' '订舱关联候选筛选语义'
|
||||
|
||||
$adminOceanBookingApi = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/ocean-bookings/api/platformOceanBookings.ts')
|
||||
$adminOceanBookingView = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/ocean-bookings/pages/OceanBookingsView.vue')
|
||||
$adminOceanBookingViewTest = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/ocean-bookings/pages/OceanBookingsView.test.ts')
|
||||
Assert-Contains $adminOceanBookingApi '/link-candidates' 'Admin 订舱关联候选调用'
|
||||
Assert-Contains $adminOceanBookingView "label: '订舱柜量'" 'Admin 订舱柜量文案'
|
||||
Assert-Contains $adminOceanBookingViewTest 'without detail N+1' 'Admin 订舱列表无详情扇出合同'
|
||||
|
||||
# 已注册但超出当前四期边界的海外出库只能作为范围漂移事实存在,不得进入根正式业务路由。
|
||||
$adminOverseasRoutes = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/modules/overseas-warehouse/routes.ts')
|
||||
Assert-Contains $adminOverseasRoutes "path: 'overseas-warehouse/outbounds'" 'Admin 海外出库现存路由'
|
||||
Assert-Contains $adminAgents '海外出库(范围外现存代码)' 'Admin 海外出库范围警示'
|
||||
Assert-NotContains $rootAgents 'OutboundsView.vue' '根正式四期业务路由'
|
||||
|
||||
# Admin 仅对静态资源过期做一次受控恢复,不能吞掉普通路由错误或覆盖未保存编辑。
|
||||
$navigationRecovery = Read-RequiredText (Join-Path $workspaceRoot 'apps/admin-web/src/core/routing/navigationRecovery.ts')
|
||||
Assert-Contains $navigationRecovery 'const RECOVERY_WINDOW_MS = 30_000' 'Admin 路由恢复窗口'
|
||||
Assert-Contains $navigationRecovery 'if (dependencies.hasUnsavedWork() && !await dependencies.confirmDiscard()) return true' 'Admin 未保存编辑保护'
|
||||
Assert-Contains $navigationRecovery 'if (!isStaleRouteAssetError(error))' 'Admin 普通路由错误边界'
|
||||
|
||||
# 国内 PDA 的 H5 地址必须来自当前构建和同源代理,不能被历史登录缓存覆盖。
|
||||
$domesticPdaRuntime = Read-RequiredText (Join-Path $workspaceRoot 'apps/pda-uniapp/config/runtime.js')
|
||||
$domesticPdaVite = Read-RequiredText (Join-Path $workspaceRoot 'apps/pda-uniapp/vite.config.js')
|
||||
$domesticPdaSession = Read-RequiredText (Join-Path $workspaceRoot 'apps/pda-uniapp/stores/session.js')
|
||||
Assert-Contains $domesticPdaRuntime "VITE_H5_API_BASE_URL || '/api'" '国内 PDA H5 同源 API'
|
||||
Assert-Contains $domesticPdaVite 'API_PROXY_TARGET' '国内 PDA H5 代理上游'
|
||||
Assert-Contains $domesticPdaSession 'session.apiBase = PLATFORM_API_ROOT' '国内 PDA 当前构建 API'
|
||||
Assert-NotContains $domesticPdaSession 'apiBase: session.apiBase' '国内 PDA 会话持久化'
|
||||
|
||||
# 同步 RestClient Trace 必须注册为公共自动配置,不能只有孤立拦截器源码。
|
||||
$commonWebAutoConfigurations = Read-RequiredText (Join-Path $backendRoot 'common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports')
|
||||
Assert-Contains $commonWebAutoConfigurations 'com.baiyue.common.web.RestClientTracePropagationAutoConfiguration' 'RestClient Trace 自动配置注册'
|
||||
|
||||
# Maven 聚合声明的全部历史/当前模块必须在后端根指引中可定位。
|
||||
[xml]$pom = Get-Content -LiteralPath (Join-Path $backendRoot 'pom.xml') -Raw
|
||||
foreach ($module in $pom.project.modules.module) {
|
||||
@@ -189,7 +231,15 @@ $criticalPaths = @(
|
||||
'apps/admin-web/src/modules/overseas-warehouse/pages/DispatchDetailView.vue',
|
||||
'apps/admin-web/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue',
|
||||
'apps/admin-web/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue',
|
||||
'apps/admin-web/src/modules/container-loading/pages/ContainerDetailView.vue',
|
||||
'apps/admin-web/src/modules/container-loading/components/ContainerOverviewPanel.vue',
|
||||
'apps/admin-web/src/modules/container-loading/api/platformContainerLoading.ts',
|
||||
'apps/admin-web/src/modules/ocean-bookings/api/platformOceanBookings.ts',
|
||||
'apps/admin-web/src/modules/ocean-bookings/pages/OceanBookingsView.vue',
|
||||
'apps/admin-web/src/core/routing/navigationRecovery.ts',
|
||||
'apps/admin-web/src/core/routing/navigationRecovery.test.ts',
|
||||
'apps/admin-web/src/modules/overseas-warehouse/api/outbounds.ts',
|
||||
'apps/admin-web/src/modules/overseas-warehouse/pages/OutboundsView.vue',
|
||||
'apps/admin-web/src/modules/iam/pages/InternalUsersView.vue',
|
||||
'apps/admin-web/src/modules/iam/api/platformIam.ts',
|
||||
'apps/client-web/src/api/overseasHttp.ts',
|
||||
@@ -199,6 +249,9 @@ $criticalPaths = @(
|
||||
'apps/admin-web/src/modules/overseas-business/api/platformOverseasBusiness.ts',
|
||||
'apps/admin-web/src/modules/overseas-master-data/api/overseasMasterData.ts',
|
||||
'apps/pda-uniapp/pages/tasks/tasks.vue',
|
||||
'apps/pda-uniapp/config/runtime.js',
|
||||
'apps/pda-uniapp/.env.example',
|
||||
'apps/pda-uniapp/vite.config.js',
|
||||
'apps/pda-uniapp/utils/evidence-upload-queue.js',
|
||||
'apps/pda-overseas-uniapp/package.json',
|
||||
'apps/pda-overseas-uniapp/src/pages.json',
|
||||
@@ -210,6 +263,8 @@ $criticalPaths = @(
|
||||
'apps/pda-overseas-uniapp/src/api/receiving-adapter.js',
|
||||
'apps/pda-overseas-uniapp/src/api/overseas.js',
|
||||
'apps/pda-overseas-uniapp/src/utils/scan-broadcast.js',
|
||||
'apps/pda-overseas-uniapp/src/manifest.json',
|
||||
'apps/pda-overseas-uniapp/tests/app-icons.test.mjs',
|
||||
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/FeePaymentSnapshotValidator.java',
|
||||
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/client/FeePaymentVoucherFileClient.java',
|
||||
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java',
|
||||
@@ -221,6 +276,9 @@ $criticalPaths = @(
|
||||
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftContainerMapper.java',
|
||||
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformSmartLoadingController.java',
|
||||
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java',
|
||||
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/controller/PlatformOceanBookingController.java',
|
||||
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingMapper.java',
|
||||
'baiyue-firstleg/firstleg/src/main/resources/openapi/ocean-bookings-v1.yaml',
|
||||
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/port/in/PriceBookQueryUseCase.java',
|
||||
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/PriceBookQueryService.java',
|
||||
'baiyue-firstleg/firstleg/src/main/java/com/baiyue/firstleg/pricebookquery/application/port/out/PriceBookQueryReader.java',
|
||||
@@ -243,6 +301,9 @@ $criticalPaths = @(
|
||||
'baiyue-firstleg/common-security/src/main/java/com/baiyue/common/security/AuthenticatedUser.java',
|
||||
'baiyue-firstleg/common-security/src/main/java/com/baiyue/common/security/JwtClaimsMapper.java',
|
||||
'baiyue-firstleg/common-openapi/src/test/java/com/baiyue/common/openapi/PdaOpenApiGroupContractTest.java',
|
||||
'baiyue-firstleg/common-web/src/main/java/com/baiyue/common/web/RestClientTraceInterceptor.java',
|
||||
'baiyue-firstleg/common-web/src/main/java/com/baiyue/common/web/RestClientTracePropagationAutoConfiguration.java',
|
||||
'baiyue-firstleg/common-web/src/test/java/com/baiyue/common/web/RestClientTraceInterceptorTest.java',
|
||||
'baiyue-firstleg/gateway-overseas/src/main/java/com/baiyue/gateway/filter/PdaSessionValidationFilter.java',
|
||||
'baiyue-firstleg/gateway-overseas/src/main/java/com/baiyue/gateway/filter/HttpPdaSessionValidationClient.java',
|
||||
'baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/security/controller/LastlegSecurityController.java',
|
||||
@@ -312,6 +373,7 @@ $indexedAgentFiles = @(
|
||||
(Join-Path $backendRoot 'lastleg-service/AGENTS.md'),
|
||||
(Join-Path $backendRoot 'iam/AGENTS.md'),
|
||||
(Join-Path $backendRoot 'common-security/AGENTS.md'),
|
||||
(Join-Path $backendRoot 'common-web/AGENTS.md'),
|
||||
(Join-Path $backendRoot 'common-openapi/AGENTS.md'),
|
||||
(Join-Path $backendRoot 'warehouse-overseas-service/AGENTS.md'),
|
||||
(Join-Path $backendRoot 'event-bridge-service/AGENTS.md'),
|
||||
@@ -353,5 +415,5 @@ if ($failures.Count -gt 0) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖快速表唯一性、部署单元、前端模块、Client active 路由、Gateway 覆盖、PDA 注册页面与能力开关、Maven 模块、Lastleg 海外安全投影映射、关键入口和排除目录。" -ForegroundColor Green
|
||||
Write-Host "AGENTS 索引合同通过:$checked 项检查,覆盖快速表唯一性、部署单元、前端模块、Client active 路由、四期范围漂移、订舱候选与柜量语义、Gateway 覆盖、PDA 页面/运行配置/能力开关、Maven 模块、Lastleg 海外安全投影、同步 Trace、关键入口和排除目录。" -ForegroundColor Green
|
||||
exit 0
|
||||
|
||||
@@ -19,7 +19,7 @@ const args = parseArgs(process.argv.slice(2))
|
||||
const gateway = trimSlash(args.gateway ?? process.env.CLIENT_PHASE2_TEST_GATEWAY
|
||||
?? 'http://8.163.30.128:9000')
|
||||
const clientUsername = args['client-username'] ?? process.env.CLIENT_PHASE2_USERNAME
|
||||
?? await promptText('二期客户端账号', 'merchant_admin')
|
||||
?? await promptText('二期客户端账号(必须由正式注册/建档产生)', '')
|
||||
const clientPassword = args['client-password'] ?? process.env.CLIENT_PHASE2_PASSWORD
|
||||
?? await promptSecret('二期客户端密码')
|
||||
const platformUsername = args['platform-username'] ?? process.env.PLATFORM_USERNAME
|
||||
@@ -30,8 +30,8 @@ const forcedOrderId = args['order-id'] ?? process.env.CLIENT_PHASE2_ORDER_ID ??
|
||||
const reportDir = resolve(args['report-dir'] ?? process.env.CLIENT_PHASE2_REPORT_DIR ?? 'reports')
|
||||
const timeoutMs = Number(args['timeout-ms'] ?? process.env.CLIENT_PHASE2_TIMEOUT_MS ?? 30_000)
|
||||
|
||||
if (!clientPassword || !platformPassword) {
|
||||
console.error('Client and platform passwords are required')
|
||||
if (!clientUsername || !clientPassword || !platformPassword) {
|
||||
console.error('An onboarded Client account and platform credentials are required')
|
||||
process.exit(2)
|
||||
}
|
||||
if (!Number.isFinite(timeoutMs) || timeoutMs < 1_000) {
|
||||
|
||||
@@ -24,14 +24,14 @@ const platformUsername = args['platform-username'] ?? process.env.PLATFORM_USERN
|
||||
const platformPassword = args['platform-password'] ?? process.env.PLATFORM_PASSWORD
|
||||
?? await promptSecret('平台审核密码')
|
||||
const existingClientUsername = args['existing-client-username'] ?? process.env.EXISTING_CLIENT_USERNAME
|
||||
?? await promptText('跨客户隔离测试账号', 'merchant_admin')
|
||||
?? await promptText('跨客户隔离测试账号(必须由正式注册/建档产生)', '')
|
||||
const existingClientPassword = args['existing-client-password'] ?? process.env.EXISTING_CLIENT_PASSWORD
|
||||
?? await promptSecret('跨客户隔离测试密码')
|
||||
const reportDir = resolve(args['report-dir'] ?? process.env.CLIENT_TEST_REPORT_DIR ?? 'reports')
|
||||
const timeoutMs = Number(args['timeout-ms'] ?? process.env.CLIENT_TEST_TIMEOUT_MS ?? 30_000)
|
||||
|
||||
if (!platformPassword) {
|
||||
console.error('Platform password is required')
|
||||
if (!platformPassword || !existingClientUsername || !existingClientPassword) {
|
||||
console.error('Platform password and an independently onboarded client account are required')
|
||||
process.exit(2)
|
||||
}
|
||||
if (!Number.isFinite(timeoutMs) || timeoutMs < 1_000) {
|
||||
|
||||
@@ -56,7 +56,7 @@ test('entry accepts a separate overseas Gateway without exposing token state', (
|
||||
assert.match(source, /async requestOverseas\(/)
|
||||
assert.match(source, /async requestOverseasBinary\(/)
|
||||
assert.match(source, /async requestOverseasMultipart\(/)
|
||||
assert.match(source, /forbiddenValues = \[this\.options\.password, this\.token, this\.refreshToken, this\.overseasToken\]/)
|
||||
assert.match(source, /forbiddenValues = \[this\.options\.password, this\.options\.merchantPassword,[\s\S]*this\.token, this\.refreshToken, this\.overseasToken\]/)
|
||||
})
|
||||
|
||||
test('dual-token refresh replaces the whole session only after a complete response', async () => {
|
||||
|
||||
Reference in New Issue
Block a user