版本: v1.0
日期: 2026-04-08
模块: gp104_escrow
1. 背景与目标
1.1 背景
PayBy 决定退出 FAB 的 SVA(Stored Value Account)功能。退出前需将所有 SVA 卡上的余额清空(归零),确保 Pool Account 资金与卡余额一致归零后,完成业务退出。
1.2 目标
- 基于 FAB 提供的余额文件,与系统内余额进行比对核验
- 在确认一致后,生成余额清空交易列表,逐笔报送 FAB
- 处理因卡状态异常导致的报送失败,通过与 FAB 协调修复后重新报送
- 最终实现所有卡余额清零,Pool Account 余额归零
1.3 约束条件
- 所有卡总余额 = Pool Account 余额(FAB 提供的文件需满足此等式)
- 需要设定一个系统截止时间点(Cutoff Time),截止后不再接受新交易
- 余额清空为不可逆操作,需经过 Operator 确认后方可执行
2. 整体流程概览
flowchart TD
A[FAB 提供余额文件] --> B[Operator 通过 Counter 上传余额文件]
B --> C[设定系统截止时间点 Cutoff Time]
C --> D[系统生成内部余额快照]
D --> E[余额比对: FAB文件 vs 系统快照]
E --> F{比对结果}
F -->|一致| G[生成汇总报告]
F -->|不一致| H[生成差异明细报告]
H --> I[Operator 审核差异]
I -->|确认差异可接受| G
I -->|需要调整| J[与 FAB 协调处理差异]
J --> A
G --> K[Operator 确认提交]
K --> L[生成余额清空交易列表]
L --> M[逐批报送 FAB]
M --> N{报送结果}
N -->|全部成功| O[余额清空完成]
N -->|部分失败| P[生成失败卡列表]
P --> Q[发送失败列表给 FAB 修改卡状态]
Q --> R[FAB 修复卡状态]
R --> M
O --> S[Pool Account 余额归零确认]
3. 详细流程设计
3.1 阶段一:余额文件上传与截止时间设定
sequenceDiagram
participant FAB as FAB Bank
participant OP as Operator
participant CT as Counter (UI)
participant SYS as Escrow System
participant UFS as UFS 文件服务
FAB->>OP: 提供余额文件(CSV)
OP->>CT: 上传余额文件 + 设定截止时间
CT->>UFS: 存储余额文件
UFS-->>CT: 返回 fileTag
CT->>SYS: 发起余额清空请求(fileTag + cutoffTime)
SYS->>SYS: 创建清空批次记录(ClearingBatch)
SYS->>SYS: 设定系统截止时间
SYS-->>CT: 返回批次号(clearingBatchId)
FAB 余额文件格式(CSV):
| 字段 | 说明 | 示例 |
|---|---|---|
| CardID | 卡ID | 1234567890 |
| Balance | 卡余额(AED) | 150.00 |
文件末尾包含汇总行:
| 字段 | 说明 |
|---|---|
| Total Cards | 卡总数 |
| Total Balance | 总余额(应等于 Pool Account 余额) |
3.2 阶段二:系统余额快照生成
sequenceDiagram
participant SYS as Escrow System
participant DB as Database
participant QRY as Query Service
SYS->>SYS: 触发余额快照(cutoffTime)
SYS->>DB: 查询所有活跃卡列表(t_issue_record + t_card_detail)
DB-->>SYS: 卡列表(cardId, memberId)
loop 分批处理(batch_size=200)
SYS->>QRY: 查询会员账户余额(BASIC + RED_PACKET_SETTLE + OUTER_TRANSFER_SETTLE)
QRY-->>SYS: 各账户类型余额
SYS->>SYS: 汇总每张卡的系统总余额
SYS->>DB: 存储余额快照明细(t_clearing_balance_detail)
end
SYS->>DB: 更新批次状态为 SNAPSHOT_DONE
3.3 阶段三:余额比对
sequenceDiagram
participant SYS as Escrow System
participant UFS as UFS 文件服务
participant DB as Database
SYS->>UFS: 下载 FAB 余额文件
UFS-->>SYS: 文件内容
SYS->>SYS: 解析 CSV 文件
SYS->>DB: 读取系统余额快照
SYS->>SYS: 逐卡比对
Note over SYS: FAB余额 vs 系统余额(BASIC+RED_PACKET+TRANSFER)
alt 完全匹配
SYS->>DB: 标记明细状态为 MATCHED
else 金额不一致
SYS->>DB: 标记明细状态为 MISMATCHED, 记录差异金额
else FAB文件中有但系统无
SYS->>DB: 标记为 FAB_ONLY
else 系统有但FAB文件无
SYS->>DB: 标记为 SYSTEM_ONLY
end
SYS->>SYS: 生成汇总信息
SYS->>DB: 更新批次状态为 RECONCILED, 存储汇总
比对汇总信息:
总卡数(FAB): 1500
总卡数(系统): 1500
匹配成功: 1485
金额不一致: 10
仅FAB存在: 3
仅系统存在: 2
FAB总余额: 500,000.00 AED
系统总余额: 500,000.00 AED
差异总额: 150.00 AED3.4 阶段四:确认与生成清空交易
sequenceDiagram
participant OP as Operator
participant CT as Counter (UI)
participant SYS as Escrow System
participant DB as Database
OP->>CT: 查看比对汇总和差异明细
OP->>CT: 确认提交(clearingBatchId)
CT->>SYS: 提交确认请求
SYS->>DB: 查询所有余额>0的卡
loop 每张有余额的卡
SYS->>SYS: 创建扣减交易记录
Note over SYS: amount = 卡余额
direction = negative
afterBalance = 0 SYS->>DB: 插入 t_clearing_transaction end SYS->>DB: 更新批次状态为 CONFIRMED SYS->>SYS: 生成清空交易汇总 SYS-->>CT: 返回交易列表汇总
direction = negative
afterBalance = 0 SYS->>DB: 插入 t_clearing_transaction end SYS->>DB: 更新批次状态为 CONFIRMED SYS->>SYS: 生成清空交易汇总 SYS-->>CT: 返回交易列表汇总
3.5 阶段五:交易报送 FAB
sequenceDiagram
participant SYS as Escrow System
participant DB as Database
participant WAVE as Wave/FAB Channel
participant FAB as FAB Bank
SYS->>DB: 查询待报送交易(status=INIT)
loop 分批报送(batch)
SYS->>WAVE: 报送扣减交易
WAVE->>FAB: 发送交易请求
FAB-->>WAVE: 返回处理结果
WAVE-->>SYS: 报送结果
alt 成功
SYS->>DB: 更新交易状态为 SUCCESS
else 失败(卡状态异常)
SYS->>DB: 更新交易状态为 FAILED, 记录失败原因
end
end
SYS->>DB: 更新批次进度
3.6 阶段六:失败重试流程
flowchart TD
A[报送完成, 存在失败记录] --> B[生成失败卡列表文件]
B --> C[Operator 下载失败列表]
C --> D[发送给 FAB 请求修复卡状态]
D --> E[FAB 修复卡状态]
E --> F[FAB 确认修复完成]
F --> G[Operator 通过 Counter 触发重新报送]
G --> H[系统重新报送失败交易]
H --> I{全部成功?}
I -->|是| J[本轮清空完成]
I -->|否| B
J --> K{所有交易完成?}
K -->|是| L[余额清空全部完成]
K -->|否| A
sequenceDiagram
participant OP as Operator
participant CT as Counter (UI)
participant SYS as Escrow System
participant FAB as FAB Bank
SYS->>SYS: 汇总失败交易列表
SYS->>SYS: 生成失败卡列表文件(CSV)
OP->>CT: 下载失败列表
OP->>FAB: 邮件发送失败卡列表, 请求修复状态
FAB->>FAB: 修复卡状态(解冻/激活等)
FAB->>OP: 确认修复完成
OP->>CT: 触发重新报送(clearingBatchId)
CT->>SYS: 重试报送请求
SYS->>SYS: 查询 FAILED 状态交易
SYS->>FAB: 重新报送
FAB-->>SYS: 返回结果
alt 全部成功
SYS->>SYS: 更新批次状态为 COMPLETED
else 仍有失败
SYS->>SYS: 更新失败记录, 等待下轮处理
end
4. 状态机设计
4.1 清空批次状态(ClearingBatch)
stateDiagram-v2
[*] --> INIT: 创建批次
INIT --> SNAPSHOT_DONE: 余额快照完成
SNAPSHOT_DONE --> RECONCILED: 余额比对完成
RECONCILED --> CONFIRMED: Operator确认提交
CONFIRMED --> REPORTING: 开始报送
REPORTING --> PARTIALLY_DONE: 部分成功/部分失败
REPORTING --> COMPLETED: 全部报送成功
PARTIALLY_DONE --> REPORTING: 重新报送失败交易
PARTIALLY_DONE --> COMPLETED: 重试后全部成功
COMPLETED --> [*]
INIT --> CANCELLED: 取消
SNAPSHOT_DONE --> CANCELLED: 取消
RECONCILED --> CANCELLED: 取消
CANCELLED --> [*]
4.2 清空交易状态(ClearingTransaction)
stateDiagram-v2
[*] --> INIT: 生成交易记录
INIT --> PROCESSING: 开始报送
PROCESSING --> SUCCESS: FAB返回成功
PROCESSING --> FAILED: FAB返回失败
FAILED --> PROCESSING: 重新报送
SUCCESS --> [*]
4.3 余额比对明细状态(ClearingBalanceDetail)
stateDiagram-v2
[*] --> PENDING: 快照生成
PENDING --> MATCHED: 比对一致
PENDING --> MISMATCHED: 金额不一致
PENDING --> FAB_ONLY: 仅FAB存在
PENDING --> SYSTEM_ONLY: 仅系统存在
MATCHED --> [*]
MISMATCHED --> [*]
FAB_ONLY --> [*]
SYSTEM_ONLY --> [*]
5. 数据库设计
5.1 新增表
t_clearing_batch(清空批次表)
| 字段 | 类型 | 说明 |
|---|---|---|
| clearing_batch_id | BIGINT AUTO_INCREMENT | 主键 |
| batch_no | VARCHAR(64) | 批次号(唯一) |
| bank_code | VARCHAR(16) | 银行编码: FAB |
| cutoff_time | DATETIME | 系统截止时间 |
| fab_file_tag | VARCHAR(256) | FAB余额文件 UFS fileTag |
| fab_total_cards | INT | FAB文件卡总数 |
| fab_total_balance | DECIMAL(18,2) | FAB文件总余额 |
| sys_total_cards | INT | 系统卡总数 |
| sys_total_balance | DECIMAL(18,2) | 系统总余额 |
| matched_count | INT | 匹配成功数 |
| mismatched_count | INT | 不匹配数 |
| fab_only_count | INT | 仅FAB存在数 |
| sys_only_count | INT | 仅系统存在数 |
| diff_amount | DECIMAL(18,2) | 差异总额 |
| total_transactions | INT | 总交易数 |
| success_count | INT | 报送成功数 |
| failed_count | INT | 报送失败数 |
| status | VARCHAR(20) | 批次状态 |
| operator | VARCHAR(64) | 操作人 |
| memo | VARCHAR(512) | 备注 |
| gmt_create | DATETIME | 创建时间 |
| gmt_modified | DATETIME | 修改时间 |
t_clearing_balance_detail(余额比对明细表)
| 字段 | 类型 | 说明 |
|---|---|---|
| detail_id | BIGINT AUTO_INCREMENT | 主键 |
| clearing_batch_id | BIGINT | 批次ID |
| card_id | VARCHAR(64) | 卡ID |
| member_id | VARCHAR(64) | 会员ID |
| fab_balance | DECIMAL(18,2) | FAB文件中余额 |
| sys_basic_balance | DECIMAL(18,2) | 系统BASIC账户余额 |
| sys_red_packet_balance | DECIMAL(18,2) | 系统RED_PACKET_SETTLE余额 |
| sys_transfer_balance | DECIMAL(18,2) | 系统OUTER_TRANSFER_SETTLE余额 |
| sys_total_balance | DECIMAL(18,2) | 系统汇总余额 |
| diff_amount | DECIMAL(18,2) | 差异金额 |
| currency | VARCHAR(8) | 币种: AED |
| status | VARCHAR(20) | 比对状态: PENDING/MATCHED/MISMATCHED/FAB_ONLY/SYSTEM_ONLY |
| memo | VARCHAR(512) | 备注 |
| gmt_create | DATETIME | 创建时间 |
| gmt_modified | DATETIME | 修改时间 |
t_clearing_transaction(清空交易表)
| 字段 | 类型 | 说明 |
|---|---|---|
| transaction_id | BIGINT AUTO_INCREMENT | 主键 |
| clearing_batch_id | BIGINT | 批次ID |
| card_id | VARCHAR(64) | 卡ID |
| member_id | VARCHAR(64) | 会员ID |
| amount | DECIMAL(18,2) | 扣减金额 |
| currency | VARCHAR(8) | 币种: AED |
| direction | VARCHAR(16) | 方向: negative |
| before_balance | DECIMAL(18,2) | 扣减前余额 |
| after_balance | DECIMAL(18,2) | 扣减后余额(应为0) |
| report_status | VARCHAR(16) | 报送状态: INIT/PROCESSING/SUCCESS/FAILED |
| fail_reason | VARCHAR(512) | 失败原因 |
| fail_count | INT DEFAULT 0 | 失败次数 |
| fab_response_code | VARCHAR(32) | FAB响应码 |
| fab_response_msg | VARCHAR(512) | FAB响应信息 |
| report_time | DATETIME | 报送时间 |
| report_success_time | DATETIME | 报送成功时间 |
| memo | VARCHAR(512) | 备注 |
| gmt_create | DATETIME | 创建时间 |
| gmt_modified | DATETIME | 修改时间 |
5.2 索引设计
-- t_clearing_batch
CREATE UNIQUE INDEX uk_batch_no ON t_clearing_batch(batch_no);
CREATE INDEX idx_status ON t_clearing_batch(status);
-- t_clearing_balance_detail
CREATE INDEX idx_batch_id ON t_clearing_balance_detail(clearing_batch_id);
CREATE INDEX idx_card_id ON t_clearing_balance_detail(card_id);
CREATE INDEX idx_status ON t_clearing_balance_detail(clearing_batch_id, status);
-- t_clearing_transaction
CREATE INDEX idx_batch_id ON t_clearing_transaction(clearing_batch_id);
CREATE INDEX idx_card_id ON t_clearing_transaction(card_id);
CREATE INDEX idx_report_status ON t_clearing_transaction(clearing_batch_id, report_status);
CREATE INDEX idx_member_id ON t_clearing_transaction(member_id);6. 接口设计
6.1 CounterFacade 新增接口
/**
* 余额清空 - 创建清空批次(上传文件+设定截止时间)
*/
CommonResponse clearingBatchCreate(ClearingBatchCreateRequest request);
/**
* 余额清空 - 查询批次详情
*/
ClearingBatchDetailResponse clearingBatchDetail(ClearingBatchDetailRequest request);
/**
* 余额清空 - 查询比对明细列表(分页)
*/
ClearingBalanceDetailListResponse clearingBalanceDetailList(ClearingBalanceDetailListRequest request);
/**
* 余额清空 - 查询清空交易列表(分页)
*/
ClearingTransactionListResponse clearingTransactionList(ClearingTransactionListRequest request);
/**
* 余额清空 - Operator确认提交
*/
CommonResponse clearingBatchConfirm(ClearingBatchConfirmRequest request);
/**
* 余额清空 - 触发报送/重新报送
*/
CommonResponse clearingBatchReport(ClearingBatchReportRequest request);
/**
* 余额清空 - 导出失败卡列表
*/
ExportFailedCardsResponse clearingExportFailedCards(ClearingExportFailedCardsRequest request);6.2 请求/响应模型
// 创建清空批次请求
public class ClearingBatchCreateRequest {
private String fabFileTag; // FAB余额文件 UFS fileTag
private Date cutoffTime; // 系统截止时间
private String operator; // 操作人
private String memo; // 备注
}
// 批次详情响应
public class ClearingBatchDetailResponse extends CommonResponse {
private String batchNo;
private String status;
private Date cutoffTime;
// 比对汇总
private Integer fabTotalCards;
private BigDecimal fabTotalBalance;
private Integer sysTotalCards;
private BigDecimal sysTotalBalance;
private Integer matchedCount;
private Integer mismatchedCount;
private BigDecimal diffAmount;
// 报送进度
private Integer totalTransactions;
private Integer successCount;
private Integer failedCount;
}
// 确认提交请求
public class ClearingBatchConfirmRequest {
private Long clearingBatchId;
private String operator;
}
// 触发报送请求
public class ClearingBatchReportRequest {
private Long clearingBatchId;
private Boolean retryFailedOnly; // true=仅重试失败的, false=全量报送
private String operator;
}
// 导出失败卡列表响应
public class ExportFailedCardsResponse extends CommonResponse {
private String fileUrl; // 失败卡列表文件下载URL
private Integer failedCount;
}7. 系统模块变更
7.1 模块变更总览
graph LR
subgraph "service/facade"
A1[CounterFacade
+7个新接口] A2[新增 Request/Response] A3[新增 ClearingStatus 枚举] end subgraph "ext/service" B1[CounterFacadeImpl
新增实现] B2[ClearingProcessor
核心处理器] B3[ClearingReportTaskHandler
报送任务处理] end subgraph "domainservice" C1[ClearingBatchService] C2[ClearingBalanceDetailService] C3[ClearingTransactionService] end subgraph "core/dal" D1[t_clearing_batch] D2[t_clearing_balance_detail] D3[t_clearing_transaction] D4[对应 DO/Mapper/XML] end subgraph "core/domain" E1[ClearingBatchDomain] E2[ClearingBalanceDetailDomain] E3[ClearingTransactionDomain] end subgraph "core/common" F1[ClearingBatchStatus 枚举] F2[ClearingTransactionStatus 枚举] F3[ClearingBalanceStatus 枚举] end subgraph "ext/integration" G1[复用 Wave/FAB Channel] G2[复用 QueryClient] G3[复用 UfsServiceClient] end A1 --> B1 B1 --> B2 B2 --> C1 B2 --> C2 B2 --> C3 B2 --> G1 B2 --> G2 B2 --> G3 C1 --> D1 C2 --> D2 C3 --> D3
+7个新接口] A2[新增 Request/Response] A3[新增 ClearingStatus 枚举] end subgraph "ext/service" B1[CounterFacadeImpl
新增实现] B2[ClearingProcessor
核心处理器] B3[ClearingReportTaskHandler
报送任务处理] end subgraph "domainservice" C1[ClearingBatchService] C2[ClearingBalanceDetailService] C3[ClearingTransactionService] end subgraph "core/dal" D1[t_clearing_batch] D2[t_clearing_balance_detail] D3[t_clearing_transaction] D4[对应 DO/Mapper/XML] end subgraph "core/domain" E1[ClearingBatchDomain] E2[ClearingBalanceDetailDomain] E3[ClearingTransactionDomain] end subgraph "core/common" F1[ClearingBatchStatus 枚举] F2[ClearingTransactionStatus 枚举] F3[ClearingBalanceStatus 枚举] end subgraph "ext/integration" G1[复用 Wave/FAB Channel] G2[复用 QueryClient] G3[复用 UfsServiceClient] end A1 --> B1 B1 --> B2 B2 --> C1 B2 --> C2 B2 --> C3 B2 --> G1 B2 --> G2 B2 --> G3 C1 --> D1 C2 --> D2 C3 --> D3
7.2 各模块详细变更
| 模块 | 变更类型 | 具体内容 |
|---|---|---|
| service/facade | 新增 | ClearingBatchCreateRequest, ClearingBatchDetailRequest/Response, ClearingBalanceDetailListRequest/Response, ClearingTransactionListRequest/Response, ClearingBatchConfirmRequest, ClearingBatchReportRequest, ClearingExportFailedCardsRequest/Response |
| service/facade | 修改 | CounterFacade 新增 7 个接口方法 |
| service/facade | 新增 | ClearingBatchStatusEnum, ClearingTransactionStatusEnum, ClearingBalanceStatusEnum |
| ext/service | 修改 | CounterFacadeImpl 新增接口实现 |
| ext/service | 新增 | ClearingProcessor - 余额清空核心处理器 |
| ext/service | 新增 | ClearingReportTaskHandler - 报送任务异步处理 |
| ext/service | 新增 | ClearingFileParser - FAB 余额文件解析 |
| ext/service | 新增 | ClearingConvert - 对象转换器 |
| domainservice | 新增 | ClearingBatchService / ClearingBalanceDetailService / ClearingTransactionService |
| domainservice | 新增 | 对应 Repository 实现 |
| core/domain | 新增 | ClearingBatchDomain, ClearingBalanceDetailDomain, ClearingTransactionDomain |
| core/dal | 新增 | 3 张表对应的 DO、Mapper 接口、MyBatis XML 映射文件 |
| core/common | 新增 | 状态枚举类 |
| ext/integration | 复用 | FabWaveChannelImpl, QueryClient, UfsServiceClient |
8. 操作流程(SOP)
8.1 完整操作流程
flowchart TD
subgraph "Phase 1: 准备"
P1[1. 与 FAB 协调确定退出日期和截止时间]
P2[2. FAB 导出全量卡余额文件]
P3[3. FAB 邮件发送余额文件给 Operator]
P1 --> P2 --> P3
end
subgraph "Phase 2: 上传与比对"
U1[4. Operator 登录 Counter 系统]
U2[5. 进入'余额清空'功能页面]
U3[6. 上传 FAB 余额文件]
U4[7. 设定截止时间 Cutoff Time]
U5[8. 点击'创建批次'按钮]
U6[9. 系统自动执行:
- 生成余额快照
- 执行余额比对
- 生成汇总报告] U7[10. Operator 查看比对汇总] U8[11. Operator 查看差异明细] U1 --> U2 --> U3 --> U4 --> U5 --> U6 --> U7 --> U8 end subgraph "Phase 3: 确认与报送" C1[12. Operator 确认比对结果, 点击'确认提交'] C2[13. 系统生成清空交易列表] C3[14. Operator 查看交易列表] C4[15. 点击'开始报送'] C5[16. 系统逐批报送至 FAB] C6[17. 查看报送进度] C1 --> C2 --> C3 --> C4 --> C5 --> C6 end subgraph "Phase 4: 失败处理" F1[18. 查看报送结果] F2{全部成功?} F3[19. 导出失败卡列表] F4[20. 邮件发送失败列表给 FAB] F5[21. FAB 修复卡状态后回复确认] F6[22. Operator 点击'重新报送'] F7[23. 完成! 确认 Pool Account 余额归零] F1 --> F2 F2 -->|否| F3 --> F4 --> F5 --> F6 --> F1 F2 -->|是| F7 end P3 --> U1 U8 --> C1 C6 --> F1
- 生成余额快照
- 执行余额比对
- 生成汇总报告] U7[10. Operator 查看比对汇总] U8[11. Operator 查看差异明细] U1 --> U2 --> U3 --> U4 --> U5 --> U6 --> U7 --> U8 end subgraph "Phase 3: 确认与报送" C1[12. Operator 确认比对结果, 点击'确认提交'] C2[13. 系统生成清空交易列表] C3[14. Operator 查看交易列表] C4[15. 点击'开始报送'] C5[16. 系统逐批报送至 FAB] C6[17. 查看报送进度] C1 --> C2 --> C3 --> C4 --> C5 --> C6 end subgraph "Phase 4: 失败处理" F1[18. 查看报送结果] F2{全部成功?} F3[19. 导出失败卡列表] F4[20. 邮件发送失败列表给 FAB] F5[21. FAB 修复卡状态后回复确认] F6[22. Operator 点击'重新报送'] F7[23. 完成! 确认 Pool Account 余额归零] F1 --> F2 F2 -->|否| F3 --> F4 --> F5 --> F6 --> F1 F2 -->|是| F7 end P3 --> U1 U8 --> C1 C6 --> F1
8.2 Counter UI 页面设计
页面 1:余额清空 - 创建批次
+----------------------------------------------------------+
| 余额清空管理 |
+----------------------------------------------------------+
| FAB 余额文件: [选择文件...] balance_20260408.csv |
| 截止时间: [2026-04-08 23:59:59] |
| 备注: [SVA退出余额清空] |
| |
| [创建批次] |
+----------------------------------------------------------+页面 2:批次详情与比对汇总
+----------------------------------------------------------+
| 批次号: CLR202604080001 状态: RECONCILED |
+----------------------------------------------------------+
| 比对汇总 |
| ┌──────────────┬──────────┬──────────┐ |
| │ │ FAB文件 │ 系统快照 │ |
| ├──────────────┼──────────┼──────────┤ |
| │ 卡总数 │ 1,500 │ 1,500 │ |
| │ 总余额(AED) │ 500,000 │ 500,000 │ |
| └──────────────┴──────────┴──────────┘ |
| |
| 匹配: 1,485 | 不匹配: 10 | 仅FAB: 3 | 仅系统: 2 |
| |
| [查看差异明细] [确认提交] [取消] |
+----------------------------------------------------------+页面 3:报送进度
+----------------------------------------------------------+
| 批次号: CLR202604080001 状态: REPORTING |
+----------------------------------------------------------+
| 报送进度 |
| ████████████████░░░░ 80% (1200/1500) |
| |
| 成功: 1,180 | 失败: 20 | 待处理: 300 |
| |
| [查看失败列表] [导出失败列表] [重新报送] |
+----------------------------------------------------------+9. 核心处理器伪代码
9.1 ClearingProcessor
@Component
public class ClearingProcessor {
/** 创建清空批次 */
public String createBatch(ClearingBatchCreateRequest request) {
// 1. 校验:不能有正在进行中的批次
// 2. 生成批次号 CLR + yyyyMMdd + seq
// 3. 存储批次记录(状态=INIT)
// 4. 异步执行:余额快照 + 比对
asyncExecuteSnapshotAndReconcile(batchId, request);
return batchNo;
}
/** 异步执行快照和比对 */
@Async
public void asyncExecuteSnapshotAndReconcile(Long batchId, ClearingBatchCreateRequest request) {
// Phase 1: 生成系统余额快照
generateSystemBalanceSnapshot(batchId, request.getCutoffTime());
// Phase 2: 解析 FAB 文件并比对
reconcileBalance(batchId, request.getFabFileTag());
}
/** 生成系统余额快照 */
private void generateSystemBalanceSnapshot(Long batchId, Date cutoffTime) {
// 1. 查询所有活跃卡(通过 t_issue_record + t_card_detail)
// 2. 分批查询 Query 服务获取各账户类型余额
// 3. 汇总每张卡余额 = BASIC + RED_PACKET_SETTLE + OUTER_TRANSFER_SETTLE
// 4. 批量存入 t_clearing_balance_detail(status=PENDING)
// 5. 更新批次状态为 SNAPSHOT_DONE
}
/** 余额比对 */
private void reconcileBalance(Long batchId, String fabFileTag) {
// 1. 下载并解析 FAB 余额文件
// 2. 构建 Map<cardId, fabBalance>
// 3. 读取系统快照 Map<cardId, sysBalance>
// 4. 逐卡比对,更新明细状态
// 5. 统计汇总信息,更新批次
// 6. 更新批次状态为 RECONCILED
}
/** Operator确认后生成清空交易 */
public void confirmAndGenerateTransactions(Long batchId) {
// 1. 校验批次状态 == RECONCILED
// 2. 查询所有余额 > 0 的明细
// 3. 为每张卡生成扣减交易(t_clearing_transaction)
// - amount = 卡余额
// - direction = negative
// - after_balance = 0
// - report_status = INIT
// 4. 更新批次状态为 CONFIRMED
}
/** 报送交易至 FAB */
@Async
public void reportTransactions(Long batchId, boolean retryFailedOnly) {
// 1. 更新批次状态为 REPORTING
// 2. 查询待报送交易
// retryFailedOnly ? status=FAILED : status=INIT
// 3. 分批报送(复用 FabWaveChannelImpl)
// 4. 逐笔更新报送结果
// 5. 汇总:若全部成功 -> COMPLETED,否则 -> PARTIALLY_DONE
}
/** 导出失败卡列表 */
public String exportFailedCards(Long batchId) {
// 1. 查询 status=FAILED 的交易
// 2. 生成 CSV 文件(cardId, amount, failReason)
// 3. 上传至 UFS
// 4. 返回下载 URL
}
}10. 异常处理与边界情况
10.1 异常处理策略
| 异常场景 | 处理方式 |
|---|---|
| FAB 文件格式错误 | 解析失败,批次标记为 INIT,提示 Operator 重新上传 |
| 系统余额查询超时 | 重试3次,仍失败则批次标记为失败,需重新创建 |
| 报送过程中网络中断 | 已报送的记录保持状态不变,未报送的保持 INIT,可续传 |
| FAB 返回未知错误码 | 标记为 FAILED,记录原始响应,人工介入处理 |
| 重复报送同一笔交易 | 使用幂等键(batchNo+cardId)防止重复扣减 |
| 截止时间后有新交易入账 | 系统截止后应阻止新交易,通过配置开关控制 |
10.2 幂等性保证
- 每笔清空交易使用
batchNo + cardId作为幂等键 - 报送前检查交易状态,已成功的不重复报送
- FAB 侧通过交易流水号去重
10.3 数据一致性
flowchart LR
A[系统截止] --> B[快照时间 = 截止时间]
B --> C[FAB文件时间 = 截止时间]
C --> D[三方数据时间点一致]
D --> E[比对结果可信]
11. 配置项
# 余额清空相关配置
escrow:
clearing:
# 报送批次大小
report-batch-size: 50
# 快照查询批次大小
snapshot-batch-size: 200
# 报送重试最大次数
max-retry-count: 5
# 报送间隔(ms),避免FAB限流
report-interval: 200
# 临时文件路径
temp-file-path: /tmp/escrow/clearing
# 是否启用截止时间交易阻断
cutoff-block-enabled: true12. 安全与审计
- 所有操作需记录操作人和操作时间
- 确认提交操作需要 Operator 权限验证
- 批次创建和确认操作需记录审计日志
- FAB 余额文件下载后需验证文件完整性
- 清空交易不可回滚,确认前需二次确认弹窗
13. 测试方案
| 测试类型 | 测试内容 |
|---|---|
| 单元测试 | 文件解析、余额比对逻辑、状态转换 |
| 集成测试 | 完整流程:上传->比对->确认->报送 |
| 场景测试 | FAB文件与系统完全匹配 |
| 场景测试 | FAB文件与系统存在差异 |
| 场景测试 | 部分卡报送失败后重试 |
| 场景测试 | 全部卡报送失败后重试 |
| 场景测试 | 空文件/格式错误文件 |
| 场景测试 | 大批量卡(>10000)性能测试 |
| 回归测试 | 现有 Counter 功能不受影响 |
14. 上线计划
gantt
title 余额清空功能上线计划
dateFormat YYYY-MM-DD
section 开发
数据库DDL与DAL层 :d1, 2026-04-09, 3d
Domain与DomainService :d2, after d1, 3d
ClearingProcessor核心逻辑 :d3, after d2, 5d
CounterFacade接口实现 :d4, after d3, 3d
Counter前端页面开发 :d5, after d3, 5d
section 测试
单元测试 :t1, after d4, 3d
集成测试 :t2, after d5, 4d
UAT测试 :t3, after t2, 5d
section 上线
预发布环境验证 :r1, after t3, 2d
生产环境发布 :r2, after r1, 1d
与FAB联调 :r3, after r2, 3d
正式执行余额清空 :milestone, after r3, 0d
15. 风险评估
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| FAB 余额文件时间与系统截止时间不一致 | 比对结果不可信 | 与 FAB 明确约定同一时间点,双方确认后再执行 |
| 大量卡状态异常导致报送失败 | 清空进度缓慢 | 提前与 FAB 沟通批量修复卡状态的 SLA |
| FAB 接口限流 | 报送速度受限 | 配置合理的报送间隔和批次大小 |
| 截止时间后仍有交易 | 余额不一致 | 系统配置开关,截止后阻断新交易 |
| 长时间运行导致系统异常 | 部分交易丢失 | 支持断点续传,已成功的不重复处理 |