diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000..83387607 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,82 @@ +# TMS(运输业务中台)—— 业财集成语境 + +TMS 与集团财务系统(金蝶星瀚)、合同合规系统(HCS)集成,单据在三个系统间流转。 +本表只收录集成语境下易混淆的业务术语,不含实现细节。 + +## Language + +**合同合规系统(HCS)**: +集团合同合规管控系统(桂物数字合同合规),接收 TMS 推送的项目、合同、履约与付款数据并做合规审批。 +_Avoid_: 桂物系统、合同系统、数字合同系统 + +**相对方**: +HCS 对签约主体(甲乙方)的称谓,与 TMS 的"客商"同指一个外部单位。 +_Avoid_: 在 HCS 集成语境用"客户/供应商" + +**来源单据键**: +TMS 单据在 HCS 侧的对账主键(接口字段 `fd_kingdee_id`,字段名沿用金蝶),统一存 TMS 业务单据主键。 +_Avoid_: 外部ID、kingdeeId(泛指金蝶单据号时易混淆) + +**来源标识**: +TMS 推送给 HCS 时声明自身身份的常量(接口字段 `fd_sources`,约定值 "TMS")。 +_Avoid_: XH(那是星瀚侧的来源标识) + +**履约规则**: +合同维度的资金计划阶段(付款/收款、款项性质、计划金额、计划日期、开票规则),由 TMS 维护并同步到 HCS。 +_Avoid_: 履约计划(旧版已废弃接口的叫法)、付款计划 + +**履约阶段**: +履约规则表中的一行;HCS 为其生成阶段ID,后续更新/删除必须凭该ID。 +_Avoid_: 阶段项、plan 行 + +**履约记录**: +付款/收款执行事实在 HCS 的登记(一笔付款申请一条记录,随实际付款推进更新),与履约规则是"计划 vs 执行"的关系。 +_Avoid_: 履约流水 + +**额度校验**: +付款提交时由 HCS 判定申请金额是否超出合同/项目剩余额度的同步检查;超额不阻断提交,转入特批链路。 +_Avoid_: 余额检查 + +**特批流程**: +额度超额(或风险不通过)时在 HCS 发起的审批流程;TMS 持有其流程ID,流程结束由 HCS 经 B103 回传结果。 +_Avoid_: 特殊审批、绿色通道 + +**特批放行**: +特批结果为"通过"后对**本次**付款申请解除推送拦截的动作;只改特批状态,不触发推送,单次有效。 +_Avoid_: 特批通过自动付款、放行推送 + +**锁单(B901)**: +金蝶确认接收 TMS 推送的单据后,经 B901 通知 TMS 将本地源单据置为不可编辑、不可作废、不可重推的状态;金蝶反审核及删除后经 B901 解锁。 +_Avoid_: 冻结、金蝶锁定(那是金蝶侧单据自己的状态) + +**解锁**: +B901 通知金蝶侧单据已反审核及删除后,TMS 解除锁单、同步状态置失败、清金蝶单号留痕;不自动重推,人工核实后重新推送。 +_Avoid_: 反审核(那是金蝶侧的动作)、自动重推 + +**合规附件ID**: +TMS 附件先上传 HCS 附件服务换取的 fdId;同一附件以 TMS 附件元数据为键缓存映射,不重复上传。 +_Avoid_: 附件fdId(指代不明) + +**同步状态**: +TMS 单据对 HCS 的推送生命周期(待同步/已同步/失败),权威状态在 TMS 侧字段,HCS 不回写业务状态。 +_Avoid_: 推送标志 + +**二次认领**: +金蝶司库完成第一轮自动认领(客户维度)后,TMS 把收款流水认领到合同/结算单维度的动作;只在 TMS 本地登记,本身不产生金蝶收款单。 +_Avoid_: 收款认领(泛指整条链路时易与金蝶第一轮自动认领混淆) + +**公告未认领金额**: +金蝶认领公告(A301-2)上尚未被认领的金额(字段 unclaimamount),是 TMS 二次认领的上限和"认领完成"的判定基准;以金蝶侧为准,推送前须重查最新值。 +_Avoid_: 剩余可认领金额(那是本地展示口径)、未认领金额(不指明公告侧/本地侧时指代不明) + +**认领完成**: +一条收款流水的累计有效认领金额等于公告未认领金额的状态;只有认领完成的流水才满足推送金蝶的条件,不认领完就不推。 +_Avoid_: 认领完毕、全部认领(未指明对照基准) + +**认领状态**: +收款流水本地认领进度(未认领/部分认领/认领完成),对照公告未认领金额计算;作废的认领分摊不计入累计。 +_Avoid_: 与推送状态混用 + +**推送状态**: +收款流水对金蝶 A301-3 的推送生命周期(未推送/已推送/推送失败),权威在流水级,认领主单仅冗余展示;已推送的流水不可二次推送。 +_Avoid_: 金蝶状态(那是 A301-1 回写的单据生命周期:已推送/审核通过/已收款) diff --git a/doc/docs/业务流程/2026-09-28-收款认领推送金蝶重构设计(认领本地化-A301-3定时+手动推送-按流水归集).md b/doc/docs/业务流程/2026-09-28-收款认领推送金蝶重构设计(认领本地化-A301-3定时+手动推送-按流水归集).md new file mode 100644 index 00000000..2f172d95 --- /dev/null +++ b/doc/docs/业务流程/2026-09-28-收款认领推送金蝶重构设计(认领本地化-A301-3定时+手动推送-按流水归集).md @@ -0,0 +1,257 @@ +# 收款认领推送金蝶重构设计(认领本地化 → A301-3 定时 + 手动推送 → 按流水归集) + +> 适用系统:TMS(blade-transport) +> 对接系统:金蝶云·星瀚(BFI,经 iPaaS 网关直连) +> 覆盖接口:A301-2 收款认领公告查询(不变)、A301-3 收款认领结果推送(**触发时机重构**)、A301-1 收款单查询回写(**轮询对象改造**) +> 前置文档:`2026-09-24-收款认领金蝶全链路业务流程(A301-2公告同步-A301-3认领推送-A301-1收款单回写).md`(下称「旧文档」,描述的是重构前现状) +> 最后更新:2026-09-28(设计稿,未实施) + +--- + +## 1. 为什么要重构 + +旧文档描述的现状(对应代码 `ReceiptFlowServiceImpl.claim()` L353):**每次认领落库后立即在事务内推送 A301-3**。带来三个问题: + +1. **部分认领立即推**:一笔流水分两次认领(40 万 + 60 万),金蝶侧生成两张收款单,司库对账时一笔到账裂成多单。 +2. **来源编码撞码**:`awrp_src_code` 用结算单号拼接,两次认领若关联同一批结算单,编码重复,A301-1 回写按 `sourcebillnumber` 匹配存在歧义(旧文档 §4.1 幂等说明已指出)。 +3. **金额口径混乱**:本地认领上限按 `receipt_amount`(收款金额)控制,而金蝶公告侧有第一轮自动认领后的 `unclaimed_amount`(未认领金额),两个口径互不参照,可能出现本地认领金额与金蝶侧待认领金额对不上的脏数据。 + +## 2. 新设计原则(对旧文档原则的修订) + +| # | 旧原则(旧文档 §1) | 新原则 | +| --- | --- | --- | +| 1 | 公告为准(A301-2 落库流水) | **不变** | +| 2 | 推送先于结果(认领与推送同事务,失败整体回滚) | **废弃**。改为:**认领本地先行,推送异步解耦**——认领动作只落本地库,永不直接调金蝶;推送由定时任务 + 前端手动按钮承担 | +| 3 | 认领公告 id 必填 | **不变**,并扩展:`unclaimed_amount` 同样必填,缺失拦截 | +| 4 | 以金蝶侧实际生成为准 | **不变**,扩展为:**推送前实时对账**——推送前重查 A301-2 拿最新 `unclaimed_amount`,校验通过才推 | +| 5 | 异常回收人工处置(作废不推金蝶) | **不变**,并强化:**已推送流水禁止二次推送**,作废后的金蝶侧冲销一律人工处置 | + +新增两条核心规则(本次重构的业务闸门): + +- **认领完成 = 累计有效认领金额 == 公告未认领金额**(`unclaimed_amount`,即 A301-2 的 `unclaimamount`;作废分摊不计入累计)。只有认领完成的流水才满足推送条件——**不认领完就不推**。 +- **推送单元 = 收款流水**:一条流水只产生一张金蝶收款单,分录取该流水下全部有效分摊行(可能跨多次认领主单),`awrp_src_code` 按流水生成、重试复用。 + +## 3. 领域模型与职责划分 + +### 3.1 三张表的分工 + +``` +blade_kingdee_receipt_flow(收款流水) ← 推送的权威载体 + 认领状态 claim_status / 累计认领 claimed_amount(展示缓存) + 公告侧 unclaimed_amount / bill_id / fee ... + 推送状态 push_status / 推送失败原因 push_fail_reason / 推送时间 pushed_time + 推送编码 awrp_src_code / 金蝶收款单号 kingdee_bill_no / 回写状态 kingdee_bill_status + +blade_receipt_claim(认领主单) ← 认领动作的留痕 + 认领人/部门/日期/附件/备注、claim_amount(本次认领金额) + kingdee_bill_no / kingdee_bill_status 仅作冗余展示(由流水级联刷新) + 【去掉】awrp_src_code(移至流水表) + +blade_receipt_claim_settlement(分摊行) ← 累计认领的权威来源 + 累计认领 = Σ(status=1 行的 allocated_receipt_amount) ← 作废行 status=0,天然不计入 +``` + +**权威口径**:累计认领永远以分摊表实时聚合为准(`flowClaimedAmount()`,现有实现),流水上的 `claimed_amount` 只是展示缓存,在认领/作废/同步/推送前校验四个时机重算维护。 + +### 3.2 流水新增字段 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `push_status` | varchar(20) | 推送状态:`none` 未推送(默认)/ `pushed` 已推送 / `failed` 推送失败 | +| `push_fail_reason` | varchar(1000) | 最近一次推送失败原因(成功后清空) | +| `pushed_time` | datetime | 首次推送成功时间 | +| `awrp_src_code` | varchar(64) | 推送来源编码 `SKCL+yyyyMMddHHmmss+流水id尾号6位`;**首次推送尝试时生成并落库,重试复用同一编码**(依赖金蝶候选键 `awrp_src_sys+awrp_src_code` 幂等,防重试双单) | + +认领状态 `claim_status` **口径变更**(字段沿用,含义改): + +``` +unclaimed 累计有效认领 = 0 +partial 0 < 累计有效认领 < unclaimed_amount +claimed 累计有效认领 == unclaimed_amount(且 > 0) ← 「认领完成」,唯一可推送状态 +``` + +不再与 `receipt_amount` 比较。前端「剩余可认领」= `unclaimed_amount − 累计有效认领`。 + +`kingdee_bill_status`(回写生命周期)从认领主单上移到流水:`none → pushed →(A301-1 回写)approved / received / failed`;认领主单保留同名冗余字段仅作展示。 + +### 3.3 状态机 + +``` +流水推送状态: + none ──推送成功──▶ pushed ──A301-1 回写──▶ approved ──▶ received(终态) + │ │ └──(A/B/退单)──▶ failed(回写失败) + └──推送失败──▶ failed ──重试(同码)──▶ pushed / 维持 failed + ※ pushed 为终态闸门:任何路径不允许二次推送(作废→重认领→再满足等式也不推) + +流水认领状态(对照 unclaimed_amount,随认领/作废/同步刷新重算): + unclaimed ⇄ partial ⇄ claimed +``` + +## 4. 流程编排 + +### 4.1 认领(POST /receipt-flow/claim,纯本地化) + +改造 `ReceiptFlowServiceImpl.claim()`: + +1. 锁流水后校验:`bill_id` 非空(沿用)+ **`unclaimed_amount` 非空**(缺失拦截「公告未认领金额缺失,请重新同步金蝶收款流水」)。 +2. 结算单侧校验全部不变(审批通过/应收类型/未作废/同项目组织收付款方/单张结算单累计 ≤ 结算金额)。 +3. **流水维度上限改为公告口径**:`累计有效认领 + 本次分摊 ≤ unclaimed_amount`(原为 ≤ receipt_amount),报错文案「累计认领金额不能超过公告未认领金额」。 +4. 本地落库:认领主单(`kingdee_bill_status=none`)+ 分摊行 + 刷新结算单收款汇总 + 更新流水 `claimed_amount/claim_status`(按 3.2 新口径)+ 认领留痕。 +5. **删除 `pushClaimResultToKingdee(...)` 调用**(L353)——认领事务到此结束,不产生任何 BFI 调用。 + +### 4.2 推送(统一入口,定时任务与手动按钮共用) + +新服务方法 `pushClaimToKingdee(Long flowId)`,**方法级事务,推送异常在事务内消化(不外抛),以返回值向调用方报告成败与原因**——保证失败路径的状态落库不被回滚: + +``` +① FOR UPDATE 锁流水 +② 静态闸门(硬校验,不满足直接返回失败): + - bill_id 非空 + - push_status != pushed ←「已推送禁止二次推送」终态闸门 + - 累计有效认领(分摊表实时聚合)> 0 且 == unclaimed_amount(compareTo==0,严格相等) +③ 实时对账:重查 A301-2 拿最新公告 + - 手动按钮:claimusers = 当前登录用户真实姓名 + - 定时任务:按候选流水的认领人姓名(认领主单 claimer_name 去重)逐人查询,§4.3 + - 按 bill_id 匹配本流水 → 刷新 unclaimed_amount / kingdee_claim_status / claim_me + - 公告查不到 → 失败:「金蝶公告不存在或已不在认领范围,请重新同步」 + - 重验:累计有效认领 == 最新 unclaimed_amount,不等 → 失败并提示两侧金额差额 + (不等说明金蝶侧有人认领过或金额有变,需人工核对后作废/补认领) +④ awrp_src_code:已有则复用;无则生成并落库(SKCL+时间戳+流水id尾号6位) +⑤ 组装 A301-3:billid=流水.bill_id,entries = 该流水全部有效分摊行 + (分摊行 join 结算单,按结算单 id 升序;金额=allocated_receipt_amount; + 结算单有 kingdee_bill_no 时携带 e_corebillno,沿用现有取数) +⑥ 调 pushClaimResult: + 成功 → push_status=pushed、push_fail_reason 清空、pushed_time=now、 + kingdee_bill_no=响应 number、kingdee_bill_status=pushed; + 级联刷新该流水名下全部未作废认领主单的 kingdee_bill_no/kingdee_bill_status + 失败 → push_status=failed、push_fail_reason=金蝶错误文本汇总(认领数据不回滚) +⑦ 推送动作留痕(blade_receipt_flow_record,operation=push) +``` + +**事务边界说明**:BFI HTTP 调用在事务内,流水行锁覆盖整个推送过程——天然防「定时任务与手动按钮并发双推」(后到者会看到 `push_status=pushed` 或等锁后走静态闸门被拦)。锁粒度为单流水行锁,持锁时长为一次接口调用,可接受。 + +### 4.3 定时任务 ReceiptClaimPushTask + +沿用 `PaymentKingdeeResultTask` 编排模式(task 类只做开关与调度,逻辑收敛在服务方法,返回 summary): + +```java +@Value("${thirdParty.bfi.receiptClaimPush.syncEnabled:false}") // 默认关闭,联调稳定后开启 +@Scheduled(cron = "${thirdParty.bfi.receiptClaimPush.cron:0 */10 * * * ?}") +``` + +扫描对象:`bill_id` 非空 AND `push_status IN (none, failed)` AND `claim_status = claimed`(缓存口径预筛缩小集合)。 + +**A301-2 重查的认领人问题**:A301-2 的 `claimusers` 必填且为单个用户姓名(`BfiClaimAnnounceQueryDTO` L50-52),定时任务没有登录用户上下文。处理: + +1. 取候选流水的有效认领主单 `claimer_name` 去重集合; +2. 逐人调 `BfiUtil.queryAllPages(queryClaimAnnounce)`(时间窗沿用 `thirdParty.bfi.claimAnnounce.months`,默认 6 个月); +3. 按公告 `id == 流水.bill_id` 匹配刷新,再走 §4.2 ③-⑦; +4. 单流水推送失败 try/catch 独立消化,不影响同批其他流水。 + +> 联调确认项:`claimusers` 若支持逗号分隔多人,可合并为一次查询(见 §8)。 + +### 4.4 手动推送按钮(收款流水页) + +- 行按钮「推送金蝶」:`POST /receipt-flow/push-kingdee`(body: flowId),按钮权限 `receipt_flow_push`。 +- 前端可用条件(展示层控制,后端硬校验为准):`claim_status=claimed` 且 `push_status != pushed` 且 `bill_id` 非空。 +- 后端走 §4.2 同一入口,用当前用户姓名重查公告;若当前用户不在公告认领范围内(查不到),提示「当前用户不在该公告认领范围内,请由认领人操作或重新同步后重试」。 +- 列表新增「推送状态」「推送失败原因」列;详情弹窗展示推送留痕。 +- 本期不做批量推送按钮(定时任务已覆盖批量场景)。 + +### 4.5 A301-1 回写改造(POST /receipt-claim-record/query-kingdee-result) + +`ReceiptClaimRecordServiceImpl.refreshKingdeeResult()` 轮询对象由认领主单改为**流水**: + +- 扫描:`push_status = pushed` 且 `kingdee_bill_status IN (pushed, approved)` 的流水; +- 匹配:金蝶收款单 `sourcebillnumber == 流水.awrp_src_code`(一流水一码,无歧义); +- 回写:`flow.kingdee_bill_no / kingdee_bill_status`(状态映射不变:A/B→pushed、C→approved、D→received、其他→failed),并级联刷新名下未作废认领主单的冗余字段; +- 定时接入复用现有任务风格(`thirdParty.bfi.receiptClaimResult.*`,本期仍可手动触发)。 + +### 4.6 作废(不变 + 两点明确) + +`voidClaim()` 现有逻辑保留: + +1. **累计认领不考虑作废**:作废行 `status=0`,分摊聚合天然排除——与「累计认领不考虑作废的」要求一致,无需改代码;作废后重算流水 `claimed_amount/claim_status`(口径按 §3.2)。 +2. **作废永不触发推送**:未推送的认领作废 → 纯本地;已推送流水的认领作废 → 本地 KDCX 冲单号 + 金蝶人工处置(沿用旧文档原则 5)。作废后即使重认领使等式再次满足,`push_status=pushed` 的终态闸门也阻止二次推送。 + +### 4.7 重新同步(A301-2 sync 补一处) + +同步 upsert 刷新 `unclaimed_amount` 后,**对值发生变化的流水重算 `claim_status`**(对照分摊聚合),保证公告漂移后认领状态不失真。 + +## 5. 边界场景与防重汇总 + +| 场景 | 行为 | +| --- | --- | +| 定时任务与手动按钮并发推同一流水 | 流水行锁串行化 + `push_status != pushed` 静态闸门,不会双推 | +| 推送超时但金蝶实际已生成收款单 | 本地记 failed;重试复用同一 `awrp_src_code`,金蝶候选键幂等,不产生重复收款单 | +| 推送成功后作废一笔认领、再补认领到等式满足 | 终态闸门拦截,不二次推送;金蝶侧冲销人工处置 | +| 公告侧 unclaimed_amount 变小(他人在金蝶认领) | 推送前实时对账发现差额 → 拦截并提示差额,人工作废多余本地认领后重推 | +| 公告侧 unclaimed_amount 变大 | 用户补认领到等式满足后推送(认领上限随新值放开) | +| unclaimamount = 0 的公告 | 认领上限为 0,不可认领;累计认领 > 0 的推送前置条件天然排除空推 | +| 手动推送者不在公告认领范围 | A301-2 按其姓名查不到公告 → 拦截提示,改由认领人操作 | + +防重键沿用旧文档 §6.3,变更点:`awrp_src_code` 由「认领主单、结算单号拼接」改为「流水、按流水生成、重试复用」,A301-1 匹配键随之移到流水上。 + +## 6. 前端改动汇总(tms-erp-web) + +| 页面 | 改动 | +| --- | --- | +| `receipt-flow.vue` | ① 认领按钮可用条件:`剩余可认领 = unclaimed_amount − 累计认领 > 0`(原按 receipt_amount);② 新增「推送金蝶」行按钮(条件 §4.4);③ 列表加「推送状态/推送失败原因」列;④ 详情展示推送留痕 | +| `receipt-flow-form.vue` | 分摊上限与提示文案改为公告未认领金额口径 | +| `receipt-claim-record.vue` | 不变(金蝶状态列继续展示冗余字段;「查询金蝶认领结果」按钮保留) | +| `api/payment/receiptFlow.js` | 新增 `pushKingdee` | + +## 7. SQL 变更 + +新增幂等脚本 `doc/sql/transport/blade_receipt_flow_push_20260928.sql`,沿用仓库惯例(`information_schema` 探测 + `PREPARE/EXECUTE` 动态 DDL),**支持重复执行**:0924 脚本执行过与否均可跑,任意顺序、任意次数,已存在的列/索引自动跳过、已不存在的列/索引自动跳过删除。 + +| # | 对象 | 变更 | 说明 | +| --- | --- | --- | --- | +| 1-6 | `blade_kingdee_receipt_flow` | ADD COLUMN `push_status`(默认 none)、`push_fail_reason`、`pushed_time`、`awrp_src_code`、`kingdee_bill_no`、`kingdee_bill_status`(默认 none) | 推送域字段上收至流水 | +| 7-8 | `blade_receipt_claim` | DROP KEY `idx_receipt_claim_src_code`、DROP COLUMN `awrp_src_code` | 推送编码上移至流水;0924 未执行过则本就无此列,探测后自动跳过 | +| 9-10 | `blade_kingdee_receipt_flow` | ADD KEY `idx_kingdee_receipt_flow_push_scan` (tenant_id, push_status, claim_status)、`idx_kingdee_receipt_flow_src_code` (tenant_id, awrp_src_code) | 定时任务扫描 + A301-1 回写匹配 | +| 11 | `blade_menu` | INSERT 按钮权限 `receipt_flow_push`(父级 2090000000000001260 收款流水,id 2090000000000001264,ON DUPLICATE 幂等) | 手动推送按钮 | + +0924 脚本(`blade_receipt_flow_claim_20260924.sql`)保持原样不动,两个脚本无执行顺序依赖。上线顺序:先执行 0924(若未执行)→ 执行 0928 → 启动服务。 + +## 8. 待联调确认项 + +| # | 事项 | +| --- | --- | +| 1 | `claimusers` 是否支持逗号分隔多人(支持则定时任务合并为一次 A301-2 查询) | +| 2 | A301-3 字段沿用旧文档 §7 待确认清单(recpaytype_number / paymenttype / recpayer / e_corebilltype / e_corebillno) | +| 3 | `unclaimamount` 是否已扣手续费(fee)口径——本方案等式直接对照 `unclaimamount` 本身,无论是否含费均自洽,但需确认推送后金蝶侧 `unclaimamount` 归零的时效,用于判断重查窗口 | +| 4 | 同一公告在金蝶侧被人工认领部分金额后,A301-2 返回的 `unclaimamount` 更新时效(影响实时对账的准确性预期) | + +## 9. 与旧文档的差异对照 + +| 环节 | 旧(2026-09-24) | 新(本设计) | +| --- | --- | --- | +| 认领事务 | 落库后同事务立即推 A301-3,失败整体回滚 | 纯本地落库,不调 BFI | +| 推送触发 | 每次认领动作 | 定时任务(`receiptClaimPush.*` 开关)+ 手动按钮,双入口同一服务方法 | +| 推送条件 | 无(认领即推) | 累计有效认领 == 公告未认领金额(推送前重查 A301-2 实时对账) | +| 推送粒度 | 每次认领一张金蝶收款单 | 一条流水一张金蝶收款单(分摊行归集) | +| awrp_src_code | 认领主单上,结算单号拼接(可撞码) | 流水上,SKCL+时间戳+流水尾号,重试复用 | +| 认领上限/认领完成口径 | receipt_amount | unclaimed_amount | +| 推送状态/失败原因 | 无(失败即回滚无痕) | 流水上 push_status / push_fail_reason / pushed_time,失败可重试可追溯 | +| A301-1 轮询对象 | 认领主单 | 流水(级联刷新认领主单冗余字段) | +| 二次推送 | 可能(作废重认领再推) | 禁止(终态闸门) | + +## 10. 代码改动索引(已实施,2026-09-28) + +| 环节 | 位置 | 改动 | +| --- | --- | --- | +| 认领 | `ReceiptFlowServiceImpl.claim()` | 上限口径改公告未认领金额(缺失/为0拦截);删除同步推送调用,事务内只落本地 | +| 推送 | `IReceiptFlowService.pushClaimToKingdee(flowId)` / `pushClaimByTask()` | 新增。共用 `pushClaimLocked`(`TransactionTemplate` 编程式事务,流水行锁覆盖闸门→对账→推送→落库,防并发双推);失败不抛异常跨事务边界,以 `ReceiptClaimPushResult` 返回 | +| 定时任务 | `task/ReceiptClaimPushTask.java`(新增) | Nacos 开关 `thirdParty.bfi.receiptClaimPush.syncEnabled/cron`(默认关闭、每10分钟);候选=认领完成且 push_status ∈ (none, failed),按认领人姓名去重逐人重查 A301-2 | +| 推送报文 | `buildClaimPushDTO` | billid=流水.bill_id;分录=该流水全部有效分摊行(结算单升序,seq 1起);e_corebillno=结算单金蝶应收单号;awrp_src_code 按流水生成(SKCL+时间戳+尾号6位)、重试复用 | +| 手动接口 | `ReceiptFlowController.pushKingdee`(`POST /receipt-flow/push-kingdee`) | 成功返回金蝶收款单号,失败 R.fail(原因);按钮权限 `receipt_flow_push` | +| 回写 | `ReceiptClaimRecordServiceImpl.refreshKingdeeResult()` | 轮询对象改流水(push_status=pushed 且回写状态 pushed/approved),按流水 awrp_src_code 匹配,级联刷新认领主单冗余字段 | +| 作废 | `ReceiptClaimRecordServiceImpl.voidClaim()` | claim_status 重算口径改对照公告未认领金额(其余不变,作废不触发推送) | +| 同步 | `ReceiptFlowServiceImpl.sync()` + 共享 `queryClaimAnnounces` | unclaimed_amount 刷新后按新口径重算 claim_status;公告查询方法供 sync/手动推送/定时任务共用 | +| 常量 | `KingdeeBfiConstant` | 新增 RECEIPT_CLAIM_PUSH_NONE/PUSHED/FAILED、RECEIPT_CLAIM_BILL_NONE | +| 实体/VO | `KingdeeReceiptFlow`(推送域6字段)、`ReceiptClaim`(删 awrpSrcCode)、`ReceiptClaimMapper.xml`(删列)、`ReceiptFlowVO/Wrapper`(pushStatusName/kingdeeBillStatusName、剩余可认领改公告口径)、`ReceiptClaimPushResult`(新增 DTO) | — | +| 详情留痕 | `ReceiptFlowServiceImpl.detail()` | claimRecords 含 claim + push 两类动作记录 | +| 批量化 | `FormalSettlementServiceImpl.refreshPaymentSummaries(Collection)`(新增)+ `calculatePaymentSummary` 拆纯计算重载 | 固定数次批量查询完成 N 张结算单收款汇总重算(关系/付款申请/申请状态/预收来源/认领分摊各一次 IN 查询 + updateBatchById);单笔 `refreshPaymentSummary` 与 `refreshPaymentSummariesForPreSettlement` 复用同一口径 | +| 批量化 | `claim()` / `voidClaim()` / `settlementCandidates()` / `sync()` | ① 结算单锁行改一次 `IN ... ORDER BY id FOR UPDATE`(与逐单加锁顺序一致防死锁);② 结算单累计认领额一次聚合查询(候选单列表页原先每页最多 200 次查询);③ 作废分摊行改单条 UPDATE(实体携带 status 保留审计字段填充);④ 同步留痕改 `Db.saveBatch` 批量插入 | +| 前端 | `receipt-flow.vue`(推送金蝶按钮+canPush、推送状态/失败原因/金蝶单号/金蝶状态列、详情推送字段)、`receipt-flow-form.vue`(剩余可认领金额字段、上限口径)、`receipt-flow-batch-form.vue`(批量口径)、`option/payment/receiptFlow.js`(新列)、`api/payment/receiptFlow.js`(pushKingdee) | — | diff --git a/doc/docs/业务流程/2026-09-28-收款认领流程(业务版).md b/doc/docs/业务流程/2026-09-28-收款认领流程(业务版).md new file mode 100644 index 00000000..4889aaf6 --- /dev/null +++ b/doc/docs/业务流程/2026-09-28-收款认领流程(业务版).md @@ -0,0 +1,112 @@ +# 收款认领流程(业务版) + +> 业务域:图06「收款查询及认领(回款认领)」的 TMS 二次认领环节 +> 核心规则:**认领只在本系统登记,不同步金蝶;一笔流水认领完成后,才推送金蝶生成收款单——不认领完就不推。** +> 最后更新:2026-09-28 + +--- + +## 一、认领的对象:收款流水从哪来 + +金蝶司库收到银行收款流水后,先自动认领到客户维度,剩余未落到合同/结算单的部分形成**未认领金额**。业务人员在 TMS「收款流水」页面点「手动同步流水」,把这些公告同步成本系统的收款流水——每条流水带一个**公告未认领金额**,这就是后续认领的额度基准(重复同步不会产生重复流水,金额有变化会刷新)。 + +--- + +## 二、认领操作流程 + +**入口**:收款流水列表/详情的「认领」按钮——流水还有剩余可认领额度(公告未认领金额 − 累计认领)时可操作。 + +**操作步骤**: + +| 步骤 | 操作 | 规则 | +| --- | --- | --- | +| 1. 选结算单 | 选择一张或多张应收正式结算单 | 须审批通过、未作废;多张须同一项目、同一组织、同一收付款方 | +| 2. 录分摊金额 | 逐张结算单录入本次认领的收款金额 | ① 单张结算单:累计被认领金额 ≤ 其结算金额;② 整条流水:累计认领金额 ≤ 公告未认领金额(只认领金蝶侧缺口) | +| 3. 确认 | 提交认领 | 生成一条认领记录(含分摊明细),刷新流水认领进度 | + +**关键规则**: + +1. 确认后认领只在本系统登记,**不与金蝶发生任何交互**。 +2. 一条流水**允许多次认领**(部分认领):今天认 40 万给 A 结算单,明天再认 60 万给 B 结算单,直到累计认领等于公告未认领金额。 + +--- + +## 三、认领状态 + +### 流水认领状态(认领进度) + +| 状态 | 含义 | +| --- | --- | +| 未认领 | 还没有任何有效认领 | +| 部分认领 | 累计认领 < 公告未认领金额 | +| **认领完成** | 累计认领 = 公告未认领金额(正好补齐金蝶侧缺口) | + +- 累计认领金额 = 该流水下所有**有效**认领分摊之和,**作废的认领不计入**; +- 剩余可认领金额 = 公告未认领金额 − 累计认领金额。 + +### 认领记录状态 + +| 状态 | 含义 | +| --- | --- | +| 已认领 | 有效认领,金额计入流水累计 | +| 已作废 | 不再计入累计,流水认领进度相应回退 | + +--- + +## 四、认领完成后的推送(认领流程的出口) + +**推送条件(闸门)**: + +1. 流水**认领完成**——累计认领 = 公告未认领金额,**不认领完就不推**; +2. 该流水**尚未推送过**——已推送的流水不允许二次推送。 + +**推送方式(两个入口,同一套规则)**: + +- 定时任务自动推送:系统定期扫描满足条件的流水自动推; +- 手动推送:流水页面「推送金蝶」按钮,条件不满足时置灰。 + +**推送前对账**:推送前先到金蝶重查该公告的**最新未认领金额**,与本地累计认领核对——一致才推;不一致(如他人在金蝶侧手工认领过)则拦截并提示差额,人工核对作废或补认领后再推。 + +**推送结果**: + +| 结果 | 表现 | +| --- | --- | +| 成功 | 金蝶生成**一张收款单**(一笔流水一张单,多张结算单是该单下的多条分摊明细);流水推送状态置"已推送",回填金蝶收款单号 | +| 失败 | 推送状态置"推送失败"并记录失败原因;**本地认领保留不回滚**;排除原因后可重新推送,重试不会在金蝶产生重复单据 | + +### 流水推送状态 + +| 状态 | 含义 | 后续 | +| --- | --- | --- | +| 未推送 | 未满足推送条件或尚未轮到推送 | 继续认领 / 等待定时任务或手动推送 | +| 已推送 | 金蝶已生成收款单 | 等待金蝶处理结果回写(审核/收款状态由回写推进);不再接受二次推送 | +| 推送失败 | 上次推送被金蝶拒绝或异常 | 按失败原因整改后重新推送 | + +> 认领记录页可随时手动「查询金蝶认领结果」,回写金蝶收款单号与处理状态(已推送 → 审核通过 → 已收款)。 + +--- + +## 五、作废认领 + +1. 仅认领人本人可作废自己的认领记录;作废后该笔分摊不再计入累计,流水认领状态相应回退(认领完成 → 部分认领/未认领)。 +2. **作废永远不会触发推送**。 +3. 按流水是否已推送区分: + - **未推送**:纯本地生效,无金蝶影响;补认领到认领完成后仍可正常推送; + - **已推送**:本地生成冲单号留痕,金蝶侧冲销/红冲由人工在金蝶处置,该流水不会被再次自动推送。 + +--- + +## 六、典型场景 + +1. **一次认领完成**:未认领金额 100 万,一次认领到 2 张结算单(60 万 + 40 万)→ 认领完成 → 推送,金蝶生成一张 100 万收款单。 +2. **分两次认领完成**:先认 40 万(部分认领,不推金蝶),再认 60 万补齐 → 认领完成 → 推送,仍是一张收款单含两笔分摊。 +3. **金额不一致被拦截**:本地累计 100 万,推送前金蝶重查最新未认领只剩 80 万 → 拦截提示差额 20 万 → 作废多余认领后再推。 +4. **推送失败重试**:因结算单尚未同步金蝶应收被拒 → 记录失败原因 → 补齐前置后重推,不产生重复收款单。 + +--- + +## 七、注意事项 + +1. **结算单前提**:认领的结算单须已完成金蝶应收侧同步(有金蝶应收单号),推送时金蝶才能按应收单核销。 +2. **认领人前提**:金蝶公告的认领人姓名须与 TMS 用户真实姓名一致,否则同步不到流水;推送前对账也要求操作者在公告认领范围内(定时任务按各认领人自动查询,不受单个人限制)。 +3. **两个口径不要混**:「公告未认领金额」是金蝶侧待认领缺口(额度与认领完成的判定基准);「累计认领金额」是本系统登记的有效认领合计。认领完成 = 两者相等。 diff --git a/doc/sql/transport/blade_receipt_flow_push_20260928.sql b/doc/sql/transport/blade_receipt_flow_push_20260928.sql new file mode 100644 index 00000000..ed40e156 --- /dev/null +++ b/doc/sql/transport/blade_receipt_flow_push_20260928.sql @@ -0,0 +1,136 @@ +-- 收款认领推送金蝶重构(认领本地化 / A301-3 定时+手动推送 / 按流水归集) +-- 支持重复执行,兼容 20260924 脚本已执行/未执行的环境,与该脚本无执行顺序要求 +-- 变更点: +-- 1. 收款流水表新增推送域字段(推送状态/失败原因/推送时间/推送编码/金蝶收款单号/回写状态) +-- 2. 认领主单移除 awrp_src_code 及其索引(推送编码上移至流水:按流水生成、重试复用) +-- 3. 收款流水表新增定时任务扫描索引与 A301-1 回写匹配索引 + +-- 1. 收款流水表:推送状态 +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `push_status` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT ''none'' COMMENT ''推送状态(none未推送/pushed已推送/failed推送失败)''', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'push_status' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 2. 收款流水表:最近一次推送失败原因 +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `push_fail_reason` varchar(1000) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''最近一次推送失败原因(成功后清空)''', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'push_fail_reason' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 3. 收款流水表:首次推送成功时间 +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `pushed_time` datetime DEFAULT NULL COMMENT ''首次推送金蝶成功时间''', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'pushed_time' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 4. 收款流水表:推送来源单据编码(A301-3 awrp_src_code,A301-1 回写匹配键,重试复用) +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `awrp_src_code` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶推送来源单据编码(SKCL+时间戳+流水id尾号6位,按流水生成重试复用)''', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'awrp_src_code' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 5. 收款流水表:金蝶收款单号(推送响应回填 / A301-1 回写推进) +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `kingdee_bill_no` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶收款单号''', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'kingdee_bill_no' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 6. 收款流水表:金蝶回写状态(A301-1 回写生命周期) +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `kingdee_bill_status` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT ''none'' COMMENT ''金蝶回写状态(none未推送/pushed已推送/approved审核通过/received已收款/failed失败)''', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND COLUMN_NAME = 'kingdee_bill_status' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 7. 认领主单:删除 awrp_src_code 索引(列上移至流水表;0924 未执行过则本就无此索引) +SET @ddl_sql = ( + SELECT IF(COUNT(*) > 0, + 'ALTER TABLE `blade_receipt_claim` DROP KEY `idx_receipt_claim_src_code`', + 'SELECT 1') + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND INDEX_NAME = 'idx_receipt_claim_src_code' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 8. 认领主单:删除 awrp_src_code 列(0924 未执行过则本就无此列) +SET @ddl_sql = ( + SELECT IF(COUNT(*) > 0, + 'ALTER TABLE `blade_receipt_claim` DROP COLUMN `awrp_src_code`', + 'SELECT 1') + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_receipt_claim' AND COLUMN_NAME = 'awrp_src_code' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 9. 收款流水表:定时任务扫描索引(push_status IN (none,failed) AND claim_status='claimed' AND bill_id 非空) +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_kingdee_receipt_flow` ADD KEY `idx_kingdee_receipt_flow_push_scan` (`tenant_id`,`push_status`,`claim_status`)', + 'SELECT 1') + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND INDEX_NAME = 'idx_kingdee_receipt_flow_push_scan' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 10. 收款流水表:A301-1 回写匹配索引(sourcebillnumber = awrp_src_code) +SET @ddl_sql = ( + SELECT IF(COUNT(*) = 0, + 'ALTER TABLE `blade_kingdee_receipt_flow` ADD KEY `idx_kingdee_receipt_flow_src_code` (`tenant_id`,`awrp_src_code`)', + 'SELECT 1') + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_kingdee_receipt_flow' AND INDEX_NAME = 'idx_kingdee_receipt_flow_src_code' +); +PREPARE ddl_stmt FROM @ddl_sql; +EXECUTE ddl_stmt; +DEALLOCATE PREPARE ddl_stmt; + +-- 11. 按钮权限:收款流水页「推送金蝶」(父级菜单 2090000000000001260 收款流水,排序 4) +INSERT INTO `blade_menu` + (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) +VALUES + (2090000000000001264,2090000000000001260,'receipt_flow_push','推送金蝶','receipt_flow_push','','',4,2,0,1,NULL,'',0) +ON DUPLICATE KEY UPDATE + `name`=VALUES(`name`), + `alias`=VALUES(`alias`), + `is_deleted`=0;