diff --git a/doc/docs/业务流程/2026-09-22-金蝶票池选票统一查镜像(收票登记+结算单认领+按票号补查+统一组件)业务流程.md b/doc/docs/业务流程/2026-09-22-金蝶票池选票统一查镜像(收票登记+结算单认领+按票号补查+统一组件)业务流程.md new file mode 100644 index 00000000..aa687e97 --- /dev/null +++ b/doc/docs/业务流程/2026-09-22-金蝶票池选票统一查镜像(收票登记+结算单认领+按票号补查+统一组件)业务流程.md @@ -0,0 +1,94 @@ +# 金蝶票池选票统一查镜像(收票登记 + 结算单认领 + 按票号补查 + 统一组件)业务流程 + +> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport;前端:tms-erp-web) +> 对接系统:金蝶发票云旗舰版·票易通(piaozone)全票池 A801-1,经业财融合 BFI 网关(rim 契约)透传 +> 关联文档:`2026-09-20-收票管理同步金蝶全票池发票(A801-1查询+批量同步+定时绑定关联)业务流程.md`(镜像同步与定时绑定链路,本文档不改变其行为)、`2026-09-20-piaozone-invoice-pool-claim`(结算单认领实时透传初版,本文档将其推翻并本地化) +> 最后更新:2026-09-22(实现完成于工作树,提交时机待定) + +--- + +## 1. 背景与决策 + +结算单认领查询曾是 A801-1 **实时透传**:接口不返回总数,后端用"多取 1 条探测 hasMore + 合成 total"实现下一页式翻页,前端分页器显示的是假总数(可跳空页);收票登记新增则查本地镜像表(`limit 200` 无分页、前端本地切页)。两处口径不一、且实时链路每次开弹窗都打远程。 + +**定稿口径(2026-09-22 逐步确认)**:两处选票**统一查本地镜像表 `blade_kingdee_invoice_pool`,数据库真分页**。镜像本地化后"金蝶接口无分页"问题直接消失,前端回归标准分页弹窗;防重不依赖金蝶 `expenseStatus`(A801-1 响应根本不回传该字段),改用本地事实 `claimed/registered` 标记;缺口由"按发票号码精确补查"兜底。 + +| 变更点 | 改造前 | 改造后 | +| --- | --- | --- | +| 结算单认领查询 | A801-1 实时透传 + 合成 total(假分页)+ expenseStatus 条件 | 查镜像真分页,条件集与收票统一,expenseStatus 删除 | +| 收票登记选票 | 镜像 `limit 200`、关键字模糊、前端本地切页、编辑态走 Mock | 查镜像真分页,统一组件;Mock 全删 | +| 防重依据 | 无(两处均可重复选) | 每行 `claimed`(存在有效结算单发票行)/`registered`(存在非作废收票登记)标记 + 三态筛选 | +| 票不在镜像里 | 无法处理 | 弹窗内"按发票号码补查":精确单查金蝶→过同一套白名单→upsert 入镜像 | +| 时效兜底 | 无 | 收票侧弹窗"立即同步"按钮(复用既有手动全票池同步端点,per-request 超时 300s) | + +## 2. 接口契约(后端) + +两个门面端点分挂各自菜单(类级 `@PreAuth` 按菜单隔离,刻意不做跨菜单合并),**同一服务方法落点**: + +| 端点 | 说明 | +| --- | --- | +| `GET /invoice-receipt/invoice-pool`、`GET /formal-settlement/invoice-pool` | 统一分页查询,收参 `KingdeeInvoicePoolQueryDTO`,返回 `R>` | +| `POST /invoice-receipt/invoice-pool/fetch?invoiceNo=`、`POST /formal-settlement/invoice-pool/fetch?invoiceNo=` | 按票号精确补查,返回单张 `R`,校验不过报业务异常 | +| `POST /invoice-receipt/sync-invoice-pool` | 既有全票池手动同步(本文档仅新增前端调用方,含 300s 超时覆盖) | + +**A801-1 统一查询口径**:所有出站查询(定时同步窗口分页 `queryPoolPage`、按票号窄查 `queryPoolByInvoiceNo`——补查/批量同步/漏绑补偿共用)一律携带 `invoiceStatus = "0"`(票易通字典:正常),作废/异常票不再进入镜像、也不可被补查命中。注意:仅约束**查询侧**,历史镜像中已存在的非正常票不受影响(如需剔除既有非正常镜像,另立清理口径)。 + +**查询条件**(`KingdeeInvoicePoolQueryDTO`):`current/size`(size 钳位 1~100)、`invoiceNo` 模糊、`invoiceDateStart/End`(服务端强校验 yyyy-MM-dd)、`salerName/buyerName` 模糊(镜像列 issuer_name/receiver_name)、`invoiceType` 名称模糊、`salerTaxNo/buyerTaxNo` 精确、`claimed/registered`(true/false/null 三态)。 + +**统一视图**(`KingdeeInvoicePoolVO`,字段名与镜像实体一致以复用收票回填):镜像票面全字段 + `availableInvoiceAmount = matchedAmount = invoiceAmount`(价税合计,认领口径不变)+ `claimed/registered`;`id` 为镜像主键(ToStringSerializer),收票登记保存仍绑 `kingdeeInvoicePoolId`。 + +## 3. 查询链路(`KingdeeInvoicePoolMapper.xml`) + +复杂查询入 XML(AGENTS 规范),自定义 SQL 不吃 MP 租户/逻辑删除插件,**手工带 `tenant_id` + `is_deleted = 0`**: + +``` +主查询:blade_kingdee_invoice_pool p(status=1、租户、动态条件、 + ORDER BY source_updated_time DESC, create_time DESC,MP 分页插件真 count) +claimed 标记/过滤:EXISTS blade_formal_settlement_invoice c + (c 租户+未删,且嵌套 EXISTS 主单 blade_formal_settlement s 未删且 approval_status != 'voided' + ——作废结算单不占坑,票据自动释回"未认领") +registered 标记/过滤:EXISTS blade_invoice_receipt r + (r 租户+未删且 approval_status != 'voided') +``` + +三张表同为 TenantEntity,EXISTS 片段经 `` 复用于 SELECT 列(打标记)与 WHERE(做过滤),单一事实源。 + +## 4. 补查链路(`InvoiceReceiptServiceImpl.fetchKingdeeInvoiceByNo`) + +``` +① 镜像优先回读(selectInvoicePoolVO,租户+status=1) +│ 命中 → 直接返回。不打远程:编辑态"同步"按钮不受金蝶可用性影响; +│ 口径:白名单只约束"入镜像",回读既有镜像不受限 +② 未命中 → A801-1 按票号窄查(复用 queryPoolByInvoiceNo,pageSize=500) +│ 返回行按票号 trim 全等匹配才取(防接口模糊语义假命中);无 → 报"金蝶票池未查询到" +③ 白名单校验(与定时同步同一套代码,非复制实现,顺序一致): +│ filterByInvoiceTypes(票种白名单)→ 不过:报"票种不在同步白名单内" +│ filterByArchiveTaxNos(customerTaxNoSet())(购、销双税号 AND 命中客商档案) +│ → 不过:报"请先在客商档案维护统一社会信用代码" +④ batchUpsertInvoicePool 入镜像(自带票面指纹比对,幂等,重复点击安全) +⑤ selectInvoicePoolVO 回读统一视图返回(含 claimed/registered) +``` + +**与定时任务的关系(刻意设计)**:同步、镜像 upsert、定时绑定(receiptMap 回填 `kingdeeInvoicePoolId`)、漏绑补偿链路一行未动;补查只是 batchUpsert 的新调用方,不触发绑定回填——补查进来的票若已有登记未绑,由下一轮补偿链路自然收敛。窗口故障自愈性:瞬时失败下轮滑动窗口重拉;票滑出 30 天窗口造成的永久缺口由补查兜底(补查不受时间窗口约束)。 + +## 5. 前端统一组件(`src/components/kingdee-invoice-pool-dialog/main.vue`) + +- Props:`modelValue / title / confirmText / multiple / initialInvoiceNo / queryApi / fetchApi / syncApi(可空)`;事件 `confirm(rows)`(单选 `[row]`、多选选中数组)。打开时重置查询、按 `initialInvoiceNo` 预填、清勾选、回第 1 页加载。 +- 查询表单:发票号码、开票日期区间、开票单位、受票单位、发票类型(可搜索下拉,选项与后端 `BfiConstant.INVOICE_TYPE_NAMES` 中文名对齐)、认领状态、登记状态(布尔下拉清空后传空串,Spring 绑定为 null,"不限"语义正确)。 +- 表格:票面列 + 三个显式状态列——**认领状态**(已认领红/未认领灰)、**登记状态**(已登记橙/未登记灰)、**金蝶同步**(未同步/已同步/同步失败,镜像 `kingdee_status`);单选模式操作列"选择"+ 双击直选,多选模式复选列 `reserve-selection`;底部左"按票号补查"输入区、右标准分页。 +- 接线:收票登记表单(`multiple=false`,注入 query/fetch/sync 三个 api,`confirmInvoice(row)` 回填零改动,页面 Mock 与本地切页全删;编辑态"同步"按钮改走补查);结算单编辑器(`multiple=true`、`confirm-text="确认认领"`、不传 syncApi——结算菜单无全量同步端点;`handleInvoiceClaimConfirm` 中转时把复数 `attachmentsJson` 归一为认领契约的单数 `attachmentJson`,`confirmInvoiceClaims` 本体零改动)。 +- 同步等待体验:服务端全量同步为分钟级接口(前端 per-request 超时 300s),点击"立即同步"展示全屏 ElLoading("预计 1~3 分钟,请勿关闭窗口"),期间锁定弹窗关闭按钮/esc/遮罩点击,完成后自动刷新列表。 + +## 6. 业务口径与已知限制 + +- **认领场景收紧**:镜像入池受"票种白名单 + 购销双税号 AND 命中客商档案"约束(见 2026-09-20 文档口径),本地化后认领可选集合从裸票池收紧为该白名单集合——**供应商未建档/未维护统一社会信用代码,其进项票在两个弹窗都选不到**,客商档案维护是前置条件。 +- `claimed` 语义 = 被任一**有效**正式结算单认领过;不做跨单认领金额校验的变更(后端仍只查本单金额上限),"已认领"票默认仍可见(claimed 过滤默认不限,拆分认领等场景需要)。 +- 金蝶侧已删除的票(delete=1)补查会以 `kingdee_status=failed` 入镜像并可见(与既有 syncKingdee 存量同模式)。 +- `invoice_no` 在 `blade_formal_settlement_invoice` 无独立索引,claimed 过滤现阶段数据量无感;后续量大建议按幂等模板补 `(tenant_id, invoice_no)` 索引。 +- 无表结构、无 Nacos、无菜单 SQL 变更。 + +## 7. 验证状态 + +- 后端:`mvn clean compile -DskipTests -pl blade-service/blade-transport -am` 通过;前端 `pnpm run build` 通过;死符号(`getInvoicePool/invoicePoolQuery/FormalSettlementInvoicePool*/Mock 函数/expenseStatusOptions`)两仓库 grep 归零。 +- 手工回归清单(9 条):`docs/superpowers/plans/2026-09-22-invoice-pool-mirror-unified-picker.md` Task 8 Step 4,含作废释放、金蝶不可用兜底、立即同步 300s 超时三个新增场景。 +- 过程与评审记录:`.superpowers/sdd/2026-09-22-invoice-pool-mirror-unified-picker/`(台账 progress.md、各任务简报/报告/评审包)。