本文是 DGJ2.0 的“改公共代码前必读”手册。它回答的不是某一个业务怎么操作,而是:修改一个被多个业务共用的文件,会影响谁、改变哪些数据事实、需要防住哪些并发与兼容风险、最低必须回归什么。
代码根目录:/Users/zhoujiangbin/code/docker-dev-env/www/dgj2.0
1. 业务目标
- 避免修复秒杀、采购、销售等单一入口时破坏普通老链路。
- 把 Controller、Service、Model、MQ、回调、外部系统和页面资源的影响面连成一张图。
- 明确“代码成功”不等于“业务成功”,识别数据库事务以外的远程调用和消息副作用。
- 给开发、评审、测试和发布提供同一份风险语言与回归清单。
- 让后续工程师不必重新全库搜索,也能知道从哪里开始核对。
2. 什么时候必须使用本文
出现以下任一情况,都应按本文执行影响面审计:
| 触发条件 | 为什么危险 | 最低动作 |
|---|---|---|
| 修改采购、销售公共 Service | PC、App、任务、回调和特殊来源可能复用同一方法 | 方法调用方扫描 + 普通/特殊来源回归 |
| 修改库存增减或库存流水 | 会直接改变可售库存和历史账 | 正向、反向、重复请求和负库存分支回归 |
| 修改支付、OA、订单中心、配送回调 | 消息可能重投,ACK/NACK 决定是否继续消费 | 幂等、乱序、重复和失败重试回归 |
修改 BaseModel、分表规则或公共 SQL | 影响所有继承模型及历史数据查询 | 分片、分页、总数、旧表兼容回归 |
修改 MqSer 或事件枚举 | 生产者成功不代表消费者能够识别 | 事件名、目标、消息体和消费者契约核对 |
修改 BaseProvider 或公共鉴权 | 所有外部 HTTP 调用的超时、日志和解析都会变化 | 同步、异步、超时、非 JSON、下载回归 |
| 修改公共视图或资源版本 | 多个页面会同时加载同一 JS/CSS | 引用页面冒烟 + 缓存和版本核对 |
| 新增枚举值、状态值或订单来源 | 列表、按钮、报表和回调分支可能漏掉新值 | 全局引用扫描 + 历史值兼容检查 |
3. 一页结论
当前静态代码扫描得到的引用文件数量如下。数量表示“包含该类名的 PHP 文件数”,用于衡量初始爆炸半径,不代表精确运行时调用次数。
| 公共类 | 引用文件数 | 主要风险 | 建议等级 |
|---|---|---|---|
InventorySer | 78 | 实时库存与流水、负库存控制、反向删除 | P0 |
MqSer | 72 | 跨系统事件契约、重复发送、提交顺序 | P0 |
InvPoService | 31 | 采购查询、关闭、退货、入库、支付 | P0 |
InvSaService | 30 | 销售、出库、对账、退货、配送 | P0 |
PoOrderSer | 21 | 新采购领域、购物车、待发货、支付状态 | P1 |
SaOrderSer | 16 | 新销售领域、App/SAAS、出库和退款 | P1 |
KzInventorySer | 10 | 外部供应库存、50 个 SKU 分批、异步结果 | P1 |
InvPoFactory | 7 | 采购事务编排、订单中心同步、临时库存释放 | P0 |
FlashSaleSer | 7 | 锁定量、使用量、采购来源与补偿 | P0(秒杀域) |
最重要的五条规则:
- 特殊业务必须用来源字段隔离。 秒杀来源以
srcOrderType=13识别;orderType=30-Cxx-77是订单类型,不能互相替代。 - 查询条件的修改也是行为修改。
WHERE、JOIN、GROUP BY、总数 SQL 和分页 SQL 必须一起检查。 - 事务只保护同一个数据库连接。 HTTP、MQ、Redis、文件和第三方系统不会随 MySQL 回滚。
- 库存必须同时解释过程账与结果账。 流水记录为什么变,实时库存表示现在剩多少,两者缺一不可。
- 回调天然会重复、乱序和迟到。 状态判断必须是可重入的业务幂等,不应只依赖消息不会重投。
flowchart LR
E["PC / App / Inner / OpenAPI"] --> C["Controller"]
C --> P["采购与销售公共 Service"]
P --> M["Model 与分表"]
P --> I["InventorySer"]
P --> Q["MqSer"]
P --> H["外部 HTTP Provider"]
CB["支付 / OA / 订单 / 配送回调"] --> P
T["定时任务与补偿"] --> P
I --> DB["流水 + 实时库存"]
Q --> OUT["下游系统"]
H --> OUT
4. 风险分级
4.1 P0:可能造成资金、库存或重复单据
| 风险 | 典型后果 | 必须证明 |
|---|---|---|
| 库存增减或反向逻辑改变 | 超卖、负库存、账实不符 | 同一业务键只产生一次有效库存事实 |
| 支付回调可重复落账 | 重复付款单、重复账户流水 | 重复消息不新增第二份资金事实 |
| 采购入库并发窗口 | 同一发货记录生成两张 CG 入库单 | 原子占用或数据库唯一约束可以阻止并发 |
| 事务内先调外部系统 | 本地回滚但外部已创建订单 | 外部幂等键、补偿和重查路径明确 |
| MQ 在提交前发送 | 消费者读不到尚未提交的数据 | 发送时序或消费者重试可以收敛 |
4.2 P1:可能改变共享业务行为
- 公共列表增加
OR条件,导致特殊来源记录进入普通入口。 - 状态映射、订单类型、交易类型增加或改值。
- 支付信息过滤、关闭入口、退货入口和按钮权限改变。
- 分片算法、
setSid()调用时机、主从库选择改变。 - HTTP Provider 的超时、表单类型、结果解析和异常转换改变。
4.3 P2:主要影响展示、操作效率或可观察性
- 公共页面资源版本、打印模板、通用组件。
- 日志模块名、RequestId、脱敏字段。
- 枚举文案、提示文案、非业务关键排序。
P2 不代表可以不回归。公共资源加载失败时,可能让多个页面无法提交或打印。
5. 改动分类
评审时先给改动贴一种标签:
| 分类 | 定义 | 例子 | 处理方式 |
|---|---|---|---|
| 业务域内隔离 | 只在可证明的来源守卫内生效 | 仅 srcOrderType=13 过滤秒杀支付方式 | 回归该域 + 守卫外普通链路 |
| 共享兼容扩展 | 增加字段或分支但保留旧行为 | 消息体新增可选字段 | 验证旧消费者忽略未知字段 |
| 共享行为改变 | 所有调用方都会获得不同结果 | 修改库存不足判断 | P0/P1 全链回归和发布门禁 |
| 风险不清 | 无法证明调用方、数据或时序边界 | 公共 SQL 拼接中直接加入新 OR | 不得直接发布,先完成影响面追踪 |
flowchart TD
A["准备修改公共文件"] --> B{"是否有明确业务来源守卫"}
B -->|是| C{"守卫字段是否来自持久化事实"}
C -->|是| D["业务域内隔离"]
C -->|否| E["风险不清"]
B -->|否| F{"旧调用方结果是否完全不变"}
F -->|是| G["共享兼容扩展"]
F -->|否| H["共享行为改变"]
D --> I["域内 + 守卫外回归"]
G --> J["契约兼容回归"]
H --> K["全链路回归"]
E --> L["先补证据再修改"]
6. 总体影响面地图
| 层 | 代表文件 | 持有的公共责任 | 常见误区 |
|---|---|---|---|
| 入口 | BaseController.php、BaseAppController.php | 登录、上下文、响应、请求锁 | 认为只影响一个 Controller |
| 领域编排 | InvPoService.php、InvSaService.php | 采购销售完整老链路 | 只看当前调用方法,不看内部复用 |
| 新领域服务 | PoOrderSer.php、SaOrderSer.php | App、活动、服务化后的领域能力 | 默认新旧 Service 完全互斥 |
| 事务工厂 | InvPoFactory.php | 创建采购、订单中心同步、支付模式 | 把远程调用误认为数据库事务的一部分 |
| 库存事实 | Storage/InventorySer.php | 实时库存和分片流水 | 只查实时库存,不查业务流水 |
| 外部库存 | KzInventorySer.php | 供应库存查询和格式归并 | 忽略批次部分失败和 allowQty 过滤差异 |
| 消息 | MqSer.php、MqEventEnums.php | 生产事件与路由契约 | 只验证 publish 返回成功 |
| 回调 | controllers/tasks/*Notify.php | 跨系统状态回写 | 假设消息只到一次且按顺序到达 |
| 数据访问 | BaseModel.php、各分片 Model | 查询、写入、双写和分片 | 布尔返回值无法表达期望影响行数 |
| HTTP | Providers/BaseProvider.php | 同步/异步请求、超时、解析、日志 | 把 HTTP 200 当作业务成功 |
| 配置契约 | tables.php、路由、API 映射、Enums | 全局名字和协议 | 改常量后遗漏字符串硬编码 |
| 前端资源 | views_v2/common/kzuilite.php | 公共组件版本 | 只打开修改页面,不回归其他引用页 |
7. InvPoService.php:采购老链路核心
文件:application/service/scm/InvPoService.php,约 8 千行。它不是单一“采购查询服务”,而是同时承载采购列表、退货、关闭、入库、支付和历史兼容。
7.1 方法簇
| 方法簇 | 代表方法 | 写入/副作用 | 主要调用方 |
|---|---|---|---|
| 查询与格式化 | getPoInfo、getInvPoList、getInvPo、getInvPoInfo | 通常只读,但决定列表和按钮 | PC 采购、OPS 日志、导出/PDF |
| 退货 | saveOrderBack、submitOrderBack、lessReturnInvPos | 退货单、退货明细、库存、订单中心 | 普通采购退货、少发、不良品、活动退货 |
| 关闭 | closeInvPoOrder、closeInvPo、cancelInvPoCG | 关闭数量、主单状态、消息 | 行关、整单关、入库单取消 |
| 入库/出库 | invPoInStorage、addInvPur、updatePoStatus | CG 入库单、库存流水、实时库存、采购状态 | PC、PDA、App WMS、配送回调 |
| 支付 | getPayInfo、getPayInfoNew、noPagePay、middlePay | 支付请求、支付方式过滤 | 采购待支付页、系统支付入口 |
| 秒杀兼容 | buildFlashActivityReturnSearchWhere、filterFlashActivityPayInfo、assertNotFlashActivityPayOrder | 限制入口,不直接写表 | 秒杀退货和支付入口 |
7.2 代表性入库请求
控制器最终传给 invPoInStorage($pageData) 的核心上下文可抽象为:
{
"JXCSID": 10001,
"JXCUID": 20001,
"JXCUNAME": "operator",
"orderId": 9000001,
"entries": [
{
"outInfoId": 8000001,
"srcOrderEntryId": 7000001,
"invId": 6000001,
"skuId": "SKU-DEMO",
"qty": 2,
"locationId": 30001,
"locationAreaId": 40001
}
]
}
字段名需以具体 Controller 的实际组装为准;本文示例只表达业务键和数据方向,不应直接作为生产请求。
7.3 入库主流程
sequenceDiagram
participant U as PC/PDA/App
participant S as InvPoService
participant O as PoOutInfo
participant P as PuInvoice
participant I as InventorySer
participant R as RealTimeInventory
participant L as InventorySub
participant Q as MQ/下游
U->>S: invPoInStorage(pageData)
S->>O: 查询可入库发货记录
S->>S: checkInvPoInStorageQty
S->>S: 开启数据库事务
S->>P: addInvPur 生成 CG 主明细
S->>I: save(inventoryRows)
I->>R: increaseQty
I->>L: 写库存分片流水
S->>O: 更新发货/入库状态
S->>S: updatePoStatus
S-->>U: 返回入库结果
S-->>Q: 库存或入库事件
需要同时核对的物理事实:
| 事实 | 表/模型 | 关键关联 | 核对问题 |
|---|---|---|---|
| 采购主单 | t_scm_po_order | id、billNo、sid | 状态是否与已入库数量一致 |
| 采购明细 | t_scm_po_order_info_{sid%32} | iid、id、invId | 采购数量和关闭数量是否正确 |
| 发货明细 | t_scm_po_out_info_{sid%32} | srcOrderId、srcOrderEntryId、billNo | 同一发货行是否被重复消费 |
| 采购入库主单 | t_scm_pu_invoice | srcOrderId、srcOrderNo | 是否出现重复 CG |
| 采购入库明细 | t_scm_pu_invoice_info_{sid%32} | srcOutNo、srcOrderEntryId、skuId | 入库数量是否超过发货数量 |
| 实时库存 | t_scm_inventory_real_time | sid+invId+货位 | 当前数量是否重复增加 |
| 库存流水 | t_scm_inventory_info_{sid%128} | iid+billNo+transType | 是否存在两份同源流水 |
7.4 重复入库并发窗口
当前代码路径先读取可入库记录,再校验数量,之后才生成入库事实。如果两个请求在“发货记录仍可入库”的同一窗口并发到达,它们可能同时通过前置查询。
sequenceDiagram
participant A as 请求 A
participant B as 请求 B
participant O as 发货明细
participant DB as 入库与库存
A->>O: 查询 billStatus=0
O-->>A: 可入库
B->>O: 查询 billStatus=0
O-->>B: 可入库
A->>DB: 创建 CG + 增库存
B->>DB: 创建 CG + 增库存
A->>O: 标记已处理
B->>O: 标记已处理
Note over DB: 若无原子占用/唯一键,可能形成重复事实
这类问题不能只靠“页面按钮禁用”解决。按钮只能降低人工重复点击,不能阻止重试、双端提交、网络超时后的再次提交或并行消费者。
7.5 推荐的原子占用模式
在同一事务中先用条件更新占用待处理行,并检查实际影响行数等于期望行数,占用成功后才创建入库和库存记录:
UPDATE t_scm_po_out_info_{shard}
SET billStatus = :processing_status,
modifyTime = NOW()
WHERE sid = :sid
AND id IN (:out_info_ids)
AND billStatus = 0
AND isDelete = 0;
随后检查:
affected_rows == count(distinct out_info_ids)
如果不相等,当前事务应回滚并提示“发货记录已被处理,请刷新”。
注意:BaseModel::updateItem() 声明为 bool,内部返回 affected_rows()。PHP 会把大于零的行数转换成 true,因此它不能用于判断“期望更新 3 行、实际只更新 1 行”。这种场景需要能够返回整数影响行数的专用 Model 方法。
7.6 重复入库只读检查
SELECT
sid,
srcOrderNo,
srcOutNo,
srcOrderEntryId,
skuId,
isGift,
COUNT(*) AS row_count,
SUM(qty) AS in_qty
FROM t_scm_pu_invoice_info_{shard}
WHERE sid = :sid
AND srcOrderNo = :po_no
AND isDelete = 0
GROUP BY sid, srcOrderNo, srcOutNo, srcOrderEntryId, skuId, isGift
HAVING COUNT(*) > 1;
不能仅以 COUNT(*) > 1 自动认定为错误:业务可能允许分批入库。还要核对每一组的累计入库数量是否超过对应发货数量,以及是否出现相同 srcOutNo + srcOrderEntryId + skuId + isGift 的重复整批提交。
7.7 公共查询条件风险
采购列表通常同时包含:主单、32 分片明细、发货汇总、入库汇总、关闭汇总。改变任一 JOIN 都可能改变列表行数。
flowchart TD
A["采购主单"] --> B["采购明细"]
B --> C["按明细聚合发货量"]
B --> D["按明细聚合入库量"]
B --> E["按明细聚合关闭量"]
C --> F["可处理量"]
D --> F
E --> F
F --> G["列表行"]
G --> H["分页总数"]
G --> I["按钮权限"]
改列表 SQL 时必须同步检查:
- 数据查询 SQL 与
countSQL 是否使用相同过滤条件。 - 一对多 JOIN 是否导致主单重复、分页数量变小。
- 新增
OR是否被正确括号包围。 billStatus=0到底指主单状态还是发货/入库明细状态。sid是否同时出现在主表和分表条件中。- 历史状态是否仍需出现在退货、关闭或报表入口。
7.8 秒杀来源守卫
| 字段 | 秒杀值 | 用途 | 不可替代原因 |
|---|---|---|---|
srcOrderType | 13 | 证明采购单来源于秒杀活动单 | 用于来源关系和业务隔离 |
orderType | 30-Cxx-77 | 采购订单类型/展示标签 | 可能被外部映射为普通订单标签 |
srcOrderId | 秒杀订单主键 | 关联活动订单 | 用于锁定量、使用量和补偿 |
srcOrderNo | 秒杀订单号 | 跨系统与人工排查 | 业务号而非类型判断 |
安全守卫示例:
if ((int)($order['srcOrderType'] ?? 0) === PoOrderEnums::SRC_ORDER_TYPE_FLASH_ACTIVITY) {
// 只对秒杀来源执行支付方式过滤或库存补偿
}
危险写法:仅凭前端参数、订单文案或 orderType 模糊判断来源。
7.9 支付入口影响
getPayInfoNew()、noPagePay()、专属支付和聚合支付并非一条完全相同的链路。修改支付限制时要列清允许入口:
| 入口 | 普通采购 | 秒杀采购 | 风险 |
|---|---|---|---|
| 获取新版支付信息 | 允许 | 允许但过滤到活动允许方式 | 过滤后为空必须返回可理解错误 |
| 免页面支付 | 按原规则 | 代码已有阻断守卫 | 遗漏入口可能绕过秒杀支付约束 |
| 专属支付/联合支付 | 按授信规则 | 需明确是否允许 | 错误组合会导致资金和订单状态不一致 |
7.10 InvPoService 最小回归
- 普通采购:创建、提交、订单中心同步、待支付/非在线支付。
- 采购入库:一次入库、分批入库、重复请求、并发请求、超量入库。
- 采购关闭:部分行关、整单关、已入库后关闭、消息重复。
- 采购退货:普通退货、不良品、少发、在线支付未发货退货。
- 特殊来源:预订单、活动、秒杀、首配、直发。
- 查询:列表数据、总数、分页最后一页、导出和详情数量一致。
8. InvPoFactory.php:采购创建事务编排
文件:application/Services/InvPoFactory.php。
8.1 动态策略
构造函数按 $type 动态加载 App\Services\InvPo\{$type}Ser。当前调用方包括:
| 类型/调用方 | 用途 | 回归重点 |
|---|---|---|
Normal | 普通采购、预订单、专属返利、秒杀转采购 | 同一工厂下的来源隔离 |
FirstMatch | 首配/撮合类入口 | 订单标签与供应商映射 |
GiftActivity | 赠品活动订单 | 赠品承担方和活动扩展字段 |
不存在的类型会抛出“无效的订单类型”,因此新增策略必须同时提供类、调用入口和回归用例。
8.2 saveOrder() 流程
sequenceDiagram
participant C as Controller/活动服务
participant R as Redis
participant F as InvPoFactory
participant S as 具体采购策略
participant DB as MySQL
participant O as 订单中心
participant P as 支付版本服务
C->>F: saveOrder(params)
F->>R: 检查采购黑名单与加锁
F->>DB: begin
F->>S: saveOrder(params)
S->>DB: 写采购主明细
F->>DB: 保存购物车关联
F->>O: toOrdercenter
O-->>F: 订单与支付模式
F->>P: queryVersion(sid)
F->>DB: 更新 paymentType/status
F->>DB: commit
F->>R: 解锁
F-->>C: success
8.3 事务边界
saveOrder() 在 MySQL 事务内调用订单中心和支付版本服务。数据库回滚无法撤销外部系统已成功创建的订单。
flowchart LR
A["本地写采购单"] --> B["订单中心创建成功"]
B --> C["后续本地更新失败"]
C --> D["MySQL rollback"]
D --> E["本地无单"]
B --> F["订单中心已有单"]
E -.不一致.-> F
评审必须回答:
- 订单中心以哪个字段幂等:通常应是
sourceOrderCode/billNo,需以对方契约确认。 - 本地重试是否会重复创建外部订单。
- 回滚后是否有查询外部状态并补偿/重建本地数据的流程。
- 临时库存释放是否覆盖所有失败点。
- Redis 锁的过期、进程崩溃和解锁归属是否安全。
8.4 参数与外部字段映射
| 本地字段 | 订单中心字段 | 意义 | 风险 |
|---|---|---|---|
billNo | sourceOrderCode | 上游采购订单号 | 应作为外部幂等候选键 |
orderType | sourceOrderType/orderLabel | 本地类型与外部标签 | 秒杀类型被映射为普通标签 1 |
srcOrderNo | reserveCode(预订单) | 来源预订单号 | 仅特定来源有效 |
sid | customerCode | 服务站 | 上下文错站会污染外部数据 |
locationId | addressCode的一部分 | 收货仓 | 仓库编码缺失会导致履约异常 |
srcOrderType=13 | multiWarehouseFlag | 秒杀多仓标识 | 必须同时满足来源和扩展开关 |
9. PoOrderSer.php:新采购领域服务
文件:application/Services/PoOrders/PoOrderSer.php,约 2.5 千行。
9.1 主要责任
checkOrderGoods():下单商品规则。addPoOrder()、addSelfPoOrder():采购单和自营订单创建。closeSelfOrder()、syncSelfOrder():自营订单同步与关闭。cancelWaitPayOrder():支付异常或超时取消。- 采购库存与待发货数量查询。
- 采购购物车新增、更新和最低起订量处理。
- 地址、自提、订单截止时间等辅助能力。
9.2 风险关系
flowchart TD
A["普通采购"] --> P["PoOrderSer"]
B["预订单"] --> P
C["活动/秒杀"] --> P
D["支付异常回调"] --> P
P --> O["采购主明细"]
P --> K["供应库存查询"]
P --> C1["购物车"]
P --> S["销售未发货数量"]
PoOrderSer 会读取 SaOrderSer 的未发货数量参与采购可用量判断时,修改销售查询可能间接改变采购展示,这是跨领域的隐性影响。
9.3 回归重点
orderType与srcOrderType的组合,不只回归其中一个字段。- 取消待支付单是否释放临时库存和活动锁定量。
- App/任务调用
service()时,CLI 返回新实例、Web 返回进程内单例的差异。 - 购物车最小起订量和最大限购失败后缓存是否清理。
- 自提地址、仓库和服务站上下文是否一致。
10. InvSaService.php:销售、出库和对账老链路
文件:application/service/scm/InvSaService.php,约 9.5 千行,是当前最大的共享业务文件之一。
10.1 方法簇
| 方法簇 | 代表方法 | 关键副作用 |
|---|---|---|
| 销售查询 | SalesListV2、SalesList、saleDetailNew | 决定列表、状态、打印和导出 |
| 销售出库 | addOutBound、outBound | 出库单、库存扣减、配送、MQ |
| 配送 | buildTransportCreateParams、handleTransportUpsert | TMS/配送建单与单号回写 |
| 对账 | reconciliation、batchReconciliation、cancelReconciliation | 核销状态和收款关系 |
| 销售退货 | importSaleReturnGoods、退货参数转换 | 退货单、反向库存、退款关系 |
| SAAS/App | addInvSaOr、cancelSaasOrder、sendInvoiceToSaas | 外部订单映射和消息 |
| 报价 | getGoodsPriceInfoV2、saveGoodsPriceInfo | 价格、替代件、询价结果 |
10.2 销售出库副作用
sequenceDiagram
participant U as PC/App/SAAS
participant S as InvSaService
participant DB as 销售订单与出库单
participant I as InventorySer
participant T as 配送/TMS
participant Q as MQ
U->>S: addOutBound/outBound
S->>DB: 校验未出库数量
S->>DB: 写销售出库主明细
S->>I: 扣减库存并写流水
S->>T: 创建或更新配送单
S->>DB: 重算销售/出库状态
S-->>Q: SAAS/App/下游状态事件
S-->>U: 出库结果
必须明确 TMS 和 MQ 调用在事务提交前还是提交后。若 TMS 已成功而本地回滚,重试必须依赖稳定业务键更新原配送单,而不是创建第二单。
10.3 修改销售公共 SQL 的危险点
sid%16的销售订单明细和_0_{sid%64}的出库明细不能混用分片公式。- 出库数量、退货数量的符号方向不同,不能只做
SUM(qty)后不看transType。 - 一张销售单可以部分出库,多张出库单参与状态重算。
- 对账、取消对账和自动核销共用出库单状态时,要保留历史状态兼容。
- SAAS 来源、App 来源和 PC 来源可能共用一张主表,但下游通知不同。
11. SaOrderSer.php:新销售领域服务
文件:application/Services/SaOrders/SaOrderSer.php。
11.1 主要入口
| 方法 | 用途 | 关键数据 |
|---|---|---|
saCreate/saCreate2 | 创建销售单 | 客户、商品、价格、来源、仓店 |
saOrderOut | 销售出库 | 销售单、出库单、库存 |
retOrder/retOrder2 | 销售退货 | 原单、原出库、退货数量 |
addPaymentForApp | App 销售收款 | 出库单、支付单和账户 |
findNotShippedOrderInfos | 查询未发货明细 | 采购和库存判断可能间接依赖 |
checkOrderOutStatus | SAAS 订单出库状态 | 外部订单号和本地状态 |
11.2 新旧销售服务关系
flowchart LR
A["PC 老入口"] --> L["InvSaService"]
B["App / SAAS / 新入口"] --> N["SaOrderSer"]
L --> O["销售主单/明细"]
N --> O
L --> I["销售出库/库存"]
N --> I
O --> Q["同步与报表"]
不要假设修改其中一个服务不会影响另一个。它们写入同一批核心业务表,并可能互相调用或共享 Model、枚举和库存服务。
12. InventorySer.php:库存事实写入核心
文件:application/Services/Storage/InventorySer.php。
12.1 save() 的真实顺序
save($inventory, $iid=0, $billType=null) 对每一行先改变实时库存,循环结束后再批量写分片库存流水。旧流水主表写入已被注释。
sequenceDiagram
participant B as 业务 Service
participant I as InventorySer
participant R as InventoryRealTimeModel
participant C as Redis 最近出入库时间
participant L as InventorySubModel
B->>I: save(rows, iid, billType)
opt iid 非空
I->>I: delete(iid,billType) 反向旧流水
end
loop 每个库存行
I->>R: increaseQty/decrementQty
I->>C: 更新 in/out 时间
end
I->>L: 批量写库存分片流水
如果调用方没有包裹数据库事务,前几行实时库存更新成功、后续一行失败时,会形成部分成功。即使调用方有事务,也要确认实时库存 Model 和流水 Model 使用同一个数据库连接。
12.2 库存写入字段
| 字段 | 作用 | 风险 |
|---|---|---|
iid | 业务单据主键 | 删除/重做流水的反向依据 |
billNo | 业务单号 | 人工排查入口 |
billType | 单据类型 | 同一 iid 在不同单据域可能重复 |
transType | 交易类型 | 决定库存方向和报表口径 |
invId/skuId | 商品内部键/外部编码 | 不能用 skuId 替代 invId 更新实时库存 |
locationId/locationAreaId | 仓库/货位 | 同商品不同货位是不同库存事实 |
qty | 有符号变动量 | 正数增加、负数扣减 |
area_type | 货位区域类型 | 不良品是否参与可售库存 |
12.3 扣减分支
changeInventoryQty() 根据商品归属和站点库存控制配置选择是否允许负库存。
flowchart TD
A{"qty > 0?"} -->|是| B["increaseQty"]
A -->|否| C{"商品 sid == 1?"}
C -->|是 快准商品| D{"服务站在快准库存控制名单?"}
D -->|是| E["decrementQty 严格控制"]
D -->|否| F["decrementQty 允许另一策略"]
C -->|否 第三方商品| G{"服务站在第三方库存控制名单?"}
G -->|是| H["decrementQty 使用第三方策略"]
G -->|否| I["decrementQty 使用相反策略"]
E --> J{"affected > 0?"}
F --> J
H --> J
I --> J
J -->|否| K["抛库存不足"]
J -->|是| L["更新最近出入库时间"]
布尔参数的准确语义应继续以 InventoryRealTimeModel::decrementQty() 的 SQL 为准,不应只凭参数名猜测。修改名单判断时必须覆盖快准商品和第三方商品的四种组合。
12.4 delete() 是业务反向操作
delete(iid,billType,transType) 不是简单删除:它先读取旧流水,对每一行用 0 - old.qty 反向实时库存,再删除分片流水。
flowchart LR
A["读取旧流水"] --> B["qty 取反"]
B --> C["更新实时库存"]
C --> D["删除流水"]
风险:
- 同一个
iid条件过宽会反向不属于本次修改的流水。 - 反向库存成功后删除流水失败,会保留旧流水但实时库存已反向。
- 调用两次时,第二次查不到流水通常不会再次反向,但需确保第一次事务完整提交。
delete()调用changeInventoryQty()的参数位置要特别小心;areaType与transType均为可选参数,位置传错会改变负库存/最近时间逻辑。
12.5 库存最小回归矩阵
| 场景 | 流水方向 | 实时库存 | 重复请求 |
|---|---|---|---|
| 采购入库 | 正 | 增加 | 不得重复增加 |
| 销售出库 | 负 | 减少 | 不得重复扣减 |
| 销售退货入库 | 正 | 增加 | 原出库关联唯一 |
| 采购退货出库 | 负 | 减少 | 退货单幂等 |
| 盘盈 | 正 | 增加到盘点事实 | 盘点提交只一次 |
| 盘亏 | 负 | 减少到盘点事实 | 库存不足策略明确 |
| 货位调整 | 一负一正 | 总量不变 | 两边同事务 |
| 编辑单据 | 旧流水反向 + 新流水 | 等于重算结果 | 失败不能留下半份 |
13. KzInventorySer.php:供应库存查询公共适配
文件:application/Services/KzInventory/KzInventorySer.php。
13.1 查询流程
sequenceDiagram
participant B as 业务调用方
participant K as KzInventorySer
participant P as KzInventoryProvider
participant S as 库存中心
B->>K: patchGetKzInventory(sid, skuIds, orderType)
K->>K: 每 50 个 SKU 分批
loop 每个批次
K->>P: asyncGetInventoryBySkuIds
P->>S: 异步 HTTP
S-->>P: company/details/allowQty
P-->>K: callback
end
K->>P: asyncWait
K->>K: 按 skuCode/companyCode 归并
K-->>B: 库存 Map
13.2 新旧格式差异
| 方法 | 明细过滤 | 主要风险 |
|---|---|---|
formatInvDetail | 过滤 allowQty <= 0 的明细 | 零库存仓不会出现在列表 |
formatInvDetailNew | 保留所有明细 | 调用方需自行判断可用量 |
formatSpeedInventory | 保留明细并聚合 speedFlag | 任一公司为 true 即总结果 true |
异步请求失败时 BaseProvider 会给 callback 空数组,整个批次可能静默缺失。调用方必须区分“真实零库存”和“外部批次查询失败”;当前静态代码不能证明所有调用方都做了这个区分。
14. MqSer.php:跨系统生产者公共门面
文件:application/Services/Mq/MqSer.php。
14.1 事件域
| 事件域 | 代表方法 | 业务键 |
|---|---|---|
| 库存 | sendInventoryEvent、sendInStorageEvent | sid+iid+billType+transType |
| 采购关闭 | sendPoCloseOrderConfirm | 采购单号/关闭单号 |
| 销售/App | sendSaOrderCloseToAPP、sendSaOrderOutToMq | 销售单号/出库单号 |
| SAAS | sendInvoiceToSass、sendOutBoundToSaas | 外部订单号和本地单号 |
| OA | sendOAResult | 模板、流程实例、业务单 |
| 渠道 | sendChannelOrderManualCreate、sendChannelOrderDelivery | 渠道订单/售后单号 |
| 对账 | sendStatementBillCreate、sendStatementSend | 账单号、策略和服务站 |
| 商品/价格 | sendPriceChange、sendGoodsUpdateToVin | SKU、服务站、变更版本 |
14.2 事务与 MQ 的四种结果
flowchart TD
A["本地业务操作"] --> B{"数据库提交成功?"}
B -->|否| C["不得让下游形成最终事实"]
B -->|是| D{"MQ 发布成功?"}
D -->|是| E["正常收敛"]
D -->|否| F["本地成功 / 下游缺事件"]
C --> G{"消息是否已提前发送?"}
G -->|是| H["本地失败 / 下游可能已处理"]
G -->|否| I["本地回滚"]
F --> J["补偿任务或 Outbox"]
H --> K["下游幂等 + 反向补偿"]
每个发送方法修改时都要核对:
- destination/routing key 是否与消费者注册一致。
- 消息体字段是新增可选、改名、删除还是类型变化。
messageId是否稳定;随机 ID 不能替代业务幂等键。- 调用发生在事务提交前还是提交后。
- publish 失败是否抛异常,调用方是否会回滚。
- 是否有补偿任务可以基于业务事实重建消息。
14.3 代表性库存消息
{
"sid": 10001,
"iid": 9000001,
"billType": "PU",
"transType": "150501",
"poOrderId": 7000001,
"inventoryData": [
{"invId": 6000001, "skuId": "SKU-DEMO", "qty": 2}
]
}
真实 routing key、包裹结构和字段类型以 MqSer 与 MqEventEnums 当前代码为准。
15. 回调消费者:支付、订单、配送与 OA
15.1 消费者地图
| 消费者 | destination | 事件数量/代表事件 | 主要写入 |
|---|---|---|---|
PayCenterNotify | DEST_PAYCENTER_NOTIFY | 支付成功、账户调整、退款、白条、逾期等 10 类 | 付款单、付款明细、账户、采购状态 |
OrderCenterNotify | DEST_ODC_NOTIFY | 订单审核/取消/关闭、售后审核/关闭/退款等 | 采购、退货、退款与来源关系 |
DispatchCenterNotify | 配送中心 destination | 配送出库、物流、自提、退货关闭、标签变化 | 出库、配送单号、采购/销售状态 |
OaNotify | DEST_OA_NOTIFY | OA 审批结果 | 活动、权限或业务审批状态 |
OaResultNotify | OA 结果同步 destination | OA 状态同步 | OA 关联业务状态 |
15.2 支付成功回调
PayCenterNotify::paycenterPayResult() 只接受 payStatus='02'。采购支付会把外部“分”转换成本地“元”,并写付款主明细、采购支付信息和订单状态。
sequenceDiagram
participant P as 支付中心
participant C as PayCenterNotify
participant O as 采购/预订单
participant F as Payment/PaymentInfo
participant A as Account
P->>C: pay_result(sourceOrderNo,payStatus,details)
C->>C: 校验 payStatus == 02
C->>O: 查询单据和允许支付状态
C->>F: 写付款主明细
C->>A: 写账户/资金事实
C->>O: 推进支付与采购状态
C-->>P: ACK
高风险点:
- 失败状态当前返回 NACK,需确认支付中心是否会持续重投以及是否符合协议。
- 采购允许从待支付和支付超时状态接收迟到成功,预订单允许状态集合不同。
- 重复消息依赖订单状态拦截,但数据库唯一键仍需线上确认。
- 外部金额除以 100,新增字段必须明确单位。
- 回调中
sleep(1)处理零元单,可能影响消费者吞吐。
15.3 ACK/NACK 决策
flowchart TD
A["收到消息"] --> B{"事件可识别?"}
B -->|否| C["记录未知事件并按协议处理"]
B -->|是| D{"业务键存在?"}
D -->|否| E["NACK 或进入人工队列"]
D -->|是| F{"已达到目标状态?"}
F -->|是| G["幂等 ACK"]
F -->|否| H{"当前状态允许迁移?"}
H -->|否| I["记录乱序/非法迁移"]
H -->|是| J["事务写入"]
J --> K{"提交成功?"}
K -->|是| G
K -->|否| L["NACK 重试"]
“目标状态已完成”应返回幂等 ACK,还是 NACK,必须逐事件按消费者契约确认。无限 NACK 一个不可恢复的历史脏消息会阻塞队列。
16. FlashSaleSer.php:秒杀域共享核心
文件:application/Services/Activity/FlashSaleSer.php,约 5 千行。
16.1 责任范围
- 服务站访问与黑白名单。
- 商品列表、详情、套包与筛选。
- 购物车新增、更新、失效清理。
- 订单预览、提交、详情和取消。
- 锁定量
locked_qty、使用量used_qty的转移与释放。 - 秒杀订单转
30-Cxx-77采购单,写srcOrderType=13来源关系。 - 支付成功、采购关闭、订单超时和补偿任务。
16.2 数量状态
stateDiagram-v2
[*] --> Available: 活动可售
Available --> Locked: 提交订单锁量
Locked --> Used: 支付成功/采购成立
Locked --> Available: 取消或超时释放
Used --> Available: 合法关闭/退款补偿
Used --> Used: 重复回调幂等
修改公共采购关闭、支付回调或库存释放时,必须确认是否会触发:
releaseByPoOrder()或相似释放逻辑。- 同一
srcOrderId是否已经释放。 - 部分关闭按行释放还是整单释放。
- 普通采购是否会误进入秒杀补偿。
16.3 采购来源关系
erDiagram
FLASH_ORDER ||--o{ FLASH_ORDER_DETAIL : contains
FLASH_ORDER ||--o{ SCM_PO_ORDER : creates
SCM_PO_ORDER ||--o{ SCM_PO_ORDER_INFO : contains
FLASH_ORDER {
bigint id
string order_no
int order_status
}
SCM_PO_ORDER {
bigint id
string billNo
int srcOrderType
bigint srcOrderId
string srcOrderNo
string orderType
}
17. BaseModel.php 与分表模型
文件:application/models/BaseModel.php。
17.1 公共行为
| 方法 | 行为 | 高风险点 |
|---|---|---|
getItemList | 统一查询、分组、排序、分页或 count | group + count 语义,offset/limit 顺序 |
addItem | 单行或批量写入,可选旧表双写 | 双写第一张成功、第二张失败 |
updateItem | 更新新表,可选旧表双写 | 返回 bool,丢失影响行数 |
createOrUpdateItem | 先查后新增/更新 | 并发下不是原子 upsert |
increaseColumn/decreaseColumn | 数据库表达式加减 | 没有返回影响行数和下限保护 |
17.2 分片上下文
分片表必须在访问前确定 sid。常见公式包括采购明细 %32、销售明细 %16、销售出库 %64、库存流水 %128、盘点 %10。具体表以 Model 的 setSid()/getTableName() 为准。
flowchart LR
A["请求 sid"] --> B["Model.setSid(sid)"]
B --> C["计算 shard"]
C --> D["设置物理表名"]
D --> E["执行 SQL"]
危险场景:
- 先查询再
setSid(),读到上一次实例残留的表名。 - Web 下
BaseSer::service()返回进程内单例,Service/Model 持有可变 sid。 - CLI 下每次
service()新建实例,行为与常驻 Web 不完全一致。 - 批量处理多个 sid 时只设置一次分片。
- 用业务号猜分片,而真正分片键是
sid。
18. BaseProvider.php:外部 HTTP 公共行为
文件:application/Providers/BaseProvider.php。
18.1 请求生命周期
flowchart LR
A["Provider 子类"] --> B["选择 form/json/body/query/multipart"]
B --> C["同步或异步 Guzzle 请求"]
C --> D{"HTTP 成功?"}
D -->|否| E["超时/Provider 异常"]
D -->|是| F{"sheet 下载?"}
F -->|是| G["输出文件并 die"]
F -->|否| H["XML 优先解析,否则 JSON"]
H --> I["返回数组或 null"]
18.2 公共风险
| 行为 | 当前代码事实 | 修改风险 |
|---|---|---|
| 同步超时 | 默认方法参数 10 秒 | 缩短会放大失败,拉长会占满 PHP worker |
| 异步超时 | 默认 2 秒 | 批量库存查询可能出现部分空结果 |
| 请求日志 | 记录 URL、headers、data | 必须避免 token、手机号、支付数据明文 |
| 结果解析 | 先尝试 XML,再 JSON decode | HTTP 200 的 HTML/空正文可能返回 null |
| 异步失败 | callback 收到空数组 | 业务可能误判为零数据 |
| 文件下载 | 直接输出流并终止 | 不能在 API JSON 入口复用 |
18.3 Provider 回归
- JSON、form、query、body 和 multipart 各选一个现有调用。
- HTTP 200 业务失败码、HTTP 500、超时、无效 JSON、XML。
- 异步三个批次:全部成功、一个失败、全部失败。
- 下载响应的 Content-Disposition 和大文件内存。
- 日志脱敏,不输出 Authorization、Cookie 和敏感请求体。
19. 公共入口基类
19.1 BaseController.php
公共责任包括登录态、总部账号上下文、请求数据、RequestId、权限/会话和统一页面行为。修改登录判断会同时影响普通账号和总部账号。
19.2 BaseAppController.php / BaseService.php
OpenAPI/老 API 基类通常负责签名、请求锁、统一错误和调用日志。代码中请求锁使用 Redis NX EX 600:
KEY_REQUEST_ID_LOCK + requestId
重复请求是否释放锁取决于 isCanReleaseLock。修改锁生命周期时要回答:
- 请求成功后是否立即释放,还是需要十分钟业务幂等窗口。
- 超时后客户端重试是否会被误拒绝。
- 不同业务但相同 requestId 是否共享命名空间。
- 锁只防并发,数据库是否仍有业务唯一键。
19.3 BaseSer.php
service() 在 CLI 中每次返回新实例,在非 CLI 中按类名缓存实例。任何 Service 内的可变属性都可能跨同一请求中的多次调用保留。
flowchart TD
A{"is_cli()?"} -->|是| B["new static 每次新实例"]
A -->|否| C{"类实例已缓存?"}
C -->|否| D["创建并缓存"]
C -->|是| E["返回已有实例"]
不要在公共 Service 单例上长期保存 sid、临时筛选条件或上一次调用结果,除非每次入口都会显式覆盖并清理。
20. 配置、枚举和路由
20.1 tables.php
表常量是 Model 与手写 SQL 的公共名字来源。改常量前全局搜索常量名和物理表名,因为旧代码可能直接写字符串。
20.2 PoOrderEnums.php / SaOrderEnums.php / TransTypeEnums.php
枚举会影响:
- 状态机允许迁移。
- 列表筛选和按钮。
- 库存正负方向和报表。
- 外部订单类型映射。
- MQ 分支和回调识别。
改“值”比改“文案”危险得多。历史数据仍保存旧值时,删除常量会让老单无法展示或处理。
20.3 routes.php / appapis.php / apis.php
路由和 API 映射变化会影响入口是否能到达、鉴权基类、参数上下文和响应格式。新增入口必须确认它走 PC、App 网关、OpenAPI 还是 inner 边界,不能只复制 Controller 方法。
21. 公共视图 kzuilite.php
文件:application/views_v2/common/kzuilite.php 当前加载公共 kzuiadmin.lite_v2 资源版本。直接或间接引用页面包括销售订单、配送列表、报价商品搜索、商品管理、门店设置和调拨申请等。
flowchart TD
K["kzuilite.php"] --> B1["new_base 系列布局"]
K --> P1["销售订单"]
K --> P2["配送列表"]
K --> P3["报价搜索"]
K --> P4["商品管理"]
K --> P5["门店设置"]
K --> P6["调拨申请"]
修改资源 URL 或版本时至少验证:
- 页面首屏无 JS 404、语法错误和组件未定义。
- 表格分页、弹窗、选择器、日期组件和提交按钮。
- 老缓存与新 HTML 混用时是否兼容。
- 页面是否通过公共 layout 间接加载,避免重复加载两次。
22. 典型跨切面风险
22.1 先写库还是先发消息
| 顺序 | 失败结果 | 推荐控制 |
|---|---|---|
| 先 MQ 后 commit | 消费者可能读不到数据;本地回滚但下游成功 | 事务后发送、Outbox 或消费者重试 |
| 先 commit 后 MQ | 发布失败导致下游缺事件 | 补偿扫描、Outbox、可重建消息 |
| 事务内 HTTP | 锁持有时间长,外部成功不可回滚 | 稳定幂等键、缩短事务、状态编排 |
| Redis 锁代替唯一键 | 过期/进程异常/不同 key 可绕过 | 数据库业务唯一约束兜底 |
22.2 状态判断与副作用分离
安全顺序:
- 根据稳定业务键查本地事实。
- 判断是否已经达到目标状态。
- 校验当前状态是否允许迁移。
- 原子占用或加版本条件更新。
- 写业务主事实和明细。
- 同一事务提交。
- 发送可重试的下游事件。
- 用业务键验证最终收敛。
22.3 不能用兜底掩盖脏数据
危险例子:
- 找不到来源单时改查“最近一张同 SKU 单据”。
- 库存中心失败时把空数组当作零库存并缓存。
- 状态未知时默认映射成“正常”。
- 分表查不到时遍历所有分片并取第一条。
- 支付账户不匹配时默认使用第一个账户。
兜底必须区分“可预期兼容”和“数据关系已损坏”,后者应报警并阻止继续写入。
23. 改动前调用方扫描
23.1 类名与方法
cd /Users/zhoujiangbin/code/docker-dev-env/www/dgj2.0
rg -n "InvPoService|InvSaService|PoOrderSer|SaOrderSer" application
rg -n "InventorySer|KzInventorySer|InvPoFactory|FlashSaleSer" application
rg -n "MqSer|sendInventoryEvent|sendPoCloseOrderConfirm|sendSaOrderOutToMq" application
23.2 状态、来源与交易类型
rg -n "SRC_ORDER_TYPE_FLASH_ACTIVITY|ORDERTYPE_FLASH_ORDER|30-Cxx-77" application
rg -n "BILLSTATUS_|SA_STATUS_|OUT_BILLSTATUS_|ORDER_STATUS_" application
rg -n "TRANSTYPE_|150501|150601|170401|170402" application
23.3 SQL 与分表
rg -n "mix_where|group_by|count_all_results|JOIN|UNION" application/service/scm application/Services
rg -n "setSid\(|% ?(10|16|32|64|128)|getTable(Name)?\(" application/models application/Services
rg -n "updateItem\(|affected_rows\(|createOrUpdateItem\(" application
23.4 回调与外部副作用
rg -n "registryCallback|MqResultEnums::ACK|MqResultEnums::NACK" application/controllers/tasks
rg -n "trans_begin|trans_commit|trans_rollback" application/service application/Services
rg -n "Provider\(|requestSer|requestSerAsync|send.*Mq" application/service application/Services
24. 方法级影响面记录模板
每次修改公共方法,在评审描述中填写:
### 公共方法影响面
- 文件/方法:
- 改动分类:业务域内隔离 / 共享兼容 / 共享行为改变 / 风险不清
- 入口调用方:PC / App / Inner / OpenAPI / MQ / CLI
- 来源守卫:字段、值、持久化表
- 读取表:
- 写入表:
- 分片公式:
- 状态迁移:
- 库存/资金副作用:
- Redis/MQ/HTTP 副作用:
- 事务开始与提交位置:
- 重复请求行为:
- 失败补偿:
- 普通链路回归:
- 特殊来源回归:
- 线上待确认:
25. 最小回归矩阵
| 修改文件 | 必须回归 | 附加回归 |
|---|---|---|
InvPoService | 采购列表/详情、创建、入库、关闭、退货、支付 | 预订单、活动、秒杀、PDA、订单中心回调 |
InvPoFactory | 普通采购创建、订单中心同步、在线/线下支付 | 首配、赠品、专属授信、秒杀转采购 |
PoOrderSer | 采购创建、购物车、待支付取消 | 销售未发货联动、MOQ、自提 |
InvSaService | 销售创建、部分/整单出库、退货、对账 | 配送、App、SAAS、报价和打印 |
SaOrderSer | App/SAAS 创建、出库、退货、支付 | PC 共享表查询、采购联动 |
InventorySer | 采购入、销售出、采退、销退、盘盈亏、货位调整 | 负库存四分支、编辑反向、ES 销量更新 |
KzInventorySer | 1/50/51 SKU、同步/异步、零库存 | 指定仓、快速库存、单批失败 |
MqSer | 修改事件的生产和消费闭环 | 发布失败、重复发送、补偿重发 |
| 支付/订单/配送/OA 回调 | 正常、重复、乱序、非法状态、业务不存在 | 消费者重启、NACK 重投、死信处理 |
BaseModel | 单行/批量 CRUD、分页、count、分组 | 旧表双写、影响行数、并发 upsert |
BaseProvider | 同步/异步、JSON、超时、业务失败 | XML、下载、日志脱敏 |
| 枚举/路由/表常量 | 所有引用点、历史值、入口可达性 | 报表、按钮、MQ 和前端缓存 |
kzuilite.php | 所有直接引用页面冒烟 | 公共 layout 间接引用、缓存混用 |
26. 症状导向排查
26.1 修秒杀后普通采购支付失败
- 查
srcOrderType守卫是否只包住秒杀分支。 - 查是否误用
orderType=30-Cxx-77替代来源。 - 对比
getPayInfoNew()、noPagePay()等所有支付入口。 - 用普通采购和秒杀采购分别输出过滤前后的支付方式。
- 查支付回调是否仍能识别普通采购状态。
26.2 同一发货单出现两张 CG 入库单
- 以
sid + srcOrderNo + srcOutNo + srcOrderEntryId + skuId查询入库明细。 - 对比每张 CG 的创建时间和操作人。
- 查发货明细的处理状态更新时间。
- 查请求日志是否同秒、同 RequestId 或来自不同入口。
- 核对是否存在原子占用和唯一索引;没有时按第 7.5 节设计修复。
- 修复业务表后继续核对库存流水、实时库存和报表层。
26.3 MQ 显示发送成功但下游没有数据
- 查 destination 和 routing key。
- 查消费者是否注册该事件。
- 查消息体业务键和字段类型。
- 查发送发生时本地事务是否已提交。
- 查下游幂等是否把消息当作重复丢弃。
- 查重试、死信和补偿任务,禁止盲目重复发送资金/库存事件。
26.4 列表数量和导出数量不同
- 比较列表 SQL、count SQL 和导出 SQL。
- 检查一对多 JOIN 是否重复主记录。
- 检查分页前后是否才执行分组。
- 检查分片和
sid条件。 - 检查新状态/来源是否只在其中一个入口过滤。
26.5 库存流水正确但实时库存错误
- 按
sid+invId+locationId+locationAreaId汇总有效流水。 - 对比实时库存同维度行,不要只按 SKU 汇总。
- 查是否发生编辑单据的
delete()+save()部分成功。 - 查负库存配置和商品归属分支。
- 查历史归档流水是否未纳入核对口径。
27. 只读 SQL 模板
27.1 采购单与来源
SELECT id, sid, billNo, billStatus, orderType,
srcOrderType, srcOrderId, srcOrderNo,
paymentType, paymentNumber, createTime, modifyTime
FROM t_scm_po_order
WHERE sid = :sid AND billNo = :bill_no AND isDelete = 0;
27.2 库存过程账
SELECT iid, billNo, billType, transType,
invId, skuId, locationId, locationAreaId,
qty, amount, createTime
FROM t_scm_inventory_info_{sid_mod_128}
WHERE sid = :sid
AND billNo = :bill_no
ORDER BY createTime, id;
27.3 实时库存
SELECT sid, inv_id, sku_id, location_id, location_area_id,
area_type, qty, modify_time
FROM t_scm_inventory_real_time
WHERE sid = :sid AND inv_id = :inv_id
ORDER BY location_id, location_area_id;
27.4 支付回调落账
SELECT p.id, p.sid, p.billNo, p.billType, p.srcOrderNo,
p.totalAmount, p.createTime
FROM t_scm_payment_{payment_shard} p
WHERE p.sid = :sid AND p.srcOrderNo = :order_no
ORDER BY p.createTime, p.id;
物理表名和字段以目标环境 DDL 为准;生产只读查询应先限制 sid、业务号和时间范围。
28. 发布前门禁
28.1 开发自检
- [ ] 已给改动分类并说明来源守卫。
- [ ] 已列出所有直接和间接调用方。
- [ ] 已列出读取表、写入表和分片公式。
- [ ] 已画出事务边界和事务外副作用。
- [ ] 已证明重复请求、并发请求和乱序回调的结果。
- [ ] 已保留普通老链路行为。
- [ ] 已检查日志不包含密钥、完整 token、客户敏感信息。
28.2 代码评审
- [ ] 新增
OR有括号,查询与 count 条件一致。 - [ ] 状态迁移只允许合法源状态。
- [ ] 批量条件更新检查了整数影响行数。
- [ ] Redis 锁不是唯一幂等手段。
- [ ] MQ/HTTP 失败行为和补偿责任明确。
- [ ] 枚举值没有破坏历史数据。
- [ ] 特殊来源未渗透普通入口。
28.3 测试证据
- [ ] 正常流程。
- [ ] 边界数量:0、1、最大值、部分处理。
- [ ] 相同请求重复两次。
- [ ] 两个并发请求。
- [ ] 外部超时、业务失败和空响应。
- [ ] MQ 重投与非法顺序。
- [ ] 数据库主单、明细、库存/资金流水和状态一致。
- [ ] 列表、详情、导出和报表口径未出现意外变化。
29. 安全修改模式
29.1 来源守卫包住最小代码块
$isFlash = (int)($order['srcOrderType'] ?? 0)
=== PoOrderEnums::SRC_ORDER_TYPE_FLASH_ACTIVITY;
if ($isFlash) {
$payInfo = $this->filterFlashActivityPayInfo($payInfo, $subAccountId);
}
不要把整个普通支付流程复制成秒杀分支,避免两份逻辑长期漂移。
29.2 条件更新推进状态
UPDATE business_order
SET status = :target_status, modify_time = NOW()
WHERE id = :id
AND status IN (:allowed_source_statuses);
只有影响行数为 1 才执行后续副作用;影响 0 行时重新读取,区分“已完成幂等”与“非法状态”。
29.3 用稳定业务键补偿
补偿任务输入应优先是 sid + billNo/orderNo + eventType,而不是数据库自增 ID 或一次请求生成的随机消息 ID。补偿前重新查询最终业务事实,再决定是否发送。
30. 当前代码已证明与待确认
30.1 静态代码已证明
InventorySer::save()先更新实时库存,最后批量写分片流水。- 旧库存流水主表写入已注释,当前写
InventorySubModel。 InventorySer::delete()会先反向实时库存再删除流水。KzInventorySer每 50 个 SKU 分批异步查询。- 新旧库存格式对
allowQty<=0明细的保留规则不同。 InvPoFactory::saveOrder()在 MySQL 事务内调用订单中心。- 秒杀来源常量为
srcOrderType=13,订单类型为30-Cxx-77。 BaseModel::updateItem()因bool返回类型不能保留批量影响行数。BaseProvider异步失败会向 callback 传空数组。PayCenterNotify注册 10 类支付事件,支付成功状态值为字符串02。
30.2 仍需环境验证
| 待确认项 | 为什么静态代码不能证明 | 验证方式 |
|---|---|---|
| 采购入库业务唯一索引 | 仓库中没有目标环境完整 DDL | SHOW CREATE TABLE 与索引检查 |
| MQ binding、重试、死信和 prefetch | 运行时 RabbitMQ 配置决定 | 管理台/部署配置和实际队列 |
| 订单中心幂等键 | 对方服务实现不在本仓库 | 接口契约 + 重复请求联调 |
| 支付中心重试协议 | ACK/NACK 后行为由对方和 Broker 决定 | 消费配置与联调记录 |
| 各公共方法线上真实流量 | 静态引用不能证明运行频率 | 日志/APM 按入口统计 |
| 历史双写表是否仍有消费者 | 注释和迁移代码不能证明外部读取 | 数据平台/报表 Owner 核对 |
| 公共 JS 资源向后兼容 | CDN 资源实现不在仓库 | 多页面浏览器回归 |
31. 代码证据
application/service/scm/InvPoService.phpapplication/Services/InvPoFactory.phpapplication/Services/PoOrders/PoOrderSer.phpapplication/service/scm/InvSaService.phpapplication/Services/SaOrders/SaOrderSer.phpapplication/Services/Storage/InventorySer.phpapplication/Services/KzInventory/KzInventorySer.phpapplication/Services/Mq/MqSer.phpapplication/Services/Activity/FlashSaleSer.phpapplication/controllers/tasks/PayCenterNotify.phpapplication/controllers/tasks/OrderCenterNotify.phpapplication/controllers/tasks/DispatchCenterNotify.phpapplication/controllers/tasks/OaNotify.phpapplication/controllers/tasks/OaResultNotify.phpapplication/models/BaseModel.phpapplication/Providers/BaseProvider.phpapplication/core/BaseController.phpapplication/core/BaseAppController.phpapplication/core/BaseService.phpapplication/Services/BaseSer.phpapplication/config/tables.phpapplication/config/routes.phpapplication/config/appapis.phpapplication/config/apis.phpapplication/KzData/Enums/PoOrderEnums.phpapplication/KzData/Enums/SaOrderEnums.phpapplication/KzData/Enums/TransTypeEnums.phpapplication/KzData/Enums/MqEventEnums.phpapplication/views_v2/common/kzuilite.php
32. 最终判断标准
一项公共文件修改只有同时满足以下条件,才算影响面已收敛:
- 能从入口追到方法、表、状态、库存/资金和外部副作用。
- 能明确普通来源和特殊来源的守卫边界。
- 能解释重复、并发、乱序、超时和部分成功后的最终结果。
- 能用 SQL、日志和消息业务键验证,而不是只看页面提示成功。
- 已执行与风险等级匹配的回归,不以单个接口成功代替全链路成功。
- 所有静态代码不能证明的环境事实都被单独列出,没有伪装成确定结论。
公共文件真正的风险不在“文件很大”,而在它把多个入口、多个数据事实和多个外部系统连接在一起。修改时先画清边界,再写代码,通常比上线后从库存或资金差异反推原因便宜得多。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| 采购公共链 | PC/App/MQ/秒杀等 | 订单类型、来源、状态、数量 | 多入口 | InvPoService/PoOrderSer/Factory | 采购主明细 | 所有采购类型共用部分逻辑 |
| 销售公共链 | PC/E站/PDA/渠道 | 来源类型、销售/出库参数 | 多入口 | InvSaService/SaOrderSer | 销售/出库单 | 多来源共用销售履约 |
| 库存公共链 | 所有出入库 | 四维键、数量、transType | 领域 Service | InventorySer | 库存流水/实时 | 任一改动影响所有库存方向 |
| MQ/Provider/BaseModel | 同步、外部调用、所有 Model | message/request/table/query | Service/Task | MqSer/BaseProvider/BaseModel | 全局基础设施边界 | 序列化、网络、数据库行为跨模块 |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 调用面盘点 | rg/静态调用图 | 公共类/方法、调用文件、入口类型 | 调用方清单覆盖 PC/App/MQ/task | 只看当前需求入口 | 调用方法映射业务类型/来源 | | 回归请求 | 各入口日志 | request_id、来源类型、业务单号 | 每个代表链路行为不变 | 非目标来源也进入新分支 | 单号回查对应领域表 | | 数据副作用 | Service/DB/MQ 日志 | 状态、transType、影响行数、routing key | 目标分支变更,旧分支等价 | 数量方向、事务、消息字段回归 | 主明细/流水/消费者对账 | | 外部兼容 | Provider/Consumer | 外部 request/message ID、版本/字段 | 新旧 payload 合同兼容 | 缺字段、类型变更、重复副作用 | 两端 ID + 业务单号 |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| 识别调用方 | rg -n "Class|method" application | 事务外 | 静态引用、动态 Factory/路由 | 无 | 零业务变化 | 调用方矩阵、入口/来源/类型 |
| 收窄条件 | 公共 Service 分支 | 业务事务 | source/type/status/feature flag | 无或目标业务表 | 只有目标条件 old -> new;其他分支等价 | 分支单测/代表请求 |
| 核心写入 | 公共 Service/Model | 现有事务边界 | 状态、库存/资金当前值 | 主明细、流水 | 影响行数和数量方向与旧合同一致 | SQL 前后快照、数量等式 |
| 异步副作用 | MqSer/Provider | commit 后 | 新业务事实 | MQ/外部 | payload 兼容;业务键不变 | 消息快照、消费者 ACK |
| 全面回归 | 采购/销售/退货/活动/任务入口 | 多链路 | 代表源类型和边界状态 | 测试数据 | 非目标路径 0 意外差异 | 日志、表、库存资金、报表四层证据 |
子模块追踪:risk-invpo-service InvPoService 采购公共链
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 公共动作 | PC/App/WMS 采购保存、入库、关闭、退货 | sid、billNo、order/source type、entryId | application/service/scm/InvPoService.php | 状态、订购/发/收/关/退量、支付和库存维度 | 现有本地事务内主明细/数量/库存 old -> new | request ID + billNo/entryId + type/transType | 改动必须用来源/类型/状态窄守卫;失败按原单事实补偿 |
| 影响回归 | 普通、秒杀、赠品、预订单、退货链 | representative billNos | application/KzData/Enums/PoOrderEnums.php | 各来源分支和共享 SQL 调用方 | 测试查询只读;非目标分支结果与改前等价 | before/after request + SQL/MQ snapshots | 最少回归创建、审核、支付、发货、入库、关闭、退货 |
子模块追踪:risk-invpo-factory InvPoFactory 采购事务编排
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 策略创建 | 不同来源调用采购 Factory | source/type、sid、sourceOrderNo | application/Services/InvPoFactory.php | 动态策略类、参数映射、来源唯一性和黑名单 | Factory 管理本地采购事务,主明细/关系 none -> created | request ID + strategy + source/po billNo | 未知策略零写入;外部字段缺失不得降级到普通采购 |
| 回滚验证 | saveOrder 中途异常 | sourceOrderNo、transaction ID | application/Services/InvPoFactory.php | 主、明细、地址、关系和后置 MQ 时点 | DB 异常整事务回滚;MQ 必须在 commit 后 | transaction/request ID + row counts + publish result | 若 MQ 失败只补消息;不得重做已提交采购单 |
子模块追踪:risk-poorder-ser PoOrderSer 新采购领域链
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 领域动作 | 新采购保存、提交、状态变化 | po billNo、source/type/status | application/Services/PoOrders/PoOrderSer.php | 新老模型关系、枚举、金额数量和当前态 | 领域本地事务 status/qty/amount old -> new | request ID + billNo + method/status | 修改前扫描所有调用方;非法迁移零写入 |
| 兼容回归 | 老 InvPo 与新服务汇合 | same source/billNo samples | application/service/scm/InvPoService.php | 双路径是否共享表、事件和幂等键 | 对比查询只读;相同业务输入最终事实一致 | both entry logs + DB/MQ snapshots | 责任迁移未确认时不删旧分支;重复调用不得双写 |
子模块追踪:risk-invsa-service InvSaService 销售公共链
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 公共销售 | PC/App/PDA/OpenAPI 销售、出库、核销 | sid、sale/invoice billNo、source/type | application/service/scm/InvSaService.php | 订单、有效出库、销退、库存、应收和状态 | 现有本地事务 qty/status/amount old -> new | request ID + sale/invoice billNos + transType | 公共 SQL 改动检查软删/分片/来源;失败按领域反向补偿 |
| 影响回归 | 创建、出库、撤销、退货、收款、同步 | representative source orders | application/KzData/Enums/SaOrderEnums.php | 新旧来源和五条状态链 | 非目标链路查询/结果与改前等价 | before/after rows + inventory/fund/MQ | 必测库存、配送、资金、下游与报表,不能只测接口返回 |
子模块追踪:risk-saorder-ser SaOrderSer 新销售领域链
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 新销售动作 | 新入口创建/更新销售单 | sourceOrderNo、sale billNo、status | application/Services/SaOrders/SaOrderSer.php | 来源唯一性、客户商品、金额和旧链事实 | 新销售本地事务主明细 none/old -> created/new | request ID + source/sale billNo + method | 来源守卫只包目标分支;异常整单回滚 |
| 新旧对照 | 新服务与 InvSaService 交汇 | same sample/input | application/service/scm/InvSaService.php | 共享表、分片、状态和同步事件 | 对照查询只读;最终事实和 payload 合同兼容 | both path logs + DB/MQ diff | 未证明无调用前不删旧代码;避免两服务重复建单 |
子模块追踪:risk-inventory-ser InventorySer 库存事实链
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 库存写入 | 所有出入库、退货、调拨、盘点 | business/inventory billNo、SKU、四维键、transType | application/Services/Storage/InventorySer.php | 当前实时量、负库存策略、已有业务流水 | 依赖外层本地事务:实时 qty old +/- n,再写主/分片流水 | request ID + billNos + SKU + transType + affected rows | 调整顺序/条件会影响全域;部分成功用反向单或幂等补偿 |
| 最小回归 | 采购入、销售出、两类退货、调拨、盘点 | representative business keys | application/config/tables.php | 流水、实时、归档、缓存预警和报表 | 对账查询只读;after=before+net flow | before/after inventory snapshots | 必测并发、负库存、删除/编辑和重复请求 |
子模块追踪:risk-kzinventory-ser KzInventorySer 供给库存链
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 供给查询 | 秒杀/商城等查询仓库供给 | sid、SKU/invId、warehouseCode、format version | application/Services/KzInventory/KzInventorySer.php | 本地参数、外部库存中心响应、在途/可售口径 | 查询事务外且业务 DB 不写;转换新旧格式 | request ID + SKU/warehouse + response code | timeout 不伪造库存;格式字段缺失显式失败/降级 |
| 兼容回归 | 新旧响应消费者 | same SKU/warehouse samples | application/Providers/InventoryCenter/KzInventoryProvider.php | 所有调用方字段读取、数量单位和空值语义 | 对照查询只读;目标业务可买结论与旧合同一致 | caller + provider + normalized snapshot | 必测单品/套包、指定仓、无库存和异常响应 |
子模块追踪:risk-mq-ser MqSer 消息生产链
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 构造发布 | 采购销售库存财务活动渠道事件 | destination、routing key、business key、payload version | application/Services/Mq/MqSer.php | 已提交业务快照、事件合同和目标 | 通常 commit 后事务外发布,核心 DB 不变 | billNo + destination/routing + message ID/result | 改公共字段需全消费者兼容;发布失败只补消息 |
| 生产回归 | Direct/Topic 和多事件域 | stable business samples | application/KzData/Enums/MqEventEnums.php | routing、序列化、必填字段和幂等键 | 测试发布不应重复业务写入 | payload snapshot + consumer ACK + business row | 随机 messageId 不作幂等键;验证未消费/重复/乱序 |
子模块追踪:risk-task-consumer 支付、订单、OA Consumer
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 消费回调 | Pay/ODC/OA/配送消息 | message ID、event、external/local key | application/controllers/tasks/PayCenterNotify.php、application/controllers/tasks/OrderCenterNotify.php、application/controllers/tasks/OaNotify.php | handler 注册、当前状态、外部最终态和幂等事实 | 单消息本地事务 old -> new;ACK/NACK 在 DB 事务外 | message ID + event + business key + ACK | 改 dispatch/异常策略会影响所有事件;未知/旧事件不回退 |
| 回归补偿 | 重复、迟到、异常、部分成功 | same business key, multiple messages | application/controllers/tasks/DispatchCenterNotify.php | 已有流水/关系和后置 MQ 状态 | 已完成重放新增副作用 0;只补缺失段 | all message IDs + affected rows | 必测 commit 后 ACK、抛异常、重投和死信/人工补偿 |
子模块追踪:risk-base-model BaseModel 数据访问链
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 公共访问 | 所有 Model 查询、写入、分表 | sid、table suffix、PK、where/order/limit | application/models/BaseModel.php | 分片上下文、软删、连接、事务和返回类型 | 调用方本地事务内 CRUD old -> new;查询只读 | request ID + model/table/sid + SQL/affected rows | 改默认过滤/返回会波及全域;禁止全表兜底掩盖缺 sid |
| 影响回归 | 16/32/64/128 等分片模型样本 | multiple sid residues + business keys | application/config/tables.php | 真实物理表、主从连接和分页排序 | 测试查询/写入命中正确分片,非目标表零变化 | sid + resolved table + before/after row | 必测软删、空结果、事务回滚、批量更新和边界 sid |
子模块追踪:risk-base-provider-view BaseProvider 与公共视图组件
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| Provider 公共链 | 所有外部 HTTP 调用 | provider、request ID、timeout、payload | application/Providers/BaseProvider.php | URL 配置、headers、序列化、响应合同和脱敏策略 | 外部调用事务外;业务 DB 通常不写 | request ID + provider + latency + HTTP/business code | 修改超时/成功判断/日志会影响所有系统;凭证不得入日志 |
| 视图公共链 | 页面加载公共 JS/权限/组件 | URI、user/sid、component version | application/views_v2/common/kzuilite.php | 菜单权限、公共请求封装和页面依赖 | 页面渲染查询只读 不写 | request ID + URI + JS/network error | 修改后回归主要 PC 页面、按钮鉴权、上传下载和错误提示;Provider/视图分别回滚 |