业务口径包花厂集采

包花厂集采开发文档

运营端 / 供应商端两服务分工、代码入口、接口清单、状态写入条件、Redis 锁与事务、定时任务、跨服务结算凭证、数据表与只读核查 SQL、已知坑

后端服务
xm-bulky-mall-admin + xm-bulky-mall-supplier
测试分支
sh/code
更新
2026-11-03

Markdown 原文(给 AI 用):https://doc.xfun.fun/bulk-purchase-dev-guide.md

结论

集采拆在两个服务里:

运营后台的「手动结算」不在 admin 里记账,而是签发 60 秒一次性凭证,经 Feign 调 supplier 内部接口,由 BulkPurchaseSettleServiceImpl 统一记账。两端操作同一批表,并发靠「Redis 锁 + 带状态条件的 UPDATE」双保险。

业务口径(给产品 / 测试 / 现场看的版本)见 包花厂集采业务逻辑。本篇讲代码在哪、怎么串、改的时候要注意什么。

说明

本文基于 sh/code 分支 d40aa32fe7(2026-10-30)核对。master 上集采代码可能落后,排查线上问题前先确认两服务实际部署版本。

代码入口

路径前缀:

业务类位置
采购计划发布 / 追加 / 撤回 / 终止 / 完成采集BpDemandLogicadmin logic/
供应商范围与资格快照BpSupplierScopeLogicadmin logic/
自动定采流程(生成供货单、完成 / 关闭计划)BpConfirmLogicadmin logic/
定采选择与分配(纯函数,有单测)BpAutoConfirmAlgorithmadmin logic/
运营看单、超量列表口径、取消BpOrderLogicadmin logic/
运营改量 / 处理超量 / 手动结算(签凭证)BpOrderSettleLogicadmin logic/
包花厂商品上下架 / 改价 / 改库存BpFactoryProductLogicadmin logic/
常量、状态、产出截止算法BulkPurchaseEnumadmin enums/、supplier-core enums/(两份,改常量要同步)
需求推进定时任务BulkPurchaseDemandExpireJobadmin task/
供应商报价、列表、截止校验BulkPurchaseLogicsupplier-core logic/
供应商列表四类筛选 SQLBulkPurchaseDemandMapper.xmlsupplier-core resources/mapper/
签到、打印、扫码、撤销、盘点、商品库存BulkPurchaseWmsLogicsupplier-core logic/
超量、补偿、改量、取消BulkPurchaseSettleLogicsupplier-core logic/
结算记账(唯一实现)BulkPurchaseSettleServiceImplsupplier-core service/impl/
后台结算凭证消费BulkPurchaseAdminSettleLogicsupplier-core logic/
盘点占用锁BulkPurchaseInventoryLocksupplier-core service/
资金锁BulkPurchaseMoneyLockadmin service/ 和 supplier-core service/ 各一份,同一个 key
厂长 / 采购工作台BulkPurchaseFactoryLogicsupplier-core logic/
扫码接口(内层 success)BulkPurchaseWmsControllersupplier-api controller/wms/
自动结算任务BulkPurchaseSettleJobsupplier-api task/
产出截止自动取消任务BulkPurchaseOrderCancelJobsupplier-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/demandlist、detail、scopePreview、skuOptions、save、appendLog、appendSupplier、appendNum、withdraw、terminate、delete、finishCollect采购计划
/bulkPurchase/confirmquoteList、quoteSuppliers定采报价、已报价供应商
/bulkPurchase/orderlist、stat、detail、updateDelivery、cancel、settle/updateNum、handleOver、settle/manual供货单、改量、超量、手动结算
/bulkPurchase/factoryProductlist、onSale、updateStock、updatePrice、log/list包花厂商品
/bulkPurchase/auditLogpage操作轨迹
/bulkPurchase/secondlist、favorite二级分类
/bulkPurchase/whiteListpage、add、remove全局白名单

供应商小程序(supplier-api)

前缀接口用途
/bulkPurchasedemand/list、demand/detail、demand/summary可见计划
/bulkPurchasequote/submit、quote/update、quote/adjustPrice、quote/history报价
/bulkPurchaseorder/list、order/detail我的供货单

