本文整理两条容易混淆的链路:一条是“蓄电池专卖服务站只能经营哪些商品”,另一条是“机器人如何按电池型号、容量等参数查询商品”。前者是经营权限,后者是查询场景,二者不能互相替代。
1. 业务目标
- 标记哪些服务站属于蓄电池专卖服务站。
- 按分类和品牌配置专卖站可经营的快准自营电池。
- 将该服务站自己的第三方商品并入可售范围。
- 缓存可售
invId和转换后的 SKU,降低多入口重复查询成本。 - 在商品列表、采购、销售、销退、采购售后、预售、导入和库存查询中统一拦截越权商品。
- 为电池专卖站隐藏不适用的菜单和普通经营入口。
- 支持无 VIN 电池参数识别和商品搜索。
- 在 Robot V1/V2 中按型号、类型、外壳、电极、容量、OE、CCA 查询和报价。
2. 核心结论
| 结论 | 说明 |
|---|---|
| 电池站身份来自主账号 | t_sys_admin 中站点主账号 roleid=0 且 isBattery=1 |
| 自营范围是分类+品牌白名单 | t_bs_battery_category_brand 关联快准基础商品 |
| 第三方范围不是分类品牌白名单 | 代码直接纳入 BS_GOODS.sid=当前站点 的全部商品 |
可售范围是 invId 集合 | 自营与第三方合并后缓存 1 小时 |
| 空集合当前不会写缓存 | 会导致每次请求重新查库,并可能在部分调用方形成“未限制”风险 |
| 页面过滤不是最终安全边界 | 采购、销售和退货提交时还会再次校验 |
| 电池查询不等于电池站权限 | Robot 的 isBattery 表示本次查询场景,不是站点身份 |
| 机器人查询依赖销售服务 | OfferProvider::searchBatteryGoods() 返回候选后再走 DGJ 商品处理、价格和库存 |
3. 两条业务链
flowchart LR
subgraph Permission["经营权限链"]
A["主账号 isBattery"] --> B["分类品牌配置"]
B --> C["生成可售 invId"]
C --> D["Redis 1小时缓存"]
D --> E["采购/销售/退货/商品列表拦截"]
end
subgraph Inquiry["电池询价链"]
F["图片/文本/AI回调"] --> G["提取电池参数"]
G --> H["OfferProvider 搜索"]
H --> I["商品/库存/价格处理"]
I --> J["机器人返回商品"]
end
两个链路可能在商品处理阶段交汇,但代码中的“电池查询场景”不能证明当前站点是专卖站,反之亦然。
4. 代码地图
4.1 经营权限
| 路径 | 责任 |
|---|---|
application/Services/BatteryUser/BatteryUserService.php | 身份判断、范围生成、Redis 缓存、差异校验和错误信息 |
application/models/bs/BatteryModel.php | 分类品牌配置关联快准商品 |
application/models/bs/AdminModel.php | 查询电池站主账号 |
application/KzData/Enums/AdminEnums.php | BATTERY_YES/BATTERY_NO |
application/KzData/Enums/KzEnums.php | 全部/自营/第三方范围类型 |
application/KzData/Enums/BatteryEnums.php | 采购/销售错误码 |
application/KzData/Enums/RedisKeys.php | 可售 invId 缓存 key |
application/config/menus_config.php | 电池站菜单显示差异 |
4.2 业务拦截
| 路径 | 场景 |
|---|---|
application/Services/PoOrders/PoMaterielSer.php | 采购商品查询和供给库存 |
application/controllers/scm/InvPo.php | 采购数量限制提交前检查 |
application/service/scm/InvSaService.php | 销售开单保存校验 |
application/Services/InvSa/NormalSaleReturnSer.php | 普通销售退货 |
application/controllers/po/AfterSale.php | 采购售后/退货 |
application/service/scm/InvPreService.php | 预售/活动商品 |
application/service/bs/InventoryService.php | Mongo/ES 商品列表和联想 |
application/Services/Materiels/MaterielFrontCacheSer.php | 前端商品缓存过滤 |
application/controllers/basedata/Import.php | 商品/业务导入过滤 |
application/Services/Import/Type/SaleOutGoodsService.php | 销售出库导入 |
application/Services/ExclusiveRebate/ExclusiveRebateSer.php | 专属返利商品范围 |
4.3 机器人查询
| 路径 | 责任 |
|---|---|
application/Services/MoveMall/RobotInquiryInput.php | V1 AI 回调与开关判断 |
application/Services/MoveMall/RobotInquirySer.php | V1 电池参数搜索和结果处理 |
application/Services/MoveMall/RobotV2/Adapters/Input/TencentImAdapter.php | V2 AI 回调标准化 |
application/Services/MoveMall/RobotV2/Services/Query/BatteryQueryService.php | V2 参数构建和 Provider 查询 |
application/Services/MoveMall/RobotV2/Services/Query/QueryDispatcher.php | 按电池场景分派 |
application/Services/MoveMall/RobotV2/Strategies/BatteryStrategy.php | 兼容旧查询策略 |
application/Providers/SaleService/OfferProvider.php | 调用销售服务电池搜索接口 |
5. 数据模型
5.1 核心表和缓存
| 类型 | 名称 | 用途 | 关键字段 |
|---|---|---|---|
| 主账号 | t_sys_admin | 电池站身份 | sid,roleid,isBattery,isDelete |
| 范围配置 | t_bs_battery_category_brand | 站点允许的分类品牌 | sid,categoryId,brandId,isDelete |
| 商品 | t_bs_goods | 自营/第三方商品 | id,sid,categoryId,brandId,skuId,isDelete |
| 商品扩展 | t_bs_goods_ext | 销售价等扩展 | sid,invId,retailPrice |
| Redis | dgj:battery_invids_sid_list:<sid><type> | 可售 invId JSON 数组 | TTL 3600 秒 |
真实前缀由 RedisKeys::PREFIX 决定,排查时从环境配置确认,不要只按文档猜 key。
5.2 关系图
erDiagram
SYS_ADMIN ||--o| BATTERY_STATION : identifies
BATTERY_STATION ||--o{ BATTERY_CATEGORY_BRAND : configures
BATTERY_CATEGORY_BRAND }o--o{ KZ_GOODS : matches_category_brand
BATTERY_STATION ||--o{ STATION_GOODS : owns_third_party
KZ_GOODS }o--o{ BATTERY_ALLOWED_IDS : contributes
STATION_GOODS }o--o{ BATTERY_ALLOWED_IDS : contributes
BATTERY_ALLOWED_IDS ||--|| REDIS_CACHE : cached_as_json
BATTERY_STATION 和 BATTERY_ALLOWED_IDS 是逻辑概念,不是独立物理表。
6. 电池站身份
6.1 判断条件
BatteryUserService::isBatteryStation($sid) 调用 AdminModel::getBatteryBySid(),条件是:
sid = 目标站点
roleid = 0
isDelete = 0
isBattery = 1
limit 1
flowchart TD
A["输入 sid"] --> B["查 t_sys_admin"]
B --> C{"存在未删除的 roleid=0 主账号且 isBattery=1"}
C -->|是| D["电池专卖服务站"]
C -->|否| E["普通服务站"]
6.2 身份风险
| 风险 | 原因 |
|---|---|
| 主账号重复 | 若同站有多条 roleid=0,limit 1 结果依赖数据一致性 |
| 主账号软删 | 新旧主账号切换时 isBattery 可能未同步 |
| 无身份缓存 | 每个入口都可能查主账号表,热点请求有额外 DB 压力 |
| 菜单与接口时点不同 | 登录会话菜单已生成后再改身份,页面可能需重新登录 |
7. 可售范围类型
枚举:KzEnums。
| 类型 | 值 | 范围 |
|---|---|---|
BATTERY_ALL | 0 | 自营白名单 + 当前站第三方商品 |
BATTERY_KZ | 1 | 只取快准自营白名单 |
BATTERY_THIRD | 2 | 只取当前站第三方商品 |
flowchart TD
A["getIdsBySid(sid,type)"] --> B{"type"}
B -->|1 自营| C["分类品牌配置 JOIN 快准商品"]
B -->|2 第三方| D["查询 BS_GOODS.sid=当前站"]
B -->|0 全部| E["array_merge 自营 + 第三方"]
C --> F["invId 数组"]
D --> F
E --> F
当前合并使用 array_merge,没有显式 array_unique。如果同一 ID 因历史数据异常重复,缓存可包含重复项,通常不影响 in_array/array_diff 语义,但会增加 key 大小和 SQL IN 长度。
8. 自营可售范围
8.1 查询语义
BatteryModel::getIdsBySid():
SELECT b.id
FROM t_bs_battery_category_brand a
LEFT JOIN t_bs_goods b ON b.categoryId = a.categoryId
WHERE a.sid = :sid
AND a.brandId = b.brandId
AND b.sid = 1
AND b.isDelete = 0
AND a.isDelete = 0;
其中 b.sid=KzEnums::KZ_SID 表示快准基础商品。
8.2 匹配逻辑
flowchart LR
A["站点配置 categoryId + brandId"] --> B["快准 BS_GOODS"]
B --> C{"分类和品牌同时相等"}
C -->|是| D["加入自营可售 invId"]
C -->|否| E["排除"]
可售范围按商品当前分类和品牌动态展开。商品中心修改分类/品牌后,即使配置表不变,下一次缓存重建结果也会变化。
8.3 不在这里判断的条件
该查询只看分类、品牌、商品未删除,不看:
- 商品供应状态。
- 当前库存是否大于 0。
- 采购价格是否存在。
- 区域/站点是否可供。
- 活动、限购或价格限制。
这些条件由后续商品、采购或销售 Service 继续判断。可售白名单只是第一层。
9. 第三方可售范围
第三方闭包查询:
BS_GOODS where sid = 当前站点,返回全部 id
代码未在该层显式增加 isDelete=0。是否由 GoodsModel::getList() 默认过滤,需要结合 Model 实现确认;生产排查不能默认已过滤。
9.1 业务含义
电池站自己的第三方商品全部进入范围,不受 t_bs_battery_category_brand 约束。这意味着配置表只治理快准自营商品,并不是整个站点商品的分类品牌白名单。
10. Redis 缓存
10.1 Key
RedisKeys::BATTERY_INVIDS_SID_LIST . sid . type
注意 sid 和 type 直接拼接,中间没有显式分隔符。若 sid/type 位数或类型扩展,存在 key 可读性和碰撞设计风险;当前 type 仅 0/1/2 时一般可区分。
10.2 读写流程
sequenceDiagram
participant Caller as 业务入口
participant Battery as BatteryUserService
participant Redis as Redis
participant DB as 配置/商品表
Caller->>Battery: getInvIdsCacheBySid(sid,type,useCache)
alt useCache=true
Battery->>Redis: GET key
alt 命中非空字符串
Redis-->>Battery: JSON invIds
Battery-->>Caller: json_decode 结果
else 未命中
Battery->>DB: getIdsBySid
DB-->>Battery: invIds
Battery->>Redis: 非空才 SET TTL=3600
Battery-->>Caller: invIds
end
else useCache=false
Battery->>DB: 强制重建
Battery->>Redis: 非空才覆盖
Battery-->>Caller: invIds
end
10.3 空集合问题
当数据库计算结果为空时,代码不写缓存,也不删除旧缓存:
- 若旧 key 仍存在且
useCache=true,会继续返回旧范围直到 TTL。 - 若 key 不存在,每次请求都会查数据库。
- 某些调用方只有在
$batteryIds非空时才附加过滤,空数组可能被解释为“不过滤”,而业务上可能应是“一个商品都不允许”。
空集合语义必须统一:电池站无配置时到底是禁止全部、允许全部还是配置异常。当前不同调用方式可能产生不同结果。
11. 缓存一致性
stateDiagram-v2
[*] --> Missing
Missing --> Valid: 首次计算非空并写入
Valid --> Stale: 身份/配置/商品变化
Stale --> Valid: TTL 到期后重建
Stale --> Valid: useCache=false 强制重建
Valid --> EmptyConfig: 配置被清空
EmptyConfig --> Stale: 旧 key 仍在 TTL 内
EmptyConfig --> Missing: key 到期且空值不缓存
建议配置写入后主动删除三个 type key,而不是等待 1 小时;空集合也应缓存短 TTL 的 [],并由调用方明确执行“禁止全部”或指定业务语义。
12. 可售 SKU
getSkuIdsBySid() 先获取可售 invId,再调用商品 Model 转为 skuId。
flowchart LR
A["sid + type"] --> B["Redis/DB invIds"]
B --> C["getGoodsBaseCollectionByInvIds"]
C --> D["array_column skuId"]
SKU 用于调用商品中心或供给服务;invId 用于 DGJ 本地业务。排查时必须区分两者,不能把 SKU 当表主键。
13. 通用校验方法
13.1 checkDiffInvIds
异常商品 = 请求 invIds - 电池站全部可售 invIds
它不先判断站点身份,调用方应先调用 isBatteryStation()。
13.2 checkGoodsMessage
对异常 invId 回查 skuId、productCode,并通过其在原始数组的位置返回 1 起始行号。
返回示意:
[
{
"row": 2,
"skuId": "SKU-EXAMPLE",
"productCode": "PRODUCT-EXAMPLE"
}
]
若原始数组含重复 invId,array_search 只返回第一次位置。
13.3 valideGoods
flowchart TD
A["sid + invIds"] --> B{"是否电池站"}
B -->|否| C["直接通过"]
B -->|是| D["计算差集"]
D -->|空| C
D -->|非空| E["取第一个异常商品"]
E --> F["抛出 第N行+SKU+业务消息"]
该方法只在错误文字中使用第一条异常,但完整错误列表可由 checkGoodsMessage 获取。
14. 商品列表过滤
14.1 Mongo 商品列表
InventoryService::getGoodsListNew() 根据查询类型决定范围:
| 页面类型 | 电池 type | 商品范围 |
|---|---|---|
all | 0 | 自营 + 第三方 |
kz | 1 | 只限自营白名单 |
| 第三方分支 | 2 | 当前站商品 |
如果页面已传 ids,则与电池可售 IDs 取交集;未传时直接用可售 IDs 作为查询条件。
flowchart TD
A["Mongo 商品查询"] --> B{"是否电池站"}
B -->|否| C["按普通条件查询"]
B -->|是| D["按页面 type 取可售 IDs"]
D --> E{"请求是否已有 IDs"}
E -->|是| F["intersection"]
E -->|否| G["使用全部可售 IDs"]
F -->|空| H["立即返回空分页"]
F --> I["Mongo id IN"]
G --> I
14.2 ES 联想
快准商品联想只使用 BATTERY_KZ,将可售 IDs 放入 ES terms id。因此 Mongo、ES 和 DB 的 ID 类型(字符串/整数)必须兼容。
14.3 前端缓存
MaterielFrontCacheSer 对电池站也会限制商品,若前端缓存构建早于范围变化,可能出现列表与提交校验不一致。
15. 采购商品查询
PoMaterielSer::queryPoMaterielByInvIds():
sequenceDiagram
participant UI as 采购页面
participant Po as PoMaterielSer
participant Battery as BatteryUserService
participant Cache as MaterielCacheStation
participant Item as 商品中心
UI->>Po: sid + invIds
Po->>Battery: isBatteryStation
alt 电池站
Po->>Battery: getInvIdsCacheBySid(type=ALL)
Po->>Po: 请求 IDs 与可售 IDs 取交集
alt 交集为空
Po-->>UI: 未查询物料信息
end
end
Po->>Cache: 查询本地采购物料
Po->>Item: 查询可采购商品和供给
Po-->>UI: 价格、库存、箱规等
这里只保留交集,若请求同时包含可售和不可售商品,可能只返回可售部分,而不是明确指出哪些被过滤。提交前 quantityLimitCheck 会返回具体错误行。
16. 采购提交前校验
接口方法:scm/InvPo::quantityLimitCheck()。
POST /scm/invpo/quantitylimitcheck
Content-Type: application/x-www-form-urlencoded
JXCSID=10001&goods=[{"invId":30001,"num":2}]
16.1 错误响应语义
{
"status": "error",
"message": "物料不允许采购",
"data": {
"errorData": [
{"row": 1, "skuId": "SKU-EXAMPLE", "productCode": "PRODUCT-EXAMPLE"}
],
"errorCode": "battery_po_error"
}
}
具体响应外壳由 splashJson 决定,示例重点是 errorData 和 battery_po_error。
17. 特殊采购限制
InvPoService 还有“订单中存在特定蓄电池,不能走普通采购,需到快准营销活动模块下单”的校验。这属于商品/活动级采购通道限制,不等同于电池站可售范围。
flowchart TD
A["采购商品已通过电池站范围"] --> B{"是否属于指定活动电池"}
B -->|是| C["拒绝普通采购,指引活动采购"]
B -->|否| D["继续采购订单校验"]
排查“明明在白名单仍不能采购”时要继续看活动通道限制。
18. 销售开单校验
InvSaService 在保存链路再次执行:
若为电池站:
batteryInvIds = 可售范围
diffIds = 请求销售 invIds - batteryInvIds
diffIds 非空 -> 返回/抛出电池销售错误
sequenceDiagram
participant UI as 销售开单
participant Sa as InvSaService
participant Battery as BatteryUserService
participant Order as 销售订单
UI->>Sa: 提交商品明细
Sa->>Battery: 判断电池站并取可售 IDs
Sa->>Sa: array_diff
alt 有越权商品
Sa-->>UI: BATTERY_SA_ERROR / 商品行信息
else 全部允许
Sa->>Order: 继续价格、库存、客户、单据校验
end
前端商品列表与销售提交之间存在时间窗口:缓存或配置变化后,提交校验可能拒绝此前已选商品,这是正确的后端兜底,但页面应明确刷新和错误行。
19. 销售退货与采购售后
| 场景 | 入口 | 行为 |
|---|---|---|
| 普通销退 | NormalSaleReturnSer | 电池站退货商品必须仍在允许范围 |
| 采购售后 | controllers/po/AfterSale.php | 调用 valideGoods() 拦截不允许退货商品 |
这里存在业务问题:历史成交时允许、后来配置移除的商品,是否还应允许退货?当前校验使用“现在的可售范围”,可能阻止合法历史退货。生产口径需确认是否应按原单来源放行。
flowchart TD
A["退货商品"] --> B{"当前仍在电池可售范围"}
B -->|是| C["继续原单/数量/库存校验"]
B -->|否| D{"业务是否应按历史原单放行"}
D -->|当前代码通常否| E["拒绝退货"]
D -->|建议规则| F["验证原单当时合法后放行"]
20. 预售与活动
InvPreService 对电池站会取可售 IDs,并用于活动计划商品查询。含义是活动商品基础范围也不能绕过专卖站经营范围。
ExclusiveRebateSer 根据自营/第三方类型取不同电池范围,再与专属返利商品相交。
flowchart LR
A["活动/返利候选商品"] --> B["活动自身范围"]
C["电池站可售范围"] --> D["intersection"]
B --> D
D --> E["最终可参与商品"]
21. 导入链路
导入必须处理两类问题:
- 模板中的商品是否存在。
- 商品存在但是否属于电池站可经营范围。
多个导入入口会先获取电池 IDs,再逐行排除不允许商品。排查时要区分“导入解析失败”和“经营范围校验失败”。错误文件应保留行号、SKU、产品码和原因,不应只返回空结果。
22. 菜单影响
menus_config.php 中:
$battery_black_list = isBatteryStation($sid) ? false : true;
变量名表示“电池黑名单控制”,实际值可理解为“非电池站是否显示”。例如首配采购菜单 display=$battery_black_list:
| 站点 | battery_black_list | 首配采购显示 |
|---|---|---|
| 电池站 | false | 隐藏 |
| 普通站 | true | 显示 |
flowchart TD
A["登录生成菜单"] --> B{"isBatteryStation"}
B -->|是| C["battery_black_list=false"]
B -->|否| D["battery_black_list=true"]
C --> E["隐藏首配等不适用入口"]
D --> F["显示普通入口"]
菜单隐藏不是权限安全,后端接口仍需校验站点身份和商品范围。
23. 电池机器人查询输入
23.1 支持字段
| 字段 | 含义 | 处理 |
|---|---|---|
productModel | 产品型号 | 原样传给销售服务 |
nsModel | 国家标准/电池型号 | 原样传递 |
batteryType | 启停、免维护等类型 | 原样传递 |
shellModel | 外壳型号 | 原样传递 |
electrode | 电极/极性 | 原样传递 |
ampereHour | 容量 Ah | 只保留数字 |
oeCode | OE 编码 | 原样传递 |
cca | 冷启动电流 | 原样传递 |
brandIds | 品牌过滤 | 逗号拼接为 brand_id |
23.2 分类
Robot 常量中:
| 分类编码 | 名称 |
|---|---|
C20101 | 启停蓄电池 |
C20102 | 免维护蓄电池 |
C20103 | 辅助电池 |
旧无车关键字映射明确包含前两类;辅助电池在不同枚举列表中存在,但完整无车查询支持范围需实测。
24. AI 解析开关
V1 读取客户配置:
| 字段 | 作用 |
|---|---|
enable_ai_parse | AI 解析总开关 |
enable_ai_parse_battery | 电池细分开关 |
enable_ai_parse_oil | 油品开关 |
enable_ai_parse_tire | 轮胎开关 |
flowchart TD
A["收到 AI 异步回调"] --> B{"enable_ai_parse=1"}
B -->|否| X["忽略解析结果"]
B -->|是| C{"productType=电池"}
C -->|否| D["走油品/轮胎/其他分支"]
C -->|是| E{"enable_ai_parse_battery=1"}
E -->|否| X
E -->|是| F["保存完整 aiParseResult"]
F --> G["进入无 VIN 电池查询"]
“客户”在这里是修理厂联系人配置,不是电池专卖服务站身份。
25. V2 AI 回调
腾讯 IM ai_callback 示例结构:
{
"msgType": "ai_callback",
"msgData": {
"ai_result": {
"isPart": true,
"productType": 2,
"data": {
"nsModel": "6-QW-60",
"batteryType": "免维护",
"shellModel": "L2",
"electrode": "正装",
"ampereHour": "60Ah",
"oeCode": "",
"cca": ""
}
},
"pre_conversation_id": 80001
}
}
25.1 适配流程
sequenceDiagram
participant AI as AI 识别
participant Adapter as TencentImAdapter
participant Metadata as MessageContext
participant State as ProductQueryingState
AI->>Adapter: ai_callback
Adapter->>Adapter: normalize productType=2 -> battery
Adapter->>Metadata: INTENT_AI_PARSE_RESULT
Adapter->>Metadata: INTENT_AI_QUERY_TYPE=battery
Adapter->>Metadata: INTENT_BATTERY_PARAMS 全字段
Metadata->>State: 构建 QueryContext
State->>State: hints.isBattery=true
电池与油品/轮胎不同:它不把某一个规格简单改写为普通文本,而是保留完整结构化参数。
26. V2 电池查询
BatteryQueryService::buildParams() 构造:
{
"sid": 10001,
"customer_id": 20001,
"page": 1,
"limit": 20,
"productModel": "",
"nsModel": "6-QW-60",
"batteryType": "免维护",
"shellModel": "L2",
"electrode": "正装",
"ampereHour": "60",
"oeCode": "",
"cca": "",
"brand_id": "601,602"
}
26.1 调用链
sequenceDiagram
participant State as ProductQueryingState
participant Dispatcher as QueryDispatcher
participant Battery as BatteryQueryService
participant Provider as OfferProvider
participant Sale as 销售服务
State->>Dispatcher: hints.isBattery=true
Dispatcher->>Battery: query(QueryContext)
Battery->>Battery: buildParams + Ah 只保留数字
Battery->>Provider: searchBatteryGoods
Provider->>Sale: HTTP 请求
Sale-->>Provider: rows/product_codes
Provider-->>Battery: 搜索结果
Battery->>Battery: 给商品附加 battery_info
Battery-->>Dispatcher: products
limit 默认 20。是否还有服务端最大值和分页能力需要结合销售服务契约确认。
27. V1 电池查询
V1 RobotInquirySer::searchBatteryGoods():
- 从 AI 解析结果提取电池参数。
- 所有核心参数都为空时直接认为无法搜索。
- 根据品牌名称查品牌编码并拼入参数。
- 调用同一个
OfferProvider::searchBatteryGoods()。 - 将返回商品交给
MoveMallGoodsSer::moveGoodsHandler()补价格、库存和可售信息。 - 补商品简称、标签和
battery_info。 - 先按分类品牌排序配置,再按同品牌年销量降序。
flowchart TD
A["AI 电池参数"] --> B{"至少一个参数非空"}
B -->|否| X["无法搜索"]
B -->|是| C["解析品牌编码"]
C --> D["OfferProvider 搜索"]
D --> E{"rows 是否为空"}
E -->|是| F["返回 product_codes + 空商品"]
E -->|否| G["MoveMallGoodsSer 处理价格库存"]
G --> H["补简称和 battery_info"]
H --> I["品牌排序 + 年销量排序"]
I --> J["机器人展示"]
28. 电池查询结果与经营范围
销售服务搜索结果进入 MoveMallGoodsSer 后还会走站点、客户、价格、库存等过滤。是否明确复用 BatteryUserService 的经营范围,应沿真实 moveGoodsHandler 路径确认。
必须分别验证:
| 条件 | 预期 |
|---|---|
| 普通站发起电池查询 | 可按普通站商品规则查询,不因 isBatteryStation=false 禁止查询 |
| 电池站查询白名单商品 | 正常返回并报价 |
| 电池站查询非白名单自营商品 | 应被经营范围过滤 |
| 电池站查询自己的第三方商品 | 按第三方规则可返回 |
| AI 查询到商品但无价格 | 可展示无价/转人工,不能伪造价格 |
29. 状态和错误码
29.1 身份状态
| 值 | 含义 |
|---|---|
AdminEnums::BATTERY_NO=0 | 普通站 |
AdminEnums::BATTERY_YES=1 | 蓄电池专卖站 |
29.2 错误码
| 错误码 | 场景 |
|---|---|
battery_po_error | 采购商品不在允许范围 |
battery_sa_error | 销售商品不在允许范围 |
29.3 Robot 会话
| 值 | 含义 |
|---|---|
CONVERSATION_NO_VIN_BATTERY=4 | 无 VIN 电池会话 |
CONVERSATION_PARSE_PRODUCT_TYPE_BATTERY=2 | AI 产品类型为电池 |
30. 配置变更流程
仓库中未发现独立、明确的电池范围管理 Controller,需要继续确认配置由 OPS、SQL、外部同步还是隐藏页面维护。无论入口在哪,正确流程应是:
flowchart TD
A["修改站点 isBattery 或分类品牌"] --> B["事务保存配置"]
B --> C["删除 sid+0/1/2 三个 Redis key"]
C --> D["重建并统计新范围"]
D --> E["与旧范围做新增/移除 diff"]
E --> F["验证 Mongo/ES/采购/销售"]
F --> G["通知站点重新登录刷新菜单"]
配置变更应输出被新增和移除的 SKU 清单,避免批量分类品牌变化无感扩大经营范围。
31. 完整排查 SOP
用户报告“这个电池为什么不能采购/销售”时:
flowchart TD
A["确认 sid + invId/SKU + 场景"] --> B{"是否电池站"}
B -->|否| C["转普通商品/供给/价格排查"]
B -->|是| D{"商品是快准自营还是本站第三方"}
D -->|自营| E["查商品 categoryId+brandId"]
E --> F["查站点分类品牌配置"]
D -->|第三方| G["查 BS_GOODS.sid 和删除状态"]
F --> H["计算 DB 可售集合"]
G --> H
H --> I["对比 Redis type=0/1/2"]
I --> J{"业务入口是否使用正确 type"}
J -->|否| K["修调用参数"]
J -->|是| L{"是否还有活动/供给/库存/价格限制"}
L -->|是| M["进入对应业务专题"]
L -->|否| N["检查前端/ES/Mongo缓存"]
32. 常见问题
32.1 配置了分类品牌仍不可售
检查顺序:
- 站点主账号是否确实
isBattery=1。 - 配置
sid/categoryId/brandId/isDelete。 - 商品是否是快准
sid=1,分类品牌是否完全相等。 - 商品是否已删除。
- Redis 自营 key 是否仍是旧值。
- 商品供给、库存、价格和活动通道限制。
32.2 删除配置后仍能看到
可能原因:旧 Redis key 最长保留 1 小时;前端/Mongo/ES 还有二级缓存;空结果不覆盖旧缓存。
32.3 商品列表看不到但提交报错行不同
列表可能已按交集静默过滤,提交按原始顺序返回差异;同时 checkGoodsMessage 对重复 ID 只返回第一次行号。
32.4 电池查询有结果但下单不允许
查询场景只负责找到候选,最终还要过电池站经营范围、商品供给、价格、库存和销售限制。
32.5 历史商品无法退货
当前可售范围已经移除该商品,而退货代码按当前范围校验。需要业务确认是否按原单合法性放行,不能直接加回白名单掩盖问题。
33. 只读 SQL
33.1 身份
SELECT uid, sid, roleid, isBattery, isDelete, status
FROM t_sys_admin
WHERE sid = :sid
AND roleid = 0
ORDER BY uid;
33.2 范围配置
SELECT id, sid, categoryId, brandId, isDelete
FROM t_bs_battery_category_brand
WHERE sid = :sid
ORDER BY categoryId, brandId;
33.3 自营展开
SELECT c.id AS config_id,
c.categoryId,
c.brandId,
g.id AS inv_id,
g.skuId,
g.productCode,
g.name
FROM t_bs_battery_category_brand c
JOIN t_bs_goods g
ON g.categoryId = c.categoryId
AND g.brandId = c.brandId
WHERE c.sid = :sid
AND c.isDelete = 0
AND g.sid = 1
AND g.isDelete = 0
ORDER BY c.categoryId, c.brandId, g.id;
33.4 第三方商品
SELECT id AS inv_id, skuId, productCode, categoryId, brandId, isDelete
FROM t_bs_goods
WHERE sid = :sid
ORDER BY id;
33.5 检查配置无商品
SELECT c.id, c.categoryId, c.brandId
FROM t_bs_battery_category_brand c
LEFT JOIN t_bs_goods g
ON g.sid = 1
AND g.categoryId = c.categoryId
AND g.brandId = c.brandId
AND g.isDelete = 0
WHERE c.sid = :sid
AND c.isDelete = 0
GROUP BY c.id, c.categoryId, c.brandId
HAVING COUNT(g.id) = 0;
34. Redis 排查
先从配置读取真实前缀和 Redis DB,再执行只读命令:
redis-cli GET '<prefix>battery_invids_sid_list:<sid>0'
redis-cli TTL '<prefix>battery_invids_sid_list:<sid>0'
redis-cli GET '<prefix>battery_invids_sid_list:<sid>1'
redis-cli GET '<prefix>battery_invids_sid_list:<sid>2'
不要在未确认环境时直接 DEL。生产刷新应通过受控脚本记录 sid、旧值摘要、新值数量和操作者。
34.1 集合对账
ALL 集合 ≈ KZ 集合 ∪ THIRD 集合
请求异常集合 = requestInvIds - ALL 集合
自营 DB 集合 = 配置分类品牌 JOIN 快准商品
由于当前代码未显式去重,数量对账应同时比较 count 和 count(unique)。
35. 代码检索
rg -n "isBatteryStation|getInvIdsCacheBySid|checkDiffInvIds|valideGoods" application
rg -n "BATTERY_INVIDS_SID_LIST|SCM_BATTERY_SID|BATTERY_PO_ERROR|BATTERY_SA_ERROR" application
rg -n "CONVERSATION_NO_VIN_BATTERY|INTENT_BATTERY_PARAMS|searchBatteryGoods" \
application/Services/MoveMall application/Providers
rg -n "enable_ai_parse_battery|batteryQueryParams|BatteryQueryService" application
36. 故障树
flowchart TD
A["电池业务异常"] --> B{"经营权限还是查询识别"}
B -->|经营权限| C{"身份是否正确"}
C -->|否| C1["主账号 isBattery/重复主账号"]
C -->|是| D{"范围是否正确"}
D -->|否| D1["分类品牌/第三方归属/删除状态"]
D -->|是| E{"缓存是否一致"}
E -->|否| E1["TTL/空集合/未主动失效"]
E -->|是| F["业务入口 type/供给/库存/价格/活动"]
B -->|查询识别| G{"AI 电池开关和参数是否完整"}
G -->|否| G1["总开关/细分开关/AI结果"]
G -->|是| H{"销售服务是否返回 rows"}
H -->|否| H1["参数映射/品牌/Ah/OE/CCA"]
H -->|是| I["MoveMall 商品后处理和报价过滤"]
37. 已识别风险
| 编号 | 风险 | 影响 |
|---|---|---|
| R42-01 | 身份查询依赖 roleid=0 单条主账号 | 重复/切换主账号时身份可能不稳定 |
| R42-02 | 第三方范围查询在 Service 层未显式过滤删除 | 删除商品可能进入候选,取决于 Model 默认条件 |
| R42-03 | 自营+第三方合并未去重 | 缓存和 SQL IN 可能膨胀 |
| R42-04 | Redis key 的 sid/type 无分隔符 | 可读性差,未来 type 扩展有碰撞风险 |
| R42-05 | 空结果不写缓存 | 每次查库;调用方可能把空理解为不过滤 |
| R42-06 | 空结果不覆盖旧 key | 清空配置后旧范围仍可使用到 TTL |
| R42-07 | 无配置变更主动失效证据 | 最长 1 小时不一致 |
| R42-08 | 多入口各自实现交集/差集 | 空集合、错误提示和 type 语义可能不一致 |
| R42-09 | 列表静默过滤、提交硬错误 | 用户体验和排查结果不一致 |
| R42-10 | 当前范围限制历史退货 | 合法历史交易可能无法退货 |
| R42-11 | 菜单配置在登录期计算 | 身份变更后需重登,且隐藏不等于授权 |
| R42-12 | checkGoodsMessage 重复 ID 只定位首行 | 导入/批量错误行可能不完整 |
| R42-13 | 电池 AI 查询开关在客户配置 | 易与电池站身份混淆 |
| R42-14 | ampereHour 删除所有非数字 | 小数、范围或多容量表达可能失真 |
| R42-15 | V1/V2 电池查询实现并存 | 参数、排序、异常降级可能不一致 |
| R42-16 | BatteryStrategy 反射调用旧私有能力 | 旧实现改名/签名会在运行时失败 |
| R42-17 | Provider 异常和空结果语义需区分 | 外部故障可能被显示为“无商品” |
| R42-18 | 查询候选与经营权限链不一定同层执行 | 可能查到后在报价/下单才被拒绝 |
38. 回归清单
38.1 身份与范围
- [ ] 普通站、电池站、主账号软删、主账号切换。
- [ ] 自营单分类单品牌、多分类多品牌。
- [ ] 分类匹配品牌不匹配、品牌匹配分类不匹配。
- [ ] 配置已删除、商品已删除、商品改分类/品牌。
- [ ] 当前站第三方商品和其他站第三方商品。
- [ ] ALL/KZ/THIRD 三类集合及去重。
- [ ] 无配置、空集合和旧缓存。
38.2 缓存
- [ ] 首次未命中重建。
- [ ] 1 小时 TTL。
- [ ]
useCache=false强制刷新。 - [ ] 配置新增、删除、清空后的三个 type key。
- [ ] Redis 不可用时的降级和错误表现。
- [ ] 大范围站点 key 大小和序列化耗时。
38.3 商品和业务
- [ ] Mongo 列表 all/kz/third。
- [ ] ES 商品联想。
- [ ] 前端缓存。
- [ ] 采购查询和供给库存。
- [ ] 采购
quantityLimitCheck错误行。 - [ ] 活动专用电池不能走普通采购。
- [ ] 销售开单及
battery_sa_error。 - [ ] 销售退货和采购售后。
- [ ] 预售、活动、专属返利。
- [ ] Excel 导入与错误文件。
38.4 Robot
- [ ] AI 总开关和电池细分开关。
- [ ] V1/V2
productType=2。 - [ ] 型号、国标型号、类型、外壳、电极、Ah、OE、CCA 单字段和组合。
- [ ] Ah 带单位、空格、范围和小数。
- [ ] 品牌单选、多选和无匹配。
- [ ] 销售服务成功、空 rows、超时、异常。
- [ ] 电池站白名单内/外商品。
- [ ] 普通站电池查询。
- [ ] 商品有库存有价、无库存、无价。
- [ ] 品牌排序和同品牌销量排序。
39. 改动影响面
| 改动 | 最小回归 |
|---|---|
isBatteryStation | 菜单、所有商品查询、采购、销售、退货、导入 |
| 分类品牌表 | 三类缓存、Mongo/ES、采购销售、历史退货 |
| Redis key/TTL | 所有依赖范围的入口和刷新工具 |
getIdsBySid | 自营/第三方边界、删除状态、去重 |
checkDiffInvIds/valideGoods | 行号、错误码、批量导入和退货 |
BatteryQueryService | 腾讯 IM AI 回调、参数映射、销售服务契约 |
RobotInquirySer | V1 微信查询、排序、商品后处理和报价 |
MoveMallGoodsSer | 电池、轮胎、油品及普通询价共同回归 |
40. 治理建议
40.1 统一范围对象
将散落的数组返回升级为明确对象:
BatterySaleScope
stationId
isBatteryStation
kzInvIds
thirdInvIds
allInvIds
sourceVersion
generatedAt
emptyPolicy
调用方不再用“数组是否为空”猜业务语义。
40.2 主动失效
配置、身份、商品分类品牌或删除状态变化后,发布 battery_scope_changed 事件,统一失效 Redis、Mongo/ES/前端缓存,并记录范围 diff。
40.3 历史退货例外
退货校验应优先验证原销售/采购单中的商品和数量,再决定是否受当前经营范围限制。允许历史合法交易退货,不代表允许新采购或销售。
40.4 查询与经营权限显式分层
Robot 的 isBattery 建议改名为 isBatteryQueryScenario,避免与 isBatteryStation 混淆。
41. 生产待确认项
- 电池站身份和分类品牌配置的真实管理入口、Owner 与审批流程。
t_sys_admin是否保证每站唯一有效roleid=0主账号。t_bs_battery_category_brand的唯一索引和全部字段。GoodsModel::getList()是否默认过滤第三方已删除/禁用商品。- 空配置的正式业务语义:禁止全部、允许全部还是配置异常。
- 配置变更是否已有 Redis 主动失效机制。
- 各入口在 Redis 故障时是查 DB、放行还是拒绝。
- 电池范围最大
invId数量、Redis value 大小和 SQL/ES terms 上限。 - 历史退货是否必须受当前可售范围限制。
- 电池站菜单隐藏的完整清单与后端接口权限。
- 销售服务
searchBatteryGoods的完整请求/响应、错误码、超时和鉴权。 ampereHour、cca的单位、范围和模糊匹配规则。- 辅助电池
C20103是否完整支持无 VIN 查询。 - Robot V1/V2 当前流量比例和 BatteryStrategy 兼容链是否仍启用。
MoveMallGoodsSer是否在所有平台统一执行电池站经营范围。
42. 证据索引
| 主题 | 代码路径 |
|---|---|
| 电池站核心服务 | application/Services/BatteryUser/BatteryUserService.php |
| 自营范围 SQL | application/models/bs/BatteryModel.php |
| 主账号身份 | application/models/bs/AdminModel.php |
| Redis key | application/KzData/Enums/RedisKeys.php |
| 范围类型 | application/KzData/Enums/KzEnums.php |
| 错误码 | application/KzData/Enums/BatteryEnums.php |
| 菜单 | application/config/menus_config.php |
| 采购查询 | application/Services/PoOrders/PoMaterielSer.php |
| 采购校验 | application/controllers/scm/InvPo.php |
| 销售校验 | application/service/scm/InvSaService.php |
| 销退 | application/Services/InvSa/NormalSaleReturnSer.php |
| 采购售后 | application/controllers/po/AfterSale.php |
| 商品列表 | application/service/bs/InventoryService.php |
| 前端商品缓存 | application/Services/Materiels/MaterielFrontCacheSer.php |
| 预售 | application/service/scm/InvPreService.php |
| 返利 | application/Services/ExclusiveRebate/ExclusiveRebateSer.php |
| V1 AI 输入 | application/Services/MoveMall/RobotInquiryInput.php |
| V1 电池搜索 | application/Services/MoveMall/RobotInquirySer.php |
| V2 AI 适配 | application/Services/MoveMall/RobotV2/Adapters/Input/TencentImAdapter.php |
| V2 查询 | application/Services/MoveMall/RobotV2/Services/Query/BatteryQueryService.php |
| 销售服务 Provider | application/Providers/SaleService/OfferProvider.php |
43. 最终理解
flowchart LR
A["身份准确"] --> F["电池业务正确"]
B["经营范围准确"] --> F
C["缓存及时一致"] --> F
D["各业务入口统一硬校验"] --> F
E["查询参数和外部搜索可靠"] --> F
电池专题的正确性不是“页面只显示电池”这么简单。它要求身份、范围、缓存、搜索、采购、销售和退货在同一时刻对同一个商品给出一致结论;同时又要把“客户正在询问电池”与“服务站是电池专卖站”分开建模,避免查询能力和经营权限互相污染。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| 电池站身份维护 | OPS/用户管理 | sid、管理员/站点类型、启停 | 管理入口 | BatteryUserService | station/admin ID | 电池专卖身份和可售范围配置 |
| 商品查询/询价 | PC/E站/Robot | sid、SKU/关键词、询价场景 | 商品/Robot Controller | Materiel Cache/Robot Inquiry | sid+SKU | 身份范围过滤后的电池商品 |
| 采购/预订单 | InvPo/InvPreService | 电池站、SKU、数量 | 采购/预订单 Controller | PoMaterielSer/采购 Service | purchase/preorder bill | 仅允许范围内商品采购 |
| 销售/退货/导入 | InvSaService/Return/Import | 销售/原单、SKU、数量 | 销售/导入入口 | Sale/Return/Import Service | sale/return bill | 销售和退货沿同一商品身份规则 |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 身份判定 | BatteryUserService | request_id、sid、admin type、battery flag | DB/Redis/Session 结论一致 | 身份缓存旧、角色与站点混淆 | sid 进入商品范围判断 | | 搜索过滤 | MaterielFrontCache/Robot | sid、SKU、query、scope key | 仅返回允许电池商品 | ES/cache/DB 口径不同 | SKU 进入采购销售校验 | | 业务校验 | Po/Sa/Return Service | business bill、sid、SKU、battery category | 合法业务 commit | 页面可见但下单拒绝或反之 | billNo 查主明细 | | 配置刷新 | Battery Service/Cache | station、config version、Redis key | 变更后各入口同时生效 | 局部缓存未清 | version/key 与请求时间对照 |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| 配置身份 | BatteryUserService/BatteryModel/AdminModel | 配置事务 | 站点/管理员原类型 | 电池/管理员配置表、Redis | battery flag/type old -> new | sid/admin、操作人、更新时间 |
| 刷新范围 | Battery/Cache Service | commit 后缓存操作 | 新身份、商品分类/范围 | Redis/商品缓存/搜索 | scope key/doc old -> new | key version、DB/缓存集合差 |
| 查询校验 | Materiel/Robot Service | 只读 | 用户询问场景、站点经营身份、SKU 属性 | 无 | 两种身份分别计算;业务表 0 变化 | sid+SKU+scene 判定结果 |
| 采购销售 | Po/Sa Service | 领域事务 | 身份、范围、订单商品 | 采购/销售主明细 | 合法才 insert;状态/数量按正常链路 | billNo、SKU、身份快照 |
| 退货 | NormalSaleReturnSer | 退货事务 | 原销售事实而非当前搜索范围 | 退货/库存表 | returnQty +n、库存 +n;不因后续身份变化阻断合法退货 | original+return bill、库存流水 |
子模块追踪:battery-identity 电池站身份
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 配置身份 | OPS/管理员设置自营或第三方电池站 | sid/admin、battery type/flag、operator | application/Services/BatteryUser/BatteryUserService.php | 站点/管理员原类型、资格和使用中订单 | 配置本地事务 battery flag/type old -> new | request ID + sid/admin + old/new type | 非法组合零写入;身份变更不重写历史订单 |
| 身份回查 | 页面/接口识别不一致 | sid/admin、cache/type version | application/models/bs/BatteryModel.php、application/models/bs/AdminModel.php | DB 双表配置、Redis 和用户上下文 | 查询只读;commit 后缓存 old -> latest | sid/admin + DB/cache versions | 双表不一致先确定权威来源;只补派生缓存 |
子模块追踪:battery-scope 自营与第三方可售范围
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 范围计算 | 商品查询/订单提交 | sid、identity type、SKU/category | application/KzData/Enums/BatteryEnums.php | 电池身份、商品属性、经营范围和例外规则 | 查询只读;分别计算 allow/deny | request ID + sid/type + SKU + matched rule | 两种身份规则不可合并兜底;无命中明确不可售 |
| 范围维护 | 商品/站点范围配置变更 | scope ID、type、SKU set | application/Services/BatteryUser/BatteryUserService.php | 原范围、商品状态和冲突 | 配置本地事务 old scope set -> new | request ID + scope/type + item count | 全量替换失败整批回滚;历史单据不受当前范围改写 |
子模块追踪:battery-cache 可售 SKU Redis 缓存
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 刷新缓存 | 身份/范围/商品变化 | sid/type、SKU set、cache key/version | application/KzData/Enums/RedisKeys.php -> application/Services/Materiels/MaterielFrontCacheSer.php | DB 身份与范围、旧 Redis 集合 | commit 后事务外 old set -> new set/deleted,DB 不变 | task/event + key + before/after count | 全部取消也必须清旧 key;TTL/键前缀需环境确认 |
| 差异回查 | DB 可售但页面不可售 | sid、SKU、key | application/Services/BatteryUser/BatteryUserService.php | DB allow 结果、Redis 成员和更新时间 | 查询只读 不写 | sid/SKU + key hit + timestamps | DB 正确只重建目标身份缓存,不改业务表 |
子模块追踪:battery-search 商品列表与搜索过滤
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 搜索列表 | 电池用户浏览/搜索商品 | sid、query/SKU、identity type | application/Services/Materiels/MaterielFrontCacheSer.php | 搜索命中、身份范围、商品状态、价格库存 | 查询只读;raw hits 经 battery scope 过滤 | request ID + query + raw/final hits + sid/type | raw 有 final 无是范围过滤,不直接重建索引 |
| 商品复核 | 列表与详情/机器人不同 | sid、SKU、scene | application/Services/BatteryUser/BatteryUserService.php | 各入口身份上下文和同一 scope 判定 | 查询只读 不写 | sid/SKU + scene decisions | 统一复用范围 Service;修调用方上下文而非硬编码白名单 |
子模块追踪:battery-purchase 采购查询与提交校验
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 采购查询 | 电池采购商品/预订单查询 | sid/type、SKU、source scene | application/Services/PoOrders/PoMaterielSer.php | 当前身份、采购可售范围、供应库存和商品态 | 查询只读 不写 | request ID + sid/type + SKU + supply result | 搜索可见不等于可采购;提交前再验范围和库存 |
| 采购提交 | PC/App 创建电池采购 | po/source billNo、sid snapshot、SKU | application/controllers/scm/InvPo.php | 提交时身份、范围、价格库存和来源唯一性 | 采购本地事务合法才 insert,保存身份/来源快照 | request ID + po/source + sid/type + SKU | 身份变化/范围失效零写入;历史单按创建快照履约 |
子模块追踪:battery-sale-return 销售、退货与采购售后
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 销售与采购售后 | 新销售/采购售后请求 | original/source billNo、sid/type、SKU | application/service/scm/InvSaService.php、application/controllers/po/AfterSale.php | 新单当前范围;售后原单快照、可退量和状态 | 领域本地事务合法时 sale/aftersale none -> created | request ID + source/original/new billNos | 售后以原单事实为准,不因当前不可售直接拒绝 |
| 销售退货 | 原销售发起退货入库 | original/return billNo、SKU、qty | application/Services/InvSa/NormalSaleReturnSer.php | 原有效出库、已退量和库存维度 | 退货与库存本地事务 returnQty +n、库存 qty +n | request ID + original/return/inventory billNos | 当前身份变化不阻断合法退货;重复原单键 0 新单 |
子模块追踪:battery-import 电池商品导入
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 文件校验 | 导入电池可售商品/销售出库商品 | file/task、rowNo、SKU/type | application/controllers/basedata/Import.php -> application/Services/Import/Type/SaleOutGoodsService.php | 表头、商品、电池属性、范围、文件内/库内重复 | 校验阶段不写;形成 valid/error rows | task + row/SKU + errors | 错误带行号;不允许部分列错位静默落库 |
| 批量落库 | 校验通过后更新范围/商品 | batch、SKU list、scope | application/Services/BatteryUser/BatteryUserService.php | 当前配置和幂等键 | 每批本地事务 old item/scope -> imported new;缓存 commit 后刷新 | task/batch + affected + cache result | 只重跑失败 SKU;DB 成功不重复导入 |
子模块追踪:battery-robot V1/V2 机器人电池查询
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| V1 查询 | 旧机器人识别电池询价 | conversation/request、sid、query/SKU | application/Services/MoveMall/RobotInquirySer.php -> application/Services/MoveMall/RobotInquiryInput.php | 用户身份、解析商品、范围、价格库存 | 查询只读;保存会话查询日志,不改商品 | request + conversation + sid/SKU + result | 无权限/无结果明确返回,不绕过 scope |
| V2 查询 | 腾讯 IM RobotV2 电池策略 | message/conversation、sid、normalized input | application/Services/MoveMall/RobotV2/Services/Query/BatteryQueryService.php -> application/Services/MoveMall/RobotV2/Strategies/BatteryStrategy.php | Adapter 用户上下文、统一范围和商品事实 | 查询只读;回复记录本地事务 pending -> sent/failed | message + conversation + query/result IDs | V1/V2 使用同一身份判断;发送失败只补回复 |