This commit is contained in:
2026-08-04 09:05:28 +08:00
parent fdb1e8af1c
commit 3afcff320a
116 changed files with 6749 additions and 749 deletions
+46
View File
@@ -0,0 +1,46 @@
name: firstleg-${DEPLOY_ENV:-test}-rocketmq
services:
rocketmq-namesrv:
image: apache/rocketmq:5.3.1
restart: unless-stopped
command: sh mqnamesrv
networks: [infra-network]
rocketmq-broker:
image: apache/rocketmq:5.3.1
restart: unless-stopped
depends_on: [rocketmq-namesrv]
environment:
ROCKETMQ_BROKER_IP: ${ROCKETMQ_BROKER_IP:?ROCKETMQ_BROKER_IP is required}
volumes:
- ./rocketmq:/config:ro
- ${ROCKETMQ_DATA_DIR:-/data/firstleg/rocketmq/store}:/home/rocketmq/store
command: sh /config/render-broker-config.sh
networks: [infra-network]
rocketmq-topic-init:
image: apache/rocketmq:5.3.1
depends_on: [rocketmq-broker]
environment:
# 必须与 File Service 完全一致;允许生产环境显式传入空字符串。
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX?MQ_TOPIC_PREFIX must be set; use test- for test and an empty value for production}
command: >-
sh -eu -c 'until sh mqadmin clusterList -n rocketmq-namesrv:9876 2>/dev/null
| grep -q "BaiyueCluster"; do sleep 2; done;
sh mqadmin updateTopic -n rocketmq-namesrv:9876 -c BaiyueCluster
-t "$${MQ_TOPIC_PREFIX}baiyue-file-export-command-v1" -r 4 -w 4;
sh mqadmin updateSubGroup -n rocketmq-namesrv:9876 -c BaiyueCluster
-g "$${MQ_TOPIC_PREFIX}baiyue-file-export-worker-v1";
sh mqadmin updateSubGroup -n rocketmq-namesrv:9876 -c BaiyueCluster
-g "$${MQ_TOPIC_PREFIX}baiyue-file-export-dlq-handler-v1";
sh mqadmin updateTopic -n rocketmq-namesrv:9876 -c BaiyueCluster
-t "%DLQ%$${MQ_TOPIC_PREFIX}baiyue-file-export-worker-v1" -r 1 -w 1 -p 6'
# Broker 注册较慢或任一初始化命令失败时整组重试;所有命令均幂等。
restart: on-failure
networks: [infra-network]
networks:
infra-network:
external: true
name: ${INFRA_NETWORK:-firstleg-test-infra-network}
+8
View File
@@ -124,6 +124,14 @@ services:
FILE_DB_USERNAME: ${FILE_DB_USERNAME}
FILE_DB_PASSWORD: ${FILE_DB_PASSWORD}
FILE_STORAGE_ROOT: /data/files
JAVA_TOOL_OPTIONS: "-Xms1g -Xmx2g -XX:+ExitOnOutOfMemoryError -Dfile.encoding=UTF-8"
ROCKETMQ_NAME_SERVER: ${ROCKETMQ_NAME_SERVER}
# 必须显式设置:测试填 test-,生产填空字符串;使用 ? 而不是 :?,允许生产空值。
MQ_TOPIC_PREFIX: ${MQ_TOPIC_PREFIX?MQ_TOPIC_PREFIX must be set; use test- for test and an empty value for production}
FILE_EXPORT_CONSUMER_ENABLED: ${FILE_EXPORT_CONSUMER_ENABLED:-false}
FILE_EXPORT_CONSUMER_CONCURRENCY: ${FILE_EXPORT_CONSUMER_CONCURRENCY:-4}
FILE_EXPORT_PUBLISHER_ENABLED: ${FILE_EXPORT_PUBLISHER_ENABLED:-false}
FILE_EXPORT_RECONCILER_ENABLED: ${FILE_EXPORT_RECONCILER_ENABLED:-false}
print:
<<: *common
+5
View File
@@ -5,3 +5,8 @@ MYSQL_DATABASE=baiyue
MYSQL_USER=baiyue
MYSQL_PASSWORD=CHANGE_ME_TEST_MYSQL_PASSWORD
REDIS_PASSWORD=CHANGE_ME_TEST_REDIS_PASSWORD
# RocketMQ 与 File Service 位于同一 Docker 网络,Broker 向客户端公布服务名。
ROCKETMQ_BROKER_IP=rocketmq-broker
# 测试环境必须显式使用 test-;生产环境在对应凭据文件中显式设置为空字符串。
MQ_TOPIC_PREFIX=test-
ROCKETMQ_DATA_DIR=/data/firstleg/rocketmq/store
+11
View File
@@ -0,0 +1,11 @@
brokerClusterName=BaiyueCluster
brokerName=baiyue-file-export-broker
brokerId=0
brokerRole=ASYNC_MASTER
flushDiskType=ASYNC_FLUSH
deleteWhen=04
fileReservedTime=72
autoCreateTopicEnable=false
autoCreateSubscriptionGroup=false
listenPort=10911
brokerIP1=__BROKER_IP__
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
set -eu
case "${ROCKETMQ_BROKER_IP:-}" in
""|localhost|127.0.0.1|__BROKER_IP__|*'${'*)
echo "ROCKETMQ_BROKER_IP must be an explicit client-reachable address" >&2
exit 1
;;
esac
sed "s/__BROKER_IP__/${ROCKETMQ_BROKER_IP}/g" /config/broker.conf.template > /tmp/broker.conf
if grep -Eq '__BROKER_IP__|\$\{' /tmp/broker.conf; then
echo "broker.conf still contains an unresolved placeholder" >&2
grep -nE '__BROKER_IP__|\$\{' /tmp/broker.conf >&2
exit 1
fi
exec sh mqbroker -n rocketmq-namesrv:9876 -c /tmp/broker.conf
@@ -32,8 +32,15 @@ CALL regression_assert_table('iam_permission');
CALL regression_assert_table('iam_role');
CALL regression_assert_table('iam_role_permission');
CALL regression_assert_table('iam_tenant');
CALL regression_assert_table('iam_tenant_review');
CALL regression_assert_table('iam_tenant_qualification');
CALL regression_assert_table('iam_membership');
DROP PROCEDURE regression_assert_table;
ALTER TABLE iam_tenant
MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'PROFILE_REQUIRED'
COMMENT '租户状态:PROFILE_REQUIRED、PENDING、ACTIVE、REJECTED、DISABLED';
SET @iam_permission_built_in_sql = IF(
EXISTS(
SELECT 1 FROM information_schema.columns
@@ -48,6 +55,50 @@ PREPARE iam_permission_built_in_stmt FROM @iam_permission_built_in_sql;
EXECUTE iam_permission_built_in_stmt;
DEALLOCATE PREPARE iam_permission_built_in_stmt;
-- 审核通过自动开户依赖全局企业管理员模板。通过自然键幂等补齐,避免存量环境
-- 已存在同编码但 ID 不同的角色时,把权限错误授予固定 ID。
INSERT INTO iam_role (
id, tenant_id, app_code, role_code, role_name, role_type, status,
description, created_by, updated_by, deleted, version
) VALUES (
4000100, 0, 'MERCHANT', 'MERCHANT_OWNER', '企业管理员', 'SYSTEM', 'ACTIVE',
'企业审核通过后自动创建的管理员角色模板', 1000001, 1000001, 0, 0
)
ON DUPLICATE KEY UPDATE
role_name = VALUES(role_name), role_type = 'SYSTEM', status = 'ACTIVE',
description = VALUES(description), deleted = 0;
-- 从一期商户权限基线复制全局模板;不创建演示租户或业务数据。
INSERT INTO iam_permission (
id, tenant_id, app_code, permission_type, parent_id, permission_code,
permission_name, route_path, http_method, sort_no, status, built_in, deleted
)
SELECT permission.id + 100000, 0, permission.app_code, permission.permission_type,
CASE WHEN permission.parent_id = 0 THEN 0 ELSE permission.parent_id + 100000 END,
permission.permission_code, permission.permission_name, permission.route_path,
permission.http_method, permission.sort_no, 'ACTIVE', 1, 0
FROM iam_permission permission
WHERE permission.tenant_id = 10001 AND permission.app_code = 'MERCHANT'
AND permission.permission_type IN ('MENU', 'BUTTON')
AND permission.deleted = 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 = VALUES(http_method), sort_no = VALUES(sort_no),
status = 'ACTIVE', built_in = 1, 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
JOIN iam_permission permission
ON permission.tenant_id = 0 AND permission.app_code = 'MERCHANT'
AND permission.permission_type IN ('MENU', 'BUTTON')
AND permission.status = 'ACTIVE' AND permission.deleted = 0
WHERE owner_role.tenant_id = 0 AND owner_role.app_code = 'MERCHANT'
AND owner_role.role_code = 'MERCHANT_OWNER'
AND owner_role.status = 'ACTIVE' AND owner_role.deleted = 0
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
-- 一期客户档案字段;与 iam/db/phase1-customer-profile.sql 保持一致。
DROP PROCEDURE IF EXISTS regression_add_iam_column;
DELIMITER $$
@@ -73,6 +124,7 @@ END$$
DELIMITER ;
CALL regression_add_iam_column('contact_wechat', 'VARCHAR(64) NULL COMMENT ''联系微信'' AFTER contact_email');
CALL regression_add_iam_column('legal_representative', 'VARCHAR(64) NULL COMMENT ''法定代表人'' AFTER legal_name');
CALL regression_add_iam_column('sales_owner_name', 'VARCHAR(64) NULL COMMENT ''销售负责人姓名'' AFTER contact_wechat');
CALL regression_add_iam_column('payment_term_days', 'SMALLINT UNSIGNED NULL COMMENT ''账期天数'' AFTER sales_owner_name');
CALL regression_add_iam_column('enterprise_type', 'VARCHAR(64) NULL COMMENT ''企业类型'' AFTER payment_term_days');
@@ -81,8 +133,40 @@ CALL regression_add_iam_column('registered_capital', 'DECIMAL(18,2) NULL COMMENT
CALL regression_add_iam_column('registered_capital_currency', 'CHAR(3) NULL COMMENT ''注册资本币种 ISO 4217'' AFTER registered_capital');
CALL regression_add_iam_column('registered_address', 'VARCHAR(500) NULL COMMENT ''注册地址文本'' AFTER registered_capital_currency');
CALL regression_add_iam_column('internal_remark', 'VARCHAR(1000) NULL COMMENT ''平台内部备注'' AFTER registered_address');
CALL regression_add_iam_column('approved_at', 'DATETIME(3) NULL COMMENT ''审核通过时间'' AFTER internal_remark');
CALL regression_add_iam_column('approved_by', 'BIGINT NULL COMMENT ''审核人ID'' AFTER approved_at');
CALL regression_add_iam_column('reject_reason', 'VARCHAR(500) NULL COMMENT ''驳回原因'' AFTER approved_by');
DROP PROCEDURE regression_add_iam_column;
-- 注册审核和资质替换依赖的辅助字段;存量测试库也必须具备同一契约。
DROP PROCEDURE IF EXISTS regression_add_iam_related_column;
DELIMITER $$
CREATE PROCEDURE regression_add_iam_related_column(
IN p_table_name VARCHAR(64),
IN p_column_name VARCHAR(64),
IN p_definition VARCHAR(1000)
)
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = p_table_name
AND column_name = p_column_name
) THEN
SET @regression_iam_related_sql = CONCAT(
'ALTER TABLE `', p_table_name, '` ADD COLUMN `', p_column_name, '` ', p_definition
);
PREPARE regression_iam_related_stmt FROM @regression_iam_related_sql;
EXECUTE regression_iam_related_stmt;
DEALLOCATE PREPARE regression_iam_related_stmt;
END IF;
END$$
DELIMITER ;
CALL regression_add_iam_related_column('iam_tenant_qualification', 'review_comment', 'VARCHAR(500) NULL COMMENT ''审核意见''');
CALL regression_add_iam_related_column('iam_membership', 'created_by', 'BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''创建人ID''');
CALL regression_add_iam_related_column('iam_membership', 'updated_by', 'BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''最后修改人ID''');
DROP PROCEDURE regression_add_iam_related_column;
-- 先写入父菜单,再写入按钮,避免新增环境出现悬空父节点。
INSERT INTO iam_permission
(id, tenant_id, app_code, permission_type, parent_id, permission_code,
@@ -122,6 +206,9 @@ VALUES
(5100012, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:edit', '编辑企业客户', NULL, NULL, 20, 'ACTIVE', 1, 0),
(5100013, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:status', '启停企业客户', NULL, NULL, 30, 'ACTIVE', 1, 0),
(5100015, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:export', '导出企业客户', NULL, NULL, 40, 'ACTIVE', 1, 0),
(5100020, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:review', '客户审核', NULL, NULL, 50, 'ACTIVE', 1, 0),
(5100021, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:approve', '审核通过', NULL, NULL, 60, 'ACTIVE', 1, 0),
(5100022, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:reject', '审核驳回', NULL, NULL, 70, 'ACTIVE', 1, 0),
(5101011, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:create', '新增内部用户', NULL, NULL, 10, 'ACTIVE', 1, 0),
(5101012, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:edit', '编辑内部用户', NULL, NULL, 20, 'ACTIVE', 1, 0),
(5101013, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:status', '启停内部用户', NULL, NULL, 30, 'ACTIVE', 1, 0),
@@ -207,6 +294,7 @@ SELECT r.id, p.id, 0
AND p.permission_code IN (
'platform:customer', 'platform:customer:list', 'platform:customer:create',
'platform:customer:view', 'platform:customer:edit', 'platform:customer:status', 'platform:customer:export',
'platform:customer:review', 'platform:customer:approve', 'platform:customer:reject',
'platform:system', 'platform:user:list', 'platform:user:create', 'platform:user:edit',
'platform:user:status', 'platform:user:reset-password', 'platform:user:grant-role',
'platform:role:list', 'platform:role:create', 'platform:role:grant',
@@ -698,6 +786,7 @@ WHERE deleted = 0
AND permission_code IN (
'platform:customer', 'platform:customer:list', 'platform:customer:create',
'platform:customer:view', 'platform:customer:edit', 'platform:customer:status', 'platform:customer:export',
'platform:customer:review', 'platform:customer:approve', 'platform:customer:reject',
'platform:system', 'platform:user:list', 'platform:user:create', 'platform:user:edit',
'platform:user:status', 'platform:user:reset-password', 'platform:user:grant-role',
'platform:role:list', 'platform:role:create', 'platform:role:grant',
@@ -719,17 +808,82 @@ WHERE deleted = 0
SELECT
'IAM客户档案字段' AS check_item,
COUNT(*) AS actual_count,
9 AS expected_count,
IF(COUNT(*) = 9, '通过', '请检查客户档案字段迁移') AS check_result
10 AS expected_count,
IF(COUNT(*) = 10, '通过', '请检查客户档案字段迁移') AS check_result
FROM information_schema.columns
WHERE table_schema = 'baiyue_iam'
AND table_name = 'iam_tenant'
AND column_name IN (
'contact_wechat', 'sales_owner_name', 'payment_term_days', 'enterprise_type',
'legal_representative', 'contact_wechat', 'sales_owner_name', 'payment_term_days', 'enterprise_type',
'established_date', 'registered_capital', 'registered_capital_currency',
'registered_address', 'internal_remark'
);
SELECT
'IAM注册审核字段' AS check_item,
COUNT(*) AS actual_count,
4 AS expected_count,
IF(COUNT(*) = 4, '通过', '请检查租户审核状态和结果字段') AS check_result
FROM information_schema.columns
WHERE table_schema = 'baiyue_iam'
AND table_name = 'iam_tenant'
AND column_name IN ('status', 'approved_at', 'approved_by', 'reject_reason');
SELECT
'IAM资质与成员审计字段' AS check_item,
(
SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_iam'
AND table_name = 'iam_tenant_qualification'
AND column_name = 'review_comment'
) + (
SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_iam'
AND table_name = 'iam_membership'
AND column_name IN ('created_by', 'updated_by')
) AS actual_count,
3 AS expected_count,
IF(
(
SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_iam'
AND table_name = 'iam_tenant_qualification'
AND column_name = 'review_comment'
) + (
SELECT COUNT(*) FROM information_schema.columns
WHERE table_schema = 'baiyue_iam'
AND table_name = 'iam_membership'
AND column_name IN ('created_by', 'updated_by')
) = 3,
'通过', '请检查资质审核备注和成员审计字段'
) AS check_result;
SELECT
'IAM全局企业管理员模板' AS check_item,
COUNT(DISTINCT owner_role.id) AS role_count,
COUNT(DISTINCT role_permission.permission_id) AS actual_permission_count,
(SELECT COUNT(*) FROM baiyue_iam.iam_permission permission
WHERE permission.tenant_id = 0 AND permission.app_code = 'MERCHANT'
AND permission.permission_type IN ('MENU', 'BUTTON')
AND permission.status = 'ACTIVE' AND permission.deleted = 0) AS expected_permission_count,
IF(
COUNT(DISTINCT owner_role.id) = 1
AND COUNT(DISTINCT role_permission.permission_id) = (
SELECT COUNT(*) FROM baiyue_iam.iam_permission permission
WHERE permission.tenant_id = 0 AND permission.app_code = 'MERCHANT'
AND permission.permission_type IN ('MENU', 'BUTTON')
AND permission.status = 'ACTIVE' AND permission.deleted = 0
)
AND COUNT(DISTINCT role_permission.permission_id) > 0,
'通过', '请检查全局MERCHANT_OWNER角色及授权'
) AS check_result
FROM baiyue_iam.iam_role owner_role
LEFT JOIN baiyue_iam.iam_role_permission role_permission
ON role_permission.role_id = owner_role.id AND role_permission.tenant_id = 0
WHERE owner_role.tenant_id = 0 AND owner_role.app_code = 'MERCHANT'
AND owner_role.role_code = 'MERCHANT_OWNER'
AND owner_role.role_type = 'SYSTEM' AND owner_role.status = 'ACTIVE' AND owner_role.deleted = 0;
SELECT
table_name,
COUNT(*) AS column_count
+46 -2
View File
@@ -16,7 +16,10 @@
1. IAM 基线表必须已存在(`iam.sql`)。
2. 权限父菜单和既有客户列表/审核权限基线(`phase1-admin-rbac.sql`、`rbac-role-management-v2.sql`)。
3. `phase1-platform-master-data-and-permission-management.sql` 中的 `platform:customer:view`、既有角色继承及管理员授权。
4. `phase1-customer-profile.sql` 中的九个 `iam_tenant` 客户档案字段。
4. `phase1-customer-profile.sql` 中的十个 `iam_tenant` 客户档案字段。
5. `phase1-onboarding.sql` 中的 `PROFILE_REQUIRED` 默认状态、审核结果字段、资质审核备注及成员审计字段。
统一入口同时幂等补齐审核自动开户所需的全局 `MERCHANT_OWNER` 角色模板、全局商户菜单/按钮权限和角色授权;执行后应使用 `iam/src/test/resources/db/verify-phase1-admin-rbac.sql` 确认模板存在且权限完整。
测试环境只执行统一入口;模块脚本用于新环境基线构建或单模块升级,不应在统一入口执行后再次串行执行。
@@ -24,4 +27,45 @@
在 Navicat、DataGrip 或 MySQL 命令行中以“运行脚本”方式完整执行,确保客户端支持 `DELIMITER`。不要只选中局部语句执行。
执行结束后必须看到 `MIGRATION_COMPLETED`,并确认其前面的 IAM 权限、IAM 客户档案九字段、Warehouse 表结构、Order 幂等表和 Firstleg 索引核验均通过。
执行结束后必须看到 `MIGRATION_COMPLETED`,并确认其前面的 IAM 权限、IAM 客户档案十字段、IAM 注册审核字段、资质与成员审计字段、Warehouse 表结构、Order 幂等表和 Firstleg 索引核验均通过。
## 公共导出中心开发阶段重建
统一迁移脚本中的 `file_export_task` 是一期旧基线,不能自动升级已经存在的表。公共导出中心尚未进入生产、测试环境确认没有需要保留的导出任务时,按下列顺序重建;这一步是公共导出中心上线的额外步骤,不得用于已有真实导出数据的生产环境。
先只读确认任务数为 0:
```sql
SELECT COUNT(*) AS export_task_count FROM baiyue_file.file_export_task;
```
只有结果为 0 才允许执行:
```sql
USE baiyue_file;
DROP TABLE IF EXISTS file_export_outbox;
DROP TABLE IF EXISTS file_export_task_guard;
DROP TABLE IF EXISTS file_export_task;
SOURCE file/src/main/resources/db/file.sql;
```
`SOURCE` 的相对路径以 MySQL 客户端启动目录为准;图形客户端可直接完整执行 `file/src/main/resources/db/file.sql`。完成后必须核验:
```sql
SELECT COUNT(*) = 3 AS export_tables_ready
FROM information_schema.tables
WHERE table_schema = 'baiyue_file'
AND table_name IN ('file_export_task', 'file_export_outbox', 'file_export_task_guard');
SELECT COUNT(*) = 1 AS creation_guard_ready
FROM baiyue_file.file_export_task_guard
WHERE guard_key = 'GLOBAL_CREATE';
SELECT COUNT(*) = 8 AS scheduler_columns_ready
FROM information_schema.columns
WHERE table_schema = 'baiyue_file' AND table_name = 'file_export_task'
AND column_name IN ('dispatch_version', 'attempt_count', 'max_attempts', 'next_retry_at',
'lease_owner', 'lease_until', 'last_heartbeat_at', 'execution_deadline_at');
```
任一结果不是 1,禁止启动 File Service。后续一旦表内存在生产数据,必须改用可重复执行的增量迁移,不能继续 DROP 重建。