本文从真实代码入口出发,说明 DGJ2.0 中 PDA、GPDA 和 PC 库存后台如何共同完成采购签收与入库、销售扫码出库、盘点、货位调整和出库验货。
这不是单纯的“移动端接口字典”。阅读后应能回答:
- PDA 请求如何完成登录、会话、设备和菜单权限校验。
- PDA 与 GPDA 的
pddNo、taskNo、盘点主键分别是什么。 - 扫码时写了什么,什么时候才真正改变库存。
- 盘盈盘亏如何变成库存流水和实时库存。
- 盘点期间发生正常出入库时,快照数量、当前数量和盘点数量如何解释。
- GPDA 的任务状态、SKU 数量和关闭通知如何经 MQ 同步。
- 采购入库、销售出库、货位调整和出库验货是否都会改变库存。
- 出现“扫码成功但库存没变”“GPDA 已完成但 DGJ 仍审核中”等现象时,按什么顺序排查。
结论先行:盘点扫码只保存盘点事实,不直接改库存;正常盘点必须由 PC 端审核通过后,才由 InvOiService::pdNewCheckUp() 写库存流水并修改实时库存。销售扫码出库和采购扫码入库则分别复用既有销售、采购库存服务,属于真实库存动作。出库验货只是事后确认,不会再次扣库存。
1. 业务范围与边界
1.1 本文覆盖的能力
| 能力 | 操作端 | 业务事实 | 是否直接改变库存 |
|---|---|---|---|
| PDA 登录与首页 | PDA | 会话、设备绑定、可见菜单 | 否 |
| 采购签收 | PDA | 箱号/发运单已签收 | 否 |
| 采购入库 | PDA | 采购入库单和入库流水 | 是 |
| 销售扫码出库 | PDA | 销售出库单和出库流水 | 是 |
| 盘点创建与扫码 | PDA/GPDA | 盘点快照、实际盘点数、差异 | 否 |
| 盘点提交 | PDA/GPDA | 状态进入审核中 | 否 |
| 盘点审核通过 | PC | 盘盈/盘亏库存流水、实时库存 | 是 |
| 盘点审核驳回 | PC | 状态回到审核不通过 | 否 |
| 货位调整 | PDA | 同仓库货位间库存转移 | 是,但仓库总量应不变 |
| 出库验货 | PDA | 已出库单的验货确认记录 | 否,不得重复扣库存 |
| GPDA 关闭任务 | MQ 消费者 | 盘点任务关闭 | 否 |
1.2 不属于本文的内容
- PC 端所有其他库存单据的完整说明,见《16_库存一致性排查手册》。
- 采购订单从下单到发货的完整流程,见《03_采购_采购退货与采购入库》和《14_采购完整排查手册》。
- 销售订单、配送和收款全链路,见《04_销售_出库_配送与收款》和《15_销售完整排查手册》。
- GPDA 外部任务平台的内部实现。当前仓库只能证明 DGJ 侧请求、数据和 MQ 契约。
2. 角色、标识与易混概念
2.1 参与者
| 参与者 | 职责 | 主要入口 |
|---|---|---|
| 服务站仓库人员 | PDA 登录、签收、入库、出库、盘点、移库、验货 | controllers/pda/* |
| GPDA/盘点任务平台 | 下发任务、调用 GPDA 接口、接收状态和 SKU 数量 | controllers/inner/gpda/*、MQ |
| 服务站/财务审核人员 | PC 审核盘点差异 | controllers/scm/InvOi.php |
| DGJ 库存服务 | 写库存流水和实时库存 | Services/Storage/InventorySer.php |
| MQ 消费者 | 关闭 GPDA 任务、延迟统计 SKU | controllers/tasks/GpdaNotify.php、SendSkuNumMsgToOps.php |
2.2 关键标识
| 名称 | 含义 | 来源/落点 | 排查时的用途 |
|---|---|---|---|
sid | 服务站 ID | 会话、GPDA customerCode、所有业务表 | 决定数据租户和分表 |
uid | 操作用户 ID | PDA 会话 | 鉴权和用户上下文 |
device | PDA 设备标识 | 登录请求、Redis Hash | 防止同账号跨设备复用 |
id / PDDId | DGJ 盘点主表主键 | t_scm_inventory_check.id | 查主明细、审核入账 |
billNo | DGJ 盘点单号 | 盘点主表 | 用户界面和 PC 业务单号 |
taskNo | GPDA 外部任务号 | 盘点主表 | GPDA 请求和 MQ 同步主键 |
GPDA 请求中的 pddNo | 实际上传的是 taskNo | GPDA 控制器构造阶段转换 | 最容易查错的字段 |
invId | 服务站商品库存档案 ID | 商品、盘点明细、库存表 | 比 skuId 更接近站内库存实体 |
skuId | 商品中心 SKU ID | 商品和实时库存 | 跨系统商品标识 |
locationId | 仓库 ID | 仓库、库存和单据 | 限定物理仓库 |
locationAreaId | 货位 ID | 货位、实时库存、盘点明细 | 最小库存空间维度 |
sysQty | 创建/扫描时记录的系统数量快照 | 盘点明细 | 计算原始盘点差异 |
checkQty | 人工实际盘点数量 | 盘点明细 | 盘点事实 |
2.3 pddNo 与 taskNo 的转换
GPDA 控制器的行为与字段名表面含义不同:除少数方法外,请求中的 pddNo 被当作外部 taskNo 查询主表,再注入内部 bill_id。
flowchart LR
A["GPDA 请求 pddNo"] --> B["按 taskNo 查询 t_scm_inventory_check"]
B -->|找到| C["注入 bill_id = 主表 id"]
B -->|找不到| D["返回 非法单号"]
C --> E["Service 按 bill_id 读写盘点明细"]
因此排查 GPDA 接口时应先按 taskNo 查主表,而不是先按 billNo 查。
3. 系统边界与代码地图
flowchart TB
PDA["PDA 客户端"] --> BAC["BaseAppController"]
BAC --> PC["controllers/pda"]
PC --> PS["Services/Pda"]
GPDA["GPDA/任务平台"] --> BGC["BaseGpdaController"]
BGC --> GC["controllers/inner/gpda"]
GC --> GS["Services/Gpda"]
PS --> DB[("业务表/盘点表")]
GS --> DB
AUDIT["PC 盘点审核"] --> IOS["InvOiService"]
IOS --> INV["Storage/InventorySer"]
INV --> LEDGER[("库存流水分表")]
INV --> RT[("实时库存")]
IOS --> MQ["RabbitMQ"]
GS --> MQ
MQ --> OPS["GPDA/OPS"]
3.1 控制器入口
| 模块 | 控制器 | 主要方法 |
|---|---|---|
| PDA 用户 | application/controllers/pda/User.php | login、home、banner |
| PDA 公共 | application/controllers/pda/Common.php | pdDescHtml、getVersion |
| PDA 采购 | application/controllers/pda/Purchase.php | 签收、入库、仓库和货位查询 |
| PDA 销售 | application/controllers/pda/Sale.php | 待出库列表、详情、货位库存、saleStockOut |
| PDA 盘点 | application/controllers/pda/TakeStock.php | 创建、扫码、统计、提交、盘前/提交前校验 |
| PDA 货位调整 | application/controllers/pda/AreaChange.php | 扫商品、扫货位、单笔/批量调整 |
| PDA 出库验货 | application/controllers/pda/OutStockCheck.php | 数量、列表、详情、扫码、确认 |
| GPDA 盘点 | application/controllers/inner/gpda/TakeStock.php | 任务转换、创建、扫码、状态、导出 |
| GPDA 门店 | application/controllers/inner/gpda/GpdaStore.php | getStoreBySsid |
| PC 盘点审核 | application/controllers/scm/InvOi.php | pdNewCheckUp |
| GPDA 任务关闭 | application/controllers/tasks/GpdaNotify.php | MQ 消费 |
| GPDA SKU 统计 | application/controllers/tasks/SendSkuNumMsgToOps.php | MQ 消费与转发 |
3.2 核心 Service
| Service | 责任 | 变更风险 |
|---|---|---|
Services/Pda/UserSer.php | 登录、设备绑定、首页权限 | 高:影响全部 PDA 入口 |
Services/Pda/PureSer.php | 采购签收和入库编排 | 高:会写采购入库和库存 |
Services/Pda/SaleSer.php | 销售查询和出库 | 高:复用销售出库公共链路 |
Services/Pda/TakeStockSer.php | PDA 盘点创建、扫码、状态 | 高:盘点事实源 |
Services/Gpda/TakeStockSer.php | GPDA 盘点、MQ 同步、导出 | 高:跨系统契约 |
Services/Pda/AreaChangeSer.php | 货位调整 | 高:同仓库存转移 |
Services/Pda/OutStockCheckSer.php | 已出库单验货确认 | 中:不应改库存 |
Services/InventoryCheck/InventoryCheckSer.php | 提交前库存和未完成单据校验 | 高:控制盘点一致性窗口 |
service/scm/InvOiService.php | PC 盘点审核、差异入账 | 极高:盘盈盘亏最终入口 |
Services/Storage/InventorySer.php | 库存流水和实时库存 | 极高:全系统共享公共服务 |
Services/Mq/MqSer.php | GPDA 状态/SKU、库存事件 | 高:异步一致性 |
4. 请求入口、鉴权与设备约束
4.1 PDA 请求包络
PDA 控制器继承 BaseAppController。公共层读取 JSON 请求、校验签名/登录/设备,并把会话上下文合并到业务参数中。
代表性请求仅用于说明字段层次,真实 URL 和签名字段以当前网关配置为准:
{
"reqCode": "pda",
"method": "addScanData",
"sessionId": "<会话标识>",
"device": "<设备标识>",
"data": {
"bill_id": 12345,
"scanType": "GOODS",
"scanData": []
}
}
公共层会补充:
{
"JXCSID": 10001,
"JXCUID": 20001,
"JXCUNAME": "当前操作人"
}
业务层不能信任客户端自行上传的 sid/uid 替代会话上下文。
4.2 PDA 登录和设备绑定
sequenceDiagram
participant D as PDA
participant C as User Controller
participant U as UserSer
participant A as UserFactory
participant S as Session
participant R as Redis
D->>C: login(account, password, device)
C->>U: login(params)
U->>A: LoginType::DGJ_PDA
A-->>U: uid/sid/name/username
U->>S: 写 jxcsys,过期时间 86400 秒
U->>R: HSET HASH_PDA_DEVICE_STATUS uid device
U-->>D: sessionId + 用户信息
| 校验点 | 代码行为 | 失败表现 |
|---|---|---|
| 账号登录 | UserFactory(LoginType::DGJ_PDA) | 登录失败或业务错误 |
| 会话 | sess_expiration = 86400 | 会话过期需重新登录 |
| 设备 | Redis Hash 按 uid 保存 device | 设备不一致被公共层拒绝 |
| 站点上下文 | Session jxcsys.sid | 查不到当前站数据 |
| 菜单权限 | PC 菜单权限映射到 PDA module | 首页功能缺失 |
同一账号在新设备登录会覆盖 Redis 中原设备值。旧设备后续请求可能被判定设备不一致,这是设计行为,不应先按接口故障处理。
4.3 PDA 首页权限
内置候选模块包括:APP_SALE、CHECK_PD、IN_STO、SIGN_IN、AREA_CHANGE,并合并数据库中 class='PDA' 的菜单。主账号取得全部候选模块,子账号与 PC 菜单权限取交集;SCAN 与 LOGIN_OUT 由代码补入。
flowchart TD
A["读取 uid/sid"] --> B{"是否主账号"}
B -->|是| C["全部 PDA 候选模块"]
B -->|否| D["用户 PC 菜单 module"]
D --> E["与 PDA 候选模块取交集"]
C --> F["查询首页菜单配置"]
E --> F
F --> G["补入扫一扫与登出"]
4.4 GPDA 请求边界
GPDA 控制器继承 BaseGpdaController,读取 JSON 和请求头,并记录 inner/gpdaApi 日志。静态代码中该基类没有像 BaseAppController 一样完成 PDA 会话和设备校验,因此:
- 不能据此断言 GPDA 无鉴权,鉴权可能位于网关、Nginx、上游签名或网络白名单。
- 生产排查必须确认实际网关路由、鉴权头、来源 IP 和重放保护。
- GPDA 日志会记录请求和响应,生产必须确认敏感字段脱敏及保留周期。
代表性请求:
{
"method": "gaddScanData",
"sid": 10001,
"pddNo": "外部任务号",
"scanType": "GOODS",
"scanData": []
}
5. 接口地图
5.1 PDA 盘点接口
| 方法 | 用途 | 核心输入 | 核心输出/副作用 |
|---|---|---|---|
getBillAddPage | 创建页基础数据 | 会话站点 | 仓库、用户等 |
getPddList | 盘点列表 | 状态、分页 | 主单列表 |
getPddInfo | 主单详情 | bill_id | 盘点类型、状态、仓库 |
addPdd | 创建盘点 | 仓库、类型、范围 | 主表及初始明细 |
getPddDetail | 明细列表 | bill_id | 系统数、盘点数、差异 |
getPddStat | 统计与变动提醒 | bill_id | SKU/货位/盈亏统计 |
getScanData | 按商品/货位读取待盘数据 | bill_id、扫码条件 | 扫描候选明细 |
getPdBarCode | 条码识别商品 | 条码、仓库 | 商品和库存维度 |
addScanData | 保存实际盘点数 | bill_id、scanType、scanData | 明细更新,状态变盘点中 |
upPddStatus | 提交、重盘、撤回 | bill_id、动作 | 主表状态变化 |
beforeSubmitCheck | 检查盘点快照与当前库存 | 主表 id | 不一致明细 |
checkBeforeInventory | 检查未完成业务单据 | 会话站点 | 单据类型列表 |
getPdAmount | 盘点金额额度 | sid | 可盘、已盘、剩余额度 |
getGoodsByScanningArea | 暗盘按货位找商品 | 盘点、仓库、货位码 | 货位商品 |
getGoodsByScanningCode | 暗盘按条码找商品 | 仓库、条码 | 商品信息 |
5.2 GPDA 盘点接口
| 方法 | 用途 | 与 PDA 的差异 |
|---|---|---|
getPddNoByTaskNo | 外部任务号换 DGJ 单号 | 明确接收 taskNo |
getResultSkuList | 查询结果 SKU | 面向外部任务结果 |
gaddPdd | 创建 GPDA 盘点 | customerCode 转为 sid,保存 taskNo |
ggetPddDetail | 查任务明细 | 请求 pddNo 实际按 taskNo 转主键 |
gaddScanData | 保存 GPDA 扫描 | 保存后可触发 SKU 数量同步 |
gupPddStatus | 提交/重盘/撤回 | 会发送状态 MQ;退站清盘规则不同 |
getGpadGoodsCount | GPDA 商品统计 | 面向外部任务视图 |
exportPddResult | 导出结果 | 外部任务结果文件 |
5.3 PDA 采购接口
| 阶段 | 方法 | 说明 |
|---|---|---|
| 待办 | getWmsList、getWmsInfo | 获取仓储/采购任务 |
| 发运 | getFydInfo、getLogisticsInfo | 查发运与物流 |
| 箱签收 | getSignBoxInfo、getSignBoxResInfo | 识别箱码和签收结果 |
| 签收确认 | getSignComfirm、signIn | 写签收事实,不等同库存入库 |
| 入库列表 | getInStoList、getInStoListV2 | 待入库任务 |
| 入库详情 | getInStoDetail、getInStoBoxDetailV2、getInStoSkuDetail | 箱/商品维度 |
| 入库确认 | getInStroComfirm、inStoIn | 复用采购入库公共链路,真实加库存 |
| 仓库货位 | getStoAreaList、getAreaList、getScanAll | 上架位置选择 |
5.4 销售、货位调整和验货接口
| 模块 | 方法 | 最终行为 |
|---|---|---|
| 销售 | getSaleList、getSaleInfo | 查询待出库单 |
| 销售 | geAreaStockData | 查询可扣货位库存 |
| 销售 | saleStockOut | 调用 InvSaService::addOutBound(),真实扣库存 |
| 货位调整 | getInvByScan、getAreaByScan、getGoodsByArea | 识别源商品、目标货位 |
| 货位调整 | submit、batchSubmit | 调用 InvlocationService::areaChangeInfo() |
| 出库验货 | num、list、detail、scan | 只查询已出库销售单 |
| 出库验货 | check | 写验货记录,不改库存 |
6. 数据模型、分表与库存事实
6.1 核心表
| 逻辑表/常量 | 物理表 | 用途 | 关键字段 |
|---|---|---|---|
SCM_INVENTORY_CHECK | t_scm_inventory_check | 盘点主表 | id、sid、billNo、taskNo、checkStatus、pdType、locationId |
SCM_INVENTORY_CHECK_INFO | t_scm_inventory_check_info{sid末位} | 盘点明细 | PDDId、invId、skuId、locationAreaId、sysQty、checkQty |
SCM_PD_CHECK | t_scm_pd_check | 服务站盘点金额上限 | sid、比例/额度字段 |
SCM_PD_REASON | t_scm_pd_reason | 盘点原因 | 原因内容、状态 |
SCM_INVENTORY_REAL_TIME | t_scm_inventory_real_time | 当前实时库存 | sid、inv_id、location_id、location_area_id、qty |
SCM_INVENTORY 分表 | t_scm_inventory_0_{sid%128} | 库存流水 | 单据号、交易类型、数量、成本、业务日期 |
SYS_PDA_OPTION_LOG | t_sys_pda_option_log | PDA 粗粒度操作日志 | day、sid、type、remark |
| 销售出库主/明细 | 销售出库分表 | 销售扫码出库事实 | billNo、transType、billStatus、货位、数量 |
| 销售验货表 | SaInvoiceCheckModel 对应表 | 出库验货确认 | billNo |
| 仓库/货位 | t_bs_storage、货位相关表 | 空间维度 | sid、仓库 ID、货位 ID、状态 |
分表规则必须先按 sid 算表名。盘点明细是 10 分表,库存流水是 128 分表,二者不能套用同一个后缀规则。
6.2 盘点实体关系
erDiagram
INVENTORY_CHECK ||--o{ INVENTORY_CHECK_INFO : "PDDId"
STORAGE ||--o{ INVENTORY_CHECK : "locationId"
STORAGE ||--o{ STORAGE_AREA : "仓库包含货位"
STORAGE_AREA ||--o{ INVENTORY_CHECK_INFO : "locationAreaId"
GOODS_INV ||--o{ INVENTORY_CHECK_INFO : "invId"
GOODS_SKU ||--o{ INVENTORY_CHECK_INFO : "skuId"
INVENTORY_CHECK_INFO ||--o| INVENTORY_LEDGER : "审核差异生成流水"
INVENTORY_CHECK_INFO }o--|| INVENTORY_REAL_TIME : "审核差异调整库存"
6.3 三类数量不能混用
| 数量 | 代表什么 | 何时形成 | 是否会继续变化 |
|---|---|---|---|
sysQty | 盘点基准快照 | 创建/生成盘点明细时 | 盘点明细内通常保持不变 |
checkQty | 仓库人员实际数到的数量 | 扫码保存时 | 可在允许状态下重扫更新 |
实时库存 qty | 当前系统库存结果 | 每次库存业务发生时 | 采购、销售、调拨等都会改变 |
原始盘点差异:
盘点差异 qty = checkQty - sysQty
审核入账时,代码把这个“原始差异”叠加到审核时的当前库存:
审核后实时库存 = 审核时实时库存 + (checkQty - sysQty)
如果盘点期间发生正常出入库,最终库存不一定等于 checkQty。这是当前代码语义,不应把“最终数量必须等于盘点数量”当成唯一正确口径。
flowchart LR
A["创建时 sysQty=10"] --> B["实际盘点 checkQty=8"]
B --> C["原始差异=-2"]
C --> D["审核前正常入库后实时库存=13"]
D --> E["审核入账 13 + (-2) = 11"]
7. 盘点类型和状态机
7.1 盘点类型
| 值 | 常量 | 业务含义 | 备注 |
|---|---|---|---|
| 1 | PDD_TYPE_TEMP_ | 临时盘点 | 常规临时任务 |
| 2 | PDD_TYPE_HALFMOON_ | 半月盘点 | 周期盘点 |
| 3 | PDD_TYPE_MONTH_ | 月度盘点 | 涉及月度额度口径 |
| 4 | PDD_TYPE_QUARTER_ | 季度盘点 | 枚举映射当前写成“临时盘点”,疑似显示缺陷 |
| 5 | PDD_TYPE_HALFYEAR | 年中盘点 | 周期盘点 |
| 6 | PDD_TYPE_TYEAR_ | 年度盘点 | 周期盘点 |
| 7 | PDD_TYPE_EXIT | 退站清盘 | GPDA 提交逻辑特殊 |
7.2 主表状态
| 值 | 常量 | 含义 | 可执行动作 |
|---|---|---|---|
| 1 | PDD_STATUS_WAIT | 待盘点 | 扫码后进入 2 |
| 2 | PDD_STATUS_DOING | 盘点中 | 继续扫码、提交 |
| 3 | PDD_STATUS_CHECK | 审核中 | PC 审核、按规则撤回 |
| 4 | PDD_STATUS_REJECT | 审核不通过 | 修改/重盘后回 2 |
| 5 | PDD_STATUS_FINISH | 已完成 | 终态,库存已按正常审核路径入账 |
| 7 | PDD_STATUS_CLOSE | 已关闭 | 外部关闭终态,不再盘点 |
7.3 PDA 正常状态机
stateDiagram-v2
[*] --> 待盘点
待盘点 --> 盘点中: 首次保存有效扫码
盘点中 --> 盘点中: 继续扫码/更新数量
盘点中 --> 审核中: SUBMIT
审核中 --> 已完成: PC审核通过并入账
审核中 --> 审核不通过: PC驳回
审核不通过 --> 盘点中: UPDATE/重盘
审核中 --> 盘点中: ROLLBACK/撤回
待盘点 --> 已关闭: 外部关闭
盘点中 --> 已关闭: 外部关闭
PDA upPddStatus() 的核心约束:
| 动作 | 合法源状态 | 目标状态 | 非法示例 |
|---|---|---|---|
SUBMIT | 2 盘点中 | 3 审核中 | 待盘点未扫码直接提交 |
UPDATE | 4 审核不通过 | 2 盘点中 | 已完成重新编辑 |
ROLLBACK | 3 审核中 | 2 盘点中 | 已完成撤回 |
7.4 GPDA 状态差异
stateDiagram-v2
[*] --> 待盘点
待盘点 --> 盘点中: 扫码
待盘点 --> 审核中: GPDA SUBMIT允许状态小于等于2
盘点中 --> 审核中: GPDA SUBMIT
审核中 --> 已完成: DGJ PC审核通过
审核中 --> 审核不通过: DGJ PC驳回
审核不通过 --> 盘点中: UPDATE
审核中 --> 盘点中: ROLLBACK
待盘点 --> 已完成: 退站清盘SUBMIT
盘点中 --> 已完成: 退站清盘SUBMIT
待盘点 --> 已关闭: GPDA关闭消息
盘点中 --> 已关闭: GPDA关闭消息
重要差异:
- GPDA 普通任务提交后向外发送状态消息。
- 退站清盘
pdType=7提交时直接把主表更新为 5。 - 静态代码未证明退站清盘在该提交点执行了与 PC 审核相同的库存差异入账,因此必须确认外部系统或其他任务的责任边界。
- DGJ 主表审核通过是状态 5,但发给 GPDA 的成功状态参数使用字符串
"6"。不要把外部状态 6 回写成 DGJ 主表状态。
8. 创建盘点单:形成库存快照
8.1 前置检查
InventoryCheckSer::checkExistOrderTypeBeforeInventory() 会检查当前站是否存在可能改变库存但尚未完成的单据,包括:
| 单据类别 | 风险 |
|---|---|
| 采购已收货待入库 | 系统库存尚未增加,实物可能已到仓 |
| 销售未完全出库 | 实物可能在拣货,系统仍有库存 |
| 站间调拨待入/待出 | 库存归属处于中间态 |
| 调拨草稿/已出库 | 可能继续改变目标或来源库存 |
flowchart TD
A["开始盘点"] --> B["checkBeforeInventory"]
B --> C{"存在未完成库存单据?"}
C -->|是| D["返回单据类型,提示先完成业务"]
C -->|否| E["选择仓库、类型、盘点范围"]
E --> F["创建主表"]
F --> G["按当时库存生成明细 sysQty"]
G --> H["状态=待盘点"]
该检查是风险提示/阻断逻辑的一部分,但不能代替数据库事务隔离。检查完成后到主表/明细创建之间,其他业务仍可能改变库存。
8.2 创建结果
创建成功至少形成两层事实:
t_scm_inventory_check:谁、在哪个站、哪个仓、以什么类型盘点,当前状态是什么。t_scm_inventory_check_info{sid末位}:每个invId + locationAreaId的系统数量快照及后续实际盘点数量。
GPDA 创建还会关联外部 taskNo,用于后续查询和 MQ 状态同步。
9. 扫码保存:只写盘点事实
9.1 PDA 扫码事务
Services/Pda/TakeStockSer::addScanData() 的核心步骤:
sequenceDiagram
participant P as PDA
participant C as TakeStock Controller
participant S as TakeStockSer
participant M as 盘点主表
participant D as 盘点明细分表
P->>C: addScanData(bill_id, scanType, scanData)
C->>C: Validate app_pdd_scan_add
C->>S: addScanData
S->>M: 查询主单并校验状态 1/2
S->>S: 每1000条分批处理
S->>D: 按扫描模式清理过期交叉明细
S->>D: 按 invId+locationAreaId 更新或新增
S->>M: 状态置2,首次记录 startDate
S-->>C: 提交事务
C-->>P: 操作成功
9.2 明细唯一业务维度
同一盘点单内,逻辑明细按下列组合识别:
PDDId + invId + locationAreaId
扫码数据超过 1000 条会分块处理。更新现有明细和批量插入新明细发生在同一 MySQL 事务内。
9.3 扫描模式
| 模式 | 用户视角 | 保存行为关注点 |
|---|---|---|
按商品 GOODS | 先扫商品,再核对各货位 | 同商品未保留的货位明细可能被清理 |
按货位 AREA | 先扫货位,再核对其中商品 | 同货位未保留的商品明细可能被清理 |
清理条件 $del_where 在当前实现中位于分块循环之外并持续追加。大批量、多个分块场景应重点回归,确认没有累积条件导致误删或 SQL 膨胀。
9.4 数量和金额字段
checkQty - sysQty > 0:盘盈。checkQty - sysQty < 0:盘亏。checkQty - sysQty = 0:盘平。- 新增明细且
checkQty <= 0时,当前代码会跳过该新增行。 - 成本价写盘点明细时存在乘以 100 的处理,排查金额必须先确认字段单位是元还是分。
flowchart TD
A["一条 scanData"] --> B{"已有 invId+货位 明细?"}
B -->|是| C["更新 checkQty/差异/成本"]
B -->|否| D{"checkQty > 0?"}
D -->|否| E["跳过新行"]
D -->|是| F["插入 is_new=1 明细"]
C --> G["计算盘盈/盘亏/盘平"]
F --> G
9.5 扫码后没有库存变化是正常现象
此阶段只更新盘点主表和明细,不调用 Storage\InventorySer::save()。因此:
checkQty已保存但实时库存没变:正常。- 提交到审核中后实时库存仍没变:正常。
- 只有 PC 审核通过后仍无库存流水/实时库存变化,才进入异常排查。
10. 提交前一致性检查
10.1 快照与当前库存比较
beforeSubmitCheck 调用 InventoryCheckSer::checkWithOrder(checkId, sid),按盘点明细中的库存维度查询当前实时库存,并比较保存的系统数量与当前数量。
flowchart LR
A["盘点明细 sysQty"] --> C{"与当前实时库存比较"}
B["t_scm_inventory_real_time.qty"] --> C
C -->|相同| D["可继续提交"]
C -->|不同| E["返回变动明细,提示业务确认"]
该检查回答的是“盘点期间系统库存是否发生变化”,不是重新计算盘点差异,也不会自动改写 sysQty。
10.2 统计缓存与变动提醒
getPddStat() 对仓库货位数量、SKU 数量等统计使用约 2 小时缓存,并会根据 startDate 检查库存流水变化。因此可能出现:
- 盘点明细已更新,但统计短时仍显示旧值。
- 盘点开始后有业务流水,页面出现库存变动提醒。
- 清缓存只能修正统计展示,不能修复盘点明细或实时库存。
11. 提交、审核与库存入账
11.1 提交盘点
PDA 正常提交把状态从 2 改为 3,并通过 PdaOptionSer 写一条“盘点提交”日志。此时仍未改变库存。
sequenceDiagram
participant D as PDA
participant T as TakeStockSer
participant P as 盘点主表
participant L as PDA操作日志
D->>T: upPddStatus(SUBMIT)
T->>P: 2 -> 3
T-->>D: 提交成功
D->>L: type=3, remark=盘点提交
11.2 PC 审核入口
PC 端 InvOi::pdNewCheckUp():
- 权限点:
PD_GENPD_SAVE。 - 使用
ReceiptService->requestCheck(json_encode($_POST))做重复请求检查。 - 核心业务:
InvOiService::pdNewCheckUp($data)。 - 只有主表状态 3 才能审核通过或驳回。
11.3 审核通过完整时序
sequenceDiagram
participant U as PC审核人
participant C as InvOi Controller
participant S as InvOiService
participant P as 盘点主/明细
participant I as InventorySer
participant R as 实时库存
participant L as 库存流水分表
participant M as MQ
U->>C: pdNewCheckUp(status=1, PDDId)
C->>C: 权限 + 重复请求检查
C->>S: pdNewCheckUp
S->>P: 校验主状态=3,读取明细
S->>S: 计算 qty=checkQty-sysQty
S->>R: 校验 currentQty+qty 不小于0
S->>P: 主状态改为5,写审核轨迹/金额/时间
loop 每条非零差异
S->>I: save(inventoryInfo)
I->>R: 加/减实时库存
I->>L: 写库存流水
end
S->>S: 提交本地事务
S->>M: sendInventoryEvent
opt GPDA任务
S->>M: sendPddStatusToOps(pdStatus=6)
end
11.4 审核入账规则
| 条件 | 行为 |
|---|---|
checkQty - sysQty > 0 | billType=OI,增加实时库存 |
checkQty - sysQty < 0 | billType=OO,减少实时库存 |
| 差异为 0 | 不写库存流水 |
| 当前库存 + 差异 < 0 | 拒绝审核,避免负库存 |
| 明细货位不属于主单仓库 | 拒绝审核 |
| 审核状态不是 3 | 拒绝重复或越级审核 |
盘点入账携带的 transType 来自主表,当前盘点口径是 180510。盘盈/盘亏方向由 billType=OI/OO 区分,不应简单按其他入库 150701、其他出库 150801 查找。
11.5 审核驳回
flowchart LR
A["审核中 3"] -->|驳回 status=0| B["审核不通过 4"]
B -->|PDA/GPDA UPDATE| C["盘点中 2"]
C --> D["重新扫码/修正数量"]
D -->|再次提交| A
驳回只更新主表状态和审核轨迹,不写库存流水。
11.6 盘点金额控制
非 GPDA 盘点,即主表 taskNo 为空时,审核会检查本月可盘金额、已盘金额和本次盘点金额。GPDA 任务在当前代码中绕过该控制。
| 口径 | 说明 |
|---|---|
| 可盘金额 | getSidCanPdAmount(sid) |
| 本月已盘金额 | getPdCheckedAmount(sid, 月初, 当前) |
| 剩余可盘金额 | max(可盘金额 - 已盘金额, 0) |
| 金额单位 | Service 内多使用分,接口展示前除以 100 |
GPDA 绕过金额控制是明确的代码分支,修改时必须确认业务意图,不能直接统一为 PC 规则。
12. GPDA MQ 同步与关闭
12.1 事件清单
| Routing key | 方向 | 用途 | 生产/消费入口 |
|---|---|---|---|
dgj_pd_status_update | DGJ -> GPDA/OPS | 同步盘点状态 | MqSer::sendPddStatusToOps() |
dgj_pd_sku_num_update | DGJ -> GPDA/OPS | 同步已盘/总 SKU 数 | MqSer::sendPddSkuNumToOPs() |
gpda_send_sku_num_msg_to_ops | DGJ 内部 | 延迟/异步触发 SKU 统计 | MqSer::sendSkuNumMsgToOps() -> SendSkuNumMsgToOps |
stationprod_spdmgr_close_task | GPDA/OPS -> DGJ | 关闭任务 | GpdaNotify |
12.2 状态与 SKU 同步
sequenceDiagram
participant G as GPDA接口
participant S as Gpda TakeStockSer
participant Q as RabbitMQ
participant C as SendSkuNum消费者
participant O as GPDA/OPS
G->>S: 扫码或状态变化
S->>Q: gpda_send_sku_num_msg_to_ops(sid,PDDId)
Q->>C: 消费内部事件
C->>S: 重新计算 SKU 数
S->>Q: dgj_pd_sku_num_update
Q->>O: SKU数量更新
S->>Q: dgj_pd_status_update
Q->>O: 状态更新
消费者异常时会走 NACK/重试语义,具体重试次数、死信和告警取决于生产 RabbitMQ 配置,静态仓库不能证明。
12.3 外部关闭任务
关闭消费者接收一个或逗号分隔的多个 taskNo,将匹配且当前不为 7 的主表更新为 7。
flowchart TD
A["收到 close_task 消息"] --> B["拆分 taskNo"]
B --> C["按 taskNo 查盘点主表"]
C --> D{"当前状态是否7"}
D -->|是| E["保持不变,幂等结束"]
D -->|否| F["更新 checkStatus=7"]
F --> G["ACK"]
C -->|异常| H["NACK/记录日志"]
关闭不是审核完成。状态 7 不代表差异已入库存;排查时必须同时看库存流水。
13. PDA 采购签收与入库
13.1 签收和入库不是同一个动作
flowchart LR
A["扫描箱码/发运单"] --> B["签收确认"]
B --> C["记录货物已到站"]
C --> D["选择仓库/货位"]
D --> E["inStoIn 入库"]
E --> F["采购入库单"]
F --> G["库存流水 + 实时库存增加"]
| 阶段 | 成功后应看到 | 不应据此判断 |
|---|---|---|
| 签收 | 箱/发运签收状态、PDA 操作日志 | 库存一定增加 |
| 入库 | 采购入库单、库存流水、实时库存 | 只有签收状态即可 |
13.2 inStoIn() 核心行为
Services/Pda/PureSer::inStoIn():
- 根据请求判断箱号或单据号模式。
- 查询可入库的采购发货明细。
- 过滤已入库或不存在的明细,而不是所有异常都中止整批。
- 调用
InvPoService::invPoDiffer()处理采购数量差异。 - 同一 SKU 对应多条发货明细时展开分配。
- 复用采购入库公共链路落入库单和库存。
部分成功是该链路的重要特征。移动端显示“成功”时,仍需核对实际接纳了哪些明细。
13.3 幂等与并发风险
- 客户端重复点击、网络重试、两个设备同时入库可能同时通过“待入库”查询。
- 业务状态检查和库存写入之间存在并发窗口。
- 排查重复入库必须同时核对采购发货明细、采购入库单、库存流水和实时库存,不能只删一张单据。
- 修复公共入库逻辑前,应按《25_高风险公共文件影响面》回归 PC、PDA、活动采购和外部回调等全部来源。
13.4 PDA 操作日志局限
PdaOptionSer 只记录:
| 字段 | 内容 |
|---|---|
day | 日期 |
sid | 服务站 |
type | 1 签收、2 入库、3 盘点提交 |
remark | 对应中文说明 |
它没有稳定记录 billNo、uid、device 或请求 ID,因此只能证明某站某日发生过某类操作,不能独立证明某一张单的完整审计链路。
14. PDA 销售扫码出库
14.1 主流程
sequenceDiagram
participant P as PDA
participant S as SaleSer
participant O as 销售订单/明细
participant I as InvSaService
participant R as 实时库存
participant L as 销售出库/库存流水
P->>S: saleStockOut(单号, qtyGrid)
S->>O: 查询订单和可出库明细
S->>S: 过滤 this_out_qty<=0 或失效 info_id
S->>I: addOutBound(transType=150601)
I->>R: 扣减指定仓库货位库存
I->>L: 写销售出库单和库存流水
I-->>P: 出库结果
14.2 业务规则
| 规则 | 说明 |
|---|---|
| 数量过滤 | this_out_qty <= 0 的行不参与出库 |
| 明细有效性 | 不属于当前订单或已经失效的 info_id 被过滤 |
| 无有效明细 | 直接返回业务错误 |
| 交易类型 | 150601 销售出库 |
| 最终服务 | InvSaService::addOutBound() |
PDA “扫码成功”不能只看条码识别响应;只有 saleStockOut 成功且销售出库单、库存流水、实时库存一致,才算出库完成。
15. PDA 货位调整
15.1 数据守恒
货位调整在同一仓库内把数量从源货位转到目标货位:
源货位变化 = -调整数量
目标货位变化 = +调整数量
仓库商品总量变化 = 0
flowchart LR
A["源货位 A qty=10"] -->|移 3| B["调整服务"]
B --> C["源货位 A qty=7"]
B --> D["目标货位 B qty=3"]
C --> E["仓库总量仍为10"]
D --> E
15.2 提交校验
AreaChangeSer::submit() 和 batchSubmit() 主要校验:
- 目标货位属于当前仓库。
- 源货位和目标货位不能相同。
- 源货位实时库存必须大于等于调整数量。
- 由
InvlocationService::areaChangeInfo()完成一负一正的库存动作。
批量路径使用同一组仓库上下文。若请求混入多个 locationId,当前代码中 current($storage) 的处理值得重点回归,避免展示/记录了错误仓库名称。
16. PDA 出库验货
16.1 只验已经出库的销售单
OutStockCheckSer 的查询条件固定为:
| 条件 | 值 |
|---|---|
transType | 150601 |
billStatus | OUTED |
sid | 当前会话服务站 |
isDelete | 0 |
扫码先把条码映射为商品,再从已出库明细中返回该商品对应仓库、货位和绝对数量。
16.2 验货确认不扣库存
flowchart TD
A["扫描已出库单/商品"] --> B["验证单据 transType=150601 且 OUTED"]
B --> C["展示出库货位和数量"]
C --> D["check 确认"]
D --> E{"是否已有验货记录"}
E -->|有| F["拒绝重复操作"]
E -->|无| G["插入 SaInvoiceCheck 记录"]
orderCheck() 只插入验货记录,不调用库存服务。因此修复验货功能时禁止额外扣减库存,否则会造成二次出库。
17. 事务、一致性和补偿边界
17.1 本地事务边界
| 链路 | 本地事务覆盖 | 事务外动作 |
|---|---|---|
| PDA 扫码保存 | 主表状态、盘点明细增删改 | 页面重试、统计缓存 |
| PC 盘点审核 | 主表审核状态、库存流水、实时库存 | 提交后库存 MQ、GPDA 状态 MQ |
| 采购入库 | 采购入库相关写入和库存写入,具体以公共服务为准 | 外部通知/MQ 可能在事务外 |
| 销售出库 | 销售出库和库存公共服务事务 | 下游同步/MQ |
| 货位调整 | 调整服务内部库存写入 | 页面重试/日志 |
17.2 MySQL 成功但 MQ 失败
盘点审核先提交本地事务,再发送库存事件和 GPDA 状态消息。因而可能出现:
flowchart LR
A["主表=5且库存已入账"] --> B["发送MQ"]
B -->|成功| C["下游一致"]
B -->|失败| D["DGJ正确,GPDA/下游滞后"]
D --> E["按业务唯一键补发,不回滚库存"]
补偿时禁止再次执行 pdNewCheckUp(),否则有重复入账风险。应补发缺失事件,并由接收方按 taskNo/业务单号幂等。
17.3 客户端超时的不确定结果
客户端超时不等于服务端失败。再次提交前先查:
- 主表状态是否已经改变。
- 明细
checkQty是否已经保存。 - 业务单据/库存流水是否已生成。
- 实时库存是否已变化。
- 日志中同请求是否已经成功。
18. 典型故障决策树
18.1 扫码后库存没变
flowchart TD
A["扫码后库存没变"] --> B{"业务是哪一种?"}
B -->|盘点扫码| C["正常:检查明细 checkQty"]
C --> D{"PC是否审核通过?"}
D -->|否| E["等待/完成审核"]
D -->|是| F["查 transType=180510 流水与实时库存"]
B -->|采购入库| G["查是否只有签收、是否执行 inStoIn"]
G --> H["查采购入库单和入库流水"]
B -->|销售出库| I["查 saleStockOut 与 150601 出库流水"]
B -->|出库验货| J["正常:验货本来不改库存"]
18.2 GPDA 显示已完成,DGJ 状态不一致
- 用 GPDA 请求
pddNo作为taskNo查主表。 - 区分 DGJ 状态 5 与外发状态 6。
- 查
dgj_pd_status_update生产日志和消息内容。 - 查外部关闭消息是否把主表改成 7。
- 若为退站清盘,确认外部入账责任系统。
- 不要直接手改状态掩盖库存流水缺失。
18.3 盘点审核通过但库存不对
flowchart TD
A["审核后库存不对"] --> B["确认 sid 和分表"]
B --> C["查主表状态/审核时间/任务号"]
C --> D["查明细 sysQty/checkQty"]
D --> E["计算 diff=checkQty-sysQty"]
E --> F["查审核前后实时库存和180510流水"]
F --> G{"是否把最终库存误认为必须等于checkQty?"}
G -->|是| H["按 current+diff 重新解释"]
G -->|否| I["查重复流水、漏流水、错误货位"]
18.4 PDA 菜单缺失
- 确认用户是否主账号。
- 查 PC 菜单权限的
module。 - 查首页候选模块及
class='PDA'配置。 - 确认 module 名转换规则,尤其
APP_SALE*。 - 重新登录刷新会话和首页权限。
19. 只读排查 SQL
以下 SQL 是模板。先确认环境、sid、真实字段名和分表后缀,只读执行;禁止直接用生产数据替换状态或数量。
19.1 查盘点主表
select id, sid, billNo, taskNo, pdType, locationId, checkStatus,
startDate, pd_check_pass_time, pd_amount, isDelete
from t_scm_inventory_check
where sid = :sid
and (id = :pdd_id or billNo = :bill_no or taskNo = :task_no)
order by id desc;
19.2 查盘点明细
{tail} 是 sid 最后一位:
select id, PDDId, invId, skuId, locationId, locationAreaId,
sysQty, checkQty, (checkQty - sysQty) as diff_qty,
checkType, is_new, isDelete
from t_scm_inventory_check_info{tail}
where PDDId = :pdd_id
order by invId, locationAreaId;
19.3 查实时库存
select sid, inv_id, sku_id, location_id, location_area_id, area_type, qty
from t_scm_inventory_real_time
where sid = :sid
and inv_id = :inv_id
and location_id = :location_id
and location_area_id = :location_area_id;
19.4 查盘点库存流水
{shard} 是 sid % 128:
select id, sid, billNo, transType, invId, skuId,
locationId, locationAreaId, qty, businessDate
from t_scm_inventory_0_{shard}
where sid = :sid
and billNo = :bill_no
and transType = 180510
order by id;
19.5 比较明细差异与流水
select invId, locationAreaId,
sum(checkQty - sysQty) as expected_diff
from t_scm_inventory_check_info{tail}
where PDDId = :pdd_id and isDelete = 0
group by invId, locationAreaId;
将结果与对应库存流水按 invId + locationAreaId 聚合比较。不要只比总数,否则可能掩盖货位错写。
19.6 查 PDA 粗粒度操作日志
select id, day, sid, type, remark
from t_sys_pda_option_log
where sid = :sid
and day between :start_day and :end_day
order by id desc;
19.7 查重复验货
先从 SaInvoiceCheckModel 确认实际表名,再执行:
select billNo, count(*) as cnt
from <销售出库验货表>
where billNo = :bill_no
group by billNo;
20. 日志与代码定位命令
20.1 入口与参数校验
rg -n "class BaseAppController|HASH_PDA_DEVICE_STATUS|dgj_app_log|pdaApi" application/core application/Services/Pda
rg -n "class BaseGpdaController|gpdaApi|requestData|taskNo" application/core application/controllers/inner/gpda
rg -n "app_pdd_|before_submit_check" application/Validate application/controllers/pda
20.2 盘点主链路
rg -n "addPdd|addScanData|upPddStatus|getPddStat" application/controllers/pda application/Services/Pda
rg -n "gaddPdd|gaddScanData|gupPddStatus|sendStatusChangeMsg|senSkuNumMsgToOps" application/controllers/inner/gpda application/Services/Gpda
rg -n "pdNewCheckUp|PD_GENPD_SAVE|sendInventoryEvent|sendPddStatusToOps" application/controllers/scm application/service/scm application/Services/Mq
20.3 仓内其他作业
rg -n "signIn|inStoIn|invPoDiffer" application/controllers/pda/Purchase.php application/Services/Pda/PureSer.php application/service/scm
rg -n "saleStockOut|addOutBound|150601" application/controllers/pda/Sale.php application/Services/Pda/SaleSer.php application/service/scm
rg -n "areaChangeInfo|batchSubmit|orderCheck" application/Services/Pda application/controllers/pda
20.4 分表与状态
rg -n "SCM_INVENTORY_CHECK_INFO|SCM_INVENTORY_SUB_NUM|SCM_INVENTORY.'_0_'" application/config application/models application/Models
rg -n "PDD_STATUS_|PDD_TYPE_|180510|GPDA_.*NOTIFY" application/KzData application/Services application/controllers/tasks
21. 故障现象与证据顺序
| 现象 | 第一证据 | 第二证据 | 第三证据 | 禁止的第一动作 |
|---|---|---|---|---|
| 扫码记录丢失 | App/GPDA 请求日志 | 盘点明细分表 | 主表状态 | 直接改 checkQty |
| 盘点提交失败 | 主表状态 | 提交前库存比较 | 未完成业务单据 | 强制改成状态 3 |
| 审核后无库存 | 主表是否状态 5 | 180510 流水 | 实时库存 | 再点一次审核 |
| GPDA 状态滞后 | taskNo 主表 | MQ 生产日志 | 外部消费确认 | 改 DGJ 主表为外部状态 6 |
| 入库重复 | 采购入库单 | 发货明细入库数量 | 库存流水/实时库存 | 只删入库主单 |
| 销售出库少扣/多扣 | 销售出库明细 | 150601 流水 | 实时库存 | 只改实时库存 |
| 货位调整总量变化 | 源/目标实时库存 | 调整流水 | 请求中仓库货位 | 单边改一个货位 |
| 验货后库存变化 | 销售出库流水时间 | 验货记录 | 其他同期操作 | 在验货逻辑补扣库存 |
22. 回归测试矩阵
22.1 登录和权限
| 场景 | 预期 |
|---|---|
| 主账号登录 | 返回会话,首页展示全部可用 PDA 模块 |
| 子账号登录 | 只展示与 PC 权限交集的模块 |
| 新设备再次登录 | Redis 设备绑定更新,旧设备请求受限 |
| 会话过期 | 业务接口拒绝并要求重新登录 |
伪造客户端 sid | 仍以会话注入的 JXCSID 为准 |
22.2 盘点创建和扫码
| 场景 | 预期 |
|---|---|
| 无未完成库存单据 | 可进入创建流程 |
| 存在采购待入库 | 返回风险单据类型 |
| 首次有效扫码 | 状态 1 -> 2,记录开始时间 |
| 重复扫同商品同货位 | 更新同一逻辑明细,不重复累计错误 |
| 新商品盘点数 0 | 按当前规则不新增无效行 |
| 1001 条以上扫码 | 分块处理完整,无误删、无漏行 |
| 按商品/按货位切换 | 清理范围符合业务预期 |
| 事务中途异常 | 主表与明细整体回滚 |
22.3 提交和审核
| 场景 | 预期 |
|---|---|
| 状态 2 提交 | 进入 3,不改库存 |
| 状态 1 直接 PDA 提交 | 被拒绝 |
| 审核通过且盘盈 | 状态 5,OI 流水,实时库存增加 |
| 审核通过且盘亏 | 状态 5,OO 流水,实时库存减少 |
| 盘平 | 状态 5,不写零差异流水 |
| 差异导致负库存 | 审核失败,事务回滚 |
| 审核驳回 | 状态 4,不改库存 |
| 重复审核 | 被状态/请求检查拦截,不重复入账 |
| 盘点期间正常入库 | 最终按当前库存 + 原始差异解释 |
| MQ 发送失败 | 本地库存仍正确,可按业务键补发消息 |
22.4 GPDA
| 场景 | 预期 |
|---|---|
pddNo 传有效 taskNo | 转换出内部 bill_id |
pddNo 为空 | 返回“任务号不能为空” |
| taskNo 不存在 | 返回“非法单号” |
| 扫码 | 保存明细并异步更新 SKU 数 |
| 普通任务提交 | DGJ 进入审核中并外发状态 |
| 审核通过 | DGJ 主表 5,外发 pdStatus=6 |
| 重复关闭任务 | 最终保持 7,幂等 |
| 退站清盘 | 验证直接完成及库存入账责任系统 |
22.5 采购、销售、货位和验货
| 场景 | 预期 |
|---|---|
| 仅签收未入库 | 签收有记录,库存不增加 |
| 正常采购入库 | 入库单、流水、实时库存三者一致 |
| 混合已入库/待入库明细 | 明确部分成功结果,无重复入库 |
| 重复点击采购入库 | 不产生重复库存事实 |
| 正常销售出库 | 150601 出库单/流水,实时库存扣减 |
| 无有效销售明细 | 返回业务错误,不写空单 |
| 单笔货位调整 | 一减一增,仓库总量不变 |
| 批量货位调整 | 每项守恒,混仓请求被拒绝或正确隔离 |
| 出库验货 | 只写一次确认记录,库存不变 |
| 重复验货 | 返回“已验货,请勿重复操作” |
23. 高风险改动清单
| 改动位置 | 可能影响 | 最小回归范围 |
|---|---|---|
BaseAppController | 所有 App/PDA 请求、日志、鉴权 | 登录、普通 App、PDA、设备切换 |
UserSer | PDA 会话、权限、设备 | 主/子账号、菜单、旧设备 |
TakeStockSer::addScanData | 所有 PDA 盘点明细 | 商品/货位模式、大批量、重扫、回滚 |
Gpda/TakeStockSer | GPDA 外部契约和 MQ | 任务号、扫码、状态、导出、关闭 |
InvOiService::pdNewCheckUp | PC/GPDA 盘点最终入账 | 盈亏平、驳回、重复、金额限制、MQ |
Storage/InventorySer | 全系统库存 | 采购、销售、退货、调拨、盘点、活动 |
PureSer::inStoIn | PDA 采购入库 | 箱/单模式、部分成功、重复入库 |
SaleSer::saleStockOut | PDA 销售出库 | 多货位、部分数量、重复提交 |
AreaChangeSer | 货位库存 | 单笔/批量、同货位、混仓、库存不足 |
OutStockCheckSer | 验货记录 | 条码识别、重复验货、确保不扣库存 |
24. 已确认事实、风险和待确认项
24.1 代码已确认
- PDA 会话有效期设置为 86400 秒,并按用户在 Redis 绑定设备。
- PDA 首页菜单由 PC 权限与 PDA 候选模块共同决定。
- GPDA 请求中的
pddNo在多数方法中按taskNo解释。 - 盘点明细物理表后缀是
sid最后一位。 - 扫码只写盘点主/明细,不改库存。
- 正常盘点只有 PC 审核通过才调用库存服务入账。
- 盘点差异是
checkQty - sysQty,叠加在审核时当前库存上。 - 盘点库存交易类型为
180510,方向由 OI/OO 区分。 - GPDA 对外审核成功状态使用 6,而 DGJ 主表完成状态是 5。
- 采购
inStoIn和销售saleStockOut是真实库存动作。 - 出库验货只写确认记录,不再次扣库存。
24.2 当前代码风险
- PDA 批量扫码删除条件可能跨 1000 条分块累积,需要专项验证。
PdaOptionSer审计粒度不足,不能定位单据、用户、设备和请求。- GPDA 请求/响应日志可能包含完整载荷,需要确认脱敏。
- GPDA 退站清盘直接完成路径的库存入账责任在本仓库静态代码中不完整。
- 盘点开始前检查与快照创建之间仍有并发窗口。
- 本地事务提交后的 MQ 失败不会自动回滚库存,需要消息补偿。
24.3 环境待确认
| 待确认项 | 为什么静态代码不能证明 | 验证方式 |
|---|---|---|
| GPDA 生产鉴权 | 可能在网关/Nginx/网络层 | 查生产路由和调用头,做无凭证拒绝测试 |
| MQ binding/重试/死信 | 部署配置不在业务代码内 | 查 RabbitMQ binding 和消费者配置 |
| 退站清盘入账方 | 提交路径未见同等库存写入 | 联调任务平台并核对库存流水 |
| GPDA 外部状态完整枚举 | 本仓库只见发送值 | 获取外部接口契约并双向核对 |
| 日志脱敏和保留周期 | 由日志平台配置决定 | 抽样生产日志和生命周期配置 |
25. 源码证据索引
| 结论 | 权威源码 |
|---|---|
| PDA 公共鉴权、日志、设备校验 | application/core/BaseAppController.php |
| GPDA 请求解析和日志 | application/core/BaseController.php 中 BaseGpdaController |
| PDA 登录、设备和首页菜单 | application/Services/Pda/UserSer.php |
| PDA 盘点控制器接口 | application/controllers/pda/TakeStock.php |
| GPDA taskNo 转换 | application/controllers/inner/gpda/TakeStock.php 构造方法 |
| PDA 盘点创建/扫码/状态 | application/Services/Pda/TakeStockSer.php |
| GPDA 盘点和消息 | application/Services/Gpda/TakeStockSer.php |
| 盘点提交前库存比较 | application/Services/InventoryCheck/InventoryCheckSer.php |
| PC 盘点审核入账 | application/service/scm/InvOiService.php::pdNewCheckUp |
| 库存流水和实时库存写入 | application/Services/Storage/InventorySer.php |
| 盘点主/明细分表模型 | application/models/app/InvCheckModel.php、InvCheckInfoModel.php |
| 状态和类型 | application/KzData/Enums/PddEnums.php |
| GPDA routing key | application/KzData/Enums/MqEventEnums.php |
| GPDA MQ 生产 | application/Services/Mq/MqSer.php |
| GPDA 关闭消费 | application/controllers/tasks/GpdaNotify.php |
| SKU 统计消费 | application/controllers/tasks/SendSkuNumMsgToOps.php |
| PDA 采购入库 | application/Services/Pda/PureSer.php::inStoIn |
| PDA 销售出库 | application/Services/Pda/SaleSer.php::saleStockOut |
| PDA 货位调整 | application/Services/Pda/AreaChangeSer.php |
| PDA 出库验货 | application/Services/Pda/OutStockCheckSer.php |
| PDA 操作日志 | application/Services/Pda/PdaOptionSer.php |
26. 一页式排查顺序
flowchart TD
A["收到 PDA/GPDA 仓内问题"] --> B["确认环境、sid、业务动作、单号/任务号"]
B --> C["查入口日志:是否到达、参数是否被公共层接受"]
C --> D["查业务主表状态"]
D --> E["按 sid 计算正确分表,查明细"]
E --> F{"动作本应改库存吗?"}
F -->|盘点扫码/提交/验货| G["不应改库存,查扫描或确认事实"]
F -->|采购入库/销售出库/货位调整/盘点审核| H["查业务单据和库存流水"]
H --> I["查实时库存四维:sid+inv+仓库+货位"]
I --> J["查 MQ/外部状态是否滞后"]
J --> K["按业务唯一键补偿,禁止重复入账"]
最重要的排查纪律是:先确定这个动作“是否本来就应该改库存”,再查主表、分表明细、库存流水和实时库存。不要从页面提示直接跳到手工改库。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| PDA 采购/销售扫描 | PDA /pda/Purchase、/pda/Sale | 登录站点、单号、条码、SKU、仓位、数量 | PDA Controller | Services/Pda/*、领域 Service | 业务单 + 扫描记录 | 校验/出入库并关联库存 |
| PDA 盘点 | /pda/TakeStock | 盘点单、条码、账面量、实盘量 | PDA TakeStock | Pda/TakeStockSer | 盘点单 + SKU | 保存实盘并在审核时调整差异 |
| GPDA 盘点/仓库 | /inner/gpda/* | token/header、盘点/仓库参数 | GPDA Controller | Gpda/TakeStockSer | GPDA 业务单 | 与盘点主明细汇合 |
| GPDA 回调/任务 | GPDA MQ、SKU 消息任务 | event、业务单、消息 ID | GpdaNotify/SendSkuNumMsgToOps | 仓内/MQ Service | 业务键 | 异步更新状态或通知 OPS |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 扫码请求 | PDA/GPDA Controller | request_id、用户、sid、条码、业务单 | 解析到唯一 SKU/单据 | 登录失效、条码多义、单据不符 | 单号+SKU 进入 Service | | 仓内校验 | Pda/Gpda Service | 盘点/出入库单、仓位、数量 | 扫描记录/明细更新 | 超量、重复扫描、仓位非法 | 主单 PK 查明细 | | 库存调整 | InventorySer | 业务单、SKU、transType、四维键 | 流水和实时量一致 | 只写盘点明细未改库、重复扣加 | 单号查库存流水 | | MQ 回调 | GpdaNotify/任务 | message/task ID、业务单 | ACK/批次完成 | NACK、部分批次失败 | 业务键回查状态与库存 |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| 登录/解析 | PDA User/Common/Controller | 事务外 | 用户站点、条码映射 | 可能的扫描会话 | 零库存变化 | request ID、用户、SKU |
| 保存盘点 | TakeStock Service | 盘点事务 | 账面量、累计实盘量 | SCM_INVENTORY_CHECK(_INFO)/盘点表 | actualQty: old -> new,状态保存中 | 盘点单、SKU、操作人 |
| 审核差异 | Pda/Gpda TakeStockSer | 审核与库存边界需确认 | diff=actual-book、当前状态 | 盘点主明细、库存流水/实时 | 差异正则库存 +diff,负则扣减;状态 -> 完成 | transType=180510、差异合计 |
| 仓内出入库 | Purchase/Sale/AreaChange Service | 领域事务 | 可操作量、仓位 | 业务明细、库存表 | 对应数量 +/-n,非库存动作保持 0 | 业务单+库存流水 |
| 异步通知 | MQ/GPDA task | commit 后 | 已完成事实 | MQ/OPS | DB 不变或只记同步态 | message ID、ACK |
子模块追踪:pda-login-common PDA 登录与公共查询
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 登录上下文 | PDA 登录/刷新用户 | device/request ID、user、sid | application/controllers/pda/User.php -> application/Services/Pda/UserSer.php | 账号状态、站点、仓库权限和 session | 登录事务外;session/token none/old -> active,业务库存不变 | request ID + user/sid + device | 失败零业务写入;凭证不入日志和文档 |
| 公共查询 | 扫码解析、仓库/货位/字典查询 | barcode、sid、query type | application/controllers/pda/Common.php -> application/Services/Pda/PdaOptionSer.php | 商品条码、仓库货位和用户范围 | 查询只读 不写 | request ID + sid + barcode/SKU | 查不到先区分条码映射与权限;不靠手工库存修复 |
子模块追踪:pda-purchase PDA 采购收货与入库
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 收货入库 | PDA 扫采购单/SKU 提交 | request ID、po billNo、SKU、location、qty | application/controllers/pda/Purchase.php -> application/Services/Pda/PureSer.php | 采购状态、可收量、已收/已关量和仓位 | 采购与库存本地事务写入库、inQty +n、实时 qty +n | request ID + po/inventory billNo + SKU + 150501 | 超收/重复/货位非法零写入;超时先查入库和流水 |
| 结果回查 | PDA 返回失败或数量不符 | po entryId、inventory billNo | application/Services/Storage/InventorySer.php | 入库单、流水、实时四维量 | 查询只读 不写;三层数量应一致 | billNos + entryId + affected rows | 只一侧成功时按事实补缺失段,不整单重放 |
子模块追踪:pda-sale PDA 销售拣货与出库
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 拣货出库 | PDA 扫销售单/SKU/货位 | sale billNo、SKU、location、qty | application/controllers/pda/Sale.php -> application/Services/Pda/SaleSer.php | 可出量、订单态、精确库存和负库存规则 | 销售出库本地事务写 invoice/流水,outQty +n、库存 qty -n | request ID + sale/invoice billNo + SKU + 150601 | 库存不足或超出可出量整单回滚;重试前查出库单 |
| 拣货回查 | 状态/库存/配送未推进 | invoice billNo、delivery key | application/Services/Storage/InventorySer.php | 出库主明细、库存流水、配送关系和后置 MQ | 查询只读;MQ/配送在 commit 后异步 | invoice/inventory billNo + routing key | 主业务成功只补后置消息;库存未成才重放领域动作 |
子模块追踪:pda-stocktake PDA 扫码盘点
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 保存实盘 | PDA 扫码录入盘点数 | stocktake billNo、SKU、location、actualQty | application/controllers/pda/TakeStock.php -> application/Services/Pda/TakeStockSer.php | 盘点状态、账面截面、已有扫描明细 | 盘点本地事务 actualQty old -> new,此时库存流水 0 | request ID + stocktake + SKU/location | 重复扫描按业务键更新/拒绝;未审核不应改实时库存 |
| 审核差异 | 盘点确认/审核 | stocktake billNo、diff | application/Services/InventoryCheck/InventoryCheckSer.php | diff=actual-book、当前盘点态和库存 | 审核与库存本地事务写 180510 流水,实时 qty +/- diff、状态完成 | billNo + SKU + diff/transType | 并发库存变化需按审核时规则重算;失败保持可重试态 |
子模块追踪:gpda-stocktake GPDA 盘点与仓库查询
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| GPDA 查询 | GPDA 查仓库/盘点任务/商品 | sid、store、stocktake ID、SKU | application/controllers/inner/gpda/GpdaStore.php | GPDA 身份、仓库范围、任务和账面库存 | 查询只读 不写 | request ID + sid/store + task/SKU | 查询口径与 PDA 区分;空结果先查仓库权限 |
| GPDA 盘点 | 保存/审核 GPDA 盘点 | task、SKU、location、actualQty | application/controllers/inner/gpda/TakeStock.php -> application/Services/Gpda/TakeStockSer.php | 任务态、已有明细、账面和差异 | 保存本地事务更新实盘;审核才库存 qty +/- diff | request ID + task + SKU + affected rows | 重复回调/提交保持一次差异流水;环境消息合同待验证 |
子模块追踪:pda-area-change 库区与货位调整
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 货位移动 | PDA 从源货位移至目标货位 | areaChange billNo、SKU、source/target、qty | application/controllers/pda/AreaChange.php -> application/Services/Pda/AreaChangeSer.php | 源精确库存、目标货位有效性和重复单 | 调整本地事务源 qty -n、目标 qty +n,写成对流水 | request ID + billNo + source/target + SKU | 目标失败必须回滚源扣减;非损耗场景总量守恒 |
| 对账回查 | 调整后货位或总量不符 | billNo、两端库存键 | application/Services/Storage/InventorySer.php | 源/目标流水和实时量 | 查询只读 不写 | billNo + both inventory keys | 只补缺失端,已完成端不重放;保留反向业务单 |
子模块追踪:pda-outcheck 出库复核
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 扫码复核 | PDA 对出库单逐品复核 | invoice billNo、SKU、scan qty、operator | application/controllers/pda/OutStockCheck.php -> application/Services/Pda/OutStockCheckSer.php | 应出、已扫、差异、单据状态 | 复核本地事务 checkedQty old -> old+n;未确认前库存不重复扣减 | request ID + invoice + SKU + checked/expected | 超扫/错品零写入;扫描记录不能再次触发出库库存 |
| 完成确认 | 全部复核完成 | invoice billNo、all-line result | application/Services/Pda/OutStockCheckSer.php | 每行已扫=应出、当前出库/配送态 | 本地事务 check status pending -> complete;后置通知 commit 后 | invoice + operator + routing key | 部分完成保持待复核;通知失败只补消息 |
子模块追踪:gpda-notify GPDA 消息回调与 OPS 通知
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| GPDA 回调 | GPDA task/event 消息 | message ID、event、task/billNo、SKU | application/controllers/tasks/GpdaNotify.php | handler、当前任务/业务态、幂等键 | 单消息本地事务 old -> new 或已完成零写入 | message ID + event + task/billNo | 未知/重复/乱序不非法推进;ACK 策略需环境确认 |
| OPS 通知 | SKU 数量等完成事实通知 OPS | business key、sid、SKU、qty | application/controllers/tasks/SendSkuNumMsgToOps.php -> application/Services/Mq/MqSer.php | 已提交库存/盘点事实和发送状态 | commit 后事务外发 MQ,核心 DB 不变 | task/message ID + sid/SKU + routing result | 发送失败只补通知;不重复执行盘点或库存动作 |