docs: 更新金蝶票池选票业务流程(可用过滤、游标补页与固定查未用)

同步2026-09-23选票方案演进:可用过滤口径、有界循环补页与nextCursor游标分页机制、
单据使用状态收敛为后端固定查未用、保存链路新增可用性复核步骤、前端弹窗条件与分页交互调整。
This commit is contained in:
刘泉佳
2026-09-23 15:06:20 +08:00
parent c91ab359ba
commit 78fabd15a5
@@ -4,7 +4,7 @@
> 对接系统:金蝶发票云旗舰版·票易通(piaozone)全票池 A801-1,经业财融合 BFI 网关(rim 契约)透传
> 关联文档:`2026-09-20-收票管理同步金蝶全票池发票(A801-1查询+批量同步+定时绑定关联)业务流程.md`(同步链已整体退役,仅存推送/回写侧仍有效的键与端点见该文档退役声明)、`2026-09-22-收票单推送金蝶全链路业务流程(A303-2保存-A302-6指定应付-A801-3预检回写).md`(图05 推送链路,镜像字段 invoice_type_code/items_json 由其取数消费)
> 设计依据:`docs/superpowers/specs/2026-09-22-invoice-pool-remote-reversal-design.md`
> 最后更新:2026-09-22(本文档原为「金蝶票池选票统一查镜像」,同日二次决策改回实时查询后整篇重写;文件名保留以免断链)
> 最后更新:2026-09-23(选票可用过滤 + 有界循环补页游标分页 + 单据使用状态收敛为后端固定查未用;2026-09-22 本文原为「金蝶票池选票统一查镜像」,同日二次决策改回实时查询后整篇重写;文件名保留以免断链)
---
@@ -14,7 +14,9 @@
**定稿口径**:
- 收票登记新增、正式结算单认领两处选票改为**实时查 A801-1 全票池**;A801-1 无总数,采用"多取 1 条探测 hasMore"的**游标式下一页翻页**(无跳页能力)。
- 收票登记新增、正式结算单认领两处选票改为**实时查 A801-1 全票池**;A801-1 无总数,翻页采用"多取 1 条探测 hasMore + 有界循环补页 + 扫描游标"的**游标式下一页翻页**(无跳页能力,机制详见 §2)。
- (2026-09-23)选票**只出可用票**:金蝶返回行须同时满足 剩余可用金额 `availableAmount > 0` 且 金额使用状态 `amountUseStatus = "0"`(未使用,2-全部使用)。A801-1 无该维度的服务端过滤条件,只能拉取后客户端过滤,由此引入补页与游标(§2)。
- (2026-09-23)**单据使用状态**不再作为前端筛选条件,后端固定下推"未用"(`expenseStatus = "1"`)。
- `blade_kingdee_invoice_pool` 语义降级为**登记存根**:只存被业务选中的发票,写入时机=**收票登记保存时服务端重查金蝶落库**(否决"选中即写")。
- 全量同步体系**退役删除**:定时任务、增量窗口、镜像批量 upsert、定时绑定(receiptMap)、漏绑补偿、白名单过滤(票种 + 客商税号 AND,客商建档不再是选票前置)、手动全量同步端点与"立即同步"按钮、按票号补查(语义并入主查询)。
- 金蝶不可用时查询与保存重查均**直接报错**(镜像不再提供读服务,无降级路径——接受"每次开弹窗/每次保存打一次金蝶"的一次性成本,换取同步体系归零)。
@@ -30,11 +32,13 @@
- 已删除端点:`POST /*/invoice-pool/fetch`(按票号补查,语义并入主查询的发票号码条件)、`POST /invoice-receipt/sync-invoice-pool`(手动全量同步)。
- **保留端点**:`POST /invoice-receipt/sync-kingdee`(列表"批量同步",职责收敛为刷新**已登记票**的镜像行与状态,镜像行此时必已存在)。
**查询条件**(`KingdeeInvoicePoolQueryDTO`):`current/size`(size 钳位 1~100,透传为 A801-1 pageSize=size+1 探测)、`invoiceNo`、`invoiceDateStart/End`(服务端强校验 yyyy-MM-dd)、`salerTaxNo/buyerTaxNo`、`invoiceType`(票易通编码,如 26/27,选项取 `BfiConstant.INVOICE_TYPE_NAMES`)、`expenseStatus`(1 未用/30 在用/60 已用/65 已入账)。**删除**了镜像时代的 `salerName/buyerName/claimed/registered` 条件(远程不支持名称模糊;本地状态不可下推)。
**查询条件**(`KingdeeInvoicePoolQueryDTO`):`current/size`(size 钳位 1~100)、`invoiceNo`、`invoiceDateStart/End`(服务端强校验 yyyy-MM-dd)、`salerTaxNo/buyerTaxNo`、`invoiceType`(票易通编码,如 26/27,选项取 `BfiConstant.INVOICE_TYPE_NAMES`)、`cursor`(2026-09-23 新增,下一页续扫游标)。`expenseStatus` 已从前端条件删除,后端固定下推"未用"(`KingdeeBfiConstant.POOL_EXPENSE_STATUS_UNUSED`)。**删除**了镜像时代的 `salerName/buyerName/claimed/registered` 条件(远程不支持名称模糊;本地状态不可下推)。
**A801-1 统一查询口径**:所有出站查询一律携带 `invoiceStatus = "0"`(正常票);返回行按 `invoiceNo` trim 全等防御过滤后再截断 size 条(防接口模糊匹配语义假命中)。
**A801-1 统一查询口径**:所有出站查询一律携带 `invoiceStatus = "0"`(正常票)与 `expenseStatus = "1"`(单据使用状态未用,后端固定;按票号单查 `queryPoolByInvoiceNo` 不下推 expenseStatus);返回行先做**选票可用过滤**(`isUsablePoolRow`:票号非空 + 剩余可用金额 `availableAmount > 0` + 金额使用状态 `amountUseStatus = "0"`,可用金额空白/解析失败按不可用兜底;口径常量集中在 `KingdeeBfiConstant`),再按 `invoiceNo` trim 全等防御过滤(防接口模糊匹配语义假命中),最后截断 size 条。
**返回体**(`KingdeeInvoicePoolPageVO`):`{ records: List<KingdeeInvoicePoolVO>, current, size, hasMore }`——**不再合成 total**。行映射:票种编码→中文名、税率小数转百分数(顶层优先、回退明细首行)、`invoiceAmount = availableInvoiceAmount = matchedAmount = totalAmount`、`attachmentJson = {source:'piaozone', path:downloadUrl, name:票号.pdf}`;`id` 置空(未入镜像)。
**有界循环补页分页(2026-09-23)**:可用过滤发生在拉取之后,单页远程返回可能大部分被过滤,因此 `invoicePoolPage` 在单请求内循环补页凑满一页——首拉 `size+1` 轻量探测,不足再按契约上限 500 行/次补拉,单请求最多 3 次远程调用(`POOL_MAX_REMOTE_CALLS`,过滤率过低时以此封顶防请求无限拉长),仍不足则返回稀疏页。远程短页(返回行数 < 请求数)即耗尽信号;`hasMore = 未耗尽 或 窗口内还有剩余过滤行`。**翻下一页必须回传上页 `nextCursor`**(格式"远程页码:已消费过滤行数",游标页码按 500 行窗口定位),否则按页码锚定会漏行/重行;游标解析失败自动降级为页码锚定,不阻断查询。
**返回体**(`KingdeeInvoicePoolPageVO`):`{ records: List<KingdeeInvoicePoolVO>, current, size, hasMore, nextCursor }`——**不再合成 total**;`nextCursor` 为下一页续扫游标,null 表示没有更多。行映射:票种编码→中文名、税率小数转百分数(顶层优先、回退明细首行)、`invoiceAmount = availableInvoiceAmount = matchedAmount = totalAmount`、`availableTaxAmount = availableAmount`(2026-09-23 新增"剩余可用金额"展示列,金蝶可用税额口径,**不改** `availableInvoiceAmount` 的价税合计口径——认领金额校验依赖该口径)、`attachmentJson = {source:'piaozone', path:downloadUrl, name:票号.pdf}`;`id` 置空(未入镜像)。`BfiInvoicePoolVO` 同步新增 `amountUseStatus` 字段承接金蝶返回。
**claimed/registered 纯展示装饰**:对本页票号批量查 `blade_formal_settlement_invoice`(关联主单未删且非 voided)与 `blade_invoice_receipt`(未删且非 voided),内存打标;只读展示,**不落库、不进查询条件**。
@@ -44,10 +48,13 @@
```
① 按 invoiceNo 调 queryPoolByInvoiceNo 远程 A801-1 精确单查(含票号全等防御)
② 未命中或 delete="1" → ServiceException("金蝶票池未查询到发票 X 或已作废,不能保存"),保存中止
② 未命中或 delete="1" → ServiceException("金蝶票池未查询到发票 X,不能保存"),保存中止
(选中→保存竞态:票被删/红冲在此拦截;不主动作废既有登记,与列表同步口径一致)
③ 命中 → fillInvoicePool 构建/刷新镜像行(保留图05 的 invoiceTypeCode/itemsJson 映射)
④ 保存/更新镜像行 → receipt.kingdeeInvoicePoolId = mirror.id,
③ 命中后按选票同口径 isUsablePoolRow 复核可用性(2026-09-23 新增):
剩余可用金额为 0 或金额使用状态已变更 → ServiceException("发票 X 剩余可用金额为0或金额使用状态已变更,不能保存")
(防选票到保存之间票被他人认领/使用造成超用;明细补查 fetchMirrorItemsFromPool 不做该过滤)
④ fillInvoicePool 构建/刷新镜像行(保留图05 的 invoiceTypeCode/itemsJson 映射)
⑤ 保存/更新镜像行 → receipt.kingdeeInvoicePoolId = mirror.id,
回填 kingdeeBillNo = serialNo、kingdeeStatus
```
@@ -56,9 +63,9 @@
## 4. 前端统一组件(`src/components/kingdee-invoice-pool-dialog/main.vue`)
- Props:`modelValue / title / confirmText / multiple / initialInvoiceNo / queryApi`(`fetchApi/syncApi` 已删除);事件 `confirm(rows)`(单选 `[row]`、多选选中数组)。打开时重置查询、按 `initialInvoiceNo` 预填、清勾选、回第 1 页加载。
- 查询表单六条件:发票号码、开票日期区间、销方税号、购方税号、发票类型(编码下拉)、单据使用状态;查询/重置按钮在加载期间禁用。
- 表格:票面列 + **认领状态/登记状态**两个 tag(仅展示);"金蝶同步"列已删除(远程票恒在池中无意义);行主键用发票号码兜底(`rowKey = invoiceNo || id`,远程票 id 恒空);单选 radio/双击直选,多选 `reserve-selection` 跨页保留。
- 分页区:**游标式**——"上一页/下一页" + "第 N 页" + 每页条数(10/20/50/100);`hasMore=false` 禁用下一页,第 1 页禁用上一页,加载中全部禁用;查询条件变更或换页大小即回第 1 页;翻页请求失败时页码回滚到发起前值(不停错页)。
- 查询表单五条件(2026-09-23 删除"单据使用状态",后端固定查未用):发票号码、开票日期区间、销方税号、购方税号、发票类型(编码下拉);查询/重置按钮在加载期间禁用。
- 表格:票面列 + **剩余可用金额**列(2026-09-23 新增,`availableTaxAmount`)+ **认领状态/登记状态**两个 tag(仅展示);"金蝶同步"列已删除(远程票恒在池中无意义);行主键用发票号码兜底(`rowKey = invoiceNo || id`,远程票 id 恒空);单选 radio/双击直选,多选 `reserve-selection` 跨页保留。
- 分页区:**游标式**——"上一页/下一页" + "第 N 页" + 每页条数(10/20/50/100);**下一页请求回传上页 `nextCursor` 续扫**(新查询/换页大小/重新打开自动重置游标与页缓存);**上一页直接回显已浏览页缓存(`pageCache`),不再发请求**;`hasMore=false` 禁用下一页,第 1 页禁用上一页,加载中全部禁用;查询条件变更或换页大小即回第 1 页;翻页请求失败时页码回滚到发起前值(不停错页)。
- 接线:收票登记表单(`multiple=false`,回填不再取 `id`,金蝶单据号取 `serialNo`、状态固定显示"未同步",保存落库后由后端刷新);结算单编辑器(`multiple=true`、`confirm-text="确认认领"`,`handleInvoiceClaimConfirm` 把复数 `attachmentsJson` 归一为认领契约单数 `attachmentJson`)。
- 翻页期间票池数据变动:游标页可能重复/漏行(组件按发票号码做行主键与选择去重,结算认领确认前还有票号集合查重);最终一致性以保存/提交时服务端重查为准,无资金风险。
@@ -68,8 +75,9 @@
## 6. 已知限制与验证
- 每次打开弹窗、每次保存都是一次金蝶远程调用(预期成本);金蝶不可用期间选票/收票保存均不可用。
- `expenseStatus` 仅作查询条件下推 A801-1,响应回传与否不影响 claimed/registered 本地装饰。
- 每次打开弹窗(最多 3 次补页调用)、每次保存都是金蝶远程调用(预期成本);金蝶不可用期间选票/收票保存均不可用。
- 单据使用状态固定后端下推"未用"(前端无此条件);选票可用过滤(`availableAmount > 0` 且 `amountUseStatus = "0"`)在拉取后进行,过滤率过低时单请求最多补页 3 次即返回稀疏页,靠 `hasMore + nextCursor` 继续翻页。
- 下一页依赖前端回传 `nextCursor`;上一页走前端缓存回显(0 请求),缓存页为浏览时快照,票池实时变动最终以保存/提交时服务端重查为准。
- 无表结构、无菜单 SQL 变更;Nacos 三份模板中 `thirdParty.bfi.invoiceReceipt.*` 同步相关键已随任务删除(图05 `sync-kingdee`/推送链使用的键保留)。
- 后端验证:`mvn clean compile -DskipTests -pl blade-service/blade-transport -am` + `mvn test-compile` 通过;前端 `pnpm run build` 通过;退役符号两仓库 grep 归零。
- 手工回归清单与过程记录:`.superpowers/sdd/2026-09-22-invoice-pool-remote-reversal/`(各任务简报/报告/评审包)。