仓库 PDA(supplier-api)

前缀接口用途
/wms/bulkPurchaseorder/list、order/detail、order/checkIn、order/deliveryRemark找单、签到
/wms/bulkPurchaseprint/page、print/normal、print/reprint打印
/wms/bulkPurchaselabel/resolve、inventory/start、inventory/release、scan、scan/revoke、scan/detail、inventory/submit盘点
/wms/bulkPurchaseorder/occupiedCandidates、order/handleOver、order/cancel、settle/updateNum、settle/manual异常与结算
/wms/bulkPurchase/factoryworkbench/stat、workbench/priority、overview、demand/list、demand/detail厂长 / 采购工作台

内部接口

接口调用方说明
POST /internal/bulkPurchase/order/settle/manualadmin BulkPurchaseSettleFeign请求头 X-Bp-Admin-Settle-Ticket 携带一次性凭证,见「跨服务手动结算」

关键常量

常量值含义
QUOTE_CLOSE_ADVANCE_SECONDS5供应商报价截止提前秒数
PRICE_SCALE2价格、补偿金额最多小数位
PRICE_MAX1000单价上限(元/扎)
AUTO_CONFIRM_CANDIDATE_LIMIT5每等级定采候选数
PRINT_MAX_TIMES2标签上限 = 整单定采扎数 × 2
SETTLE_DELAY_DAYS3计划结算时间 = 提交盘点 + 3 天
COLLECT_END_TIME_OF_DAY / COLLECT_END_MIN_BUFFER_HOURS12:00 / 2产出截止 = max(场次次日 12:00,最晚签到截止 + 2h)
ACCOUNT_CHANGE_INCOME / ACCOUNT_CHANGE_EXPENSE95 / 96集采收入 / 集采支出账变类型
MSG_TYPE_FACTORY_PURCHASE14消息中心集采消息类型
SHELF_CODE_PREFIXJC货架码前缀

枚举值(计划 05、供货单 10/20/30/40/90、标签 10/30/90、范围 16、超量类型 1/2、补偿方式 1/2/3)见 包花厂集采业务逻辑 的「主流程」和各规则章节。补充只在代码里出现的:

枚举取值
报价选中 selectStatus0 待定 · 1 中选 · 2 未中选
报价版本 current1 当前 · 0 历史
计划关闭原因 closeReason1 无人报价 · 2 手动 · 3 全部未中选
计划完成方式1 手动完成采集 · 2 自动
终止方式1 自然截止 · 2 提前终止
取消方式 cancelType1 手动 · 2 产出截止自动
结算方式 settleType1 自动 · 2 手动
扫码拒绝 rejectReason2 重复 · 3 作废 · 4 跨单 · 5 不存在 · 6 单据状态不允许
资格来源1 发布 · 2 追加
操作端 operator_endadmin · pda · system

状态流转与写入条件

所有状态推进都是带旧状态条件的 UPDATE,affected=0 视为被并发抢先,回滚或返回失败。改代码时不要去掉这些条件。

动作关键写入条件失败表现
撤回status=1 且无报价「已有供应商报价,不能撤回,请使用提前终止」
报价截止流转status=1 且两个截止都已过跳过
提交盘点(抢占)beginInventorySubmit:status=20 且占用人是本人,先把状态推到 30返回 false
扫码加产出increaseActualNum:status=20抛「供货单已提交盘点,本次扫码未生效」,整次扫码回滚
扫码占位occupyForScan:标签 status=10affected=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_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:demandExpire240 秒多实例下只有一个实例跑需求推进任务
定采锁bp:confirm:lock:{demandId}30 秒同一需求同时只跑一次定采
商品映射锁bp:mapping:lock:15 秒防止并发自动建品建出重复商品

资金类操作的固定顺序:先 getById 拿 demandId → 加资金锁 → getByIdForUpdate 重读并校验 demandId 未变。事务隔离级别 READ_COMMITTED,避免拿锁后读到旧快照。

注意

