业务口径仓库配货 / 打包

推荐箱型代码导读

现有按重量推荐箱型的代码入口、四处调用点、晚场重算与落库字段,附接手体积设箱(XYLZ-3678)要注意的坑

后端服务
admin
需求单
XYLZ-3678
更新
2026-11-02

Markdown 原文(给 AI 用):https://doc.xfun.fun/box-suggest-code-guide.md

结论

推荐箱型目前只按重量算,核心算法集中在一个工具类 PackSuggestUtil。面单生成、城市仓合并面单、晚场重算、打单报给物流的件数,这四处都调用同一个 computePlan,所以面单张数和运单件数能对上。改算法(比如体积设箱)时,四处必须一起改。

业务口径(给产品 / 测试看的版本)见 推荐箱型 / 设箱校验。本篇只讲代码在哪、怎么串起来。

说明

本篇路径默认以 xm-bulky-mall-admin/app/src/main/java/com/xunmeng/xmbulkymalladmin/ 为根,行号对应 2026-11-02 的 master(b94751a387),后续提交可能有偏移,按方法名搜索即可。

核心算法 PackSuggestUtil

文件:utils/PackSuggestUtil.java

方法行号作用
computePlan(totalWeight, specialWeight, ztoExpress, boxList)49唯一入口。普通箱可装重量 = 总重 − 特殊花材重量,再按物流选算法;返回 PackPlan(推荐箱型列表、件数、是否兜底)
splitByBox71非中通:先按最大箱容量整除放满,余量选能装下的最小箱
splitByBoxZto108中通:每件按 min(45kg, 剩余重量) 选能装下的最小箱,每轮固定扣 45kg,件数恒为 ceil(总重 / 45);箱容量只用来选箱型
matchSpecialFlowerBox136特殊花材专用箱按箱名匹配(先全等,再包含),不要求配容量
specialFlowerBoxIds142当前配置下专用箱的 id 集合,晚场重算用它区分专用箱面单
availableBoxes180参与推荐的箱子:flower_weight > 0,按名称排除专用箱,按 flower_weight 从小到大

兜底:一个可用箱子都没有时,中通按 45kg、其他按 60kg 计件,不给推荐箱型,PackPlan.fallback = true,调用方打 warn 日志。

text
例:非中通,可用箱容量 15 / 21 / 28.5 kg,可装重量 70kg
  整除:70 ÷ 28.5 = 2 箱,余 13kg
  余量:能装下 13kg 的最小箱 = 15kg 箱
  结果:28.5 + 28.5 + 15,共 3 张面单

特殊花材

四处调用点

场景位置说明
发货单面单件数与推荐箱型service/impl/OrderServiceImpl.java 1335 行,方法 handleAddSendOrderListDetail结果写进 SendOrderDto.expressPackageNum / suggestPackList。特殊花材重量和种类由同文件 addExtraExpressPackageNum(1425 行)统计。这个方法后台发货单列表、导出也在调
昆明→店(type=1)、仓→店(type=3)面单落库service/impl/TaskServiceImpl.java 2645 行 generateXmWmsExpressPackage不重新算,直接用发货单 DTO 上的结果;每个推荐箱型一行面单,专用箱面单排在普通面单后面(pickSuggest,2997 行)
城市仓合并面单(type=2)TaskServiceImpl.java 2838 行 generateWareWmsExpressPackage先 groupSameWareExpressPackage(3182 行)分组:繁花按货位、大客户按独立货位、其余普通单合一组;组内重量求和后调 computePlan
打印运单报给物流的件数logic/PrintOrderLogic.java 479 行 → utils/LogisticsUtil.java 191 行 getOrderNumByWeightAndConfigId内部同样调 computePlan,再按专用箱种类加件数;补单(isRepair = 1)直接用请求传入的件数

中通判断有两种写法,口径一致:面单侧按物流名称包含「中通」,打单侧按 configId 是否为 ZT_KY / ZT_LL / ZT_SMALL。

定时任务与晚场重算

任务入口查哪些发货单
白场生成task/order/GenerateWmsExpressPackageJob(SchedulerX)→ TaskServiceImpl.generateWmsExpressPackage(2569 行)当轮、创建时间早于 18:05
晚场生成 / 重算task/order/GenerateWmsExpressPackageNightJob → generateWmsExpressPackageNight(2584 行)当轮全部

