fix: 修正金蝶解锁回调单号定位并新增解锁终态页面标识

- 回调方向与锁定相反:按金蝶单号+我方单号双条件反查最新locked锁记录(targetBillType回调为空不参与匹配)
- 回调处理批量化:一次IN查询+updateBatchById批量落库,消除逐张查询/更新N+1
- 新增KingdeeUnlockMark展示契约与fillUnlockMarks通用填充,付款申请/结算单/开票申请VO回填解锁时间、操作人、动作名称
- 更新金蝶单据锁定与解锁业务流程文档(回调方向、解锁后数据变化、事务语义、联调实测结论)
This commit is contained in:
刘泉佳
2026-10-15 11:29:11 +08:00
parent f5a7b76205
commit f23e24e808
10 changed files with 233 additions and 47 deletions
@@ -1,6 +1,7 @@
# 金蝶单据锁定与解锁业务流程(lockBusiData 锁定 - unLockNotifyBusiData 回调 - 失败补偿 - 删单还原)
- 日期:2026-10-09(2026-10-10 变更:① lockBusiData 确认为 1对1 接口,不支持逗号拼接批量,锁定/补偿改为逐张调用;② 付款申请单纳入锁单范围并完成接线)
- 日期:2026-10-09(2026-10-10 变更:① lockBusiData 确认为 1对1 接口,不支持逗号拼接批量,锁定/补偿改为逐张调用;② 付款申请单纳入锁单范围并完成接线。
2026-10-15 联调修正:③ 回调方向与锁定相反,解锁定位改为「金蝶单号+我方单号」双条件反查(targetBillType 回调为空不参与匹配);④ 回调处理批量化去 N+1;⑤ 付款申请 DELETE 还原链路实测通过;⑥ 解锁终态页面标识通用方案(KingdeeUnlockMark))
- 范围:正式结算单(应付 A102-2 / 应收 A302-2)、开票申请单(A303-1)、付款申请单(A103-1 应付下推 / A103-3 直接保存,2026-10-10 新纳入)推送金蝶成功后的单据锁定、金蝶解锁/删单回调、锁定失败定时补偿
- 设计文档:`docs/superpowers/specs/2026-10-09-kingdee-bill-lock-unlock-design.md`
- 锁记录表:`blade_kingdee_bill_lock`(一行 = 一张金蝶目标单)
@@ -46,31 +47,71 @@
### 2. 金蝶侧解锁回调(金蝶 → WLTMS)
金蝶在星瀚侧对单据解锁或删单后,回调 `POST /kingdee-bfi/unLockNotifyBusiData`(`UNLOCK`;targetBillNo 兼容单张/逗号拼接):
金蝶在星瀚侧对单据解锁或删单后,回调 `POST /kingdee-bfi/unLockNotifyBusiData`(`UNLOCK`)。
**回调方向与锁定相反(2026-10-15 真实报文确认)**:lockBusiData 由 WLTMS 发起(source=我方、target=金蝶),
unLockNotifyBusiData 由金蝶发起(source=金蝶、target=我方)。因此回调报文中:
`sourceBillNo`=金蝶单号(即锁记录 `targetBillNo`)、`targetBillNo`=WLTMS 单号(即锁记录 `sourceBillNo`)、
`targetBillType` **实测为空**(不参与匹配)。真实报文示例:
```json
{"lockAction":"UNLOCK","operationType":"DELETE","operator":"刘泉佳","sourceBillNo":"FKSQ-00000227",
"sourceBillType":"ap_payapply","sourceSystem":"XH","targetBillNo":"FKD2026101400001",
"targetBillType":"","targetSystem":"WLTMS","time":"2026-10-15 10:10:56"}
```
处理逻辑(`KingdeeBillLockServiceImpl.onUnlockNotify`):
```
校验:sourceSystem=XH、targetSystem=WLTMS、lockAction=UNLOCK(否则应答失败)
│
├─ operationType=SAVE(仅解锁,没删单)
│ └─ 锁记录 → unlocked,记录回调操作人/时间,解锁动作留痕
│
└─ operationType=DELETE(金蝶把单删了)
├─ 锁记录 → unlocked(同上)
└─ 按 targetBillType 反查 WLTMS 单据类型,分发还原:
├─ SETTLEMENT → 结算单作废(当前节点→已作废),
│ 同步信息还原推送前状态:kingdeeSyncStatus→unsynced、
│ 清 kingdeeBillNo/kingdeeBillId/syncedTime
│ 变更流水记"金蝶删单回调自动废除",操作人=金蝶报文 operator
├─ INVOICE_APPLY → 开票申请作废,kingdeeStatus→unsynced、清单据编号,
│ 刷新关联结算单开票状态,动作流水记 kingdee_delete
└─ PAY_APPLY → 付款申请复用 unlockBill(id, "金蝶删单回调")(ADR-0005 口径):
kingdeeStatus→生成失败、清 kingdeeBillId/kingdeeBillNo(原值日志留档),
不作废源单、不自动重推,人工核实后重推(A103-2 勾稽预检防重复推单)
校验:sourceSystem=XH、targetSystem=WLTMS、lockAction=UNLOCK、sourceBillNo/targetBillNo 非空(否则应答失败)
│
├─ 一次 IN 查询:target_bill_no in (回调sourceBillNo拆分, 支持逗号批量) and source_bill_no = 回调targetBillNo,
│ 按 id 降序取回,内存归并出各金蝶单号最新一条锁记录(避免逐张查询 N+1)
│
├─ 逐张匹配:无记录或非 locked → 幂等跳过(warn 留痕,重复通知防金蝶重试风暴)
│
├─ 命中 locked → 解锁状态批量落库:lock_status→unlocked、operationType、unlockTime、notifyOperator
│ (updateBatchById 一次批量更新,避免逐张 update N+1)
│
└─ operationType=DELETE(金蝶把单删了)→ 按「锁记录自身 targetBillType」反查 WLTMS 单据类型,分发还原:
├─ SETTLEMENT → 结算单作废(当前节点→已作废),
│ 同步信息还原推送前状态:kingdeeSyncStatus→unsynced、
│ 清 kingdeeBillNo/kingdeeBillId/syncedTime
│ 变更流水记"金蝶删单回调自动废除",操作人=金蝶报文 operator
├─ INVOICE_APPLY → 开票申请作废,kingdeeStatus→unsynced、清单据编号,
│ 刷新关联结算单开票状态,动作流水记 kingdee_delete
└─ PAY_APPLY → 付款申请复用 unlockBill(id, "金蝶删单回调")(ADR-0005 口径):
kingdeeStatus→生成失败、清 kingdeeBillId/kingdeeBillNo(原值日志留档),
不作废源单、不自动重推,人工核实后重推(A103-2 勾稽预检防重复推单)
应答:{updateCount: 处理张数, updateResultList: [1]}
```
重复回调幂等:单据已作废或锁记录已 unlocked → 该张跳过,不重复还原。
**事务语义**:整个回调处理在 `onUnlockNotify` 单一事务内(还原分支 `voidByKingdee`/`unlockBill` 均为 REQUIRED 传播加入同事务)——
任一张还原失败,**整批回滚**(包括已批量落库的解锁状态),应答失败触发金蝶重试,重试时按幂等口径从头重处理。
不存在"部分成功"中间态;若单张问题单阻塞整批回调,需人工处理后金蝶重试自动闭环。
**解锁后数据变化**(2026-10-15 实测口径):
锁记录(三类单据通用,`blade_kingdee_bill_lock`):
| 字段 | 变化 |
| --- | --- |
| lock_status | locked → unlocked(终态) |
| operation_type | 回调报文 operationType(SAVE-仅解锁 / DELETE-删单) |
| unlock_time | WLTMS 处理时间(注意:非金蝶报文 time,金蝶侧操作时间以日志为准) |
| notify_operator | 回调报文 operator(金蝶侧操作人) |
业务单据(按类型):
| 业务单据 | 作废状态 | 金蝶同步信息 | 其他 |
| --- | --- | --- | --- |
| 正式结算单(SETTLEMENT) | approval_status → voided(已作废,终态),当前节点→已作废 | kingdeeSyncStatus → unsynced,清 kingdeeBillNo/kingdeeBillId/syncedTime | 变更流水记"金蝶删单回调自动废除",操作人=金蝶报文 operator |
| 开票申请(INVOICE_APPLY) | approval_status → voided(作废) | kingdeeStatus → unsynced,清单据编号 | 刷新关联结算单开票状态;动作流水记 kingdee_delete |
| 付款申请(PAY_APPLY) | **不作废**,approval_status 不变 | kingdeeStatus synced/paying → failed(仅这两态可变,幂等守卫),清 kingdeeBillId/kingdeeBillNo(原值日志留档) | 不自动重推,人工核实后重推;页面靠解锁标识区分金蝶删单与推送失败 |
### 3. 锁定失败补偿(定时兜底,不依赖人工)
```
@@ -113,8 +154,9 @@ failed(锁定失败,待补偿)→ locked(金蝶锁定中)→ unlocked
1. **先占位再锁**:锁记录落库先于远程调用,补偿任务扫 `failed` 就是完整的待重试集合,不漏单
2. **锁失败不回滚推送**:金蝶单已生成,本地回滚只会制造断链;代价是短暂"已推未锁"窗口,由补偿任务闭合
3. **1对1逐张锁**:lockBusiData 单次仅支持一个源单号对一个金蝶单号,逐张调用互不连坐;成败结果 updateBatchById 批量落库,避免逐张 update
4. **回调幂等**:按 targetBillType+targetBillNo 找最新 locked 记录,找不到/已 unlocked 直接跳过应答成功,防金蝶重试风暴
5. **DELETE 还原口径**:作废源单 + 清金蝶同步信息 + 关联状态刷新;作废是终态,重推需新单重走流程(口径待业务联调确认)
4. **回调幂等**(2026-10-15 修正):回调方向与锁定相反,按「金蝶单号(回调 sourceBillNo)+ 我方单号(回调 targetBillNo)」双条件找最新 locked 记录(targetBillType 回调为空不参与匹配),找不到/已 unlocked 直接跳过应答成功,防金蝶重试风暴;
批量回调一次 IN 查询 + updateBatchById 批量落库,无逐张 N+1
5. **DELETE 还原口径**:作废源单 + 清金蝶同步信息 + 关联状态刷新(结算单/开票申请);付款申请不作废源单,走生成失败+人工重推(2026-10-15 联调实测通过);作废是终态,重推需新单重走流程
6. **前端锁状态展示**(2026-10-10 新增):结算单/开票申请列表与详情 VO 填充 `kingdeeLockStatus`/`kingdeeLockFailReason`/`kingdeeTryCount`
(后端 `mapActiveLocks` 每页一次批量查锁表,无 N+1;unlocked 终态不返回即 null=无锁/已解锁)。
前端渲染:`locked` 红 tag「金蝶锁定中」并禁用编辑/作废(后端守卫是权威);`failed` 且 `tryCount<5` 橙 tag「锁定失败·补偿中」tooltip 显示原因;
@@ -123,6 +165,12 @@ failed(锁定失败,待补偿)→ locked(金蝶锁定中)→ unlocked
DELETE 还原不作废源单,复用既有 `unlockBill`(ADR-0005:生成失败+清编号内码+人工重推),与 A103-2 反审核删除兜底同口径;
推送结果即时回填 kingdeeBillNo(`requireBfiBillIdAndFillNo`),推送事务提交后即以该单号 `lockAfterPush`;
锁定失败摘要拼入「同步金蝶」返回值(与开票申请同款),页面直接可见
8. **解锁终态页面标识**(2026-10-15 新增,通用方案):付款申请解锁后不作废(仅 kingdeeStatus=failed),与推送失败无法区分;
结算单/开票申请虽变已作废,但看不出是金蝶删单导致。统一方案:
VO 实现 `KingdeeUnlockMark` 展示契约(解锁时间/操作人/动作名称三字段),
`KingdeeBillLockService.fillUnlockMarks` 对无活跃锁的行一次 IN 反查最新 unlocked 锁记录(`mapUnlockedLocks`)统一回填;
前端三个页面(付款申请/开票申请/正式结算单)金蝶锁状态列渲染:`kingdeeUnlockTime` 非空 →
「金蝶删单」红 tag /「金蝶解锁」橙 tag,tooltip 显示操作人+时间;后续单据(如收票单)接入锁单时 VO 实现契约即可复用
## 五、代码落点速查
@@ -131,6 +179,7 @@ failed(锁定失败,待补偿)→ locked(金蝶锁定中)→ unlocked
| 锁单 Feign/DTO/应答 VO | `blade-bfi-api`:`BfiBillLockDTO`、`BfiBillLockResultVO`、`IBfiClient.lockBusiData`、`IBfiService.lockBusiData` |
| 锁类型常量与映射 | `transport/constant/KingdeeLockConstant`(PAY_APPLY/ap_payapply_oth_BT_S 已进 TARGET_TO_SOURCE_MAP) |
| 锁状态展示填充 | `IKingdeeBillLockService.mapActiveLocks`(批量查活跃锁)+ `FormalSettlementServiceImpl/InvoiceApplicationServiceImpl/PaymentApplicationServiceImpl.fillKingdeeLockStatus`(列表/详情填充)+ 三个 VO 的 `kingdeeLockStatus/kingdeeLockFailReason/kingdeeTryCount` 字段 |
| 解锁终态标识(2026-10-15) | 展示契约 `KingdeeUnlockMark`(三 VO 实现)+ `IKingdeeBillLockService.mapUnlockedLocks/fillUnlockMarks`(批量反查 unlocked 留痕并回填);VO 字段 `kingdeeUnlockTime/kingdeeUnlockOperator/kingdeeUnlockOperationName`;前端 `payment-application.vue`/`invoice-application.vue`/`formal-settlement-table-panel.vue` 的 `lockTag` 渲染金蝶删单/解锁标签 |
| 锁记录实体/服务 | `KingdeeBillLock`、`IKingdeeBillLockService` + `KingdeeBillLockServiceImpl` |
| 回调端点 | `KingdeeBfiPublicController`:`POST /kingdee-bfi/unLockNotifyBusiData`(PERMIT_ALL + TenantIgnore) |
| 补偿定时任务 | `KingdeeBillLockRetryTask`(`thirdParty.bfi.billLock.retryEnabled`,默认每5分钟) |
@@ -150,4 +199,5 @@ failed(锁定失败,待补偿)→ locked(金蝶锁定中)→ unlocked
Nacos 实际配置已核对一致;环境级 blade-dev/test/prod.yaml 无 secure 段覆盖。
联调验证:服务启动后无令牌 POST 应返回业务参数校验错误(JSON),返回 401「请求未授权」即白名单未生效
4. 与金蝶确认 WLTMS 单据类型编码(SETTLEMENT/INVOICE_APPLY/PAY_APPLY)与 DELETE 还原口径
(付款申请 DELETE 不作废源单、走生成失败+人工重推,与 A103-2 反审核删除兜底同口径,待业务确认)
(付款申请 DELETE 不作废源单、走生成失败+人工重推,与 A103-2 反审核删除兜底同口径;
2026-10-15 付款申请链路已联调实测通过:回调方向反转定位、DELETE 还原、解锁标识均验证 OK)