本文是 DGJ2.0 的“改公共代码前必读”手册。它回答的不是某一个业务怎么操作,而是:修改一个被多个业务共用的文件,会影响谁、改变哪些数据事实、需要防住哪些并发与兼容风险、最低必须回归什么。

代码根目录:/Users/zhoujiangbin/code/docker-dev-env/www/dgj2.0

1. 业务目标

  • 避免修复秒杀、采购、销售等单一入口时破坏普通老链路。
  • 把 Controller、Service、Model、MQ、回调、外部系统和页面资源的影响面连成一张图。
  • 明确“代码成功”不等于“业务成功”,识别数据库事务以外的远程调用和消息副作用。
  • 给开发、评审、测试和发布提供同一份风险语言与回归清单。
  • 让后续工程师不必重新全库搜索,也能知道从哪里开始核对。

2. 什么时候必须使用本文

出现以下任一情况,都应按本文执行影响面审计:

触发条件为什么危险最低动作
修改采购、销售公共 ServicePC、App、任务、回调和特殊来源可能复用同一方法方法调用方扫描 + 普通/特殊来源回归
修改库存增减或库存流水会直接改变可售库存和历史账正向、反向、重复请求和负库存分支回归
修改支付、OA、订单中心、配送回调消息可能重投,ACK/NACK 决定是否继续消费幂等、乱序、重复和失败重试回归
修改 BaseModel、分表规则或公共 SQL影响所有继承模型及历史数据查询分片、分页、总数、旧表兼容回归
修改 MqSer 或事件枚举生产者成功不代表消费者能够识别事件名、目标、消息体和消费者契约核对
修改 BaseProvider 或公共鉴权所有外部 HTTP 调用的超时、日志和解析都会变化同步、异步、超时、非 JSON、下载回归
修改公共视图或资源版本多个页面会同时加载同一 JS/CSS引用页面冒烟 + 缓存和版本核对
新增枚举值、状态值或订单来源列表、按钮、报表和回调分支可能漏掉新值全局引用扫描 + 历史值兼容检查

3. 一页结论

当前静态代码扫描得到的引用文件数量如下。数量表示“包含该类名的 PHP 文件数”,用于衡量初始爆炸半径,不代表精确运行时调用次数。

公共类引用文件数主要风险建议等级
InventorySer78实时库存与流水、负库存控制、反向删除P0
MqSer72跨系统事件契约、重复发送、提交顺序P0
InvPoService31采购查询、关闭、退货、入库、支付P0
InvSaService30销售、出库、对账、退货、配送P0
PoOrderSer21新采购领域、购物车、待发货、支付状态P1
SaOrderSer16新销售领域、App/SAAS、出库和退款P1
KzInventorySer10外部供应库存、50 个 SKU 分批、异步结果P1
InvPoFactory7采购事务编排、订单中心同步、临时库存释放P0
FlashSaleSer7锁定量、使用量、采购来源与补偿P0(秒杀域)

