Visii改动
分支: fea-260301-iban-migrate一、改动总览
Visii改动)) MQ消息监听 KycFinishListener
KYC完成自动开户 KycDeleteListener
KYC删除关闭账户 Dubbo接口 retryOpenAccount
Counter重试开户 CGS接口
小程序API VamAuthInit
初始化查询 VamAuthActive
激活开户 VamAuthInfoQuery
账户信息查询 VamAuthInfoQueryV2
V2版本查询 核心服务 ZandVaServiceImpl
ZAND开户核心逻辑 VaConverter/VaBuilder
VA实体构建 基础设施 CmsClient
白名单服务 MemberClient
新增isVip/grcKycType RetryApplyVamHandler
补偿重试
二、用例图
(App)"] Counter["Counter人员
(运营后台)"] MQ["MQ消息
(KYC系统推送)"] end subgraph UseCases["用例"] UC1["UC1: KYC完成自动开户"] UC2["UC2: KYC删除关闭账户"] UC3["UC3: 查询IBAN开户状态"] UC4["UC4: 激活开户"] UC5["UC5: 查询IBAN账户信息"] UC6["UC6: Counter批量重试开户"] end MQ --> UC1 MQ --> UC2 User --> UC3 User --> UC4 User --> UC5 Counter --> UC6 UC1 -. "开户失败时" .-> UC6 UC4 -. "依赖" .-> UC3
三、各功能模块详细说明
3.1 KYC完成自动开户 (KycFinishListener)
改动说明
| 项目 | 说明 |
|---|---|
| 类名 | KycFinishListener |
| MQ Exchange | exchange.kyc.finish |
| Route Key | kyc.eid.finish / kyc.eid.finish.vip |
| Queue | kyc.visii.queue |
| 触发条件 | KYC系统KYC完成后推送MQ消息 |
| 核心逻辑 | 接收消息 → 检查开关/白名单 → 检查VA是否已存在 → 构建VA → 异步调用ZAND开户 |
流程图
或 白名单命中?} E -- 否 --> Z2[跳过: 未开启自动开户] E -- 是 --> F{VA记录已存在?} F -- 是 --> Z3[跳过: 幂等] F -- 否 --> G[VaBuilder构建VA实体] G --> H[事务: 保存VA + 保存补偿事件] H --> I[异步执行: 调用ZAND开户API] I --> J[完成] style Z1 fill:#f9f,stroke:#333 style Z2 fill:#ff9,stroke:#333 style Z3 fill:#9ff,stroke:#333 style J fill:#9f9,stroke:#333
配置项
| 配置键 | 类型 | 默认值 | 说明 |
|---|---|---|---|
visii.config.zand.customerAutoOpen | String | Y | 自动开户总开关 |
CMS白名单 VISII_KYC_MID_WHITELIST | - | - | 白名单命中也可开户 |
测试要点
| 测试场景 | 输入 | 预期 | 对应用例 |
|---|---|---|---|
| 空消息 | null / "" | 不抛异常,跳过 | KycFinishListenerTest#testHandle_emptyMessage |
| 开关关闭 | customerAutoOpen=N,memberId不在白名单 | 跳过 | testHandle_switchOff |
| VA已存在 | 相同memberId发两次 | 幂等,第二次跳过 | testHandle_vaExists |
| 正常流程 | 有效KYC消息 | VA记录创建,触发异步开户 | testHandle_normalFlow |
3.2 KYC删除关闭账户 (KycDeleteListener)
改动说明
| 项目 | 说明 |
|---|---|
| 类名 | KycDeleteListener |
| MQ Exchange | exchange.kyc.delete |
| Route Key | kyc.delete.eid |
| Queue | kyc.delete.visii.queue |
| 核心逻辑 | 接收删除消息 → 触发MemberLogoutEventHandler关闭VA |
流程图
参数: memberId, deleteMethod] E --> F[保存补偿事件] F --> G[异步执行: 关闭VA] G --> H[完成] style Z1 fill:#f9f,stroke:#333 style H fill:#9f9,stroke:#333
消息示例
{
"memberId": "100021922209",
"deleteMethod": "EID_BASIS_MANUAL_DELETE",
"kycType": "EID"
}测试要点
| 测试场景 | 输入 | 预期 | 对应用例 |
|---|---|---|---|
| 空消息 | null / "" | 不抛异常,跳过 | KycDeleteListenerTest#testHandle_emptyMessage |
| memberId为空 | {"deleteMethod":"X"} | 跳过 | testHandle_emptyMemberId |
| 无VA记录 | 不存在的memberId | 不报错,保存关闭事件 | testHandle_noVaExists |
| 正常关闭 | 有效memberId | 触发关闭VA | testHandle_normalFlow |
3.3 Counter批量重试开户 (retryOpenAccount)
改动说明
| 项目 | 说明 |
|---|---|
| 接口 | CounterOperateFacade#retryOpenAccount |
| 入参 | RetryOpenAccountRequest(bankCode, memberIdList) |
| 出参 | CommonResponse |
| 核心逻辑 | 遍历memberIdList → 查询/创建VA → 触发补偿重试 |
流程图
bankCode: ZAND/PAYBY
memberIdList: 非空] B -- 校验失败 --> Z1[返回FAIL] B -- 校验通过 --> C[遍历memberIdList] C --> D{查询VA记录} D -- VA存在且VALID --> E1[跳过: 已开户] D -- VA存在且CLOSED --> E2[跳过: 已关闭] D -- VA存在且非VALID/CLOSED --> F[触发RetryApplyVamHandler] D -- VA不存在 --> G[vaConverter.initVa创建VA] G --> G1{initVa成功?} G1 -- 否 --> E3[跳过: 无法创建] G1 -- 是 --> G2[insert VA记录] G2 --> F F --> H[保存补偿事件 + 异步执行] H --> C E1 --> C E2 --> C E3 --> C C -- 遍历完成 --> I[返回SUCCESS] style Z1 fill:#f66,stroke:#333 style I fill:#9f9,stroke:#333
测试要点
| 测试场景 | 输入 | 预期 | 对应用例 |
|---|---|---|---|
| bankCode为空 | bankCode=null | FAIL | CounterOperateFacadeImplTest#testRetryOpenAccount_validateFail |
| memberIdList为空 | memberIdList=null | FAIL | 同上 |
| memberIdList空列表 | memberIdList=[] | FAIL | 同上 |
| member无VA | 随机memberId | SUCCESS,创建VA并重试 | testRetryOpenAccount_noVaExists |
| 正常重试 | 已有VA的memberId | SUCCESS | testRetryOpenAccount_normalFlow |
3.4 小程序CGS接口 (VamAuth系列)
接口清单
| Provider | ServiceCode | 方法 | 说明 |
|---|---|---|---|
VamAuthInitProvider | /visii/vam/v1/auth/init | 查询开户状态 | 返回openStatus + vipType |
VamAuthActiveProvider | /visii/vam/v1/auth/active | 激活开户 | 触发ZAND开户流程 |
VamAuthInfoQueryProvider | /visii/vam/v1/auth/query-info | 查询账户信息 | 返回IBAN/bankName等 |
VamAuthInfoQueryV2Provider | /visii/vam/v2/auth/query-info | V2查询 | 同上,newVersion=true |
VamAuthInit 流程图
VamAuthActive 流程图
异步开户] H -- 否 --> J I --> J["返回openStatus=VA状态"]
VamAuthInfoQuery 流程图
设置vamLimit] I -- 否 --> K[返回Response] J --> K
测试要点
| 测试场景 | 接口 | 预期 | 对应用例 |
|---|---|---|---|
| memberId为空 | Init/Active/Query | FAIL | VamAuthProcessorTest#testVamAuthInit_memberIdNull 等 |
| 无VA记录查询Init | Init | openStatus=I, vipType有值 | testVamAuthInit_noVa |
| 有VA记录查询Init | Init | openStatus=VA实际状态 | testVamAuthInit_withVa |
| 新会员激活 | Active | SUCCESS, DB创建VA | testVamAuthActive_newMember |
| 带name激活 | Active | SUCCESS | testVamAuthActive_withName |
| 重复激活 | Active | 幂等,SUCCESS | testVamAuthActive_idempotent |
| 无VA查询Info | Query | SUCCESS, bankCode=null | testVamAuthInfoQuery_noVa |
| 有VA查询Info | Query | bankCode=ZAND | testVamAuthInfoQuery_withVa |
| V2版本查询 | QueryV2 | bankCode=ZAND | testVamAuthInfoQuery_v2 |
四、新增/修改文件清单
4.1 新增文件
| 文件 | 模块 | 说明 |
|---|---|---|
KycFinishInfo.java | core/domain/dto | KYC完成MQ消息DTO |
KycDeleteInfo.java | core/domain/dto | KYC删除MQ消息DTO |
KycFinishListener.java | domainservice/mq | KYC完成MQ监听器 |
KycDeleteListener.java | domainservice/mq | KYC删除MQ监听器 |
VaBuilder.java | domainservice/builder | 从KycFinishInfo构建VA实体 |
RetryOpenAccountRequest.java | facade/domain/request | 重试开户请求DTO |
RetryOpenAccountProcessor.java | domainservice/processor | 重试开户业务处理器 |
VamAuthInitProcessor.java | domainservice/service/impl/va | Init接口处理器 |
VamAuthActiveProcessor.java | domainservice/service/impl/va | Active接口处理器 |
VamAuthInfoQueryProcessor.java | domainservice/service/impl/va | InfoQuery接口处理器 |
VamAuthInitProvider.java | ext/service/provider | CGS Init接口Provider |
VamAuthActiveProvider.java | ext/service/provider | CGS Active接口Provider |
VamAuthInfoQueryProvider.java | ext/service/provider | CGS InfoQuery接口Provider |
VamAuthInfoQueryV2Provider.java | ext/service/provider | CGS InfoQuery V2 Provider |
CmsClient.java / CmsClientImpl.java | ext/integration/internal/cms | CMS白名单服务客户端 |
BaseConverter.java | domainservice/converter | CGS请求基础信息转换 |
4.2 修改文件
| 文件 | 改动点 |
|---|---|
VisiiConstants.java | 新增 Kyc/KycDelete/ServiceCode 常量接口 |
ZandConfig.java | 删除 customerOpenPercent(改用CMS白名单控制) |
MemberClient.java | 新增 isVip(memberId) / grcKycType(memberId) |
MemberClientImpl.java | 实现isVip/grcKycType, 重命名私有方法isVip→isVipKycType |
CounterOperateFacade.java | 新增 retryOpenAccount 方法 |
CounterOperateFacadeImpl.java | 注入RetryOpenAccountProcessor并委托 |
ZandVaServiceImpl.java | 完整实现vamAuthInit/vamAuthActive/vamAuthInfoQuery |
VaConverter.java | 新增 initVa(memberId) 方法 |
4.3 新增测试文件
| 文件 | 说明 |
|---|---|
KycFinishListenerTest.java | KYC完成监听器测试 (4个用例) |
KycDeleteListenerTest.java | KYC删除监听器测试 (4个用例) |
CounterOperateFacadeImplTest.java | Counter接口测试 (含retryOpenAccount 5个用例) |
VamAuthProcessorTest.java | CGS接口处理器测试 (11个用例) |
五、端到端测试流程
六、关键验证点 Checklist
MQ消息监听
- [ ]
exchange.kyc.finish消息能正常消费 - [ ]
exchange.kyc.delete消息能正常消费 - [ ] 空消息/格式错误消息不会导致异常
- [ ]
customerAutoOpen=N且不在白名单时,KYC消息被跳过 - [ ]
customerAutoOpen=Y时正常开户 - [ ] 白名单命中时即使开关关闭也能开户
- [ ] 重复MQ消息幂等处理(不重复创建VA)
Counter接口
- [ ]
retryOpenAccount参数校验生效(bankCode必填且为ZAND/PAYBY,memberIdList非空) - [ ] 已开户(VALID)的member跳过
- [ ] 已关闭(CLOSED)的member跳过
- [ ] 无VA记录的member自动创建VA并重试
- [ ] 批量处理中部分失败不影响整体
CGS接口(提供给app)
- [ ]
/visii/vam/v1/auth/init无VA时返回openStatus=I - [ ]
/visii/vam/v1/auth/initVIP用户返回vipType=VIP - [ ]
/visii/vam/v1/auth/active新用户触发开户 - [ ]
/visii/vam/v1/auth/active重复调用幂等 - [ ]
/visii/vam/v1/auth/active带name参数正常处理 - [ ]
/visii/vam/v1/auth/query-info无VA返回空 - [ ]
/visii/vam/v1/auth/query-info有VA返回bankCode=ZAND - [ ]
/visii/vam/v2/auth/query-info返回含vamLimit限额信息 - [ ] 未登录用户(无token)被CGS网关拦截
数据库验证
- [ ] t_va表新增记录字段完整(memberId, bankCode, vaId, status, iban, name, extension等)
- [ ] VA状态流转正确: INIT → PROCESSING → VALID / FAIL
- [ ] 关闭后状态: CLOSED
七、配置变更说明
| 变更类型 | 配置项 | 说明 |
|---|---|---|
| 删除 | visii.config.zand.customerOpenPercent | 不再使用百分比控制,改用CMS白名单 |
| 保留 | visii.config.zand.customerAutoOpen | 总开关,默认Y |
| 新增 | CMS白名单 VISII_KYC_MID_WHITELIST | 配合autoOpen使用,白名单命中可绕过开关 |
八、交易状态变更MQ通知
8.1 改动说明
| 项目 | 说明 |
|---|---|
| Exchange | exchange.visii.trans |
| RoutingKey格式 | status.${statusCode}.${memberId} |
| 触发时机 | 交易首次插入(WV)、每次状态变更(WV→V/MC、通用流转、P→S) |
| 失败策略 | try-catch包裹,失败仅log.warn,不影响主流程 |
8.2 消息体 TransStatusChangeMessage
| 字段 | 类型 | 说明 |
|---|---|---|
| orderId | Long | 订单ID |
| memberId | String | 会员ID |
| bankCode | String | 银行编码 |
| status | String | 当前状态码 |
| bankOrderNo | String | 银行订单号 |
| previousStatus | String | 变更前状态码(首次插入时为null) |
| amount | BigDecimal | 交易金额 |
| currency | String | 币种 |
| iban | String | IBAN |
| direction | String | 方向 (I/O) |
| gmtModified | LocalDateTime | 修改时间 |
8.3 发布触发点
| 触发点 | 类 | 方法 | 状态变更 |
|---|---|---|---|
| 首次接收交易通知 | NotifyTransProcessor | saveAndValidate4Bank | null → WV |
| 验证后状态更新 | VamOrderRepositoryImpl | updateStatus(VamOrder, VamOrderStatusEnum, VisiiUnityResultCodeEnum) | WV → V/MC |
| 通用状态流转 | VamOrderRepositoryImpl | updateStatus(VamOrder, DbResultCarrier) | 各种状态流转 |
| 充值成功 | VamOrderRepositoryImpl | updateToSuccess(VamOrder, DepositInfo) | P → S |
8.4 流程图
insert订单] B --> C["发布MQ: status=WV, prev=null"] C --> D{验证结果} D -- 通过 --> E[updateStatus WV→V] D -- 需人工 --> F[updateStatus WV→MC] E --> G["发布MQ: status=V, prev=WV"] F --> H["发布MQ: status=MC, prev=WV"] G --> I[后续充值流程] I --> J[updateToSuccess P→S] J --> K["发布MQ: status=S, prev=P"] style C fill:#e6f3ff style G fill:#e6f3ff style H fill:#e6f3ff style K fill:#e6f3ff
8.5 测试要点
| 测试场景 | 预期 | 对应用例 |
|---|---|---|
| 首次插入状态(previousStatus=null) | 不抛异常,MQ消息发送 | TransStatusMqPublisherTest#testPublish_initialStatus |
| 状态变更 WV→V | 不抛异常,routingKey=status.V.{memberId} | testPublish_statusChange |
| 状态变更 P→S | 不抛异常,routingKey=status.S.{memberId} | testPublish_pendingToSuccess |
8.6 新增/修改文件
| 文件 | 操作 | 说明 |
|---|---|---|
VisiiConstants.java | 修改 | 新增 TransNotify 常量接口 |
TransStatusChangeMessage.java | 新建 | MQ消息体DTO |
TransStatusMqPublisher.java | 新建 | MQ发布器组件 |
VamOrderRepositoryImpl.java | 修改 | 3个update方法中调用publisher |
NotifyTransProcessor.java | 修改 | insert后调用publisher |
TransStatusMqPublisherTest.java | 新建 | Publisher测试用例 |
Vis修改
1. 功能概览
本次改动实现了 FAB → PAYBY(ZAND) IBAN 迁移的完整链路,涵盖以下模块:
| 模块 | Feature | 说明 |
|---|---|---|
| 迁移文件上传 | Feature 2 | Excel 文件解析、IBAN 加密、批量入库 |
| 个人用户迁移引擎 | Feature 3 | 定时拾取 PENDING 个人记录,KYC 校验 → 开户 → 通知 |
| 商户迁移引擎 | Feature 5 | 定时拾取 PENDING 商户记录,EID/贸易许可校验 → 开户 → 通知 |
| 迁移通知 & TodoCard | Feature 4 | 迁移成功后发送 TodoCard + Botim 消息,交易后移除 |
| Mini-Program 查询 | Feature 6 | CGS 接口查询用户迁移状态和新旧 IBAN |
| ZAND 交易通知 | Feature 6.1 | 消费 ZAND 交易完成通知,移除 TodoCard |
| 自动重试调度 | Feature 7 | 失败记录自动重试(最多3次,递增延迟) |
| 手动修改状态 | Feature 8.1 | Counter 手动设置迁移记录状态 |
| 手动重试 | Feature 8.2 | Counter 手动重试失败记录 |
| 交易状态变更通知 | Feature 9 | 消费 Visii 交易状态变更消息,WV 状态移除 TodoCard |
status=PENDING)] F2_UPLOAD --> F2_PARSE --> F2_DB end subgraph "Feature 3 - 个人用户迁移" F3_JOB[CustomerMigrateJob
定时任务 每2分钟] F3_CAS[CAS: P → I] F3_SERVICE[CustomerMigrateService
KYC校验 → PAYBY开户] F3_RESULT{结果} F3_JOB --> F3_CAS --> F3_SERVICE --> F3_RESULT F3_RESULT -->|成功| F3_C[status=C + 加密newIban] F3_RESULT -->|失败| F3_F[status=F + retryCount++] F3_RESULT -->|不合格| F3_S[status=S 跳过] end subgraph "Feature 5 - 商户迁移" F5_JOB[MerchantMigrateJob
定时任务 每2分钟] F5_CAS[CAS: P → I] F5_SERVICE[MerchantMigrateService
EID/贸易许可校验 → PAYBY开户] F5_RESULT{结果} F5_JOB --> F5_CAS --> F5_SERVICE --> F5_RESULT F5_RESULT -->|成功| F5_C[status=C + 加密newIban] F5_RESULT -->|失败| F5_F[status=F + retryCount++] F5_RESULT -->|不合格| F5_S[status=S 跳过] end subgraph "Feature 4 - 通知 & TodoCard" F4_NOTIFY[MigrateNotifyService] F4_ADD[addTodoCard + sendMessage
notifyStatus=S] F4_REMOVE[removeTodoCard
notifyStatus=R] F4_NOTIFY --> F4_ADD F4_NOTIFY --> F4_REMOVE end subgraph "Feature 6 - Mini-Program 查询" F6_API[QueryMigrateVamsProvider] F6_DECRYPT[解密 oldIban / newIban] F6_API --> F6_DECRYPT end subgraph "Feature 7 - 自动重试调度" F7_JOB[MigrateRetryJob
定时任务 每5分钟] F7_FF[retryCount >= 3 → FF] F7_RETRY[CAS: F → I → 重新迁移] F7_JOB --> F7_FF F7_JOB --> F7_RETRY end subgraph "Feature 8 - Counter 手动操作" F8_PUT[putMigrateStatus
手动修改状态] F8_RETRY[retryMigrate
手动重试] end subgraph "Feature 9 - 交易状态变更通知" F9_MQ[exchange.visii.trans
RabbitMQ] F9_HANDLER[TransStatusChangeHandler] F9_CHECK{个人用户 + WV?} F9_MQ --> F9_HANDLER --> F9_CHECK F9_CHECK -->|是| F4_REMOVE F9_CHECK -->|否| F9_IGNORE[忽略] end F2_DB --> F3_JOB F2_DB --> F5_JOB F3_C --> F4_ADD F5_C --> F4_ADD F3_F --> F7_JOB F5_F --> F7_JOB
2. 数据模型
2.1 迁移记录表 t_migrate_record
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Long | 主键 |
| fileId | Long | 关联的上传文件ID |
| memberId | String | 会员ID |
| memberType | String | CUSTOMER / MERCHANT |
| name | String | 会员姓名 |
| oldIban | String | FAB IBAN(加密存储) |
| newIban | String | PAYBY IBAN(加密存储,迁移成功后写入) |
| String | 邮箱 | |
| mobile | String | 手机号 |
| status | String | 迁移状态码 |
| message | String | 状态说明/错误信息 |
| retryCount | Integer | 重试次数 |
| notifyStatus | String | 通知状态:null=未发送, S=已发送, R=已移除 |
| extension | String | 扩展信息 |
| gmtCreate | LocalDateTime | 创建时间 |
| gmtModified | LocalDateTime | 修改时间 |
2.2 迁移状态枚举 MigrateStatus
| Code | 枚举值 | 说明 |
|---|---|---|
| P | PENDING | 待处理 |
| I | IN_PROGRESS | 处理中 |
| C | COMPLETED | 已完成 |
| F | FAILED | 失败(可重试) |
| FF | FINAL_FAILED | 最终失败(超过重试上限) |
| S | SKIPPED | 已跳过(KYC未通过/EID缺失等) |
2.3 通知状态 notifyStatus 流转
| 值 | 说明 | 触发动作 |
|---|---|---|
| null | 未通知 | 初始状态 |
| S | 已发送 | 迁移成功后 addTodoCard + sendMessage |
| R | 已移除 | 用户首笔交易后 removeTodoCard |
3. Feature 2: 迁移文件上传 (MigrateFileStrategy)
3.1 Excel 文件格式
| 列 | 字段 | 必填 | 说明 |
|---|---|---|---|
| A | memberId | Y | 会员ID |
| B | memberType | Y | CUSTOMER / MERCHANT |
| C | name | N | 姓名 |
| D | oldIban | Y | FAB IBAN |
| E | N | 邮箱 | |
| F | mobile | N | 手机号 |
第一行为表头,从第二行开始解析。
3.2 处理流程
/ oldIban 必填校验} VALIDATE -->|缺失| ERROR[返回 FAIL
报错行号和字段] VALIDATE -->|通过| DEDUP{同文件内
memberId 去重} DEDUP -->|重复| SKIP_DUP[跳过该行] DEDUP -->|不重复| BUILD[构建 MigrateRecordDO
加密 oldIban
status=P, retryCount=0] BUILD --> BATCH[事务内批量入库
每1000条一批] BATCH -->|DuplicateKey| DUP_ERR[返回 FAIL
重复记录] BATCH -->|成功| DONE[返回 SUCCESS]
3.3 安全说明
- oldIban 在解析时立即通过
localUesClient.encryptIban()加密存储 - 原始 IBAN 不会以明文形式持久化
3.4 测试用例
| # | 用例 | 预期结果 |
|---|---|---|
| 1 | 正常 Excel(CUSTOMER + MERCHANT) | 全部入库,status=P |
| 2 | memberId 为空行 | 返回 FAIL,报错行号 |
| 3 | oldIban 为空行 | 返回 FAIL,报错行号 |
| 4 | 文件内 memberId 重复 | 仅入库一条,后续跳过 |
| 5 | 空文件(仅表头) | SUCCESS,0条记录 |
| 6 | 大批量文件(>1000条) | 分批入库,全部成功 |
4. Feature 3: 个人用户迁移引擎
4.1 调度配置
| 配置项 | 说明 | 默认值 |
|---|---|---|
elastic.job.task.cron.customer.migrate | 定时任务 cron | 0 0/2 * * * ? *(每2分钟) |
| MigrateEngineConfiguration.enabled | 总开关 | true |
| MigrateEngineConfiguration.startHour | 允许执行开始小时 | 配置 |
| MigrateEngineConfiguration.endHour | 允许执行结束小时 | 配置 |
| MigrateEngineConfiguration.batchSize | 每批查询数量 | 100 |
| migrateExecutor.corePoolSize | 线程池核心线程数 | 5 |
| migrateExecutor.maxPoolSize | 线程池最大线程数 | 10 |
| migrateExecutor.queueCapacity | 队列容量 | 500 |
4.2 迁移流程 (CustomerMigrateService.migrateOne)
BASIC 账户?} CHECK_EXIST -->|是| DONE_EXIST[status=C
填入已有 IBAN
跳过开户] CHECK_EXIST -->|否| CHECK_KYC{KYC 已通过?} CHECK_KYC -->|否| MARK_SKIP[status=S
KYC未通过跳过] CHECK_KYC -->|是| QUERY_INFO[查询客户信息
name / mobile / email
EID / nationality / birthDate] QUERY_INFO --> BUILD_REQ[构建 IBAN 申请请求
accountType=BASIC
idType=eid
日期格式YYYYMMDD] BUILD_REQ --> APPLY[调用 ibanApplyService
.applyIban] APPLY -->|成功| SUCCESS[status=C
加密并存储 newIban] APPLY -->|失败| FAILED[status=F
retryCount++
message=错误信息] SUCCESS --> NOTIFY[调用 migrateNotifyService
.sendMigrateNotification
Best-Effort] FAILED --> END_F[等待自动重试] style DONE_EXIST fill:#d4edda style MARK_SKIP fill:#fff3cd style SUCCESS fill:#d4edda style FAILED fill:#f8d7da
4.3 个人用户迁移 - 关键逻辑说明
| 步骤 | 说明 |
|---|---|
| 查重 | 先查是否已有 PAYBY BASIC 账户,有则直接标记 COMPLETED |
| KYC 校验 | 调用 memberClient.isKycPassed(),未通过标记 SKIPPED |
| 客户信息 | 调用 memberClient.queryFullInfo(),获取 name/mobile/EID/nationality/birthDate |
| ID 类型 | 硬编码 idType = "eid" |
| 日期格式 | birthDate / idExpiry 格式化为 YYYYMMDD |
| 加密 | newIban 通过 localUesClient.encryptIban() 加密后存储 |
| 通知 | 迁移成功后 Best-Effort 发送通知,通知失败不回滚迁移状态 |
4.4 测试用例
| # | 用例 | 预期结果 |
|---|---|---|
| 1 | 正常个人用户,KYC 通过,开户成功 | status=C, newIban 已加密, 发送通知 |
| 2 | 已有 PAYBY 账户 | status=C, 填入已有 IBAN, 不重复开户 |
| 3 | KYC 未通过 | status=S, 不调用开户 |
| 4 | IBAN 申请失败 | status=F, retryCount++, message=错误信息 |
| 5 | 申请过程抛异常 | status=F, retryCount++, 异常被捕获 |
| 6 | 通知发送失败 | status 仍然为 C(不影响迁移结果) |
| 7 | CustomerMigrateJob disabled | 不查询不处理 |
5. Feature 5: 商户迁移引擎
5.1 调度配置
| 配置项 | 说明 | 默认值 |
|---|---|---|
elastic.job.task.cron.merchant.migrate | 定时任务 cron | 0 0/2 * * * ? *(每2分钟) |
其余配置与个人用户共享 MigrateEngineConfiguration。
5.2 迁移流程 (MerchantMigrateService.migrateOne)
BASIC 账户?} CHECK_EXIST -->|是| DONE_EXIST[status=C
填入已有 IBAN
跳过开户] CHECK_EXIST -->|否| QUERY_MERCHANT[查询商户信息
merchantClient.queryMerchantInfo
name / mobile / EID] QUERY_MERCHANT --> CHECK_EID{商户信息存在
且 EID 非空?} CHECK_EID -->|否| MARK_SKIP[status=S
EID缺失跳过] CHECK_EID -->|是| QUERY_LICENSE[查询贸易许可信息
memberClient.queryMerchantInfo
licenseNumber / expiryDate] QUERY_LICENSE --> BUILD_REQ[构建 IBAN 申请请求
memberType=COMPANY
idType=eid
licenseIssuer=MAINLAND] BUILD_REQ --> APPLY[调用 ibanApplyService
.applyIban] APPLY -->|成功| SUCCESS[status=C
加密并存储 newIban] APPLY -->|失败| FAILED[status=F
retryCount++
message=错误信息] SUCCESS --> NOTIFY[调用 migrateNotifyService
.sendMigrateNotification
Best-Effort] FAILED --> END_F[等待自动重试] style DONE_EXIST fill:#d4edda style MARK_SKIP fill:#fff3cd style SUCCESS fill:#d4edda style FAILED fill:#f8d7da
5.3 商户迁移 vs 个人用户迁移对比
| 维度 | 个人用户 (Customer) | 商户 (Merchant) |
|---|---|---|
| memberType | CUSTOMER | COMPANY |
| 资格校验 | KYC 是否通过 | 商户信息是否存在 & EID 非空 |
| 信息来源 | memberClient.queryFullInfo | merchantClient + memberClient |
| 开户字段 | name/EID/nationality/birthDate | name/EID/贸易许可号/许可到期日 |
| 许可信息 | 无 | licenseNumber + expiryDate, issuer=MAINLAND |
| idType | eid | eid |
| 日期格式 | YYYYMMDD | YYYYMMDD |
| 通知 | 相同(addTodoCard + sendMessage) | 相同 |
5.4 测试用例
| # | 用例 | 预期结果 |
|---|---|---|
| 1 | 正常商户,EID 存在,开户成功 | status=C, newIban 已加密, 发送通知 |
| 2 | 已有 PAYBY 账户 | status=C, 填入已有 IBAN, 不重复开户 |
| 3 | 商户信息为 null | status=S |
| 4 | 商户 EID 为空 | status=S |
| 5 | IBAN 申请失败 | status=F, retryCount++, message=错误信息 |
| 6 | 申请过程抛异常 | status=F, retryCount++, 异常被捕获 |
| 7 | 通知发送失败 | status 仍然为 C |
| 8 | MerchantMigrateJob disabled | 不查询不处理 |
| 9 | MerchantMigrateJob 时间窗口外 | 跳过执行 |
6. Feature 4: 迁移通知 & TodoCard (MigrateNotifyService)
6.1 通知发送流程 (sendMigrateNotification)
触发时机: 迁移成功后(status=C)由 CustomerMigrateService / MerchantMigrateService 调用
status=C] --> CHECK_SENT{notifyStatus
已设置?} CHECK_SENT -->|是| SKIP_DUP[跳过,防止重复通知] CHECK_SENT -->|否| ADD_CARD[调用 CSimpleClient
.addTodoCard
businessId=IBAN_MIGRATE_{memberId}] ADD_CARD --> SEND_MSG[调用 CSimpleClient
.sendGeneralMessage
Botim 官方账号推送] SEND_MSG --> UPDATE_STATUS[notifyStatus = S] ADD_CARD -->|异常| LOG_WARN[log.warn
不影响迁移状态] style SKIP_DUP fill:#fff3cd
6.2 TodoCard 移除流程 (removeTodoCard)
触发时机: 用户首笔 ZAND 交易完成后
memberId] --> QUERY[查询该用户所有迁移记录] QUERY -->|无记录| SKIP_EMPTY[跳过] QUERY -->|有记录| FILTER{存在 status=C
且 notifyStatus=S
的记录?} FILTER -->|否| SKIP_NO_CARD[跳过,无需移除] FILTER -->|是| REMOVE[调用 CSimpleClient
.removeTodoCard
businessId=IBAN_MIGRATE_{memberId}] REMOVE --> UPDATE[所有已通知记录
notifyStatus = R] style SKIP_EMPTY fill:#fff3cd style SKIP_NO_CARD fill:#fff3cd
6.3 测试用例
| # | 用例 | 预期结果 |
|---|---|---|
| 1 | 迁移成功,发送通知 | addTodoCard + sendMessage 被调用, notifyStatus=S |
| 2 | 重复调用 sendMigrateNotification | 仅发送一次(幂等) |
| 3 | addTodoCard 失败 | log.warn, 不影响迁移 |
| 4 | removeTodoCard 正常 | CSimple 被调用, notifyStatus=R |
| 5 | removeTodoCard 无迁移记录 | 不调用 CSimple |
| 6 | removeTodoCard notifyStatus!=S | 不调用 CSimple |
7. Feature 6: Mini-Program 查询 (QueryMigrateVamsProvider)
7.1 接口定义
| 项目 | 说明 |
|---|---|
| 接口类型 | CGS 认证接口(需 AccessMember) |
| 处理器 | QueryMigrateVamsProvider |
| 请求 | QueryMigrateVamsRequestBody(无额外参数,memberId 取自 AccessMember) |
| 响应 | QueryMigrateVamsResponseBody |
7.2 响应参数
| 字段 | 类型 | 说明 |
|---|---|---|
| migrateList | List\<MigrateVamInfo\> | 迁移记录列表 |
| totalNum | Integer | 记录总数 |
MigrateVamInfo 字段:
| 字段 | 说明 |
|---|---|
| oldIban | FAB IBAN(解密后) |
| newIban | PAYBY IBAN(解密后) |
| name | 姓名 |
| memberType | CUSTOMER / MERCHANT |
| status | 状态码 |
| statusDesc | 状态描述 |
| message | 错误信息 |
7.3 处理流程
memberId 取自 AccessMember] --> QUERY[查询该用户所有迁移记录] QUERY --> CONVERT[转换为 MigrateVamInfo] CONVERT --> DECRYPT[解密 oldIban / newIban
via UES] DECRYPT --> MAP_STATUS[状态码 → 可读描述] MAP_STATUS --> RESP[返回 migrateList + totalNum]
7.4 测试用例
| # | 用例 | 预期结果 |
|---|---|---|
| 1 | 有迁移记录的用户 | 返回解密后的 oldIban/newIban + 状态描述 |
| 2 | 无迁移记录的用户 | 返回空列表, totalNum=0 |
| 3 | 多条记录(不同状态) | 正确映射各状态描述 |
| 4 | newIban 为 null(未完成迁移) | newIban 返回 null |
8. Feature 6.1: ZAND 交易通知 (ZandTransNotifyHandler)
8.1 MQ 配置
| 项目 | 值 |
|---|---|
| Exchange | exchange.visii.zand.transaction |
| Exchange Type | Topic |
| Queue | queue.vis.zandTransNotify |
| RoutingKey | #(接收所有消息) |
| 触发时机 | ZAND 交易完成 |
8.2 处理流程
exchange.visii.zand.transaction] --> PARSE[解析 ZandTransNotifyMessage
提取 memberId] PARSE --> CHECK{memberId 非空?} CHECK -->|否| SKIP[跳过] CHECK -->|是| REMOVE[调用 migrateNotifyService
.removeTodoCard-memberId-] REMOVE -->|成功| DONE[处理完成] REMOVE -->|异常| LOG[log.warn 不影响主流程]
8.3 测试用例
| # | 用例 | 预期结果 |
|---|---|---|
| 1 | 正常消息 memberId 非空 | 调用 removeTodoCard |
| 2 | memberId 为空 | 不调用 removeTodoCard |
| 3 | 解析失败 | 不抛异常 |
| 4 | removeTodoCard 异常 | 不传播 |
9. Feature 7: 自动重试调度 (MigrateRetryJob)
9.1 重试策略
| retryCount | 延迟时间 | 动作 |
|---|---|---|
| 1 | 5 分钟 | 第一次重试 |
| 2 | 30 分钟 | 第二次重试 |
| >= 3 | - | 标记为 FINAL_FAILED,不再自动重试 |
9.2 处理流程
cron: 每5分钟] --> CHECK_ENABLED{enabled?} CHECK_ENABLED -->|No| END_DISABLED[跳过] CHECK_ENABLED -->|Yes| CHECK_WINDOW{在时间窗口内?
startHour ~ endHour} CHECK_WINDOW -->|No| END_WINDOW[跳过] CHECK_WINDOW -->|Yes| QUERY[查询 FAILED 记录
retryCount < 3
LIMIT batchSize] QUERY -->|空| END_EMPTY[无可重试记录] QUERY -->|有记录| LOOP[遍历每条记录] LOOP --> CHECK_RETRY{retryCount >= 3?} CHECK_RETRY -->|Yes| MARK_FF[CAS: F → FF
标记最终失败] CHECK_RETRY -->|No| CHECK_DELAY{gmtModified + delay <= now?} CHECK_DELAY -->|No| SKIP_DELAY[延迟未到,跳过] CHECK_DELAY -->|Yes| CAS[CAS: F → I] CAS -->|成功| DISPATCH{memberType?} CAS -->|冲突| SKIP_CAS[CAS冲突,跳过] DISPATCH -->|CUSTOMER| EXEC_C[customerMigrateService.migrateOne] DISPATCH -->|MERCHANT| EXEC_M[merchantMigrateService.migrateOne]
9.3 配置项
| 配置项 | 说明 | 默认值 |
|---|---|---|
elastic.job.task.cron.migrate.retry | 定时任务 cron | 0 0/5 * * * ? * |
| MigrateEngineConfiguration.enabled | 总开关 | true |
| MigrateEngineConfiguration.startHour | 允许执行开始小时 | 配置 |
| MigrateEngineConfiguration.endHour | 允许执行结束小时 | 配置 |
| MigrateEngineConfiguration.batchSize | 每批查询数量 | 100 |
| MIGRATE_MAX_RETRY_COUNT | 最大重试次数 | 3 |
10. Feature 8.1: 手动修改状态 (putMigrateStatus)
10.1 接口定义
| 项目 | 说明 |
|---|---|
| 接口 | CounterFacade#putMigrateStatus |
| 请求类 | PutMigrateStatusRequest |
| 处理器 | PutMigrateStatusProcessor |
10.2 请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| idList | List\<Long\> | Y | 迁移记录ID列表 |
| status | String | Y | 目标状态码(P/C/F/FF/S) |
10.3 处理逻辑
status 必须是有效枚举值] VALIDATE --> QUERY[queryByIds 查询记录] QUERY -->|空| SUCCESS_NOOP[返回 SUCCESS
无记录需更新] QUERY -->|有记录| LOOP[遍历每条记录] LOOP --> SET_STATUS[设置 status = 目标状态] SET_STATUS --> CHECK_PENDING{目标状态 == PENDING?} CHECK_PENDING -->|Yes| RESET[retryCount = 0
message = null] CHECK_PENDING -->|No| NO_RESET[保留原 retryCount 和 message] RESET --> UPDATE[updateById] NO_RESET --> UPDATE UPDATE --> SUCCESS[返回 SUCCESS]
10.4 测试用例
| # | 用例 | 预期结果 |
|---|---|---|
| 1 | 将 FAILED 记录设为 COMPLETED | status=C, retryCount/message 不变 |
| 2 | 将 FINAL_FAILED 记录设为 PENDING | status=P, retryCount=0, message=null |
| 3 | 查询不到记录 | SUCCESS (no-op) |
| 4 | 将 FAILED 记录设为 SKIPPED | status=S, retryCount/message 不变 |
11. Feature 8.2: 手动重试 (retryMigrate)
11.1 接口定义
| 项目 | 说明 |
|---|---|
| 接口 | CounterFacade#retryMigrate |
| 请求类 | RetryMigrateRequest |
| 处理器 | RetryMigrateProcessor |
11.2 请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| idList | List\<Long\> | Y | 迁移记录ID列表 |
11.3 处理逻辑
无记录需重试] QUERY -->|有记录| LOOP[遍历每条记录] LOOP --> CHECK{status == FAILED
或 FINAL_FAILED?} CHECK -->|Yes| RESET[status = PENDING
retryCount = 0
message = null] CHECK -->|No| SKIP[跳过该记录] RESET --> UPDATE[updateById] UPDATE --> SUCCESS[返回 SUCCESS]
11.4 测试用例
| # | 用例 | 预期结果 |
|---|---|---|
| 1 | FAILED 记录 | 重置为 PENDING, retryCount=0, message=null |
| 2 | FINAL_FAILED 记录 | 重置为 PENDING, retryCount=0, message=null |
| 3 | COMPLETED 记录 | 跳过,不做修改 |
| 4 | 混合状态(F + FF + C) | 仅 F 和 FF 被重置,C 跳过 |
| 5 | 查询不到记录 | SUCCESS (no-op) |
12. Feature 9: 交易状态变更通知 (TransStatusChangeHandler)
12.1 MQ 配置
| 项目 | 值 |
|---|---|
| Exchange | exchange.visii.trans |
| Exchange Type | Topic |
| Queue | queue.vis.transStatusChange |
| RoutingKey (发送端) | status.${statusCode}.${memberId} |
| RoutingKey (消费端) | status.# |
| 触发时机 | 交易首次插入(WV)、每次状态变更 |
| 失败策略 | try-catch 包裹,失败仅 log.warn |
12.2 消息体 TransStatusChangeMessage
| 字段 | 类型 | 说明 |
|---|---|---|
| orderId | Long | 订单ID |
| memberId | String | 会员ID |
| bankCode | String | 银行编码 |
| status | String | 当前状态码 |
| bankOrderNo | String | 银行订单号 |
| previousStatus | String | 变更前状态码(首次插入为null) |
| amount | BigDecimal | 交易金额 |
| currency | String | 币种 |
| iban | String | IBAN |
| direction | String | 方向 (I/O) |
| gmtModified | String | 修改时间 |
12.3 处理流程
exchange.visii.trans] --> PARSE[解析 JSON → TransStatusChangeMessage] PARSE -->|解析失败| LOG_WARN[log.warn 跳过] PARSE -->|成功| CHECK_MEMBER{memberId 为空?} CHECK_MEMBER -->|是| LOG_EMPTY[log.warn 跳过] CHECK_MEMBER -->|否| CHECK_PREFIX{memberId 以 1 开头?} CHECK_PREFIX -->|否 商户| IGNORE[忽略,不处理] CHECK_PREFIX -->|是 个人用户| CHECK_STATUS{status == WV?} CHECK_STATUS -->|否| SKIP_STATUS[非首笔交易,跳过] CHECK_STATUS -->|是| REMOVE[调用 migrateNotifyService
.removeTodoCard-memberId-] REMOVE -->|成功| DONE[处理完成] REMOVE -->|异常| LOG_FAIL[log.warn 不影响主流程]
12.4 核心判断逻辑
以 1 开头?} B -->|1xxxxxxx
个人用户| C{status == WV?} B -->|2xxxxxxx / 3xxxxxxx
商户| D[直接忽略] C -->|WV 首笔交易| E[移除 TodoCard] C -->|V / MC / S / P
其他状态| F[跳过]
12.5 测试用例
| # | 用例 | 消息内容 | 预期结果 |
|---|---|---|---|
| 1 | 个人用户 WV 状态 | memberId=1xxx, status=WV | 调用 removeTodoCard |
| 2 | 个人用户 WV + 完整字段 | 包含 orderId/amount/iban 等 | 调用 removeTodoCard |
| 3 | 个人用户 V 状态 | memberId=1xxx, status=V | 不调用 removeTodoCard |
| 4 | 个人用户 MC 状态 | memberId=1xxx, status=MC | 不调用 removeTodoCard |
| 5 | 个人用户 S 状态 | memberId=1xxx, status=S | 不调用 removeTodoCard |
| 6 | 商户 WV 状态 | memberId=2xxx, status=WV | 不调用 removeTodoCard |
| 7 | 商户 3 开头 | memberId=3xxx, status=WV | 不调用 removeTodoCard |
| 8 | 空消息 | "" | 不处理 |
| 9 | null 消息 | null | 不处理 |
| 10 | memberId 为空 | memberId="", status=WV | 不处理 |
| 11 | memberId 为 null | memberId=null, status=WV | 不处理 |
| 12 | status 为 null | memberId=1xxx, status=null | 不调用 removeTodoCard |
| 13 | 非法 JSON | "invalid json" | 不抛异常,不处理 |
| 14 | removeTodoCard 抛异常 | memberId=1xxx, status=WV | 异常被捕获,不传播 |
13. 改动文件清单
13.1 新增文件
| 文件 | Feature | 说明 |
|---|---|---|
facade/domain/TransStatusChangeMessage.java | 9 | 交易状态变更消息体 |
facade/domain/ZandTransNotifyMessage.java | 6.1 | ZAND交易通知消息体 |
facade/domain/cgs/MigrateVamInfo.java | 6 | 迁移信息VO |
facade/domain/cgs/QueryMigrateVamsRequestBody.java | 6 | 查询迁移请求 |
facade/domain/cgs/QueryMigrateVamsResponseBody.java | 6 | 查询迁移响应 |
facade/request/PutMigrateStatusRequest.java | 8.1 | 手动修改状态请求 |
facade/request/RetryMigrateRequest.java | 8.2 | 手动重试请求 |
facade/enums/MigrateStatus.java | 全局 | 迁移状态枚举 |
core/dal/entity/MigrateRecordDO.java | 全局 | 迁移记录实体 |
core/dal/mapper/MigrateRecordMapper.java | 全局 | MyBatis Mapper |
core/dal/configuration/MigrateEngineConfiguration.java | 3/5/7 | 迁移引擎配置 |
core/dal/configuration/ZandTransNotifyConfiguration.java | 6.1 | ZAND通知MQ配置 |
core/dal/configuration/TransStatusChangeConfiguration.java | 9 | 交易变更MQ配置 |
domainservice/service/CustomerMigrateService.java | 3 | 个人迁移服务接口 |
domainservice/service/impl/CustomerMigrateServiceImpl.java | 3 | 个人迁移服务实现 |
domainservice/service/MerchantMigrateService.java | 5 | 商户迁移服务接口 |
domainservice/service/impl/MerchantMigrateServiceImpl.java | 5 | 商户迁移服务实现 |
domainservice/service/MigrateNotifyService.java | 4 | 通知服务接口 |
domainservice/service/impl/MigrateNotifyServiceImpl.java | 4 | 通知服务实现 |
domainservice/service/MigrateRecordService.java | 全局 | 迁移记录服务接口 |
domainservice/service/impl/MigrateRecordServiceImpl.java | 全局 | 迁移记录服务实现 |
domainservice/repo/MigrateRecordRepository.java | 全局 | 数据访问层接口 |
domainservice/repo/impl/MigrateRecordRepositoryImpl.java | 全局 | 数据访问层实现 |
domainservice/file/impl/MigrateFileStrategy.java | 2 | 迁移文件解析策略 |
domainservice/mq/ZandTransNotifyHandler.java | 6.1 | ZAND交易通知消费者 |
domainservice/mq/TransStatusChangeHandler.java | 9 | 交易状态变更消费者 |
ext/daemon/CustomerMigrateJob.java | 3 | 个人迁移定时任务 |
ext/daemon/MerchantMigrateJob.java | 5 | 商户迁移定时任务 |
ext/daemon/MigrateRetryJob.java | 7 | 自动重试定时任务 |
ext/daemon/executor/TaskExecutorConfig.java | 3/5/7 | 线程池配置 |
ext/integration/CSimpleClient.java | 4 | CSimple通知客户端接口 |
ext/integration/impl/CSimpleClientImpl.java | 4 | CSimple通知客户端实现 |
ext/service/manage/PutMigrateStatusProcessor.java | 8.1 | 手动修改状态处理器 |
ext/service/manage/RetryMigrateProcessor.java | 8.2 | 手动重试处理器 |
ext/service/cgs/QueryMigrateVamsProvider.java | 6 | Mini-Program查询接口 |
MigrateRecordMapper.xml | 全局 | MyBatis SQL映射 |
13.2 修改文件
| 文件 | 改动说明 |
|---|---|
facade/CounterFacade.java | 新增 putMigrateStatus / retryMigrate 接口方法 |
ext/service/CounterFacadeImpl.java | 新增 putMigrateStatus / retryMigrate 实现 |
core/common/VisConstants.java | 新增迁移相关常量 |
core/common/CgsConstants.java | 新增 CGS 服务码 |
facade/enums/FileType.java | 新增 MIGRATE 文件类型 |
domain/enums/IdKey.java | 新增 MIGRATE_RECORD ID生成键 |
| pom.xml (各模块) | 依赖调整 |
13.3 测试文件
| 文件 | 用例数 | 覆盖范围 |
|---|---|---|
CustomerMigrateServiceTest.java | 7 | 正常开户/已有账户/KYC失败/开户失败/异常 |
MerchantMigrateServiceTest.java | 9 | 正常开户/已有账户/EID缺失/开户失败/异常 |
CustomerMigrateJobTest.java | 6 | 开关/时间窗口/CAS/批量提交 |
MerchantMigrateJobTest.java | 6 | 开关/时间窗口/CAS/批量提交 |
MigrateNotifyServiceTest.java | 14 | 发送通知/幂等/移除TodoCard/异常 |
MigrateFileStrategyTest.java | 11 | 正常解析/必填校验/去重/大批量/空文件 |
ZandTransNotifyHandlerTest.java | 8 | 正常消费/空消息/异常 |
QueryMigrateVamsProviderTest.java | 8 | 有记录/无记录/多状态/解密 |
QueryMemberAccountsProcessorTest.java | 13 | 账户查询相关 |
FileFlowManageFacadeTest.java | 4 | 文件流程管理 |
MigrateRetryJobTest.java | 8 | 开关/时间窗口/延迟/重试/FF/CAS冲突 |
PutMigrateStatusProcessorTest.java | 4 | 设为C/P/S、无记录 |
RetryMigrateProcessorTest.java | 5 | F重置/FF重置/C跳过/混合/无记录 |
TransStatusChangeHandlerTest.java | 14 | 个人WV/非WV/商户/空消息/异常 |
| 合计 | 117 |
14. 测试指南
14.1 个人用户迁移 - 测试步骤
前置条件: 上传包含个人用户的迁移 Excel 文件
| 步骤 | 操作 | 验证点 |
|---|---|---|
| 1 | 上传 Excel,包含 memberId=1xxx, memberType=CUSTOMER | t_migrate_record 中新增记录,status=P, oldIban 已加密 |
| 2 | 确认 enabled=true 且在时间窗口内 | CustomerMigrateJob 开始运行 |
| 3 | 等待定时任务触发(每2分钟) | 记录 status 变为 I |
| 4 | 该用户 KYC 已通过 | 调用 PAYBY 开户接口 |
| 5 | 开户成功 | status=C, newIban 已加密存储 |
| 6 | 检查通知 | Botim 收到推送消息 + TodoCard 出现 |
| 7 | 检查 DB | notifyStatus=S |
| 8 | 该用户 KYC 未通过 | status=S, 不调用开户 |
| 9 | 开户接口返回失败 | status=F, retryCount=1, message=错误信息 |
| 10 | 该用户已有 PAYBY 账户 | status=C, 填入已有 IBAN, 不重复开户 |
14.2 商户迁移 - 测试步骤
前置条件: 上传包含商户的迁移 Excel 文件
| 步骤 | 操作 | 验证点 |
|---|---|---|
| 1 | 上传 Excel,包含 memberId=2xxx, memberType=MERCHANT | t_migrate_record 中新增记录,status=P |
| 2 | 等待 MerchantMigrateJob 触发 | 记录 status 变为 I |
| 3 | 商户 EID 存在,贸易许可有效 | 调用 PAYBY 开户(COMPANY 类型) |
| 4 | 开户成功 | status=C, newIban 已加密 |
| 5 | 检查通知 | Botim 推送 + TodoCard |
| 6 | 商户信息为 null 或 EID 为空 | status=S |
| 7 | 开户失败 | status=F, retryCount++ |
14.3 文件上传 - 测试步骤
| 步骤 | 操作 | 验证点 |
|---|---|---|
| 1 | 上传正常 Excel(混合 CUSTOMER + MERCHANT) | 全部入库,status=P |
| 2 | 上传 memberId 为空的行 | 返回 FAIL,报错行号 |
| 3 | 上传 oldIban 为空的行 | 返回 FAIL,报错行号 |
| 4 | 上传文件内有重复 memberId | 仅入库一条 |
| 5 | 重复上传同一文件 | DuplicateKey 报错 |
| 6 | 大文件(>1000行) | 分批入库,全部成功 |
14.4 Mini-Program 查询 - 测试步骤
| 步骤 | 操作 | 验证点 |
|---|---|---|
| 1 | 用户有迁移记录,已完成 | 返回 oldIban + newIban(明文)+ status=COMPLETED |
| 2 | 用户有迁移记录,未完成 | 返回 oldIban + newIban=null + status 描述 |
| 3 | 用户无迁移记录 | 返回空列表 |
14.5 自动重试 - 测试步骤
前置条件: 已有 FAILED 状态的迁移记录
| 步骤 | 操作 | 验证点 |
|---|---|---|
| 1 | 确认 enabled=true | 定时任务运行 |
| 2 | 准备 status=F, retryCount=1 的记录,gmtModified 为 6 分钟前 | 5分钟延迟已满足 |
| 3 | 等待定时任务触发(每5分钟) | 记录状态变为 I,然后变为 C 或 F |
| 4 | 准备 retryCount=2 的记录,gmtModified 为 31 分钟前 | 30分钟延迟已满足 |
| 5 | 等待定时任务触发 | 记录被重试 |
| 6 | 准备 retryCount=3 的 FAILED 记录 | 不再重试 |
| 7 | 等待定时任务触发 | 记录状态变为 FF (FINAL_FAILED) |
| 8 | 将 enabled 设为 false | 定时任务跳过,不查询任何记录 |
14.6 手动修改状态 - 测试步骤
接口: CounterFacade#putMigrateStatus
| 步骤 | 操作 | 验证点 |
|---|---|---|
| 1 | 调用 putMigrateStatus, idList=[记录ID], status="C" | status=C, retryCount/message 不变 |
| 2 | 调用 putMigrateStatus, idList=[记录ID], status="P" | status=P, retryCount=0, message=null |
| 3 | 调用 putMigrateStatus, idList=[记录ID], status="S" | status=S |
| 4 | 调用 putMigrateStatus, idList=[不存在的ID] | SUCCESS (no-op) |
| 5 | 调用 putMigrateStatus, status="INVALID" | FAIL(无效状态码) |
14.7 手动重试 - 测试步骤
接口: CounterFacade#retryMigrate
| 步骤 | 操作 | 验证点 |
|---|---|---|
| 1 | 准备 FAILED 记录,调用 retryMigrate(idList) | 记录变为 PENDING, retryCount=0, message=null |
| 2 | 准备 FINAL_FAILED 记录,调用 retryMigrate(idList) | 同上,FF 也能被重置 |
| 3 | 准备 COMPLETED 记录,调用 retryMigrate(idList) | 记录不变 |
| 4 | 传入混合状态记录的 idList | 仅 F 和 FF 被重置 |
| 5 | 重置后等待引擎拾取 | PENDING 记录被引擎重新拾取并执行迁移 |
14.8 交易通知 - 测试步骤
前置条件: 用户已迁移成功(status=C, notifyStatus=S)
| 步骤 | 操作 | 验证点 |
|---|---|---|
| 1 | ZAND 交易完成,发送 ZandTransNotifyMessage | removeTodoCard 被调用, notifyStatus=R |
| 2 | Visii 发送 TransStatusChangeMessage, memberId=1xxx, status=WV | removeTodoCard 被调用 |
| 3 | Visii 发送 TransStatusChangeMessage, memberId=2xxx, status=WV | 忽略(商户) |
| 4 | Visii 发送 TransStatusChangeMessage, memberId=1xxx, status=V | 忽略(非 WV) |
| 5 | 该用户无迁移记录或 TodoCard 已移除 | 内部跳过,无副作用 |
| 6 | CSimple 服务异常 | VIS 仅打印 warn 日志,不影响主流程 |
15. 端到端完整场景
Counter修改
本次改动分支:fea-260301-iban-migrate,涉及依赖升级和四个功能点。
依赖升级
| 依赖 | 旧版本 | 新版本 |
|---|---|---|
vis-service-facade | 1.1.2-SNAPSHOT | 1.1.4-SNAPSHOT |
visii-service-facade | 1.0.0-SNAPSHOT | 1.0.1-SNAPSHOT |
vis-service-facade1.1.4 新增了CUSTOMER_MIGRATE_FILE和MERCHANT_MIGRATE_FILE两个文件类型枚举visii-service-facade1.0.1 新增了retryOpenAccount接口vis-service-facade1.1.4 新增了CounterFacade#putMigrateStatus和CounterFacade#retryMigrate接口
改动一:文件上传(/upload)新增两种迁移文件类型
vis-service-facade 的 FileType 枚举新增:
| 枚举值 | Code | 描述 |
|---|---|---|
CUSTOMER_MIGRATE_FILE | CMG | CMG-个人迁移文件 |
MERCHANT_MIGRATE_FILE | MMG | MMG-商户迁移文件 |
由于文件上传页面(/vis/upload)通过 FileType.values() 动态渲染下拉选项,升级依赖后自动生效,无需额外代码改动。
改动二:新增 Retry Open Account(重试VAM开户)功能
在 VIS Virtual Account 页面新增"Retry Open Account"按钮,支持批量重试VAM开户。
改动文件清单:
| 文件 | 改动类型 | 说明 |
|---|---|---|
VisiiClient.java | 接口新增 | 新增 retryOpenAccount(memberIds, bankCode) 方法 |
VisiiClientImpl.java | 实现新增 | 解析逗号分隔的memberIds,调用 counterOperateFacade.retryOpenAccount() |
VisAccountController.java | 接口新增 | 新增 GET(弹窗页面)和 POST(提交处理)两个接口 |
account.html | 前端按钮 | 新增"Retry Open Account"按钮 |
account.js | 前端逻辑 | 新增按钮点击事件,弹出重试开户表单 |
retry_open_account.html | 新建文件 | 弹窗表单:memberIds输入框 + bankCode下拉 |
retry_open_account.js | 新建文件 | 表单提交逻辑:校验 + AJAX POST |
改动三:新增 Put Migrate Status(手动设置迁移状态)功能
在 VIS Virtual Account 页面新增"Put Migrate Status"按钮,支持手动批量设置迁移记录状态。
改动文件清单:
| 文件 | 改动类型 | 说明 |
|---|---|---|
VisClient.java | 接口新增 | 新增 putMigrateStatus(idList, status) 方法 |
VisClientImpl.java | 实现新增 | 解析逗号分隔的idList为List<Long>,调用 counterFacade.putMigrateStatus() |
VisAccountController.java | 接口新增 | 新增 GET(弹窗页面)和 POST(提交处理)两个接口 |
account.html | 前端按钮 | 新增"Put Migrate Status"按钮 |
account.js | 前端逻辑 | 新增按钮点击事件,弹出设置迁移状态表单 |
put_migrate_status.html | 新建文件 | 弹窗表单:idList输入框 + status输入框 |
put_migrate_status.js | 新建文件 | 表单提交逻辑:校验 + AJAX POST |
改动四:新增 Retry Migrate Record(重试迁移记录)功能
在 VIS Virtual Account 页面新增"Retry Migrate Record"按钮,支持手动批量重试迁移记录。
改动文件清单:
| 文件 | 改动类型 | 说明 |
|---|---|---|
VisClient.java | 接口新增 | 新增 retryMigrate(idList) 方法 |
VisClientImpl.java | 实现新增 | 解析逗号分隔的idList为List<Long>,调用 counterFacade.retryMigrate() |
VisAccountController.java | 接口新增 | 新增 GET(弹窗页面)和 POST(提交处理)两个接口 |
account.html | 前端按钮 | 新增"Retry Migrate Record"按钮 |
account.js | 前端逻辑 | 新增按钮点击事件,弹出重试迁移记录表单 |
retry_migrate_record.html | 新建文件 | 弹窗表单:idList输入框 |
retry_migrate_record.js | 新建文件 | 表单提交逻辑:校验 + AJAX POST |
用例图
CMG/MMG] C[重试VAM开户
Retry Open Account] F[手动设置迁移状态
Put Migrate Status] G[重试迁移记录
Retry Migrate Record] end A --> B A --> C A --> F A --> G B --> |选择文件类型CMG或MMG
上传文件| D[(VIS服务)] C --> |提交memberIds + bankCode| E[(VISII服务)] F --> |提交idList + status| D G --> |提交idList| D
整体流程图
改动一:上传迁移文件
选择CMG或MMG] A3 --> A4[选择文件并填写备注] A4 --> A5[提交上传] A5 --> A6{上传结果} A6 --> |成功| A7[返回成功提示] A6 --> |失败| A8[返回错误信息]
改动二:重试VAM开户
逗号分隔多个] B4 --> B5[选择bankCode
ZAND 或 PAYBY] B5 --> B6[点击提交] B6 --> B7{前端校验} B7 --> |校验失败| B8[提示字段不能为空] B7 --> |校验通过| B9[POST /vis/virtualAccount/
retryOpenAccountSubmit] B9 --> B10[VisiiClientImpl
解析memberIds为List] B10 --> B11[调用counterOperateFacade
.retryOpenAccount] B11 --> B12{调用结果} B12 --> |SUCCESS| B13[返回成功提示] B12 --> |FAIL| B14[返回错误信息]
改动三:手动设置迁移状态
逗号分隔多个] C4 --> C5[输入status] C5 --> C6[点击提交] C6 --> C7{前端校验} C7 --> |校验失败| C8[提示字段不能为空] C7 --> |校验通过| C9[POST /vis/migrate/
putMigrateStatusSubmit] C9 --> C10[VisClientImpl
解析idList为List] C10 --> C11[调用counterFacade
.putMigrateStatus] C11 --> C12{调用结果} C12 --> |SUCCESS| C13[返回成功提示] C12 --> |FAIL| C14[返回错误信息]
改动四:重试迁移记录
逗号分隔多个] D4 --> D5[点击提交] D5 --> D6{前端校验} D6 --> |校验失败| D7[提示idList不能为空] D6 --> |校验通过| D8[POST /vis/migrate/
retryMigrateSubmit] D8 --> D9[VisClientImpl
解析idList为List] D9 --> D10[调用counterFacade
.retryMigrate] D10 --> D11{调用结果} D11 --> |SUCCESS| D12[返回成功提示] D11 --> |FAIL| D13[返回错误信息]
测试一:文件上传 - 新增CMG/MMG文件类型
前置条件: 已部署包含 vis-service-facade:1.1.4-SNAPSHOT 的环境
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 进入 VIS File Flow Management 页面 | 页面正常加载 |
| 2 | 点击上传按钮,打开文件上传弹窗 | 弹窗正常弹出 |
| 3 | 展开 fileType 下拉框 | 下拉框中包含 CMG-个人迁移文件 和 MMG-商户迁移文件 两个新选项 |
| 4 | 选择 CMG,上传一个合法的个人迁移文件,填写备注,提交 | 提交成功,文件进入处理流程 |
| 5 | 选择 MMG,上传一个合法的商户迁移文件,填写备注,提交 | 提交成功,文件进入处理流程 |
| 6 | 上传一个格式不正确的文件 | 返回明确的错误提示 |
测试二:Retry Open Account - 重试VAM开户
前置条件: 已部署包含 visii-service-facade:1.0.1-SNAPSHOT 的环境,且存在开户失败的member记录
2.1 页面展示
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 进入 VIS Virtual Account 页面 | 页面正常加载 |
| 2 | 观察工具栏按钮 | 在 Retry Trans 和 Write Off 按钮旁边,出现 Retry Open Account 按钮 |
| 3 | 点击 Retry Open Account 按钮 | 弹出 750x550 的表单弹窗,标题为"Retry Open Account" |
2.2 表单校验
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 不填任何字段,直接点击提交 | 提示 bankCode 不允许为空 |
| 2 | 填写 memberIds,不选 bankCode(如果下拉框允许空选) | 提示 bankCode 不允许为空 |
| 3 | 不填 memberIds,选择 bankCode = ZAND,点击提交 | 提示 memberIds 不允许为空 |
| 4 | 点击重置按钮 | 表单清空还原 |
2.3 正常提交
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 输入单个 memberId(如 100001),bankCode 选择 ZAND,提交 | 返回成功提示 "Success!" |
| 2 | 输入多个 memberId,逗号分隔(如 100001,100002,100003),bankCode 选择 PAYBY,提交 | 返回成功提示 "Success!" |
| 3 | 输入含空格的 memberId(如 100001, 100002 , 100003),提交 | 返回成功提示(后端会自动trim空格) |
2.4 异常场景
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 输入不存在的 memberId,提交 | 返回明确的错误提示 |
| 2 | 输入已经开户成功的 memberId,提交 | 返回对应的业务提示(非系统异常) |
| 3 | 后端服务不可用时提交 | 返回 "Vis retryOpenAccount failed!" 错误提示 |
测试三:Put Migrate Status - 手动设置迁移状态
前置条件: 已部署包含 vis-service-facade:1.1.4-SNAPSHOT 的环境,且存在迁移记录
3.1 页面展示
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 进入 VIS Virtual Account 页面 | 页面正常加载 |
| 2 | 观察工具栏按钮 | 在 Retry Open Account 按钮旁边,出现 Put Migrate Status 按钮 |
| 3 | 点击 Put Migrate Status 按钮 | 弹出 750x550 的表单弹窗,标题为"Put Migrate Status" |
3.2 表单校验
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 不填任何字段,直接点击提交 | 提示 status 不允许为空 |
| 2 | 不填 idList,填写 status,点击提交 | 提示 idList 不允许为空 |
| 3 | 填写 idList,不填 status,点击提交 | 提示 status 不允许为空 |
| 4 | 点击重置按钮 | 表单清空还原 |
3.3 正常提交
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 输入单个 id(如 100001),填写 status,提交 | 返回成功提示 "Success!" |
| 2 | 输入多个 id,逗号分隔(如 100001,100002,100003),填写 status,提交 | 返回成功提示 "Success!" |
| 3 | 输入含空格的 id(如 100001, 100002 , 100003),提交 | 返回成功提示(后端会自动trim空格) |
3.4 异常场景
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 输入不存在的 id,提交 | 返回明确的错误提示 |
| 2 | 输入非数字的 id,提交 | 返回错误提示 |
| 3 | 后端服务不可用时提交 | 返回 "Vis putMigrateStatus failed!" 错误提示 |
测试四:Retry Migrate Record - 重试迁移记录
前置条件: 已部署包含 vis-service-facade:1.1.4-SNAPSHOT 的环境,且存在需要重试的迁移记录
4.1 页面展示
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 进入 VIS Virtual Account 页面 | 页面正常加载 |
| 2 | 观察工具栏按钮 | 在 Put Migrate Status 按钮旁边,出现 Retry Migrate Record 按钮 |
| 3 | 点击 Retry Migrate Record 按钮 | 弹出 750x550 的表单弹窗,标题为"Retry Migrate Record" |
4.2 表单校验
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 不填 idList,直接点击提交 | 提示 idList 不允许为空 |
| 2 | 点击重置按钮 | 表单清空还原 |
4.3 正常提交
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 输入单个 id(如 100001),提交 | 返回成功提示 "Success!" |
| 2 | 输入多个 id,逗号分隔(如 100001,100002,100003),提交 | 返回成功提示 "Success!" |
| 3 | 输入含空格的 id(如 100001, 100002 , 100003),提交 | 返回成功提示(后端会自动trim空格) |
4.4 异常场景
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 输入不存在的 id,提交 | 返回明确的错误提示 |
| 2 | 输入非数字的 id,提交 | 返回错误提示 |
| 3 | 后端服务不可用时提交 | 返回 "Vis retryMigrate failed!" 错误提示 |
API 接口参考
| 接口 | 方法 | 路径 | 参数 |
|---|---|---|---|
| 弹窗页面 | GET | /vis/virtualAccount/retryOpenAccount | 无 |
| 提交处理 | POST | /vis/virtualAccount/retryOpenAccountSubmit | memberIds(String, 逗号分隔), bankCode(String, ZAND/PAYBY) |
| 弹窗页面 | GET | /vis/migrate/putMigrateStatus | 无 |
| 提交处理 | POST | /vis/migrate/putMigrateStatusSubmit | idList(String, 逗号分隔), status(String) |
| 弹窗页面 | GET | /vis/migrate/retryMigrateRecord | 无 |
| 提交处理 | POST | /vis/migrate/retryMigrateSubmit | idList(String, 逗号分隔) |