13 KiB
选品:跨平台仓序 + 拆量落表
| 文档属性 | 内容 |
|---|---|
| 日期 | 2026-10-02 |
| 状态 | 已锁定,本轮实施依据 |
| 范围 | 选品流程/ 规则、班次判定、表 2 落盘。不加购,不推接口 2 |
| 替代 | 选品流程/README.md 规则节;口径文档 3.1 / 3.3 中「单仓满量、两边都写满量」的作业语义 |
| 不变 | 接口 1 收件、表结构、查询键(条形码)、两平台登录/搜索、接口 2 报文形态 |
一张 omsId 一个品种。status 仍是该品种整单采购状态。
1. 要做成什么
从表 1 领一行,两个商城都搜,合并候选后统一判定:
- 硬过滤:自营仓、毛利 ≥ 4%、效期 ≥ 12 个月。库存不再是过滤项。
- 按四级仓序(同仓低价、再高库存)排序。
- 同一平台同一仓的多个 sku 都参与拆量,不再只留低价那条。
- 按序贪心拆量,直到计划采购量凑齐。
- 凑齐:每条拆量一行写入表 2,
status=1,数量是该条拆到的件数,合计等于计划量。 - 凑不齐:不写成功行,表 2
status=2,表 1deal_flag=2。
完成标准:选品流程 单测全绿;用内存仓库能演示「杭州 3 + 广东 7 = 计划 10」落两行,以及「合计 9 < 10」整单失败且成功行为 0。
2. 不做什么
- 真加购、清空购物车、页面点击
- 接口 2 推送(
push_status仍为 0) - 批准文号搜索、商家名模糊匹配
- 中包装 / 起订量
- 改表结构、改接口 1
3. 已锁定决策
| 项 | 决定 |
|---|---|
| 毛利 | (salePrice - 平台价) / salePrice >= 0.04,恰好 4% 过 |
| 拆量 | 跨仓跨平台贪心拆。凑不齐一件都不落成功行 |
| 仓序 | 见第 4 节全称表。级内顺序即距离顺序 |
| 湖南壹号、甘肃壹号云 | 移出自营集合 |
| 同一平台同一仓 | 只留 1 条(低价,再高库存)。余量给下一仓 |
multi_platform_flag |
实际拆到两个 platform_type 时为 1;同一平台拆两仓为 0 |
| 一边故障、另一边单独凑齐 | 按成功落,不写故障平台失败行 |
| 一边故障、剩下的凑不齐 | 系统故障(可把 deal_flag 改回 0 重跑),禁止写成库存不足 |
| 两边都搜到、合计不够 | 业务失败「库存不足」,该 omsId 不重跑选品 |
| 查询键 | 仍是条形码 |
4. 自营仓全称与顺序
商家名去掉 [自营]、压缩空白后必须等于下表某一行。对不上 = 非自营,淘汰。
| 序 | 级 | 平台 | 全称 |
|---|---|---|---|
| 0 | 1 | 药帮忙 | 杭州小药药自营旗舰店 |
| 1 | 1 | 药帮忙 | 长沙小药药自营旗舰店 |
| 2 | 1 | 药帮忙 | 湖北小药药自营旗舰店 |
| 3 | 2 | 1号药城 | 广东壹号药业有限公司 |
| 4 | 2 | 1号药城 | 昆山亿方药业有限公司 |
| 5 | 2 | 1号药城 | 湖北亿昊药业有限公司 |
| 6 | 3 | 药帮忙 | 济南小药药自营旗舰店 |
| 7 | 3 | 1号药城 | 天津壹号药业有限公司 |
| 8 | 3 | 1号药城 | 重庆亿昊药业有限公司 |
| 9 | 4 | 1号药城 | 辽宁亿昊药业有限公司 |
| 10 | 4 | 1号药城 | 陕西壹号药业有限公司 |
| 11 | 4 | 1号药城 | 河南壹号医药有限公司 |
| 12 | 4 | 1号药城 | 重庆壹号医药有限公司 |
| 13 | 4 | 药帮忙 | 重庆小药药自营旗舰店 |
删除 WAREHOUSE_ALIASES 里甘肃那条。平台字段与名单不一致(例如 1 号药城搜出杭州小药药)仍按商家全称定序,不按搜索来源改序。
5. 算法
计划量 qty = int(plan_purchase_qty)。qty <= 0 仍走现有 NO_QTY。库存按件向下取整后再拆。
5.1 过滤(单条候选)
evaluate_candidate 不再看库存。淘汰原因仅:
NOT_SELF_WAREHOUSEPRICE_UNPARSEABLEMARGIN_LT_4PCT(原MARGIN_LT_5PCT)EXPIRY_UNPARSEABLE/EXPIRY_LT_12MNO_SALE_PRICE(采购行级,先于搜品)
库存为 None 或向下取整后 <= 0:可以过过滤,但拆量时当作 0,拆不到量。
5.2 同仓
不再按 (mall_name, vendor_norm) 折成一条。过过滤的 sku 全部进入拆量。同仓低价先吃,不够再吃同仓下一条;仓序仍优先于跨仓低价。不再产生 SAME_WAREHOUSE_NOT_BEST。
5.3 排序
(WAREHOUSE_RANK[vendor_norm], price, -stock)。price 缺失视为 +inf(过滤阶段已淘汰,排序不应再见到)。
5.4 拆量
remaining = qty
lines = []
for candidate in sorted_passed:
take = min(int(stock), remaining)
if take <= 0:
continue
lines.append(Allocation(candidate, take))
remaining -= take
if remaining == 0:
break
库存 None 时 take 当 0,不要对 None 做 int()。
remaining == 0 → 成功,lines 即为表 2 成功行。
remaining > 0 → 见第 6 节,丢弃 lines,不落成功行。
禁止:先落部分成功行再标失败。
6. 班次:先搜齐再判定
job.run_batch 对一行先跑完本次启用的全部平台搜索,再调用一次统一判定。禁止再「每平台独立选仓、独立满量写入」。
每个平台的搜索结果是三种之一:
| 结果 | 含义 |
|---|---|
candidates |
搜到 0..n 条(空列表 = 无命中,不是故障) |
LOGIN_FAILED:... |
本平台本班不可用 |
QUERY_FAILED:... |
本行本平台查询异常 |
无条形码:两个平台都不搜,业务失败 NO_BAR_CODE(与现在相同)。
统一判定:
- 采购行缺销售价 / 数量:
SKIPPED,不搜也行(保持现有,在搜之前拦)。 - 把所有成功搜到的
candidates合并,走第 5 节。 - 凑齐:成功。故障平台的错误忽略,不写该平台失败行。
- 凑不齐,且存在任一平台
LOGIN_FAILED/QUERY_FAILED:整单 系统故障。 - 凑不齐,且无系统故障:业务失败。
只跑 --platform yaoex 时,可用候选只有 1 号药城。凑不齐且该平台无故障 → 业务失败;该平台故障 → 系统故障。
7. 落表
成功行(每条 Allocation 一行):
| 列 | 值 |
|---|---|
operate_type |
1 |
oms_id |
本行 |
platform_type |
48 药帮忙 / 66 1号药城 |
supplier_name |
vendor_raw |
bar_code / org_code |
原样 |
purchase_price |
该候选平台价,四位小数 |
purchase_qty |
该条 take,不是计划全量 |
purchase_total |
单价 × 该条数量,四位小数 |
status |
1 |
fail_reason |
null |
push_status |
0 |
multi_platform_flag |
本单成功行里出现 ≥2 个不同 platform_type 则为 1,否则 0 |
表 1:deal_flag=1,deal_result 形如 选品成功:药帮忙、1号药城(按成功行出现的商城去重,顺序:药帮忙、1号药城)。
业务失败:每个已启用且已判定的平台落一行 status=2,purchase_qty 仍用计划量(造数校验:数量 > 0),purchase_price 仍用销售价兜底。supplier_name 仍为「无符合规则供应商」。multi_platform_flag 按失败行平台数(≥2 个平台则为 1)。
业务失败原因(fail_reason / deal_result)。先看过滤后是否留下可拆候选(含库存为 0。以过过滤的集合为准):
NO_BAR_CODE/NO_SALE_PRICE/NO_QTY:沿用现有中文- 过过滤集合非空,但拆不满:一律
无符合规则商品,库存不足。rejected 里即使同时有非自营、效期、毛利淘汰,也不改这条。 - 过过滤集合为空时,才按候选清单(含淘汰原因)往下看,取第一条能成立的:
- 含
MARGIN_LT_4PCT:无符合规则商品,毛利不足 4% - 含
EXPIRY_LT_12M/EXPIRY_UNPARSEABLE:无符合规则商品,效期不足 12 个月 - 含
NOT_SELF_WAREHOUSE:无符合规则商品,非自营仓 - 全部无命中:
无符合规则商品,平台无命中 - 其它:
无符合规则商品
- 含
第 9 节必测补一条:杭州自营库存 4 + 若干非自营/效期短淘汰、计划 10 → 原因必须是库存不足,不能是非自营或效期。
系统故障:每个启用平台一行 status=2,原因带 系统故障:。有故障的平台写该平台错误;无故障但被连带失败的平台写 系统故障:对端平台不可用,合计库存不足。表 1 deal_result 以 系统故障: 开头。SelectionResult.mall_errors 按平台给出原始错误串,persist 据此写各行,禁止所有失败行共用一句。
JobSummary 按表 2 行计:added = status=1 行数,skipped = 业务失败行数,failed = 系统故障行数。
8. 模块怎么改
| 文件 | 改什么 |
|---|---|
rules.py |
新名单;MIN_MARGIN=0.04;evaluate_candidate 去掉库存;新增 allocate()(过滤+同仓全留+排序+拆量);select_warehouse 删除或改为调用 allocate 后取第一仓——禁止再作为班次入口 |
pipeline.py |
新增 run_selection(purchase, mall_outcomes),一次产出整单结果。run_one_platform 只留给单测过渡,班次不再调用 |
job.py |
先收集各平台搜索结果,再 run_selection |
persist.py |
吃整单结果:成功按拆量多行;multi_platform_flag 按平台种类数;失败原因 5% → 4%,并增加库存不足 |
README.md |
规则节换成本文摘要 |
资料/RPA订单中心接口开发口径_20260930.md |
3.1 改为毛利 4%、库存改为拆量;3.2 示例「毛利不足 5%」改为 4%;3.3 「只凑到一部分」改为整单失败、不落部分成功 |
资料/采购平台选品条件优先项_20260902.txt |
文首加「已被 2026-10-02 spec 替代」;不改历史原文 |
搜索、登录、repo.py、表 DDL 不动。
建议的数据结构(名称可改,语义不要改):
@dataclass
class MallOutcome:
mall_name: str
candidates: list[Candidate] | None = None
failed_reason: str | None = None # LOGIN_FAILED: / QUERY_FAILED:
@dataclass
class Allocation:
candidate: Candidate
qty: int
@dataclass
class SelectionResult:
kind: str # SUCCESS / BUSINESS_FAIL / SYSTEM_FAIL
reason: str # SELECTED / STOCK_INSUFFICIENT / NO_HIT / LOGIN_FAILED:... 等
allocations: list[Allocation]
rejected: list[Rejected]
malls: list[str] # 本行启用过的平台,落失败行用
mall_errors: dict[str, str] # 平台 → LOGIN_FAILED:... / QUERY_FAILED:...
reason 在 run_selection / allocate 里用过过滤集合算完。persist 只做中文映射,禁止从 rejected 反推库存不足(库存为 0 的过过滤候选不在 rejected 里)。
kind 决定落表,不要用「有没有 FAILED 决策」去猜:
SUCCESS:allocations合计数量必须等于int(qty);表 1deal_flag=1,表 2 只写成功行BUSINESS_FAIL:allocations必须为空;表 1deal_flag=2,原因无「系统故障」前缀,该omsId不重跑SYSTEM_FAIL:allocations必须为空;表 1deal_flag=2,deal_result以系统故障:开头
pipeline 的展示字段若仍用 结论=ADDED/SKIPPED/FAILED,映射为 SUCCESS→ADDED、BUSINESS_FAIL→SKIPPED、SYSTEM_FAIL→FAILED。persist 只认 kind。
成功行 platform_type 按该候选的 mall_name(搜索来源)映射 48/66,仓序只按商家全称。第 4 节「不按搜索来源改序」只约束排序,不改 platform_type。
9. 必须覆盖的行为(单测)
规则:
- 毛利恰好 4% 过;3.9% 淘汰(销售价 100、平台价 96 过,96.1 淘汰)
- 杭州小药药优先于广东壹号
- 湖南壹号、甘肃壹号云视为非自营
- 近仓 3、次仓 20、计划 10 → 近仓 3 + 次仓 7
- 合计 9、计划 10 → 无 allocation
- 同一仓两 SKU(价 90 库存 5、价 92 库存 100)、计划 10 → 价 90 拿 5,价 92 拿 5,不把余量给下一仓
- 库存 3.9 按 3 件拆
班次 / 落表:
- 两平台拆开:药帮忙 3 + 1 号药城 7,两行
status=1,数量分别为 3 和 7,multi_platform_flag=1,表 1deal_flag=1 - 只在药帮忙拆两仓:两行、同一
platform_type=48、multi_platform_flag=0 - 药帮忙登录失败、广东壹号库存 20、计划 10 → 只落 1 号药城成功一行
- 药帮忙登录失败、1 号药城库存 4、计划 10 → 系统故障,无成功行
- 两边都搜到、合计库存 4、计划 10 → 业务失败「库存不足」,无成功行
- 杭州自营库存 4 + 路边店/效期短淘汰、计划 10 → 业务失败「库存不足」(不是非自营、不是效期)
- 无条形码仍不搜
10. 风险
- 搜到落表之间库存会变。这期只落判定,真加购时再处理。
- 中包装可能导致拆出的件数加不进车。真加购时再对。
- 订单中心若假定「一个
omsId+ 一个platform_type只有一行」,同一平台拆两仓会撞上。表结构允许多行;联调时若对方拒,再改成同平台合一行(本期不合)。