最重要的五条规则:

  1. 特殊业务必须用来源字段隔离。 秒杀来源以 srcOrderType=13 识别;orderType=30-Cxx-77 是订单类型,不能互相替代。
  2. 查询条件的修改也是行为修改。 WHERE、JOIN、GROUP BY、总数 SQL 和分页 SQL 必须一起检查。
  3. 事务只保护同一个数据库连接。 HTTP、MQ、Redis、文件和第三方系统不会随 MySQL 回滚。
  4. 库存必须同时解释过程账与结果账。 流水记录为什么变,实时库存表示现在剩多少,两者缺一不可。
  5. 回调天然会重复、乱序和迟到。 状态判断必须是可重入的业务幂等,不应只依赖消息不会重投。
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.phpApp、活动、服务化后的领域能力默认新旧 Service 完全互斥
事务工厂InvPoFactory.php创建采购、订单中心同步、支付模式把远程调用误认为数据库事务的一部分
库存事实Storage/InventorySer.php实时库存和分片流水只查实时库存,不查业务流水
外部库存KzInventorySer.php供应库存查询和格式归并忽略批次部分失败和 allowQty 过滤差异
消息MqSer.php、MqEventEnums.php生产事件与路由契约只验证 publish 返回成功
回调controllers/tasks/*Notify.php跨系统状态回写假设消息只到一次且按顺序到达
数据访问BaseModel.php、各分片 Model查询、写入、双写和分片布尔返回值无法表达期望影响行数
HTTPProviders/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、updatePoStatusCG 入库单、库存流水、实时库存、采购状态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_orderid、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_invoicesrcOrderId、srcOrderNo是否出现重复 CG
采购入库明细t_scm_pu_invoice_info_{sid%32}srcOutNo、srcOrderEntryId、skuId入库数量是否超过发货数量
实时库存t_scm_inventory_real_timesid+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 与 count SQL 是否使用相同过滤条件。
  • 一对多 JOIN 是否导致主单重复、分页数量变小。
  • 新增 OR 是否被正确括号包围。
  • billStatus=0 到底指主单状态还是发货/入库明细状态。
  • sid 是否同时出现在主表和分表条件中。
  • 历史状态是否仍需出现在退货、关闭或报表入口。

7.8 秒杀来源守卫

字段秒杀值用途不可替代原因
srcOrderType13证明采购单来源于秒杀活动单用于来源关系和业务隔离
orderType30-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 参数与外部字段映射

本地字段订单中心字段意义风险
billNosourceOrderCode上游采购订单号应作为外部幂等候选键
orderTypesourceOrderType/orderLabel本地类型与外部标签秒杀类型被映射为普通标签 1
srcOrderNoreserveCode(预订单)来源预订单号仅特定来源有效
sidcustomerCode服务站上下文错站会污染外部数据
locationIdaddressCode的一部分收货仓仓库编码缺失会导致履约异常
srcOrderType=13multiWarehouseFlag秒杀多仓标识必须同时满足来源和扩展开关

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、handleTransportUpsertTMS/配送建单与单号回写
对账reconciliation、batchReconciliation、cancelReconciliation核销状态和收款关系
销售退货importSaleReturnGoods、退货参数转换退货单、反向库存、退款关系
SAAS/AppaddInvSaOr、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销售退货原单、原出库、退货数量
addPaymentForAppApp 销售收款出库单、支付单和账户
findNotShippedOrderInfos查询未发货明细采购和库存判断可能间接依赖
checkOrderOutStatusSAAS 订单出库状态外部订单号和本地状态

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、sendInStorageEventsid+iid+billType+transType
采购关闭sendPoCloseOrderConfirm采购单号/关闭单号
销售/AppsendSaOrderCloseToAPP、sendSaOrderOutToMq销售单号/出库单号
SAASsendInvoiceToSass、sendOutBoundToSaas外部订单号和本地单号
OAsendOAResult模板、流程实例、业务单
渠道sendChannelOrderManualCreate、sendChannelOrderDelivery渠道订单/售后单号
对账sendStatementBillCreate、sendStatementSend账单号、策略和服务站
商品/价格sendPriceChange、sendGoodsUpdateToVinSKU、服务站、变更版本

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事件数量/代表事件主要写入
PayCenterNotifyDEST_PAYCENTER_NOTIFY支付成功、账户调整、退款、白条、逾期等 10 类付款单、付款明细、账户、采购状态
OrderCenterNotifyDEST_ODC_NOTIFY订单审核/取消/关闭、售后审核/关闭/退款等采购、退货、退款与来源关系
DispatchCenterNotify配送中心 destination配送出库、物流、自提、退货关闭、标签变化出库、配送单号、采购/销售状态
OaNotifyDEST_OA_NOTIFYOA 审批结果活动、权限或业务审批状态
OaResultNotifyOA 结果同步 destinationOA 状态同步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统一查询、分组、排序、分页或 countgroup + 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 decodeHTTP 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 状态判断与副作用分离

安全顺序:

  1. 根据稳定业务键查本地事实。
  2. 判断是否已经达到目标状态。
  3. 校验当前状态是否允许迁移。
  4. 原子占用或加版本条件更新。
  5. 写业务主事实和明细。
  6. 同一事务提交。
  7. 发送可重试的下游事件。
  8. 用业务键验证最终收敛。

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、报价和打印
SaOrderSerApp/SAAS 创建、出库、退货、支付PC 共享表查询、采购联动
InventorySer采购入、销售出、采退、销退、盘盈亏、货位调整负库存四分支、编辑反向、ES 销量更新
KzInventorySer1/50/51 SKU、同步/异步、零库存指定仓、快速库存、单批失败
MqSer修改事件的生产和消费闭环发布失败、重复发送、补偿重发
支付/订单/配送/OA 回调正常、重复、乱序、非法状态、业务不存在消费者重启、NACK 重投、死信处理
BaseModel单行/批量 CRUD、分页、count、分组旧表双写、影响行数、并发 upsert
BaseProvider同步/异步、JSON、超时、业务失败XML、下载、日志脱敏
枚举/路由/表常量所有引用点、历史值、入口可达性报表、按钮、MQ 和前端缓存
kzuilite.php所有直接引用页面冒烟公共 layout 间接引用、缓存混用

26. 症状导向排查

26.1 修秒杀后普通采购支付失败

  1. 查 srcOrderType 守卫是否只包住秒杀分支。
  2. 查是否误用 orderType=30-Cxx-77 替代来源。
  3. 对比 getPayInfoNew()、noPagePay() 等所有支付入口。
  4. 用普通采购和秒杀采购分别输出过滤前后的支付方式。
  5. 查支付回调是否仍能识别普通采购状态。

26.2 同一发货单出现两张 CG 入库单

  1. 以 sid + srcOrderNo + srcOutNo + srcOrderEntryId + skuId 查询入库明细。
  2. 对比每张 CG 的创建时间和操作人。
  3. 查发货明细的处理状态更新时间。
  4. 查请求日志是否同秒、同 RequestId 或来自不同入口。
  5. 核对是否存在原子占用和唯一索引;没有时按第 7.5 节设计修复。
  6. 修复业务表后继续核对库存流水、实时库存和报表层。

26.3 MQ 显示发送成功但下游没有数据

  1. 查 destination 和 routing key。
  2. 查消费者是否注册该事件。
  3. 查消息体业务键和字段类型。
  4. 查发送发生时本地事务是否已提交。
  5. 查下游幂等是否把消息当作重复丢弃。
  6. 查重试、死信和补偿任务,禁止盲目重复发送资金/库存事件。

26.4 列表数量和导出数量不同

  1. 比较列表 SQL、count SQL 和导出 SQL。
  2. 检查一对多 JOIN 是否重复主记录。
  3. 检查分页前后是否才执行分组。
  4. 检查分片和 sid 条件。
  5. 检查新状态/来源是否只在其中一个入口过滤。

26.5 库存流水正确但实时库存错误

  1. 按 sid+invId+locationId+locationAreaId 汇总有效流水。
  2. 对比实时库存同维度行,不要只按 SKU 汇总。
  3. 查是否发生编辑单据的 delete()+save() 部分成功。
  4. 查负库存配置和商品归属分支。
  5. 查历史归档流水是否未纳入核对口径。

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 仍需环境验证

待确认项为什么静态代码不能证明验证方式
采购入库业务唯一索引仓库中没有目标环境完整 DDLSHOW CREATE TABLE 与索引检查
MQ binding、重试、死信和 prefetch运行时 RabbitMQ 配置决定管理台/部署配置和实际队列
订单中心幂等键对方服务实现不在本仓库接口契约 + 重复请求联调
支付中心重试协议ACK/NACK 后行为由对方和 Broker 决定消费配置与联调记录
各公共方法线上真实流量静态引用不能证明运行频率日志/APM 按入口统计
历史双写表是否仍有消费者注释和迁移代码不能证明外部读取数据平台/报表 Owner 核对
公共 JS 资源向后兼容CDN 资源实现不在仓库多页面浏览器回归

31. 代码证据

  • application/service/scm/InvPoService.php
  • application/Services/InvPoFactory.php
  • application/Services/PoOrders/PoOrderSer.php
  • application/service/scm/InvSaService.php
  • application/Services/SaOrders/SaOrderSer.php
  • application/Services/Storage/InventorySer.php
  • application/Services/KzInventory/KzInventorySer.php
  • application/Services/Mq/MqSer.php
  • application/Services/Activity/FlashSaleSer.php
  • application/controllers/tasks/PayCenterNotify.php
  • application/controllers/tasks/OrderCenterNotify.php
  • application/controllers/tasks/DispatchCenterNotify.php
  • application/controllers/tasks/OaNotify.php
  • application/controllers/tasks/OaResultNotify.php
  • application/models/BaseModel.php
  • application/Providers/BaseProvider.php
  • application/core/BaseController.php
  • application/core/BaseAppController.php
  • application/core/BaseService.php
  • application/Services/BaseSer.php
  • application/config/tables.php
  • application/config/routes.php
  • application/config/appapis.php
  • application/config/apis.php
  • application/KzData/Enums/PoOrderEnums.php
  • application/KzData/Enums/SaOrderEnums.php
  • application/KzData/Enums/TransTypeEnums.php
  • application/KzData/Enums/MqEventEnums.php
  • application/views_v2/common/kzuilite.php

32. 最终判断标准

一项公共文件修改只有同时满足以下条件,才算影响面已收敛:

  1. 能从入口追到方法、表、状态、库存/资金和外部副作用。
  2. 能明确普通来源和特殊来源的守卫边界。
  3. 能解释重复、并发、乱序、超时和部分成功后的最终结果。
  4. 能用 SQL、日志和消息业务键验证,而不是只看页面提示成功。
  5. 已执行与风险等级匹配的回归,不以单个接口成功代替全链路成功。
  6. 所有静态代码不能证明的环境事实都被单独列出,没有伪装成确定结论。

公共文件真正的风险不在“文件很大”,而在它把多个入口、多个数据事实和多个外部系统连接在一起。修改时先画清边界,再写代码,通常比上线后从库存或资金差异反推原因便宜得多。

请求-日志-数据变更追踪卡

多入口请求链路

场景调用方与入口请求载荷/上下文Controller/ConsumerService/Provider汇合点最终业务事实
采购公共链PC/App/MQ/秒杀等订单类型、来源、状态、数量多入口InvPoService/PoOrderSer/Factory采购主明细所有采购类型共用部分逻辑
销售公共链PC/E站/PDA/渠道来源类型、销售/出库参数多入口InvSaService/SaOrderSer销售/出库单多来源共用销售履约
库存公共链所有出入库四维键、数量、transType领域 ServiceInventorySer库存流水/实时任一改动影响所有库存方向
MQ/Provider/BaseModel同步、外部调用、所有 Modelmessage/request/table/queryService/TaskMqSer/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/Providercommit 后新业务事实MQ/外部payload 兼容;业务键不变消息快照、消费者 ACK
全面回归采购/销售/退货/活动/任务入口多链路代表源类型和边界状态测试数据非目标路径 0 意外差异日志、表、库存资金、报表四层证据

子模块追踪:risk-invpo-service InvPoService 采购公共链

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
公共动作PC/App/WMS 采购保存、入库、关闭、退货sid、billNo、order/source type、entryIdapplication/service/scm/InvPoService.php状态、订购/发/收/关/退量、支付和库存维度现有本地事务内主明细/数量/库存 old -> newrequest ID + billNo/entryId + type/transType改动必须用来源/类型/状态窄守卫;失败按原单事实补偿
影响回归普通、秒杀、赠品、预订单、退货链representative billNosapplication/KzData/Enums/PoOrderEnums.php各来源分支和共享 SQL 调用方测试查询只读;非目标分支结果与改前等价before/after request + SQL/MQ snapshots最少回归创建、审核、支付、发货、入库、关闭、退货

子模块追踪:risk-invpo-factory InvPoFactory 采购事务编排

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
策略创建不同来源调用采购 Factorysource/type、sid、sourceOrderNoapplication/Services/InvPoFactory.php动态策略类、参数映射、来源唯一性和黑名单Factory 管理本地采购事务,主明细/关系 none -> createdrequest ID + strategy + source/po billNo未知策略零写入;外部字段缺失不得降级到普通采购
回滚验证saveOrder 中途异常sourceOrderNo、transaction IDapplication/Services/InvPoFactory.php主、明细、地址、关系和后置 MQ 时点DB 异常整事务回滚;MQ 必须在 commit 后transaction/request ID + row counts + publish result若 MQ 失败只补消息;不得重做已提交采购单

子模块追踪:risk-poorder-ser PoOrderSer 新采购领域链

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
领域动作新采购保存、提交、状态变化po billNo、source/type/statusapplication/Services/PoOrders/PoOrderSer.php新老模型关系、枚举、金额数量和当前态领域本地事务 status/qty/amount old -> newrequest ID + billNo + method/status修改前扫描所有调用方;非法迁移零写入
兼容回归老 InvPo 与新服务汇合same source/billNo samplesapplication/service/scm/InvPoService.php双路径是否共享表、事件和幂等键对比查询只读;相同业务输入最终事实一致both entry logs + DB/MQ snapshots责任迁移未确认时不删旧分支;重复调用不得双写

子模块追踪:risk-invsa-service InvSaService 销售公共链

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
公共销售PC/App/PDA/OpenAPI 销售、出库、核销sid、sale/invoice billNo、source/typeapplication/service/scm/InvSaService.php订单、有效出库、销退、库存、应收和状态现有本地事务 qty/status/amount old -> newrequest ID + sale/invoice billNos + transType公共 SQL 改动检查软删/分片/来源;失败按领域反向补偿
影响回归创建、出库、撤销、退货、收款、同步representative source ordersapplication/KzData/Enums/SaOrderEnums.php新旧来源和五条状态链非目标链路查询/结果与改前等价before/after rows + inventory/fund/MQ必测库存、配送、资金、下游与报表,不能只测接口返回

子模块追踪:risk-saorder-ser SaOrderSer 新销售领域链

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
新销售动作新入口创建/更新销售单sourceOrderNo、sale billNo、statusapplication/Services/SaOrders/SaOrderSer.php来源唯一性、客户商品、金额和旧链事实新销售本地事务主明细 none/old -> created/newrequest ID + source/sale billNo + method来源守卫只包目标分支;异常整单回滚
新旧对照新服务与 InvSaService 交汇same sample/inputapplication/service/scm/InvSaService.php共享表、分片、状态和同步事件对照查询只读;最终事实和 payload 合同兼容both path logs + DB/MQ diff未证明无调用前不删旧代码;避免两服务重复建单

子模块追踪:risk-inventory-ser InventorySer 库存事实链

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
库存写入所有出入库、退货、调拨、盘点business/inventory billNo、SKU、四维键、transTypeapplication/Services/Storage/InventorySer.php当前实时量、负库存策略、已有业务流水依赖外层本地事务:实时 qty old +/- n,再写主/分片流水request ID + billNos + SKU + transType + affected rows调整顺序/条件会影响全域;部分成功用反向单或幂等补偿
最小回归采购入、销售出、两类退货、调拨、盘点representative business keysapplication/config/tables.php流水、实时、归档、缓存预警和报表对账查询只读;after=before+net flowbefore/after inventory snapshots必测并发、负库存、删除/编辑和重复请求

子模块追踪:risk-kzinventory-ser KzInventorySer 供给库存链

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
供给查询秒杀/商城等查询仓库供给sid、SKU/invId、warehouseCode、format versionapplication/Services/KzInventory/KzInventorySer.php本地参数、外部库存中心响应、在途/可售口径查询事务外且业务 DB 不写;转换新旧格式request ID + SKU/warehouse + response codetimeout 不伪造库存;格式字段缺失显式失败/降级
兼容回归新旧响应消费者same SKU/warehouse samplesapplication/Providers/InventoryCenter/KzInventoryProvider.php所有调用方字段读取、数量单位和空值语义对照查询只读;目标业务可买结论与旧合同一致caller + provider + normalized snapshot必测单品/套包、指定仓、无库存和异常响应

子模块追踪:risk-mq-ser MqSer 消息生产链

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
构造发布采购销售库存财务活动渠道事件destination、routing key、business key、payload versionapplication/Services/Mq/MqSer.php已提交业务快照、事件合同和目标通常 commit 后事务外发布,核心 DB 不变billNo + destination/routing + message ID/result改公共字段需全消费者兼容;发布失败只补消息
生产回归Direct/Topic 和多事件域stable business samplesapplication/KzData/Enums/MqEventEnums.phprouting、序列化、必填字段和幂等键测试发布不应重复业务写入payload snapshot + consumer ACK + business row随机 messageId 不作幂等键;验证未消费/重复/乱序

子模块追踪:risk-task-consumer 支付、订单、OA Consumer

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
消费回调Pay/ODC/OA/配送消息message ID、event、external/local keyapplication/controllers/tasks/PayCenterNotify.php、application/controllers/tasks/OrderCenterNotify.php、application/controllers/tasks/OaNotify.phphandler 注册、当前状态、外部最终态和幂等事实单消息本地事务 old -> new;ACK/NACK 在 DB 事务外message ID + event + business key + ACK改 dispatch/异常策略会影响所有事件;未知/旧事件不回退
回归补偿重复、迟到、异常、部分成功same business key, multiple messagesapplication/controllers/tasks/DispatchCenterNotify.php已有流水/关系和后置 MQ 状态已完成重放新增副作用 0;只补缺失段all message IDs + affected rows必测 commit 后 ACK、抛异常、重投和死信/人工补偿

子模块追踪:risk-base-model BaseModel 数据访问链

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
公共访问所有 Model 查询、写入、分表sid、table suffix、PK、where/order/limitapplication/models/BaseModel.php分片上下文、软删、连接、事务和返回类型调用方本地事务内 CRUD old -> new;查询只读request ID + model/table/sid + SQL/affected rows改默认过滤/返回会波及全域;禁止全表兜底掩盖缺 sid
影响回归16/32/64/128 等分片模型样本multiple sid residues + business keysapplication/config/tables.php真实物理表、主从连接和分页排序测试查询/写入命中正确分片,非目标表零变化sid + resolved table + before/after row必测软删、空结果、事务回滚、批量更新和边界 sid

子模块追踪:risk-base-provider-view BaseProvider 与公共视图组件

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
Provider 公共链所有外部 HTTP 调用provider、request ID、timeout、payloadapplication/Providers/BaseProvider.phpURL 配置、headers、序列化、响应合同和脱敏策略外部调用事务外;业务 DB 通常不写request ID + provider + latency + HTTP/business code修改超时/成功判断/日志会影响所有系统;凭证不得入日志
视图公共链页面加载公共 JS/权限/组件URI、user/sid、component versionapplication/views_v2/common/kzuilite.php菜单权限、公共请求封装和页面依赖页面渲染查询只读 不写request ID + URI + JS/network error修改后回归主要 PC 页面、按钮鉴权、上传下载和错误提示;Provider/视图分别回滚