业务口径仓库配货 / 打包
推荐箱型代码导读
现有按重量推荐箱型的代码入口、四处调用点、晚场重算与落库字段,附接手体积设箱(XYLZ-3678)要注意的坑
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(推荐箱型列表、件数、是否兜底) |
splitByBox | 71 | 非中通:先按最大箱容量整除放满,余量选能装下的最小箱 |
splitByBoxZto | 108 | 中通:每件按 min(45kg, 剩余重量) 选能装下的最小箱,每轮固定扣 45kg,件数恒为 ceil(总重 / 45);箱容量只用来选箱型 |
matchSpecialFlowerBox | 136 | 特殊花材专用箱按箱名匹配(先全等,再包含),不要求配容量 |
specialFlowerBoxIds | 142 | 当前配置下专用箱的 id 集合,晚场重算用它区分专用箱面单 |
availableBoxes | 180 | 参与推荐的箱子:flower_weight > 0,按名称排除专用箱,按 flower_weight 从小到大 |
兜底:一个可用箱子都没有时,中通按 45kg、其他按 60kg 计件,不给推荐箱型,PackPlan.fallback = true,调用方打 warn 日志。
例:非中通,可用箱容量 15 / 21 / 28.5 kg,可装重量 70kg
整除:70 ÷ 28.5 = 2 箱,余 13kg
余量:能装下 13kg 的最小箱 = 15kg 箱
结果:28.5 + 28.5 + 15,共 3 张面单特殊花材
- 定义在
enums/OrderEnum.java:SPECIAL_FLOWER_BOX_NAME_MAP(132 行)商品关键词 → 专用箱名,NEED_EXTRA_WMS_PACKAGE_PRODUCT_NAME是它的 key 集合。 - 映射:蝴蝶兰 / 红掌 / 大花蕙兰 → 蝴蝶兰箱,蓝色妖姬 → 蓝妖箱。商品名包含关键词即算。
- 业务前提:特殊花材到仓时已装箱,只贴面单,所以重量不占普通箱额度,每种专用箱固定追加 1 张面单(
OrderEnum.distinctSpecialFlowerBoxKeywords按专用箱去重,映射到同一箱的只算 1 张)。 - 特殊花材重量:
utils/OrderUtil.calcSpecialFlowerWeight(71 行),按订单行total_weight求和。
四处调用点
| 场景 | 位置 | 说明 |
|---|---|---|
| 发货单面单件数与推荐箱型 | 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 三路。
晚场时已存在面单的单子会重算:
- type=1 / 3:
replanSendOrderSuggests(2737 行),按发货单(同一货架当天同一个 sendNo)重算。 - type=2:只有存在 18:05 之后创建的发货单才重算,走
renightWareGroupSuggests。 - 两条路径都进
replanWareGroupSuggests(3018 行,static,有单测),结果由applyReplanResult落库。
replanWareGroupSuggests 的规则:
- 已出库(
expressStatus ≥ 20)的普通面单不动,从总重里扣掉它们的箱容量:优先实际箱型package_id,没有再用推荐箱型suggest_pack_id,都取不到按 0 并打 warn。 - 剩余重量重新
computePlan,按serialNum顺序覆盖未出库普通面单的推荐箱型;件数变多就追加,变少多余的不处理。 - 特殊花材:白场每种专用箱 1 张,晚场新增发货单再出现的每种再 1 张,与已有专用箱面单比差额补齐。
- 所有未出库面单同步更新
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 条「件数一致」指的就是「四处调用点」那张表里的四处。只改其中一处,面单张数和运单件数会对不上。
- 已有体积字段和数据:提交
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,里面有各箱体积的初始值。 utils/PackVolumeSuggestUtil不能直接用:它是验证用副本,只按体积拆、不看承重,算法是「最大箱整除 + 余量」,和需求要求的「重量、体积都满足才算装得下,装不下先放最大箱再循环」不同。- 可复用的查询:
mapper/dbBulky/PackageVolumeCompareMapper.listRankSkuVolume按 secondId 批量查等级体积。 - 实体缺字段:admin 的
XmBulkyPackConfig实体还没有flowerVolume,要补。 - 箱型配置有 Redis 缓存:
XmBulkyPackConfigServiceImpl.findAll()缓存 keyPACK_CONFIG,过期参数 100;上线时旧缓存里没有体积字段,改完箱型配置可调delCache()。 - 订单行查询要补字段:
OrderServiceImpl.handleAddSendOrderListDetail里查明细的select只取了部分列,没有secondId、rankNum;PrintOrderLogic用的getOrderItemsBySendNo是select c.*,不用改。 - 试点判断要的字段都在:
SendOrderDto/XmBulkySendOrder上有province和deliveryType(2 = 自提);province_area表按省份名直接匹配,TaskServiceImpl刷采购商大区时就是这么用的(ProvinceArea::getProvince做 key)。 - 城市仓合并面单:一组发货单的重量是求和得到的(
buildWareGroupPackageContext),体积也要按同一分组求和。 - 晚场重算:
replanWareGroupSuggests目前只建了 id →flower_weight的容量表,体积模式要再建一张 id →flower_volume,已出库箱同时扣两种容量。 - 记录推荐方式:新字段要在生成(
buildXmExpressPackage、buildWareExpressPackage)和重算(applyReplanResult的追加行、未出库行更新)两处都写,专用箱面单记成和同单普通面单相同的值。