业务口径包花厂集采
包花厂集采开发文档
运营端 / 供应商端两服务分工、代码入口、接口清单、状态写入条件、Redis 锁与事务、定时任务、跨服务结算凭证、数据表与只读核查 SQL、已知坑
Markdown 原文(给 AI 用):https://doc.xfun.fun/bulk-purchase-dev-guide.md
结论
集采拆在两个服务里:
- 运营端
xm-bulky-mall-admin/app:采购计划(发布、追加、撤回、终止)、自动定采、运营看单 / 改量 / 处理超量 / 取消、包花厂商品管理、需求推进定时任务。 - 供应商端
xm-bulky-mall-supplier:供应商报价、仓库 PDA 作业(签到、打印、扫码、盘点)、超量处理、唯一的结算记账实现、自动结算与自动取消任务。
运营后台的「手动结算」不在 admin 里记账,而是签发 60 秒一次性凭证,经 Feign 调 supplier 内部接口,由 BulkPurchaseSettleServiceImpl 统一记账。两端操作同一批表,并发靠「Redis 锁 + 带状态条件的 UPDATE」双保险。
业务口径(给产品 / 测试 / 现场看的版本)见 包花厂集采业务逻辑。本篇讲代码在哪、怎么串、改的时候要注意什么。
本文基于 sh/code 分支 d40aa32fe7(2026-10-30)核对。master 上集采代码可能落后,排查线上问题前先确认两服务实际部署版本。
代码入口
路径前缀:
- admin:
xm-bulky-mall-admin/app/src/main/java/com/xunmeng/xmbulkymalladmin/ - supplier-core:
xm-bulky-mall-supplier/supplier-core/src/main/java/com/xunmeng/xmbulkymallsupplier/suppliercore/ - supplier-api:
xm-bulky-mall-supplier/supplier-api/src/main/java/com/xunmeng/xmbulkymallsupplier/supplierapi/
| 业务 | 类 | 位置 |
|---|---|---|
| 采购计划发布 / 追加 / 撤回 / 终止 / 完成采集 | BpDemandLogic | admin logic/ |
| 供应商范围与资格快照 | BpSupplierScopeLogic | admin logic/ |
| 自动定采流程(生成供货单、完成 / 关闭计划) | BpConfirmLogic | admin logic/ |
| 定采选择与分配(纯函数,有单测) | BpAutoConfirmAlgorithm | admin logic/ |
| 运营看单、超量列表口径、取消 | BpOrderLogic | admin logic/ |
| 运营改量 / 处理超量 / 手动结算(签凭证) | BpOrderSettleLogic | admin logic/ |
| 包花厂商品上下架 / 改价 / 改库存 | BpFactoryProductLogic | admin logic/ |
| 常量、状态、产出截止算法 | BulkPurchaseEnum | admin enums/、supplier-core enums/(两份,改常量要同步) |
| 需求推进定时任务 | BulkPurchaseDemandExpireJob | admin task/ |
| 供应商报价、列表、截止校验 | BulkPurchaseLogic | supplier-core logic/ |
| 供应商列表四类筛选 SQL | BulkPurchaseDemandMapper.xml | supplier-core resources/mapper/ |
| 签到、打印、扫码、撤销、盘点、商品库存 | BulkPurchaseWmsLogic | supplier-core logic/ |
| 超量、补偿、改量、取消 | BulkPurchaseSettleLogic | supplier-core logic/ |
| 结算记账(唯一实现) | BulkPurchaseSettleServiceImpl | supplier-core service/impl/ |
| 后台结算凭证消费 | BulkPurchaseAdminSettleLogic | supplier-core logic/ |
| 盘点占用锁 | BulkPurchaseInventoryLock | supplier-core service/ |
| 资金锁 | BulkPurchaseMoneyLock | admin service/ 和 supplier-core service/ 各一份,同一个 key |
| 厂长 / 采购工作台 | BulkPurchaseFactoryLogic | supplier-core logic/ |
扫码接口(内层 success) | BulkPurchaseWmsController | supplier-api controller/wms/ |
| 自动结算任务 | BulkPurchaseSettleJob | supplier-api task/ |
| 产出截止自动取消任务 | BulkPurchaseOrderCancelJob | supplier-api task/ |
仓库内已有的历史资料在 xm-bulky-mall-admin/docs/(bulk-purchase-tech-design.md、bulk-purchase-api-admin.html、bulk-purchase-api-supplier.html、bulk-purchase-admin-settle-over-frontend.md、DDL 在 docs/sql/2026-09-*-bulk-purchase-*.sql)。
接口清单
全部是 POST。两个服务都有 /bulkPurchase 前缀,但网关、鉴权不同,不要混用地址。
运营后台(admin)
| 前缀 | 接口 | 用途 |
|---|---|---|
/bulkPurchase/demand | list、detail、scopePreview、skuOptions、save、appendLog、appendSupplier、appendNum、withdraw、terminate、delete、finishCollect | 采购计划 |
/bulkPurchase/confirm | quoteList、quoteSuppliers | 定采报价、已报价供应商 |
/bulkPurchase/order | list、stat、detail、updateDelivery、cancel、settle/updateNum、handleOver、settle/manual | 供货单、改量、超量、手动结算 |
/bulkPurchase/factoryProduct | list、onSale、updateStock、updatePrice、log/list | 包花厂商品 |
/bulkPurchase/auditLog | page | 操作轨迹 |
/bulkPurchase/second | list、favorite | 二级分类 |
/bulkPurchase/whiteList | page、add、remove | 全局白名单 |
供应商小程序(supplier-api)
| 前缀 | 接口 | 用途 |
|---|---|---|
/bulkPurchase | demand/list、demand/detail、demand/summary | 可见计划 |
/bulkPurchase | quote/submit、quote/update、quote/adjustPrice、quote/history | 报价 |
/bulkPurchase | order/list、order/detail | 我的供货单 |
仓库 PDA(supplier-api)
| 前缀 | 接口 | 用途 |
|---|---|---|
/wms/bulkPurchase | order/list、order/detail、order/checkIn、order/deliveryRemark | 找单、签到 |
/wms/bulkPurchase | print/page、print/normal、print/reprint | 打印 |
/wms/bulkPurchase | label/resolve、inventory/start、inventory/release、scan、scan/revoke、scan/detail、inventory/submit | 盘点 |
/wms/bulkPurchase | order/occupiedCandidates、order/handleOver、order/cancel、settle/updateNum、settle/manual | 异常与结算 |
/wms/bulkPurchase/factory | workbench/stat、workbench/priority、overview、demand/list、demand/detail | 厂长 / 采购工作台 |
内部接口
| 接口 | 调用方 | 说明 |
|---|---|---|
POST /internal/bulkPurchase/order/settle/manual | admin BulkPurchaseSettleFeign | 请求头 X-Bp-Admin-Settle-Ticket 携带一次性凭证,见「跨服务手动结算」 |
关键常量
| 常量 | 值 | 含义 |
|---|---|---|
QUOTE_CLOSE_ADVANCE_SECONDS | 5 | 供应商报价截止提前秒数 |
PRICE_SCALE | 2 | 价格、补偿金额最多小数位 |
PRICE_MAX | 1000 | 单价上限(元/扎) |
AUTO_CONFIRM_CANDIDATE_LIMIT | 5 | 每等级定采候选数 |
PRINT_MAX_TIMES | 2 | 标签上限 = 整单定采扎数 × 2 |
SETTLE_DELAY_DAYS | 3 | 计划结算时间 = 提交盘点 + 3 天 |
COLLECT_END_TIME_OF_DAY / COLLECT_END_MIN_BUFFER_HOURS | 12:00 / 2 | 产出截止 = max(场次次日 12:00,最晚签到截止 + 2h) |
ACCOUNT_CHANGE_INCOME / ACCOUNT_CHANGE_EXPENSE | 95 / 96 | 集采收入 / 集采支出账变类型 |
MSG_TYPE_FACTORY_PURCHASE | 14 | 消息中心集采消息类型 |
SHELF_CODE_PREFIX | JC | 货架码前缀 |
枚举值(计划 05、供货单 10/20/30/40/90、标签 10/30/90、范围 16、超量类型 1/2、补偿方式 1/2/3)见 包花厂集采业务逻辑 的「主流程」和各规则章节。补充只在代码里出现的:
| 枚举 | 取值 |
|---|---|
报价选中 selectStatus | 0 待定 · 1 中选 · 2 未中选 |
报价版本 current | 1 当前 · 0 历史 |
计划关闭原因 closeReason | 1 无人报价 · 2 手动 · 3 全部未中选 |
| 计划完成方式 | 1 手动完成采集 · 2 自动 |
| 终止方式 | 1 自然截止 · 2 提前终止 |
取消方式 cancelType | 1 手动 · 2 产出截止自动 |
结算方式 settleType | 1 自动 · 2 手动 |
扫码拒绝 rejectReason | 2 重复 · 3 作废 · 4 跨单 · 5 不存在 · 6 单据状态不允许 |
| 资格来源 | 1 发布 · 2 追加 |
操作端 operator_end | admin · pda · system |
状态流转与写入条件
所有状态推进都是带旧状态条件的 UPDATE,affected=0 视为被并发抢先,回滚或返回失败。改代码时不要去掉这些条件。
| 动作 | 关键写入条件 | 失败表现 |
|---|---|---|
| 撤回 | status=1 且无报价 | 「已有供应商报价,不能撤回,请使用提前终止」 |
| 报价截止流转 | status=1 且两个截止都已过 | 跳过 |
| 提交盘点(抢占) | beginInventorySubmit:status=20 且占用人是本人,先把状态推到 30 | 返回 false |
| 扫码加产出 | increaseActualNum:status=20 | 抛「供货单已提交盘点,本次扫码未生效」,整次扫码回滚 |
| 扫码占位 | occupyForScan:标签 status=10 | affected=0 即重复扫码(唯一判定依据) |
| 取消 | status=原状态 且 actual_label_num=0 | 返回 false(期间刚好扫码成功) |
| 处理超量 | status=30 且 over_flag=1;先占标识再写补偿 | 重复提交只生效一次 |
| 登记补款加目标结算数 | 目标 status=30 且旧 settle_label_num 未变 | 整个处理事务回滚 |
| 结算置已结算 | status=30 且 total_amount=读到的值 | 抛异常(钱已加,不能只返回 false,否则下一轮重复打款) |
扫码事务
BulkPurchaseWmsLogic#scan 在一个事务里:查标签 → 跨单校验 → 拿盘点锁 → 重读标签 → 作废 / 状态校验 → 占用人校验 → 解析商品映射 → occupyForScan → 等级扫码数 +1 → 整单产出 +1 → 商品 remain_stock、max_stock +1 → 商品日志 / 分类聚合;开放平台库存推送在事务提交后执行。
- 失败流水
bulk_purchase_scan_reject_log一律记在当前作业单(expectOrderId)下,便于按单查扫码明细。 - 非占用人扫码是前置条件错误,直接抛异常,不写失败流水。
- 撤销扫码扣库存时把
remain_stock >= 1写进条件,不够扣说明已售出,整次撤销回滚。
提交盘点
不信任扫码时的累加值,提交时按 bulk_purchase_order_rank.scanned_label_num 重算 actual_label_num、settle_label_num、amount、total_amount,作废未扫标签,写 plan_settle_time = now + 3 天,actual > confirm 时置 over_flag=1。最后走带 SET inventory_by = NULL 的 SQL 释放占用(MyBatis-Plus 默认不更新 null 字段)。
自动定采
BpConfirmLogic#autoConfirm:读当前版本报价 → BpAutoConfirmAlgorithm.allocate(需求含追加, 候选, 5) → 报价定性(中选 / 未中选)→ 每个中选供应商一张供货单(冻结各等级单价,生成 JC 货架码)→ 计划直接 finishConfirmedDemand 为已完成;没有中选则关闭(closeReason=3)。货架码序号在 Redis bp:shelf:seq:,定采按需求加锁 bp:confirm:lock:(30 秒)。
算法排序:价格升序 → 提交时间升序 → 供应商 ID 升序(第三级保证同一数据跑两次结果一致)。永不超采、可能少采,缺口写进 traces。
并发控制
| 锁 | Key | 租约 | 用途 |
|---|---|---|---|
| 盘点锁 | bp:inventory:order:{orderId} | 120 秒,30 秒续期 | 扫码、撤销、开始 / 释放 / 提交盘点互斥,持有至事务完成 |
| 资金锁 | bp:money:demand:{demandId} | 120 秒,30 秒续期 | 改量、处理超量、补偿、结算;按需求加锁,因为补偿会跨同需求下多张供货单;admin 与 supplier 共用 |
| 需求任务锁 | bp:task:demandExpire | 240 秒 | 多实例下只有一个实例跑需求推进任务 |
| 定采锁 | bp:confirm:lock:{demandId} | 30 秒 | 同一需求同时只跑一次定采 |
| 商品映射锁 | bp:mapping:lock: | 15 秒 | 防止并发自动建品建出重复商品 |
资金类操作的固定顺序:先 getById 拿 demandId → 加资金锁 → getByIdForUpdate 重读并校验 demandId 未变。事务隔离级别 READ_COMMITTED,避免拿锁后读到旧快照。
锁超过 2 分钟仍在不一定是异常(有续期)。先查是否有正常请求或事务在跑,不要现场删锁。
定时任务
| 任务 | 所在服务 | 调度 | 每批 | 做什么 |
|---|---|---|---|---|
BulkPurchaseDemandExpireJob | admin | 应用内 @Scheduled,每 5 分钟;需 scheduling.enabled=true | 200 | ① 报价截止流转 + 自动定采 ② 卡在待定采且未过采集截止的重试定采 ③ 历史已确认且到采集截止的计划推成已完成 |
BulkPurchaseSettleJob | supplier-api | SchedulerX(BaseTaskProcessor),频率由控制台配置 | 200 | 结算到期且 over_flag=0 的单;每轮 warn 打印「已到期但超量未处理」积压数 |
BulkPurchaseOrderCancelJob | supplier-api | SchedulerX | 500 | 取消到产出截止、无产出的待签到 / 待盘点单 |
- 三个步骤各自 try/catch,一步失败不影响其他步。
- 待定采过了采集截止就不再重试:此时生成的供货单会立刻被自动取消任务取消。日志
集采需求卡在待定采且已过采集截止,需人工处理。 - 自动结算单张独立事务,乐观锁冲突的单留在待结算等下一轮。
跨服务手动结算
运营后台 POST /bulkPurchase/order/settle/manual
└─ BpOrderSettleLogic:校验后台登录态,生成 UUID 凭证
Redis SET bp:settle:admin-ticket:{uuid} = {orderId, remark, operatorId, operatorName} EX 60
└─ Feign POST /internal/bulkPurchase/order/settle/manual Header X-Bp-Admin-Settle-Ticket: {uuid}
└─ BulkPurchaseAdminSettleLogic:Lua 原子 GET+DEL 消费凭证(防重放)
└─ BulkPurchaseSettleServiceImpl#settleManual(..., END_ADMIN)- 操作人来自凭证,supplier 不接受客户端自报的操作人。
- admin 与 supplier 必须连同一个 Redis 库,否则凭证永远「无效或已过期」。
- Feign 异常不打印内容(可能带凭证)。调用超时不代表没结算,先查订单和账变。
记账流程
BulkPurchaseSettleServiceImpl#settle:
- 自动路径再挡一次
over_flag=1(兜底)。 - 金额 ≤ 0:直接置已结算、写审计、提交后发通知,不产生账变。
- 供货方
increaseAccountMoney(乐观锁,冲突抛「操作冲突,请刷新后重试」)→ 写 95 账变。 - 发布主体
decreaseAccountMoney,不校验余额,允许负数 → 写 96 账变(金额为负)。 finishSettle带status=30 + total_amount条件置 40,失败抛异常回滚。- 写审计
settle/settleManual;afterCommit发消息(类型 14,FACTORY_ORDER_COMPLETED,只发给供货方)。
数据表
| 表 | 用途与关联键 |
|---|---|
bulk_purchase_demand、bulk_purchase_demand_rank | 计划与等级配置,id / demand_id |
bulk_purchase_demand_supplier、bulk_purchase_white_list | 发布资格快照(全部供应商用 supplier_id=0 哨兵行)与全局白名单 |
bulk_purchase_quote、bulk_purchase_quote_rank | 报价版本、当前标志、选中状态、逐等级量价,quote_id |
bulk_purchase_order、bulk_purchase_order_rank | 供货单、冻结价、打印 / 扫码 / 结算数量;demand_id、quote_id、supplier_id |
bulk_purchase_label、bulk_purchase_scan_reject_log | 标签状态、扫码失败原因 |
bulk_purchase_over_compensate | 补偿台账,order_id(源单)/ occupied_order_id(被占用方) |
bulk_purchase_audit_log | 全链路审计,biz_type + biz_no |
bulk_purchase_product_mapping、bulk_purchase_product_log | 商品映射与商品变更日志 |
xm_bulky_product | 商品库存 remain_stock(单位扎)与上下架 |
xm_bulky_supplier_account_change | 账变,order_sn = 供货单号,type IN (95, 96) |
只读核查 SQL
先确认连的是哪个环境,再按单号 / ID 精确查询。以下只提供 SELECT,不提供生产修改语句。
供货单与等级
SELECT id, order_no, demand_id, demand_no, quote_id,
supplier_id, publish_supplier_id, round_no, status,
confirm_label_num, actual_label_num, settle_label_num, total_amount,
over_flag, over_handle_time, over_handle_remark,
inventory_by, inventory_submit_time, output_end_time,
plan_settle_time, settle_time, settle_flag, settle_type
FROM bulk_purchase_order
WHERE order_no = '替换为完整供货单号';
SELECT order_id, rank_code, price, confirm_label_num,
printed_label_num, scanned_label_num, settle_label_num, amount
FROM bulk_purchase_order_rank
WHERE order_id = 123456
ORDER BY rank_code;核对顺序:
actual_label_num= Σ 等级scanned_label_num;settle_label_num= Σ 等级settle_label_num;total_amount= Σ(等级settle_label_num×price)。
被登记补款的目标单,应结算数可能大于扫码数,属正常。
标签与扫码失败
SELECT id, label_code, order_id, rank_code, seq_no, status,
product_id, scan_by, scan_time, revoke_count, void_time, void_reason
FROM bulk_purchase_label
WHERE order_id = 123456
ORDER BY rank_code, seq_no;
SELECT label_code, order_id, rank_code, reject_reason, fail_msg,
first_scan_time, operator_name, create_time
FROM bulk_purchase_scan_reject_log
WHERE order_id = 123456
ORDER BY id DESC
LIMIT 100;补偿、账变、审计
SELECT order_id, order_no, occupied_order_id, occupied_order_no,
occupied_supplier_id, rank_code, occupied_label_num,
price, compensate_type, compensate_amount, handler_name, create_time
FROM bulk_purchase_over_compensate
WHERE order_id = 123456 OR occupied_order_id = 123456
ORDER BY id;
SELECT supplier_id, type, order_sn, old_money, money, new_money, create_time
FROM xm_bulky_supplier_account_change
WHERE order_sn = '替换为完整供货单号'
AND type IN (95, 96)
ORDER BY id;
SELECT biz_type, biz_id, biz_no, action, before_value, after_value,
reason, operator_end, operator_name, create_time
FROM bulk_purchase_audit_log
WHERE biz_no = '替换为完整供货单号'
ORDER BY id;正金额结算应有一条 95 + 一条 96。只有单边、同单号多次入账、金额与订单不一致时,保留查询结果和时间线升级,不要自行冲正。
日志与审计关键词
按 traceId、demandNo、orderNo、orderId 搜对应服务日志。
| 服务 | 日志关键词 |
|---|---|
| admin | 集采需求报价截止流转、集采报价截止后自动定采失败、集采待定采重试、集采需求卡在待定采且已过采集截止、集采自动定采完成、集采采集兜底完结 |
| supplier | 集采盘点提交、集采超量处理、集采结算失败、集采自动结算完成、集采结算余额乐观锁冲突、超量未处理,不会自动结算、集采供货单取消、集采供货单产出截止自动取消完成 |
审计 action:publish、appendNum、appendSupplier、quoteExpire、autoConfirm、finishCollect、submitQuote、updateQuote、adjustPrice、checkIn、normal_print、reprint、revokeScan、submitInventory、updateSettleNum、handleOver、overCompensateIn、settle、settleManual、cancel。
扫码成功不写审计,以标签表和商品库存为准;失败看 bulk_purchase_scan_reject_log。不能靠找成功审计判断是否入库。
配置与依赖
| 配置 / 依赖 | 说明 |
|---|---|
scheduling.enabled | admin bootstrap.properties 为 true,关掉则需求推进任务不跑;supplier 为 ${SUPPLIER_SCHEDULING_ENABLED:true} |
bulk-purchase.factory.supplier-ids | 运营端包花厂商品管理可见的发布主体范围;各环境值不同,不要照抄 |
| SchedulerX | supplier 自动结算、自动取消任务;核对控制台启用状态、频率、最近执行结果 |
| 消息中心 | 定采采用、结算完成通知,类型 14;30 秒内按 bizType + msgType + bizId + extraJson + 接收人幂等 |
| 开放平台 | 扫码库存变更在事务提交后推送;推送失败不影响本地库存 |
联调注意与已知坑
| 项 | 说明 |
|---|---|
| 扫码响应要看内层 | /wms/bulkPurchase/scan 被拒时外层 code=0,data.success=false + rejectReason,前端不能只看外层 |
| 打印入参是目标累计张数 | planNumMap 每个等级传目标总张数;同一等级重复会被 Map 覆盖,控制器组装前就要校验 |
| 重打状态限制宽 | 只拦已取消;待结算、已结算单也能重打(不新增数量) |
| 定采前调价基本不可达 | adjustPrice 要求计划 status=2,又要求未过「截止 − 5 秒」;自然截止流转到 2 时截止必然已过,只有「提前终止后定采失败卡在待定采」才可能走通。是否保留该入口待产品确认 |
两份 BulkPurchaseEnum | admin 与 supplier-core 各一份,PRINT_MAX_TIMES、OVER_*、COMPENSATE_* 等两边都用,改一边要同步另一边 |
两份 BulkPurchaseMoneyLock | key 都是 bp:money:demand:,改实现要两边一致,否则跨端互斥失效 |
| 测试环境定时任务 | 测试服 SchedulerX 不通时,supplier 的自动结算 / 自动取消不会跑,需要手动结算或在测试服另行触发;admin 需求推进是 @Scheduled,不受影响 |
| 0 元结算也发通知 | 金额为 0 置已结算、无账变,但仍给供货方发结算完成消息 |
| 处理超量可一步完成 | handleOver 可同时传 rankSettles + settleReason,在一个事务里先改量再处理;只传 settleReason 不传等级数量会被拒 |
| 列表与详情超量口径不同 | 运营端列表对「over_flag=0 且有处理时间」的单返回 overNum=0、overAmount=0.00,详情按等级扫码数与定采数保留历史超量差额 |