搜 索

ZAND IBAN Migration - 测试指南

  • 40阅读
  • 2026年03月20日
  • 0评论
首页 / Default / 正文

Visii改动

分支: fea-260301-iban-migrate

一、改动总览

mindmap root((IBAN Migration
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
补偿重试

二、用例图

graph TB subgraph Actors["参与者"] User["终端用户
(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 Exchangeexchange.kyc.finish
Route Keykyc.eid.finish / kyc.eid.finish.vip
Queuekyc.visii.queue
触发条件KYC系统KYC完成后推送MQ消息
核心逻辑接收消息 → 检查开关/白名单 → 检查VA是否已存在 → 构建VA → 异步调用ZAND开户
流程图
flowchart TD A[接收MQ消息] --> B{消息为空?} B -- 是 --> Z1[跳过] B -- 否 --> C[解析KycFinishInfo] C --> D{memberId为空?} D -- 是 --> Z1 D -- 否 --> E{autoOpen开关=Y
或 白名单命中?} 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.customerAutoOpenStringY自动开户总开关
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 Exchangeexchange.kyc.delete
Route Keykyc.delete.eid
Queuekyc.delete.visii.queue
核心逻辑接收删除消息 → 触发MemberLogoutEventHandler关闭VA
流程图
flowchart TD A[接收MQ消息] --> B{消息为空?} B -- 是 --> Z1[跳过] B -- 否 --> C[解析KycDeleteInfo] C --> D{memberId为空?} D -- 是 --> Z1 D -- 否 --> E[构建MemberLogoutEvent
参数: 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触发关闭VAtestHandle_normalFlow

3.3 Counter批量重试开户 (retryOpenAccount)

改动说明
项目说明
接口CounterOperateFacade#retryOpenAccount
入参RetryOpenAccountRequest(bankCode, memberIdList)
出参CommonResponse
核心逻辑遍历memberIdList → 查询/创建VA → 触发补偿重试
流程图
flowchart TD A[接收retryOpenAccount请求] --> B[参数校验
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=nullFAILCounterOperateFacadeImplTest#testRetryOpenAccount_validateFail
memberIdList为空memberIdList=nullFAIL同上
memberIdList空列表memberIdList=[]FAIL同上
member无VA随机memberIdSUCCESS,创建VA并重试testRetryOpenAccount_noVaExists
正常重试已有VA的memberIdSUCCESStestRetryOpenAccount_normalFlow

3.4 小程序CGS接口 (VamAuth系列)

接口清单
ProviderServiceCode方法说明
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-infoV2查询同上,newVersion=true
VamAuthInit 流程图
flowchart TD A["/visii/vam/v1/auth/init"] --> B[获取memberId] B --> C{查询VA记录} C -- VA不存在 --> D1["openStatus = I (INIT)"] C -- VA存在 --> D2["openStatus = VA实际状态"] D1 --> E{isVip?} D2 --> E E -- 是 --> F1["vipType = VIP"] E -- 否 --> F2["vipType = NON_VIP"] F1 --> G[返回Response] F2 --> G
VamAuthActive 流程图
flowchart TD A["/visii/vam/v1/auth/active"] --> B[获取memberId] B --> C{查询VA记录} C -- "VA存在且VALID" --> D1["返回openStatus=V"] C -- "VA不存在" --> E[vaConverter.initVa创建VA] E --> E1{创建成功?} E1 -- 否 --> D2["返回openStatus=F"] E1 -- 是 --> F[设置accountSeq/name] F --> G[insert VA] G -- 重复键 --> G1[重新查询VA] G -- 成功 --> H G1 --> H{VA状态=INIT/FAIL?} C -- "VA存在且非VALID" --> H H -- 是 --> I[触发RetryApplyVamHandler
异步开户] H -- 否 --> J I --> J["返回openStatus=VA状态"]
VamAuthInfoQuery 流程图
flowchart TD A["/visii/vam/v1/auth/query-info"] --> B[查询VA记录] B -- VA不存在 --> C1[返回空Response] B -- VA存在 --> C2[填充bankCode/bankName/accountType] C2 --> D{IBAN不为空?} D -- 是 --> E1[设置iban/accountNo] D -- 否 --> F E1 --> F{name不为空?} F -- 是 --> G[解密name设置accountHolder] F -- 否 --> H[查询GRC限额] G --> H H --> I{kycType不为空?} I -- 是 --> J[查询limitConfig
设置vamLimit] I -- 否 --> K[返回Response] J --> K
测试要点
测试场景接口预期对应用例
memberId为空Init/Active/QueryFAILVamAuthProcessorTest#testVamAuthInit_memberIdNull
无VA记录查询InitInitopenStatus=I, vipType有值testVamAuthInit_noVa
有VA记录查询InitInitopenStatus=VA实际状态testVamAuthInit_withVa
新会员激活ActiveSUCCESS, DB创建VAtestVamAuthActive_newMember
带name激活ActiveSUCCESStestVamAuthActive_withName
重复激活Active幂等,SUCCESStestVamAuthActive_idempotent
无VA查询InfoQuerySUCCESS, bankCode=nulltestVamAuthInfoQuery_noVa
有VA查询InfoQuerybankCode=ZANDtestVamAuthInfoQuery_withVa
V2版本查询QueryV2bankCode=ZANDtestVamAuthInfoQuery_v2

四、新增/修改文件清单

4.1 新增文件

文件模块说明
KycFinishInfo.javacore/domain/dtoKYC完成MQ消息DTO
KycDeleteInfo.javacore/domain/dtoKYC删除MQ消息DTO
KycFinishListener.javadomainservice/mqKYC完成MQ监听器
KycDeleteListener.javadomainservice/mqKYC删除MQ监听器
VaBuilder.javadomainservice/builder从KycFinishInfo构建VA实体
RetryOpenAccountRequest.javafacade/domain/request重试开户请求DTO
RetryOpenAccountProcessor.javadomainservice/processor重试开户业务处理器
VamAuthInitProcessor.javadomainservice/service/impl/vaInit接口处理器
VamAuthActiveProcessor.javadomainservice/service/impl/vaActive接口处理器
VamAuthInfoQueryProcessor.javadomainservice/service/impl/vaInfoQuery接口处理器
VamAuthInitProvider.javaext/service/providerCGS Init接口Provider
VamAuthActiveProvider.javaext/service/providerCGS Active接口Provider
VamAuthInfoQueryProvider.javaext/service/providerCGS InfoQuery接口Provider
VamAuthInfoQueryV2Provider.javaext/service/providerCGS InfoQuery V2 Provider
CmsClient.java / CmsClientImpl.javaext/integration/internal/cmsCMS白名单服务客户端
BaseConverter.javadomainservice/converterCGS请求基础信息转换

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.javaKYC完成监听器测试 (4个用例)
KycDeleteListenerTest.javaKYC删除监听器测试 (4个用例)
CounterOperateFacadeImplTest.javaCounter接口测试 (含retryOpenAccount 5个用例)
VamAuthProcessorTest.javaCGS接口处理器测试 (11个用例)

五、端到端测试流程

sequenceDiagram participant Tester as 测试人员 participant MQ as RabbitMQ participant Visii as Visii服务 participant DB as 数据库(t_va) participant ZAND as ZAND API participant App as 小程序 rect rgb(230, 243, 255) Note over Tester,ZAND: 场景1: KYC完成自动开户 Tester->>MQ: 发送KYC完成消息到exchange.kyc.finish MQ->>Visii: KycFinishListener接收 Visii->>DB: 检查autoOpen开关 + 白名单 Visii->>DB: 创建VA记录(status=INIT) Visii->>ZAND: 异步调用CreateVA API ZAND-->>Visii: 返回IBAN Visii->>DB: 更新VA(status=VALID, iban=xxx) Tester->>DB: 验证: t_va有记录, status=V end rect rgb(255, 230, 230) Note over Tester,ZAND: 场景2: KYC删除关闭账户 Tester->>MQ: 发送KYC删除消息到exchange.kyc.delete MQ->>Visii: KycDeleteListener接收 Visii->>DB: 触发MemberLogout关闭VA Visii->>DB: 更新VA(status=CLOSED) Tester->>DB: 验证: t_va status=C end rect rgb(230, 255, 230) Note over Tester,App: 场景3: 小程序查询与开户 App->>Visii: /visii/vam/v1/auth/init Visii-->>App: openStatus + vipType App->>Visii: /visii/vam/v1/auth/active Visii->>DB: 创建VA + 异步开户 Visii-->>App: openStatus Note over Tester: 等待ZAND回调完成 App->>Visii: /visii/vam/v1/auth/query-info Visii-->>App: iban + bankName + vamLimit end rect rgb(245, 245, 245) Note over Tester,ZAND: 场景4: Counter批量重试 Tester->>Visii: retryOpenAccount(ZAND, [mid1,mid2]) Visii->>DB: 查询/创建VA Visii->>ZAND: 异步重试开户 Visii-->>Tester: SUCCESS end

六、关键验证点 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/init VIP用户返回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 改动说明

项目说明
Exchangeexchange.visii.trans
RoutingKey格式status.${statusCode}.${memberId}
触发时机交易首次插入(WV)、每次状态变更(WV→V/MC、通用流转、P→S)
失败策略try-catch包裹,失败仅log.warn,不影响主流程

8.2 消息体 TransStatusChangeMessage

字段类型说明
orderIdLong订单ID
memberIdString会员ID
bankCodeString银行编码
statusString当前状态码
bankOrderNoString银行订单号
previousStatusString变更前状态码(首次插入时为null)
amountBigDecimal交易金额
currencyString币种
ibanStringIBAN
directionString方向 (I/O)
gmtModifiedLocalDateTime修改时间

8.3 发布触发点

触发点方法状态变更
首次接收交易通知NotifyTransProcessorsaveAndValidate4Banknull → WV
验证后状态更新VamOrderRepositoryImplupdateStatus(VamOrder, VamOrderStatusEnum, VisiiUnityResultCodeEnum)WV → V/MC
通用状态流转VamOrderRepositoryImplupdateStatus(VamOrder, DbResultCarrier)各种状态流转
充值成功VamOrderRepositoryImplupdateToSuccess(VamOrder, DepositInfo)P → S

8.4 流程图

flowchart TD A[交易通知到达] --> B[NotifyTransProcessor
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 2Excel 文件解析、IBAN 加密、批量入库
个人用户迁移引擎Feature 3定时拾取 PENDING 个人记录,KYC 校验 → 开户 → 通知
商户迁移引擎Feature 5定时拾取 PENDING 商户记录,EID/贸易许可校验 → 开户 → 通知
迁移通知 & TodoCardFeature 4迁移成功后发送 TodoCard + Botim 消息,交易后移除
Mini-Program 查询Feature 6CGS 接口查询用户迁移状态和新旧 IBAN
ZAND 交易通知Feature 6.1消费 ZAND 交易完成通知,移除 TodoCard
自动重试调度Feature 7失败记录自动重试(最多3次,递增延迟)
手动修改状态Feature 8.1Counter 手动设置迁移记录状态
手动重试Feature 8.2Counter 手动重试失败记录
交易状态变更通知Feature 9消费 Visii 交易状态变更消息,WV 状态移除 TodoCard
graph TB subgraph "Feature 2 - 迁移文件上传" F2_UPLOAD[Counter 上传 Excel] F2_PARSE[MigrateFileStrategy 解析] F2_DB[(t_migrate_record
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

字段类型说明
idLong主键
fileIdLong关联的上传文件ID
memberIdString会员ID
memberTypeStringCUSTOMER / MERCHANT
nameString会员姓名
oldIbanStringFAB IBAN(加密存储)
newIbanStringPAYBY IBAN(加密存储,迁移成功后写入)
emailString邮箱
mobileString手机号
statusString迁移状态码
messageString状态说明/错误信息
retryCountInteger重试次数
notifyStatusString通知状态:null=未发送, S=已发送, R=已移除
extensionString扩展信息
gmtCreateLocalDateTime创建时间
gmtModifiedLocalDateTime修改时间

2.2 迁移状态枚举 MigrateStatus

Code枚举值说明
PPENDING待处理
IIN_PROGRESS处理中
CCOMPLETED已完成
FFAILED失败(可重试)
FFFINAL_FAILED最终失败(超过重试上限)
SSKIPPED已跳过(KYC未通过/EID缺失等)
stateDiagram-v2 [*] --> P : Excel文件导入 P --> I : 引擎拾取 (CAS P→I) I --> C : 迁移成功 (PAYBY开户成功) I --> F : 迁移失败 (retryCount++) I --> S : 不合格跳过 (KYC/EID缺失) F --> I : 自动重试 (retryCount < 3) F --> FF : 超过3次重试 FF --> P : 手动重试 (retryMigrate) F --> P : 手动重试 (retryMigrate) F --> C : 手动修改 (putMigrateStatus) F --> S : 手动修改 (putMigrateStatus) P --> S : 手动修改 (putMigrateStatus)

2.3 通知状态 notifyStatus 流转

说明触发动作
null未通知初始状态
S已发送迁移成功后 addTodoCard + sendMessage
R已移除用户首笔交易后 removeTodoCard

3. Feature 2: 迁移文件上传 (MigrateFileStrategy)

3.1 Excel 文件格式

字段必填说明
AmemberIdY会员ID
BmemberTypeYCUSTOMER / MERCHANT
CnameN姓名
DoldIbanYFAB IBAN
EemailN邮箱
FmobileN手机号

第一行为表头,从第二行开始解析。

3.2 处理流程

flowchart TD UPLOAD[Counter 上传 Excel 文件] --> OPEN[POI 打开 Workbook] OPEN --> LOOP[逐行解析] LOOP --> VALIDATE{memberId / memberType
/ 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
2memberId 为空行返回 FAIL,报错行号
3oldIban 为空行返回 FAIL,报错行号
4文件内 memberId 重复仅入库一条,后续跳过
5空文件(仅表头)SUCCESS,0条记录
6大批量文件(>1000条)分批入库,全部成功

4. Feature 3: 个人用户迁移引擎

4.1 调度配置

配置项说明默认值
elastic.job.task.cron.customer.migrate定时任务 cron0 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)

flowchart TD START[CustomerMigrateJob 拾取 PENDING 记录] --> CAS[CAS: P → I] CAS -->|冲突| SKIP[跳过] CAS -->|成功| SUBMIT[提交到 migrateExecutor 线程池] SUBMIT --> CHECK_EXIST{已有 PAYBY
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, 不重复开户
3KYC 未通过status=S, 不调用开户
4IBAN 申请失败status=F, retryCount++, message=错误信息
5申请过程抛异常status=F, retryCount++, 异常被捕获
6通知发送失败status 仍然为 C(不影响迁移结果)
7CustomerMigrateJob disabled不查询不处理

5. Feature 5: 商户迁移引擎

5.1 调度配置

配置项说明默认值
elastic.job.task.cron.merchant.migrate定时任务 cron0 0/2 * * * ? *(每2分钟)

其余配置与个人用户共享 MigrateEngineConfiguration。

5.2 迁移流程 (MerchantMigrateService.migrateOne)

flowchart TD START[MerchantMigrateJob 拾取 PENDING 记录] --> CAS[CAS: P → I] CAS -->|冲突| SKIP[跳过] CAS -->|成功| SUBMIT[提交到 migrateExecutor 线程池] SUBMIT --> CHECK_EXIST{已有 PAYBY
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)
memberTypeCUSTOMERCOMPANY
资格校验KYC 是否通过商户信息是否存在 & EID 非空
信息来源memberClient.queryFullInfomerchantClient + memberClient
开户字段name/EID/nationality/birthDatename/EID/贸易许可号/许可到期日
许可信息licenseNumber + expiryDate, issuer=MAINLAND
idTypeeideid
日期格式YYYYMMDDYYYYMMDD
通知相同(addTodoCard + sendMessage)相同

5.4 测试用例

#用例预期结果
1正常商户,EID 存在,开户成功status=C, newIban 已加密, 发送通知
2已有 PAYBY 账户status=C, 填入已有 IBAN, 不重复开户
3商户信息为 nullstatus=S
4商户 EID 为空status=S
5IBAN 申请失败status=F, retryCount++, message=错误信息
6申请过程抛异常status=F, retryCount++, 异常被捕获
7通知发送失败status 仍然为 C
8MerchantMigrateJob disabled不查询不处理
9MerchantMigrateJob 时间窗口外跳过执行

6. Feature 4: 迁移通知 & TodoCard (MigrateNotifyService)

6.1 通知发送流程 (sendMigrateNotification)

触发时机: 迁移成功后(status=C)由 CustomerMigrateService / MerchantMigrateService 调用

flowchart TD TRIGGER[迁移成功
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 交易完成后

flowchart TD TRIGGER[收到交易通知
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仅发送一次(幂等)
3addTodoCard 失败log.warn, 不影响迁移
4removeTodoCard 正常CSimple 被调用, notifyStatus=R
5removeTodoCard 无迁移记录不调用 CSimple
6removeTodoCard notifyStatus!=S不调用 CSimple

7. Feature 6: Mini-Program 查询 (QueryMigrateVamsProvider)

7.1 接口定义

项目说明
接口类型CGS 认证接口(需 AccessMember)
处理器QueryMigrateVamsProvider
请求QueryMigrateVamsRequestBody(无额外参数,memberId 取自 AccessMember)
响应QueryMigrateVamsResponseBody

7.2 响应参数

字段类型说明
migrateListList\<MigrateVamInfo\>迁移记录列表
totalNumInteger记录总数

MigrateVamInfo 字段:

字段说明
oldIbanFAB IBAN(解密后)
newIbanPAYBY IBAN(解密后)
name姓名
memberTypeCUSTOMER / MERCHANT
status状态码
statusDesc状态描述
message错误信息

7.3 处理流程

flowchart TD REQ[Mini-Program 请求
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多条记录(不同状态)正确映射各状态描述
4newIban 为 null(未完成迁移)newIban 返回 null

8. Feature 6.1: ZAND 交易通知 (ZandTransNotifyHandler)

8.1 MQ 配置

项目
Exchangeexchange.visii.zand.transaction
Exchange TypeTopic
Queuequeue.vis.zandTransNotify
RoutingKey#(接收所有消息)
触发时机ZAND 交易完成

8.2 处理流程

flowchart TD MQ[RabbitMQ 消息
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
2memberId 为空不调用 removeTodoCard
3解析失败不抛异常
4removeTodoCard 异常不传播

9. Feature 7: 自动重试调度 (MigrateRetryJob)

9.1 重试策略

retryCount延迟时间动作
15 分钟第一次重试
230 分钟第二次重试
>= 3-标记为 FINAL_FAILED,不再自动重试

9.2 处理流程

flowchart TD START[MigrateRetryJob 触发
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定时任务 cron0 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 请求参数

字段类型必填说明
idListList\<Long\>Y迁移记录ID列表
statusStringY目标状态码(P/C/F/FF/S)

10.3 处理逻辑

flowchart TD REQ[请求: idList + status] --> VALIDATE[校验参数
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 记录设为 COMPLETEDstatus=C, retryCount/message 不变
2将 FINAL_FAILED 记录设为 PENDINGstatus=P, retryCount=0, message=null
3查询不到记录SUCCESS (no-op)
4将 FAILED 记录设为 SKIPPEDstatus=S, retryCount/message 不变

11. Feature 8.2: 手动重试 (retryMigrate)

11.1 接口定义

项目说明
接口CounterFacade#retryMigrate
请求类RetryMigrateRequest
处理器RetryMigrateProcessor

11.2 请求参数

字段类型必填说明
idListList\<Long\>Y迁移记录ID列表

11.3 处理逻辑

flowchart TD REQ[请求: idList] --> QUERY[queryByIds 查询记录] QUERY -->|空| SUCCESS_NOOP[返回 SUCCESS
无记录需重试] 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 测试用例

#用例预期结果
1FAILED 记录重置为 PENDING, retryCount=0, message=null
2FINAL_FAILED 记录重置为 PENDING, retryCount=0, message=null
3COMPLETED 记录跳过,不做修改
4混合状态(F + FF + C)仅 F 和 FF 被重置,C 跳过
5查询不到记录SUCCESS (no-op)

12. Feature 9: 交易状态变更通知 (TransStatusChangeHandler)

12.1 MQ 配置

项目
Exchangeexchange.visii.trans
Exchange TypeTopic
Queuequeue.vis.transStatusChange
RoutingKey (发送端)status.${statusCode}.${memberId}
RoutingKey (消费端)status.#
触发时机交易首次插入(WV)、每次状态变更
失败策略try-catch 包裹,失败仅 log.warn

12.2 消息体 TransStatusChangeMessage

字段类型说明
orderIdLong订单ID
memberIdString会员ID
bankCodeString银行编码
statusString当前状态码
bankOrderNoString银行订单号
previousStatusString变更前状态码(首次插入为null)
amountBigDecimal交易金额
currencyString币种
ibanStringIBAN
directionString方向 (I/O)
gmtModifiedString修改时间

12.3 处理流程

flowchart TD MQ[RabbitMQ 消息
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 核心判断逻辑

graph LR A[收到消息] --> B{memberId
以 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空消息""不处理
9null 消息null不处理
10memberId 为空memberId="", status=WV不处理
11memberId 为 nullmemberId=null, status=WV不处理
12status 为 nullmemberId=1xxx, status=null不调用 removeTodoCard
13非法 JSON"invalid json"不抛异常,不处理
14removeTodoCard 抛异常memberId=1xxx, status=WV异常被捕获,不传播

13. 改动文件清单

13.1 新增文件

文件Feature说明
facade/domain/TransStatusChangeMessage.java9交易状态变更消息体
facade/domain/ZandTransNotifyMessage.java6.1ZAND交易通知消息体
facade/domain/cgs/MigrateVamInfo.java6迁移信息VO
facade/domain/cgs/QueryMigrateVamsRequestBody.java6查询迁移请求
facade/domain/cgs/QueryMigrateVamsResponseBody.java6查询迁移响应
facade/request/PutMigrateStatusRequest.java8.1手动修改状态请求
facade/request/RetryMigrateRequest.java8.2手动重试请求
facade/enums/MigrateStatus.java全局迁移状态枚举
core/dal/entity/MigrateRecordDO.java全局迁移记录实体
core/dal/mapper/MigrateRecordMapper.java全局MyBatis Mapper
core/dal/configuration/MigrateEngineConfiguration.java3/5/7迁移引擎配置
core/dal/configuration/ZandTransNotifyConfiguration.java6.1ZAND通知MQ配置
core/dal/configuration/TransStatusChangeConfiguration.java9交易变更MQ配置
domainservice/service/CustomerMigrateService.java3个人迁移服务接口
domainservice/service/impl/CustomerMigrateServiceImpl.java3个人迁移服务实现
domainservice/service/MerchantMigrateService.java5商户迁移服务接口
domainservice/service/impl/MerchantMigrateServiceImpl.java5商户迁移服务实现
domainservice/service/MigrateNotifyService.java4通知服务接口
domainservice/service/impl/MigrateNotifyServiceImpl.java4通知服务实现
domainservice/service/MigrateRecordService.java全局迁移记录服务接口
domainservice/service/impl/MigrateRecordServiceImpl.java全局迁移记录服务实现
domainservice/repo/MigrateRecordRepository.java全局数据访问层接口
domainservice/repo/impl/MigrateRecordRepositoryImpl.java全局数据访问层实现
domainservice/file/impl/MigrateFileStrategy.java2迁移文件解析策略
domainservice/mq/ZandTransNotifyHandler.java6.1ZAND交易通知消费者
domainservice/mq/TransStatusChangeHandler.java9交易状态变更消费者
ext/daemon/CustomerMigrateJob.java3个人迁移定时任务
ext/daemon/MerchantMigrateJob.java5商户迁移定时任务
ext/daemon/MigrateRetryJob.java7自动重试定时任务
ext/daemon/executor/TaskExecutorConfig.java3/5/7线程池配置
ext/integration/CSimpleClient.java4CSimple通知客户端接口
ext/integration/impl/CSimpleClientImpl.java4CSimple通知客户端实现
ext/service/manage/PutMigrateStatusProcessor.java8.1手动修改状态处理器
ext/service/manage/RetryMigrateProcessor.java8.2手动重试处理器
ext/service/cgs/QueryMigrateVamsProvider.java6Mini-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.java7正常开户/已有账户/KYC失败/开户失败/异常
MerchantMigrateServiceTest.java9正常开户/已有账户/EID缺失/开户失败/异常
CustomerMigrateJobTest.java6开关/时间窗口/CAS/批量提交
MerchantMigrateJobTest.java6开关/时间窗口/CAS/批量提交
MigrateNotifyServiceTest.java14发送通知/幂等/移除TodoCard/异常
MigrateFileStrategyTest.java11正常解析/必填校验/去重/大批量/空文件
ZandTransNotifyHandlerTest.java8正常消费/空消息/异常
QueryMigrateVamsProviderTest.java8有记录/无记录/多状态/解密
QueryMemberAccountsProcessorTest.java13账户查询相关
FileFlowManageFacadeTest.java4文件流程管理
MigrateRetryJobTest.java8开关/时间窗口/延迟/重试/FF/CAS冲突
PutMigrateStatusProcessorTest.java4设为C/P/S、无记录
RetryMigrateProcessorTest.java5F重置/FF重置/C跳过/混合/无记录
TransStatusChangeHandlerTest.java14个人WV/非WV/商户/空消息/异常
合计117

14. 测试指南

14.1 个人用户迁移 - 测试步骤

前置条件: 上传包含个人用户的迁移 Excel 文件

步骤操作验证点
1上传 Excel,包含 memberId=1xxx, memberType=CUSTOMERt_migrate_record 中新增记录,status=P, oldIban 已加密
2确认 enabled=true 且在时间窗口内CustomerMigrateJob 开始运行
3等待定时任务触发(每2分钟)记录 status 变为 I
4该用户 KYC 已通过调用 PAYBY 开户接口
5开户成功status=C, newIban 已加密存储
6检查通知Botim 收到推送消息 + TodoCard 出现
7检查 DBnotifyStatus=S
8该用户 KYC 未通过status=S, 不调用开户
9开户接口返回失败status=F, retryCount=1, message=错误信息
10该用户已有 PAYBY 账户status=C, 填入已有 IBAN, 不重复开户

14.2 商户迁移 - 测试步骤

前置条件: 上传包含商户的迁移 Excel 文件

步骤操作验证点
1上传 Excel,包含 memberId=2xxx, memberType=MERCHANTt_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)

步骤操作验证点
1ZAND 交易完成,发送 ZandTransNotifyMessageremoveTodoCard 被调用, notifyStatus=R
2Visii 发送 TransStatusChangeMessage, memberId=1xxx, status=WVremoveTodoCard 被调用
3Visii 发送 TransStatusChangeMessage, memberId=2xxx, status=WV忽略(商户)
4Visii 发送 TransStatusChangeMessage, memberId=1xxx, status=V忽略(非 WV)
5该用户无迁移记录或 TodoCard 已移除内部跳过,无副作用
6CSimple 服务异常VIS 仅打印 warn 日志,不影响主流程

15. 端到端完整场景

sequenceDiagram participant Counter as Counter App participant VIS as VIS System participant DB as t_migrate_record participant KYC as Member/KYC Service participant PAYBY as PAYBY IBAN Service participant CS as CSimple (通知) participant MP as Mini-Program participant Visii as Visii participant MQ as RabbitMQ Note over Counter,DB: 阶段1: 文件上传 Counter->>VIS: 上传 Excel 迁移文件 VIS->>VIS: MigrateFileStrategy 解析 VIS->>DB: 批量插入, status=P, oldIban加密 Note over VIS,PAYBY: 阶段2: 个人用户迁移 VIS->>DB: CustomerMigrateJob 查询 PENDING CUSTOMER VIS->>DB: CAS P → I VIS->>KYC: isKycPassed(memberId) KYC-->>VIS: true VIS->>KYC: queryFullInfo(memberId) KYC-->>VIS: name/EID/nationality/birthDate VIS->>PAYBY: applyIban(BASIC, eid) PAYBY-->>VIS: 成功, 新IBAN VIS->>DB: status=C, 加密存储 newIban Note over VIS,CS: 阶段3: 发送通知 VIS->>CS: addTodoCard(IBAN_MIGRATE_{memberId}) VIS->>CS: sendGeneralMessage VIS->>DB: notifyStatus=S Note over MP,VIS: 阶段4: 用户查询 MP->>VIS: QueryMigrateVams(memberId) VIS->>DB: 查询迁移记录 VIS-->>MP: oldIban + newIban (解密) + 状态描述 Note over Visii,CS: 阶段5: 首笔交易 → 移除TodoCard Visii->>MQ: TransStatusChangeMessage(memberId=1xxx, status=WV) MQ->>VIS: TransStatusChangeHandler VIS->>VIS: 个人用户 + WV → removeTodoCard VIS->>CS: removeTodoCard(IBAN_MIGRATE_{memberId}) VIS->>DB: notifyStatus=R Note over VIS,DB: 阶段6: 失败重试 VIS->>DB: MigrateRetryJob 查询 FAILED VIS->>DB: retryCount<3 → CAS F→I → 重新迁移 VIS->>DB: retryCount>=3 → CAS F→FF Note over Counter,DB: 阶段7: 手动干预 Counter->>VIS: retryMigrate(idList) VIS->>DB: FF→P, retryCount=0 Counter->>VIS: putMigrateStatus(idList, "S") VIS->>DB: status=S

Counter修改

本次改动分支:fea-260301-iban-migrate,涉及依赖升级和四个功能点。

依赖升级

依赖旧版本新版本
vis-service-facade1.1.2-SNAPSHOT1.1.4-SNAPSHOT
visii-service-facade1.0.0-SNAPSHOT1.0.1-SNAPSHOT
  • vis-service-facade 1.1.4 新增了 CUSTOMER_MIGRATE_FILEMERCHANT_MIGRATE_FILE 两个文件类型枚举
  • visii-service-facade 1.0.1 新增了 retryOpenAccount 接口
  • vis-service-facade 1.1.4 新增了 CounterFacade#putMigrateStatusCounterFacade#retryMigrate 接口

改动一:文件上传(/upload)新增两种迁移文件类型

vis-service-facadeFileType 枚举新增:

枚举值Code描述
CUSTOMER_MIGRATE_FILECMGCMG-个人迁移文件
MERCHANT_MIGRATE_FILEMMGMMG-商户迁移文件

由于文件上传页面(/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

用例图

graph LR subgraph 操作人员 A((测试/运营人员)) end subgraph Counter系统 - VIS模块 B[上传迁移文件
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

整体流程图

改动一:上传迁移文件

flowchart TB A1[进入 VIS File Flow 页面] --> A2[点击上传按钮] A2 --> A3[在fileType下拉框中
选择CMG或MMG] A3 --> A4[选择文件并填写备注] A4 --> A5[提交上传] A5 --> A6{上传结果} A6 --> |成功| A7[返回成功提示] A6 --> |失败| A8[返回错误信息]

改动二:重试VAM开户

flowchart TB B1[进入 VIS Virtual Account 页面] --> B2[点击 Retry Open Account 按钮] B2 --> B3[弹出表单窗口] B3 --> B4[输入memberIds
逗号分隔多个] 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[返回错误信息]

改动三:手动设置迁移状态

flowchart TB C1[进入 VIS Virtual Account 页面] --> C2[点击 Put Migrate Status 按钮] C2 --> C3[弹出表单窗口] C3 --> C4[输入idList
逗号分隔多个] 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[返回错误信息]

改动四:重试迁移记录

flowchart TB D1[进入 VIS Virtual Account 页面] --> D2[点击 Retry Migrate Record 按钮] D2 --> D3[弹出表单窗口] D3 --> D4[输入idList
逗号分隔多个] 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/retryOpenAccountSubmitmemberIds(String, 逗号分隔), bankCode(String, ZAND/PAYBY)
弹窗页面GET/vis/migrate/putMigrateStatus
提交处理POST/vis/migrate/putMigrateStatusSubmitidList(String, 逗号分隔), status(String)
弹窗页面GET/vis/migrate/retryMigrateRecord
提交处理POST/vis/migrate/retryMigrateSubmitidList(String, 逗号分隔)
无标签
评论区
暂无评论
avatar