1. 什么时候先看这篇文档
出现以下任一现象时,先定位:
- 同一个采购单中有两行相同物料,但“未发货关闭”列表只显示一行;
- 页面显示的待关闭数量为 0,或同一数量被重复扣减;
getInvPoInfo/getInvPoInfoFormat返回的采购明细和 OPS 数量不一致;- 关闭校验提示“可关闭数量不足”,但按采购明细实际数量应该可以关闭。
首个代码入口:
/Users/zhoujiangbin/code/docker-dev-env/www/dgj2.0/application/models/scm/InvPoModel.php::queryInvPoInfo()
2. 结论先看
采购数量必须按“采购订单 ID + 采购明细 ID”归属,不能按“订单 + 物料 + 赠品标识”归属。
正确链路是:
flowchart LR
PO[采购主单 iid] --> D[采购明细 id]
D --> O[出库明细 srcOrderEntryId]
O --> I[入库明细 srcOrderEntryId 指向 po_out_info.id]
I --> O2[回到出库明细 srcOrderEntryId]
O2 --> D
D --> C[关闭明细 srcOrderEntryId]
D --> W[waitQty = qty - outQty - inQty - closeQty]
修复规则:
- 出库量、关闭量按
srcOrderId + srcOrderEntryId聚合。 - 入库量通过
pu_invoice_info.srcOrderEntryId -> po_out_info.id -> po_out_info.srcOrderEntryId找回采购明细。 - 已有有效采购明细 ID 时,不再把
isGift当作唯一关系条件;同一isGift可能有多行。 srcOrderEntryId=0/NULL的历史入库记录没有可验证的行归属,不在查询层按最小 ID、物料或价格猜测绑定。
3. 多入口请求链路
| 子模块 | 入口 | 代码链路 | 关键事实 |
|---|---|---|---|
| 列表查询 | scm/InvPo/getInvPoInfo | InvPo.php::getInvPoInfo -> InvPoService::getInvPoInfo -> InvPoModel::queryInvPoInfo | 用 waitQty 和 status=5 条件决定列表是否显示 |
| 关闭预览 | scm/InvPo/getInvPoInfoFormat | InvPo.php::getInvPoInfoFormat -> InvPoService::getInvPoInfoFormat -> queryInvPoInfo | 根据选中的采购明细 ID重新计算可关闭数量 |
| 提交关闭 | scm/InvPo/closeInvPo | InvPo.php::closeInvPo -> InvPoService::closeInvPo -> 再次调用 queryInvPoInfo | 先重新校验出库/入库数量,再进入在线退款或挂账关闭 |
| 在线关闭金额 | closeInvPo -> closeOnlineList | InvPoService.php::closeOnlineList | 按实际 closeQty 和折扣/一口价规则重新计算;不直接使用列表原始 amount |
4. 代码修改位置
本次最小修复文件:
/Users/zhoujiangbin/code/docker-dev-env/www/dgj2.0/application/models/scm/InvPoModel.php
queryInvPoInfo():约第 91-205 行;三个数量子查询改为按采购明细 ID连接。- 出库和关闭:连接条件为
a.iid = srcOrderId AND a.id = srcOrderEntryId。 - 入库:先连接
pu_invoice_info和po_out_info.id,再使用出库行的srcOrderEntryId回到采购明细。 - 历史零来源:不参与明细级
inQty,避免把数量伪造到某一行。
相关读取和金额代码:
/Users/zhoujiangbin/code/docker-dev-env/www/dgj2.0/application/service/scm/InvPoService.php:855-989:关闭预览返回采购明细原始amount。/Users/zhoujiangbin/code/docker-dev-env/www/dgj2.0/application/service/scm/InvPoService.php:2136-2148:提交关闭前按选中的采购明细重新查询数量。/Users/zhoujiangbin/code/docker-dev-env/www/dgj2.0/application/service/scm/InvPoService.php:2215-2295:按实际closeQty计算在线关闭金额。
金额要分开理解:
- 列表的
amount是原采购行金额,例如原采购数量乘原单价; - 本次关闭金额是实际
closeQty乘关闭单价,再叠加既有折扣/一口价规则; - 因此不能用列表原始
amount直接作为退款或关闭金额。
5. 日志与证据矩阵
| 证据 | 搜索/查看位置 | 成功信号 | 关联键 |
|---|---|---|---|
| 列表接口 | scm/InvPo/getInvPoInfo 返回 JSON | 明细行、waitQty、outQty、inQty、closeQty 与页面一致 | iid + detail id |
| 关闭预览 | scm/InvPo/getInvPoInfoFormat | 选中的每个明细均返回,can_close_qty 不被同物料其他行影响 | 采购明细 id |
| 关闭提交 | InvPoService::closeInvPo、closeOnlineList | 重新查询后校验通过,关闭/退款结果可回读 | token + detail id |
| 业务页面 | 未发货关闭详情/退款结果页 | 数量、实际关闭金额、状态与接口回读一致 | 采购单号、采购明细 ID |
| 数据库只读核验 | t_scm_po_order_info_<sid%32>、t_scm_po_out_info_<sid%32>、t_scm_pu_invoice_info_<sid%32>、t_scm_po_close_info | 精确来源分别落到对应明细;无来源记录不被猜测归属 | sid + srcOrderId + srcOrderEntryId |
当前查询代码没有专门的“聚合成功”审计日志,所以不能只看 HTTP 200;必须同时看接口行数据和真实业务页面/数据库结果。
6. 排查步骤
- 固定
sid、采购主单 ID、采购明细 ID,先算出对应分表后缀sid % 32。 - 查采购明细,确认同一
iid下是否有相同invId、相同isGift的多行。 - 查出库明细,确认
srcOrderId和srcOrderEntryId是否指向采购主单/采购明细。 - 查入库明细,不要直接把
pu_invoice_info.srcOrderEntryId当成采购明细 ID;先按po_out_info.id中转。 - 查关闭明细,确认是否按采购明细 ID记录。
- 对每一行分别计算:
waitQty = qty - outQty - inQty - closeQty。 - 同时验证列表查询、关闭预览和提交关闭前的再次查询,避免只修了列表而提交校验仍使用旧口径。
7. 只读 SQL 骨架
以下仅为排查骨架,执行时必须替换实际分表和参数;生产环境使用只读连接,不执行 UPDATE/DELETE。
-- 1. 采购明细:确认是否同物料多行
SELECT id, iid, invId, isGift, qty, price, amount
FROM t_scm_po_order_info_<sid_mod_32>
WHERE iid = :po_order_id AND isDelete = 0
ORDER BY id;
-- 2. 出库:按采购明细来源核对
SELECT srcOrderId, srcOrderEntryId, SUM(qty) AS out_qty
FROM t_scm_po_out_info_<sid_mod_32>
WHERE sid = :sid AND isDelete = 0 AND billStatus = 0
GROUP BY srcOrderId, srcOrderEntryId;
-- 3. 入库:先确认入库明细指向哪一条出库明细
SELECT p.srcOrderEntryId AS outbound_detail_id,
o.srcOrderId, o.srcOrderEntryId AS purchase_detail_id,
SUM(p.qty) AS in_qty
FROM t_scm_pu_invoice_info_<sid_mod_32> p
JOIN t_scm_po_out_info_<sid_mod_32> o ON o.id = p.srcOrderEntryId
WHERE p.sid = :sid AND p.isDelete = 0 AND o.isDelete = 0
GROUP BY p.srcOrderEntryId, o.srcOrderId, o.srcOrderEntryId;
-- 4. 关闭:按采购明细来源核对
SELECT srcOrderId, srcOrderEntryId, SUM(qty) AS close_qty
FROM t_scm_po_close_info
WHERE sid = :sid AND isDelete = 0
GROUP BY srcOrderId, srcOrderEntryId;
8. 验证记录
- 等价 SQL 回归覆盖:同物料多明细、精确来源、精确来源与历史零来源混合、出库/入库/关闭同时存在等场景。
- 规划单元测试:
python3 -m unittest discover -s doc/ai/tests,18 项通过。 - 代码检查:
git diff --check通过。 - 内网目标容器 PHP 7.3 lint 通过,部署文件 SHA-256 为
68af3f944b7ed12b3c1d908e226070d679b0879c02f8aca28995c13db382fe9f。 - 真实业务回读:两个采购明细分别显示 84、12;关闭金额分别为 840、120,合计 960。
- 任务包:
BUG-20260928-PURCHASE-DETAIL-AGGREGATION已完成并归档,详细证据位于 DGJ2 仓库doc/ai/changes/archive/2026/。
9. 失败路径与历史边界
- 旧逻辑以物料维度汇总后连接多条采购明细,同一数量可能被每行重复扣减,导致有效行被
waitQty > 0过滤。 - 不能用
物料 + 价格或物料 + isGift推断历史行归属;这些字段都可能重复。 srcOrderEntryId=0/NULL的历史入库记录暂不参与明细级inQty。这可能使部分历史单据显示未扣除入库量,但比错误扣到另一条采购明细更安全。- 历史零来源数据需要单独审计/补偿规则,不能作为本次查询修复的隐式兜底。
- 本次修复只改查询,不修改订单、入库、关闭数据;如果页面仍异常,应继续检查数据源关系和部署版本,不要直接改状态或数量。
10. 回滚与文件定位
- 代码文件:
application/models/scm/InvPoModel.php。 - 当前分支保存的修复提交以 Git 记录为准;部署内网时使用目标文件备份和
rollback.sh,不删除原备份。 - 若只需恢复代码,回滚本次提交前先确认是否包含其他未发布改动;若需恢复内网文件,使用部署批次目录中的回滚入口并重新执行 PHP lint 和业务回读。