MyTask 里旧的 @Scheduled 已注释,两个任务都走 SchedulerX。两者最终进入 generateWmsExpressPackageBySendOrderList(2600 行),按 wareId 和城市仓类型拆成 type=1 / 3 / 2 三路。

晚场时已存在面单的单子会重算:

replanWareGroupSuggests 的规则:

  1. 已出库(expressStatus ≥ 20)的普通面单不动,从总重里扣掉它们的箱容量:优先实际箱型 package_id,没有再用推荐箱型 suggest_pack_id,都取不到按 0 并打 warn。
  2. 剩余重量重新 computePlan,按 serialNum 顺序覆盖未出库普通面单的推荐箱型;件数变多就追加,变少多余的不处理。
  3. 特殊花材:白场每种专用箱 1 张,晚场新增发货单再出现的每种再 1 张,与已有专用箱面单比差额补齐。
  4. 所有未出库面单同步更新 total_weight、package_num、item_num。

落库与下游

表 / 字段说明
xm_bulky_wms_express_package.suggest_pack_id / suggest_pack_info推荐箱型 id 和名称,每张面单一行
xm_bulky_wms_express_package.package_id / package_info仓库实际设的箱
xm_bulky_pack_config.flower_weight箱子可装花材重量(kg),0 不参与推荐

供应商端(仓库打包)读 suggest_pack_id 做设箱校验:xm-bulky-mall-supplier/supplier-core/.../logic/PackageLogic.java(111 行附近判断实际箱与推荐不一致,calcBoxMatchStatus 算匹配状态)。

单测

xm-bulky-mall-admin/app/src/test/java/com/xunmeng/xmbulkymalladmin/ 下:

测试覆盖
utils/PackSuggestUtilTest拆箱算法、中通、兜底、专用箱匹配
service/impl/TaskServiceImplReplanTest晚场重算(已出库扣容量、追加、专用箱补差)
service/impl/OrderServiceImplSpecialFlowerStageTest特殊花材白场 / 晚场拆分
utils/PackConfigTestFixtures测试用箱型配置

接手体积设箱(XYLZ-3678)的注意事项

重要

需求第 4 条「件数一致」指的就是「四处调用点」那张表里的四处。只改其中一处,面单张数和运单件数会对不上。

  1. 已有体积字段和数据:提交 499aa5cf43(2026-09-13,按体积推荐对比功能)已给 xm_bulky_pack_config 加 flower_volume(cm³),给 xm_bulky_class_second_rank_sku 加 volume(cm³/扎,当前默认 3000)。DDL 在 xm-bulky-mall-admin/docs/sql/2026-09-13-package-volume-compare.sql,里面有各箱体积的初始值。
  2. utils/PackVolumeSuggestUtil 不能直接用:它是验证用副本,只按体积拆、不看承重,算法是「最大箱整除 + 余量」,和需求要求的「重量、体积都满足才算装得下,装不下先放最大箱再循环」不同。
  3. 可复用的查询:mapper/dbBulky/PackageVolumeCompareMapper.listRankSkuVolume 按 secondId 批量查等级体积。
  4. 实体缺字段:admin 的 XmBulkyPackConfig 实体还没有 flowerVolume,要补。
  5. 箱型配置有 Redis 缓存:XmBulkyPackConfigServiceImpl.findAll() 缓存 key PACK_CONFIG,过期参数 100;上线时旧缓存里没有体积字段,改完箱型配置可调 delCache()。
  6. 订单行查询要补字段:OrderServiceImpl.handleAddSendOrderListDetail 里查明细的 select 只取了部分列,没有 secondId、rankNum;PrintOrderLogic 用的 getOrderItemsBySendNo 是 select c.*,不用改。
  7. 试点判断要的字段都在:SendOrderDto / XmBulkySendOrder 上有 province 和 deliveryType(2 = 自提);province_area 表按省份名直接匹配,TaskServiceImpl 刷采购商大区时就是这么用的(ProvinceArea::getProvince 做 key)。
  8. 城市仓合并面单:一组发货单的重量是求和得到的(buildWareGroupPackageContext),体积也要按同一分组求和。
  9. 晚场重算:replanWareGroupSuggests 目前只建了 id → flower_weight 的容量表,体积模式要再建一张 id → flower_volume,已出库箱同时扣两种容量。
  10. 记录推荐方式:新字段要在生成(buildXmExpressPackage、buildWareExpressPackage)和重算(applyReplanResult 的追加行、未出库行更新)两处都写,专用箱面单记成和同单普通面单相同的值。