feat(finance): add customer wallet recharge flow

This commit is contained in:
2026-09-05 08:34:40 +08:00
parent 6002e4223b
commit 5792630231
24 changed files with 1285 additions and 24 deletions
@@ -4910,8 +4910,8 @@ BEGIN
'status VARCHAR(16) NOT NULL DEFAULT ''PENDING'',attempt_count INT NOT NULL DEFAULT 0,',
'compensation_count INT NOT NULL DEFAULT 0,next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),',
'lease_owner VARCHAR(128) NULL,lease_until DATETIME(3) NULL,last_error VARCHAR(1000) NULL,',
'dead_at DATETIME(3) NULL,sent_at DATETIME(3) NULL,created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),',
'updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),',
'dead_at DATETIME(3) NULL,sent_at DATETIME(3) NULL,created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间',',
'updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',',
'PRIMARY KEY(event_id),KEY idx_audit_outbox_publish(status,next_retry_at,lease_until,created_at),',
'KEY idx_audit_outbox_retention(status,sent_at)) ENGINE=InnoDB COMMENT=''可靠审计事件本地发送箱'''
);
@@ -16047,8 +16047,8 @@ CREATE TABLE IF NOT EXISTS baiyue_iam.iam_notification_template (
template_name VARCHAR(128) NOT NULL, category VARCHAR(64) NOT NULL, channel VARCHAR(16) NOT NULL DEFAULT 'IN_APP',
title_template VARCHAR(256) NOT NULL, body_template VARCHAR(10000) NOT NULL,
variable_whitelist_json JSON NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'DRAFT', version INT NOT NULL DEFAULT 1,
created_by BIGINT NOT NULL, updated_by BIGINT NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
created_by BIGINT NOT NULL, updated_by BIGINT NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
PRIMARY KEY (id), UNIQUE KEY uk_notification_template_code (tenant_id, template_code),
KEY idx_notification_template_list (tenant_id, status, updated_at)
) ENGINE=InnoDB COMMENT='消息模板;首期仅IN_APP';
@@ -16060,7 +16060,7 @@ CREATE TABLE IF NOT EXISTS baiyue_iam.iam_notification_task (
idempotency_key VARCHAR(128) NOT NULL, source_event_id VARCHAR(128) NULL, source_event_type VARCHAR(128) NULL,
status VARCHAR(16) NOT NULL, recipient_count INT NOT NULL DEFAULT 0, attempt_count INT NOT NULL DEFAULT 0,
scheduled_at DATETIME(3) NULL, sent_at DATETIME(3) NULL, last_error VARCHAR(1000) NULL, created_by BIGINT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
PRIMARY KEY (id), UNIQUE KEY uk_notification_task_no (task_no), UNIQUE KEY uk_notification_task_idempotency (idempotency_key),
UNIQUE KEY uk_notification_source_event (source_event_id, source_event_type), KEY idx_notification_task_dispatch (status, scheduled_at, created_at),
KEY idx_notification_task_tenant (tenant_id, created_at)
@@ -16070,7 +16070,7 @@ CREATE TABLE IF NOT EXISTS baiyue_iam.iam_notification_recipient (
id BIGINT NOT NULL, task_id BIGINT NOT NULL, tenant_id BIGINT NOT NULL, membership_id BIGINT NOT NULL,
user_id BIGINT NOT NULL, display_name VARCHAR(128) NOT NULL, app_code VARCHAR(16) NOT NULL, channel VARCHAR(16) NOT NULL DEFAULT 'IN_APP',
status VARCHAR(24) NOT NULL DEFAULT 'RESOLVED', failure_reason VARCHAR(1000) NULL, delivered_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
PRIMARY KEY (id), UNIQUE KEY uk_notification_recipient (task_id, membership_id, channel),
KEY idx_notification_recipient_member (tenant_id, membership_id, created_at)
) ENGINE=InnoDB COMMENT='消息接收人不可变快照';
@@ -16080,7 +16080,7 @@ CREATE TABLE IF NOT EXISTS baiyue_iam.iam_notification_inbox (
membership_id BIGINT NOT NULL, app_code VARCHAR(16) NOT NULL, category VARCHAR(64) NOT NULL,
title VARCHAR(256) NOT NULL, body VARCHAR(10000) NOT NULL, source_type VARCHAR(128) NULL, source_id VARCHAR(128) NULL,
status VARCHAR(16) NOT NULL DEFAULT 'UNREAD', sent_at DATETIME(3) NOT NULL, read_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
PRIMARY KEY (id), UNIQUE KEY uk_notification_inbox_recipient (recipient_id),
KEY idx_notification_inbox_list (tenant_id, membership_id, app_code, status, sent_at),
KEY idx_notification_inbox_category (tenant_id, membership_id, app_code, category, sent_at)
@@ -16089,8 +16089,8 @@ CREATE TABLE IF NOT EXISTS baiyue_iam.iam_notification_inbox (
CREATE TABLE IF NOT EXISTS baiyue_iam.iam_notification_event_inbox (
id BIGINT NOT NULL, source_event_id VARCHAR(128) NOT NULL, event_type VARCHAR(128) NOT NULL,
payload_hash CHAR(64) NOT NULL, task_id BIGINT NULL, status VARCHAR(16) NOT NULL DEFAULT 'PROCESSING',
last_error VARCHAR(1000) NULL, processed_at DATETIME(3) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
last_error VARCHAR(1000) NULL, processed_at DATETIME(3) NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
PRIMARY KEY (id), UNIQUE KEY uk_notification_event (source_event_id, event_type),
KEY idx_notification_event_status (status, updated_at)
) ENGINE=InnoDB COMMENT='业务事件消费幂等记录';
@@ -17606,3 +17606,208 @@ CALL baiyue_firstleg.assert_unified_final_schema();
DROP PROCEDURE IF EXISTS baiyue_firstleg.assert_unified_final_schema;
SELECT 'PASS' AS migration_status,
'统一增量全部执行完成;父子订单、履约分片、装柜门禁、订舱拖车、报关船期、船司查询、仓库角色、费用、Finance人工财务、公共资源、可靠审计及IAM安全事件结构校验通过' AS message;
-- 一件代发客户资金账户:充值先行,订单消费与冻结/解冻后续接入。
USE baiyue_finance;
CREATE TABLE IF NOT EXISTS fin_wallet_account (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和隔离键',
currency_code CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '当前仅CNY',
actual_balance DECIMAL(20,2) NOT NULL DEFAULT 0 COMMENT '账户实际余额,包含冻结资金',
frozen_balance DECIMAL(20,2) NOT NULL DEFAULT 0 COMMENT '冻结金额',
available_balance DECIMAL(20,2) NOT NULL DEFAULT 0 COMMENT '可用余额=实际余额-冻结金额',
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '余额乐观锁版本',
created_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建操作人ID',
updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后操作人ID',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_fin_wallet_account_customer (customer_id, currency_code),
CONSTRAINT chk_fin_wallet_account_customer CHECK (customer_id > 0),
CONSTRAINT chk_fin_wallet_account_currency CHECK (currency_code = 'CNY'),
CONSTRAINT chk_fin_wallet_account_balance CHECK (actual_balance >= 0 AND frozen_balance >= 0 AND available_balance >= 0 AND actual_balance = frozen_balance + available_balance)
) ENGINE=InnoDB COMMENT='客户CNY资金账户主账';
CREATE TABLE IF NOT EXISTS fin_recharge_request (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和隔离键',
request_no VARCHAR(32) NOT NULL COMMENT '充值申请业务编号RC-yyMMdd-####',
amount DECIMAL(20,2) NOT NULL COMMENT '申请充值金额',
payment_method VARCHAR(16) NOT NULL COMMENT 'WECHAT或ALIPAY',
voucher_file_id BIGINT UNSIGNED NOT NULL COMMENT '付款截图File ID',
voucher_file_name VARCHAR(255) NOT NULL COMMENT '付款截图文件名快照',
status VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT '审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回',
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '审核乐观锁版本',
submitted_by BIGINT UNSIGNED NOT NULL COMMENT '客户提交人ID',
submitted_by_name VARCHAR(128) NULL COMMENT '客户提交人名称快照',
reviewed_by BIGINT UNSIGNED NULL COMMENT '平台审核人ID',
reviewed_by_name VARCHAR(128) NULL COMMENT '平台审核人名称快照',
review_remark VARCHAR(500) NULL COMMENT '审核说明或驳回原因',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间',
reviewed_at DATETIME(3) NULL COMMENT '平台审核完成时间',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_fin_recharge_request_no (request_no),
KEY idx_fin_recharge_customer_status (customer_id, status, created_at, id),
CONSTRAINT chk_fin_recharge_customer CHECK (customer_id > 0),
CONSTRAINT chk_fin_recharge_amount CHECK (amount > 0),
CONSTRAINT chk_fin_recharge_method CHECK (payment_method IN ('WECHAT','ALIPAY')),
CONSTRAINT chk_fin_recharge_status CHECK (status IN ('PENDING','APPROVED','REJECTED'))
) ENGINE=InnoDB COMMENT='客户付款截图充值申请;平台审核通过后入账';
CREATE TABLE IF NOT EXISTS fin_wallet_ledger (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和隔离键',
business_no VARCHAR(32) NOT NULL COMMENT '账户变动业务编号AC-yyMMdd-####',
change_type VARCHAR(16) NOT NULL COMMENT '变动类型:RECHARGE充值、DEDUCTION扣款、FREEZE冻结、UNFREEZE解冻、CONSUMPTION消费',
amount DECIMAL(20,2) NOT NULL COMMENT '带方向变动金额,充值为正、扣款为负',
actual_balance_after DECIMAL(20,2) NOT NULL COMMENT '变动后实际余额',
frozen_balance_after DECIMAL(20,2) NOT NULL COMMENT '变动后冻结金额',
available_balance_after DECIMAL(20,2) NOT NULL COMMENT '变动后可用余额',
source_type VARCHAR(32) NOT NULL COMMENT 'CLIENT_RECHARGE/MANUAL_RECHARGE/MANUAL_DEDUCTION等',
source_no VARCHAR(128) NULL COMMENT '充值申请号或外部凭证号',
occurred_date DATE NOT NULL COMMENT '业务发生日期',
payment_method VARCHAR(32) NULL COMMENT '充值或扣款方式',
reference_no VARCHAR(128) NULL COMMENT '收款凭证号或关联单据号',
operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID',
operator_name VARCHAR(128) NULL COMMENT '操作人名称快照',
remark VARCHAR(500) NULL COMMENT '变动说明',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_fin_wallet_ledger_no (business_no),
KEY idx_fin_wallet_ledger_customer_time (customer_id, created_at, id),
KEY idx_fin_wallet_ledger_source (customer_id, source_type, source_no),
CONSTRAINT chk_fin_wallet_ledger_customer CHECK (customer_id > 0),
CONSTRAINT chk_fin_wallet_ledger_amount CHECK (amount <> 0),
CONSTRAINT chk_fin_wallet_ledger_balance CHECK (actual_balance_after >= 0 AND frozen_balance_after >= 0 AND available_balance_after >= 0 AND actual_balance_after = frozen_balance_after + available_balance_after)
) ENGINE=InnoDB COMMENT='客户资金账户不可变余额流水';
-- 客户资金账户权限初始化。
-- 客户资金账户与充值权限。只读访问和资金写操作使用独立权限。
USE baiyue_iam;
SET NAMES utf8mb4;
DROP PROCEDURE IF EXISTS migrate_wallet_permissions;
DELIMITER $$
CREATE PROCEDURE migrate_wallet_permissions()
BEGIN
IF EXISTS (
SELECT 1
FROM iam_permission
WHERE (id = 5105056 AND (tenant_id <> 0 OR app_code <> 'PLATFORM'
OR permission_type <> 'BUTTON'
OR permission_code <> 'platform:finance:recharge:review'))
OR (id = 5105057 AND (tenant_id <> 0 OR app_code <> 'PLATFORM'
OR permission_type <> 'BUTTON'
OR permission_code <> 'platform:finance:customer-account:adjust'))
OR (id = 5305020 AND (tenant_id <> 0 OR app_code <> 'MERCHANT'
OR permission_type <> 'MENU'
OR permission_code <> 'merchant:finance:account:view'))
OR (id = 5305021 AND (tenant_id <> 0 OR app_code <> 'MERCHANT'
OR permission_type <> 'BUTTON'
OR permission_code <> 'merchant:finance:recharge:create'))
OR (permission_code = 'platform:finance:recharge:review'
AND (id <> 5105056 OR tenant_id <> 0 OR app_code <> 'PLATFORM'))
OR (permission_code = 'platform:finance:customer-account:adjust'
AND (id <> 5105057 OR tenant_id <> 0 OR app_code <> 'PLATFORM'))
OR (permission_code = 'merchant:finance:account:view'
AND (id <> 5305020 OR tenant_id <> 0 OR app_code <> 'MERCHANT'))
OR (permission_code = 'merchant:finance:recharge:create'
AND (id <> 5305021 OR tenant_id <> 0 OR app_code <> 'MERCHANT'))
) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='资金账户权限初始化失败:权限ID、编码或类型冲突';
END IF;
INSERT INTO iam_permission
(id, tenant_id, app_code, permission_type, parent_id, permission_code,
permission_name, route_path, http_method, sort_no, status, built_in, deleted)
VALUES
(5105056,0,'PLATFORM','BUTTON',5105040,'platform:finance:recharge:review',
'审核客户充值',NULL,'POST',100,'ACTIVE',1,0),
(5105057,0,'PLATFORM','BUTTON',5105040,'platform:finance:customer-account:adjust',
'调整客户资金账户',NULL,'POST',110,'ACTIVE',1,0),
(5305020,0,'MERCHANT','MENU',5300000,'merchant:finance:account:view',
'资金账户','/finance/account','GET',50,'ACTIVE',1,0),
(5305021,0,'MERCHANT','BUTTON',5305020,'merchant:finance:recharge:create',
'提交充值申请',NULL,'POST',10,'ACTIVE',1,0)
ON DUPLICATE KEY UPDATE
permission_name=VALUES(permission_name), parent_id=VALUES(parent_id),
route_path=VALUES(route_path), http_method=VALUES(http_method),
sort_no=VALUES(sort_no), status='ACTIVE', built_in=1, deleted=0;
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
SELECT 0, role.id, permission.id, CURRENT_TIMESTAMP(3)
FROM iam_role role
JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
WHERE role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='PLATFORM_ADMIN'
AND role.status='ACTIVE' AND role.deleted=0
AND permission.permission_code IN (
'platform:finance:recharge:review',
'platform:finance:customer-account:adjust')
AND permission.status='ACTIVE' AND permission.deleted=0;
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
SELECT 0, role.id, permission.id, CURRENT_TIMESTAMP(3)
FROM iam_role role
JOIN iam_permission permission ON permission.tenant_id=0 AND permission.app_code='MERCHANT'
WHERE role.tenant_id=0 AND role.app_code='MERCHANT' AND role.role_code='MERCHANT_OWNER'
AND role.status='ACTIVE' AND role.deleted=0
AND permission.permission_code IN (
'merchant:finance:account:view',
'merchant:finance:recharge:create')
AND permission.status='ACTIVE' AND permission.deleted=0;
IF (SELECT COUNT(*) FROM iam_permission
WHERE tenant_id=0 AND app_code='PLATFORM' AND permission_type='BUTTON'
AND parent_id=5105040 AND status='ACTIVE' AND built_in=1 AND deleted=0
AND permission_code IN (
'platform:finance:recharge:review',
'platform:finance:customer-account:adjust')) <> 2 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='资金账户权限初始化失败:平台写权限资源不完整';
END IF;
IF (SELECT COUNT(*) FROM iam_permission
WHERE tenant_id=0 AND app_code='MERCHANT' AND status='ACTIVE' AND built_in=1 AND deleted=0
AND ((id=5305020 AND permission_type='MENU' AND parent_id=5300000
AND permission_code='merchant:finance:account:view')
OR (id=5305021 AND permission_type='BUTTON' AND parent_id=5305020
AND permission_code='merchant:finance:recharge:create'))) <> 2 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='资金账户权限初始化失败:客户权限资源不完整';
END IF;
IF (SELECT COUNT(*) FROM iam_role_permission grant_row
JOIN iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
JOIN iam_permission permission ON permission.id=grant_row.permission_id
WHERE role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='PLATFORM_ADMIN'
AND role.status='ACTIVE' AND role.deleted=0
AND permission.tenant_id=0 AND permission.app_code='PLATFORM'
AND permission.status='ACTIVE' AND permission.deleted=0
AND permission.permission_code IN (
'platform:finance:recharge:review',
'platform:finance:customer-account:adjust')) <> 2 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='资金账户权限初始化失败:平台超级管理员授权不完整';
END IF;
IF (SELECT COUNT(*) FROM iam_role_permission grant_row
JOIN iam_role role ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id
JOIN iam_permission permission ON permission.id=grant_row.permission_id
WHERE role.tenant_id=0 AND role.app_code='MERCHANT' AND role.role_code='MERCHANT_OWNER'
AND role.status='ACTIVE' AND role.deleted=0
AND permission.tenant_id=0 AND permission.app_code='MERCHANT'
AND permission.status='ACTIVE' AND permission.deleted=0
AND permission.permission_code IN (
'merchant:finance:account:view',
'merchant:finance:recharge:create')) <> 2 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='资金账户权限初始化失败:企业负责人授权不完整';
END IF;
END$$
DELIMITER ;
CALL migrate_wallet_permissions();
DROP PROCEDURE IF EXISTS migrate_wallet_permissions;
SELECT '统一增量全部执行完成;父子订单、履约分片、装柜门禁、订舱拖车、报关船期、船司查询、仓库角色、费用、Finance人工财务、客户资金账户、公共资源、可靠审计及IAM安全事件结构校验通过' AS message;