锁超过 2 分钟仍在不一定是异常(有续期)。先查是否有正常请求或事务在跑,不要现场删锁。

定时任务

任务所在服务调度每批做什么
BulkPurchaseDemandExpireJobadmin应用内 @Scheduled,每 5 分钟;需 scheduling.enabled=true200① 报价截止流转 + 自动定采 ② 卡在待定采且未过采集截止的重试定采 ③ 历史已确认且到采集截止的计划推成已完成
BulkPurchaseSettleJobsupplier-apiSchedulerX(BaseTaskProcessor),频率由控制台配置200结算到期且 over_flag=0 的单;每轮 warn 打印「已到期但超量未处理」积压数
BulkPurchaseOrderCancelJobsupplier-apiSchedulerX500取消到产出截止、无产出的待签到 / 待盘点单

跨服务手动结算

text
运营后台 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)

记账流程

BulkPurchaseSettleServiceImpl#settle:

  1. 自动路径再挡一次 over_flag=1(兜底)。
  2. 金额 ≤ 0:直接置已结算、写审计、提交后发通知,不产生账变。
  3. 供货方 increaseAccountMoney(乐观锁,冲突抛「操作冲突,请刷新后重试」)→ 写 95 账变。
  4. 发布主体 decreaseAccountMoney,不校验余额,允许负数 → 写 96 账变(金额为负)。
  5. finishSettle 带 status=30 + total_amount 条件置 40,失败抛异常回滚。
  6. 写审计 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,不提供生产修改语句。

供货单与等级

sql
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;

核对顺序:

  1. actual_label_num = Σ 等级 scanned_label_num;
  2. settle_label_num = Σ 等级 settle_label_num;
  3. total_amount = Σ(等级 settle_label_num × price)。

被登记补款的目标单,应结算数可能大于扫码数,属正常。

标签与扫码失败

sql
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;

补偿、账变、审计

sql
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.enabledadmin bootstrap.properties 为 true,关掉则需求推进任务不跑;supplier 为 ${SUPPLIER_SCHEDULING_ENABLED:true}
bulk-purchase.factory.supplier-ids运营端包花厂商品管理可见的发布主体范围;各环境值不同,不要照抄
SchedulerXsupplier 自动结算、自动取消任务;核对控制台启用状态、频率、最近执行结果
消息中心定采采用、结算完成通知,类型 14;30 秒内按 bizType + msgType + bizId + extraJson + 接收人幂等
开放平台扫码库存变更在事务提交后推送;推送失败不影响本地库存

联调注意与已知坑

项说明
扫码响应要看内层/wms/bulkPurchase/scan 被拒时外层 code=0,data.success=false + rejectReason,前端不能只看外层
打印入参是目标累计张数planNumMap 每个等级传目标总张数;同一等级重复会被 Map 覆盖,控制器组装前就要校验
重打状态限制宽只拦已取消;待结算、已结算单也能重打(不新增数量)
定采前调价基本不可达adjustPrice 要求计划 status=2,又要求未过「截止 − 5 秒」;自然截止流转到 2 时截止必然已过,只有「提前终止后定采失败卡在待定采」才可能走通。是否保留该入口待产品确认
两份 BulkPurchaseEnumadmin 与 supplier-core 各一份,PRINT_MAX_TIMES、OVER_*、COMPENSATE_* 等两边都用,改一边要同步另一边
两份 BulkPurchaseMoneyLockkey 都是 bp:money:demand:,改实现要两边一致,否则跨端互斥失效
测试环境定时任务测试服 SchedulerX 不通时,supplier 的自动结算 / 自动取消不会跑,需要手动结算或在测试服另行触发;admin 需求推进是 @Scheduled,不受影响
0 元结算也发通知金额为 0 置已结算、无账变,但仍给供货方发结算完成消息
处理超量可一步完成handleOver 可同时传 rankSettles + settleReason,在一个事务里先改量再处理;只传 settleReason 不传等级数量会被拒
列表与详情超量口径不同运营端列表对「over_flag=0 且有处理时间」的单返回 overNum=0、overAmount=0.00,详情按等级扫码数与定采数保留历史超量差额