# 推荐箱型 / 设箱校验 > 打包小程序设箱确认弹窗、货架列表合规筛选与角标、包裹详情设箱不符标识;运营后台打包配置预估可装重量 - 类型:前端对接 - 分类:仓库配货 / 打包 - 端:打包小程序 / 运营后台 - 后端服务:supplier / admin - 更新:2026-08-07 - 网页版:https://doc.xfun.fun/box-suggest-api 包裹单生成时后端会写入系统推荐箱型。打包小程序需要新增三处交互:设箱不符时的确认弹窗、打包完成货架列表的合规筛选与角标、包裹详情的"设箱不符"标识;运营后台需要在打包配置页维护"预估可装花材重量"。本页覆盖全部接口变更与字段口径。 供应商端 · 打包小程序 + 运营后台 服务:**supplier-api**(小程序) / **bulky-admin**(运营后台) 全部 **POST** · JSON Body 更新:**2026-08-07** ## 核心概念:boxMatchStatus 所有包裹对象新增的匹配状态字段,前端展示只认它 每个包裹可能带有推荐箱型(`suggestPackId` / `suggestPackInfo`)。打包员设箱后,实际箱型与推荐箱型的比对结果由后端算好放在 `boxMatchStatus`,前端 **不要自行比对 id**: | boxMatchStatus | 含义 | 展示 | 说明 | |----|----|----|----| | `0` | 无推荐 | 什么都不展示 | 历史包裹、中通普通包裹、未设箱的包裹都是 0,界面与现状一致。特殊花材附加面单**有**推荐(蝴蝶兰/红掌/大花蕙兰→蝴蝶兰箱,蓝色妖姬→蓝妖箱),仅当配置里没有对应箱型时才为 0 | | `1` | 设箱合规 | 正常态,无强调 | 实际箱型 = 推荐箱型;筛选 chips 里的「设箱合规」对应此态 | | `2` | 设箱不符 | 红色标识 | 实际箱型 ≠ 推荐箱型(小程序为确认强制使用;PDA 扫码不拦截直接落库)。详情打红标,货架卡片打「不符」角标 | ## 设箱确认弹窗流程 打包完成 / 改箱时,箱型与推荐不符会被后端拦下,需要二次确认 #### 正常调用打包完成接口 扫箱型码后按现有逻辑调 `/package/pack/finish`(或 scan / change),参数不变,`forceFlag` 不传或传 0。 #### 后端返回 code = 6001 时弹确认框 新错误码 `6001`(箱型与推荐不符)。**用 code 判断,不要匹配 msg 文案**。msg 已带推荐箱型名,可直接作为弹窗正文。其余错误码照旧当普通失败 toast。 弹窗示意:正文直接渲染后端 msg,例如「该箱型与系统推荐箱型(110\*60\*60)不符,请确认合理性并强制使用」,按钮为【否】【确认使用】。 #### 按用户选择分支处理 - **点「否」**:关闭弹窗即可,不发任何请求,箱型不变更,包裹仍是待打包状态。 - **点「确认使用」**:用 **完全相同的原参数**重调同一接口,仅追加 `forceFlag: 1`。成功后按现有成功逻辑走,后端自动记录强制使用留痕。 > [!NOTE] > **注意:** 中通冷链禁用大箱的报错仍是普通错误码 `1`(msg「中通冷链不允许50\*50以上的箱子」),**不能**通过 forceFlag 绕过,不要对它弹确认框。 ## 接口变更:打包完成 ×3 三个接口入参与错误码变更完全一致 ### POST /package/pack/finish · 打包师傅 · 拍照设箱 同规则适用:`/package/pack/finish/change`(打包完成后改箱)。**扫码枪 `/package/pack/finish/scan`(PDA)除外**:PDA 无弹窗交互,设箱不符 **不拦截、不返回 6001**,直接完成并由后端留痕,PDA 前端无需任何改造。 #### 入参新增 **新增** | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | forceFlag | int | 否 | 默认 0。用户在确认弹窗点「确认使用」后重调时传 **1**,其余参数保持与首次调用一致 | #### 新增错误码 `6001` 失败响应示例(触发确认弹窗) ```json { "code": 6001, "msg": "该箱型与系统推荐箱型(110*60*60)不符,请确认合理性并强制使用", "data": null } ``` 确认后重调请求示例(仅多一个 forceFlag) ```json { "codeContent": "88231", "packId": 14, "packName": "110*50*50箱", "url": "https://…/finish.jpg", "shelfPackage": 1, "forceFlag": 1 } ``` ## 接口变更:货架列表筛选 打包师傅首页「打包完成」tab 的 chips 筛选与卡片角标 ### POST /package/shelf/page · 打包师傅 · 货架分页 #### 入参新增 **新增** | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | boxMatchFilter | int | 否 | 不传 / 0 = 全部;**1** = 仅设箱合规;**2** = 仅设箱不符。chips 仅在 `status=1`(打包完成)tab 下展示,切 tab 或取消选中时不传 | #### 出参 records\[\] 新增 **新增** | 字段 | 类型 | 说明 | |----|----|----| | boxMismatchFlag | int | 0 / 1。为 1 时货架卡片右上角展示红色「不符」角标。该货架下任一包裹设箱不符即为 1 | > [!NOTE] > 筛选「仅设箱不符」且当日没有任何不符货架时,接口正常返回空列表(total=0),按空态展示即可。 ## 接口变更:详情与列表的包裹字段 包裹对象(WmsExpressPackageInfoVo)统一新增三个字段 ### POST /package/shelf/detail · 货架打包详情 同字段出现于:`/package/info`(扫码包裹信息)、`/package/page`、`/package/list`(已废弃)。凡返回包裹对象的地方都带。 #### 包裹对象新增字段 **新增** | 字段 | 类型 | 说明 | |----|----|----| | suggestPackId | int | 推荐箱型配置 id,**0 = 无推荐** | | suggestPackInfo | string | 推荐箱型名称,如 110\*60\*60箱;无推荐时为空串 | | boxMatchStatus | int | 0 无推荐 / 1 设箱合规 / 2 设箱不符,见[核心概念](#核心概念boxmatchstatus) | 响应片段示例 ```json { "id": 88231, "packageId": 14, "packageInfo": "110*50*50箱", "suggestPackId": 17, "suggestPackInfo": "110*60*60箱", "boxMatchStatus": 2, // 展示红色「设箱不符」标签 "expressStatus": 10 } ``` ## 接口变更:运营后台(bulky-admin) 打包配置维护预估花材容量 + 发货单列表展示推荐箱型 ### POST /system/add/packConfig · 仓库管理 · 打包配置 · 新增/编辑 #### 入参新增 **新增** | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | flowerWeight | decimal | **是** | 预估可装花材重量(kg),范围 0~1000,最多 2 位小数。**0 表示不参与推荐箱型**(专用箱如蝴蝶兰箱、蓝妖箱就配 0)。不传后端报「预估可装花材重量不能为空」 | > [!NOTE] > **必须与后端同步上线:** 字段为必填,旧版配置页不传该字段时,新增/编辑都会保存失败。 ### POST /system/packConfigList · 仓库管理 · 打包配置 · 列表 #### 出参新增 **新增** | 字段 | 类型 | 说明 | |----|----|----| | flowerWeight | decimal | 预估可装花材重量(kg)。上线时存量配置会统一清零,由运营逐个补录 | #### 配置页展示建议 - 列表增加「预估可装花材重量」列;值为 0 的箱型标注「未配置,不参与推荐」,提示运营补全。 - 多个箱型容量相同时推荐取 **排序更靠前**的那个,建议保存时对重复容量给个提醒(不阻断)。 ### POST /order/wmsExpressPackage/list · 打单页 · 物流面单列表 #### 出参 records\[\] 新增 **新增** | 字段 | 类型 | 说明 | |----|----|----| | suggestPackId | int | 推荐箱型配置 id,0 = 无推荐 | | suggestPackInfo | string | 推荐箱型名称 | | suggestPackFlowerWeight | decimal | 推荐箱型的预估可装花材重量(kg);无推荐或该箱型未配置容量时为 `null` | `/order/wmsExpressPackage/listByIds` 同样返回这三个字段。实际箱型仍看 `packageId` / `packageInfo`(设箱后才有值)。 ### POST /order/sendOrder/list · 发货单 · 分页列表/导出 #### 出参 records\[\] 新增 **新增** | 字段 | 类型 | 说明 | |----|----|----| | suggestPackList | array | 按序对应每个 **普通面单**的推荐箱型,元素为 `{packId, packName}`。中通或未配置箱型容量时为空数组 | 响应片段示例 ```json { "sendNo": "SD20260807001", "allTotalWeight": 180.00, "expressPackageNum": 3, "suggestPackList": [ { "packId": 17, "packName": "110*60*60箱" }, { "packId": 17, "packName": "110*60*60箱" }, { "packId": 3, "packName": "小件90*20" } ] } ``` > [!NOTE] > **expressPackageNum 口径变化:** 普通物流现在等于推荐箱型个数(不再固定按 60kg 拆);中通仍按 45kg 拆、航邦 50kg 特殊拆分已取消;所有箱型都未配置容量时回退 60kg 拆分。特殊花材的附加面单不在 `suggestPackList` 里,件数口径不变。 ## UI 落点 对照产品图的三个展示位 #### ① 货架列表 · 筛选 chips 设箱合规 设箱不符 仅「打包完成」tab 下展示,单选可取消。选中「设箱不符」传 `boxMatchFilter=2`,合规传 1,取消不传。 #### ② 货架卡片 · 角标 12-197 卡片右上角 不符 `boxMismatchFlag=1` 时展示红色角标,任意 tab 下有数据就展示,不依赖筛选是否开启。 #### ③ 打包详情 · 包裹标签 包裹类型:110\*50\*50箱 `设箱不符` `boxMatchStatus=2` 时在「包裹类型」旁展示红标;为 1 时不额外展示;为 0 时保持现状。 ## 联调注意 容易踩的坑集中在这里 - **错误码判断只认 code=6001**,msg 文案(含推荐箱型名)可能调整,别做字符串匹配。 - **forceFlag=1 重调必须复用首次请求的全部原参数**(codeContent、packId、packName、url 等),只追加 forceFlag,否则会以错误的箱型落库。 - `suggestPackId=0` 的包裹(历史数据、中通普通面单)永远不会返回 6001,也不展示任何标识——测试时别拿存量包裹验证弹窗,需用 **新生成的包裹**。 - **特殊花材附加面单同样有推荐箱型**(蝴蝶兰箱 / 蓝妖箱),设箱不符也会返回 6001、展示不符标识;前提是打包配置里存在同名箱型,测试前先确认配置。 - 弹窗「否」纯前端行为,不发请求;不要把「否」实现成带 forceFlag=0 的重调。 - 扫码枪接口 `/pack/finish/scan`(PDA) **不返回 6001**:设箱不符直接打包成功,后端自动留痕,包裹仍会显示"设箱不符"标识;确认弹窗只需小程序端实现。 - 防重锁与校验顺序已调整:6001 被拦时 **不占用**10 秒扫码频控,确认后立即重调不会报「扫码频繁」。 - 推荐箱型展示(如设箱前提示「推荐:110\*60\*60箱」)可直接用 `/package/info` 返回的 `suggestPackInfo`,为空串就不展示。 --- # 推荐箱型代码导读 > 现有按重量推荐箱型的代码入口、四处调用点、晚场重算与落库字段,附接手体积设箱(XYLZ-3678)要注意的坑 - 类型:业务口径 - 分类:仓库配货 / 打包 - 后端服务:admin - 需求单:XYLZ-3678 - 更新:2026-11-02 - 网页版:https://doc.xfun.fun/box-suggest-code-guide ## 结论 推荐箱型目前**只按重量**算,核心算法集中在一个工具类 `PackSuggestUtil`。面单生成、城市仓合并面单、晚场重算、打单报给物流的件数,这四处都调用同一个 `computePlan`,所以面单张数和运单件数能对上。改算法(比如体积设箱)时,四处必须一起改。 业务口径(给产品 / 测试看的版本)见 [推荐箱型 / 设箱校验](/box-suggest-logic)。本篇只讲代码在哪、怎么串起来。 > [!NOTE] > 本篇路径默认以 `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 日志。 ```text 例:非中通,可用箱容量 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` 的规则: 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)的注意事项 > [!IMPORTANT] > 需求第 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` 的追加行、未出库行更新)两处都写,专用箱面单记成和同单普通面单相同的值。 --- # 推荐箱型 / 设箱校验 > 白场拆箱、中通 45kg 逐箱匹配、晚场加单重算(含昆明→店/仓→店按货架重算)、特殊花材专用箱、设箱校验完整口径 - 类型:业务口径 - 分类:仓库配货 / 打包 - 更新:2026-08-11 - 网页版:https://doc.xfun.fun/box-suggest-logic ## 一句话总览 运营端生成面单时,系统按发货单重量 **自动推荐每张面单用什么箱**;仓库打包时实际设箱与推荐不一致会 **校验提示**。晚场加单后,未出库的面单按新重量 **整体重算**推荐。 ## 前提配置 | 配置 | 说明 | |----|----| | 箱型配置(打包配置) | 每种箱录入「可装花材重量」(kg)。**没录容量的箱不参与推荐**。 | | 特殊花材专用箱 | 需新增两条配置:**蝴蝶兰箱**(蝴蝶兰 / 红掌 / 大花蕙兰 共用)、**蓝妖箱**(蓝色妖姬)。按 **箱名**匹配(先找同名,找不到再找名称包含的),不要求录容量。 | | 特殊花材定义 | 商品名包含:蝴蝶兰、红掌、大花蕙兰、蓝色妖姬。**业务前提:到仓时已装箱,仓库只需贴面单**,所以不存在装不下的问题。 | ## 白场面单生成(运营端) 1. **算可装重量**:可装重量 = 发货单总重 − 特殊花材重量(特殊花材单独装箱,不占普通箱额度) 2. **按物流拆箱**:普通物流与中通算法不同,见下表;每拆出一箱 = 一张面单,并写上推荐箱型 3. **特殊花材追加专用面单**:按 **专用箱**追加面单,不是按种类:**蓝色妖姬**单独 1 张(蓝妖箱),**蝴蝶兰 / 红掌 / 大花蕙兰**合并 1 张(蝴蝶兰箱)。即最多 2 张,映射到同一箱的多种花材只算 1 张 ### 两种拆箱算法 | 物流 | 算法 | |----|----| | 普通物流 (顺丰等) | **最大箱优先**:先用容量最大的箱整箱装,剩余零头选 **能装下的最小箱**。 | | 中通 (快运/落地/小件) | **45kg 逐箱匹配**:件数固定为 ceil(总重÷45),与兜底口径一致。每轮取 min(45kg, 剩余重量) 作为一件的重量,选 **能装下这件的最小箱**作推荐,再按 **min(45kg, 剩余重量)** 扣减剩余重量(**箱容量只决定推荐哪个箱,不参与扣减**)。所有箱都装不下 45kg 时用最大箱。 | - **普通物流例**:180kg,箱型 6 / 70 / 80 / 87 → 87箱 ×2(装 174)+ 零头 6kg → 6箱 ×1,共 3 箱。 - **中通例**:100kg,有容量 60 的 100\*60 箱 → 45 + 45 + 10 三件,每件都匹配到 100\*60 箱 → 共 **3 个 100\*60 箱**(件数 = ceil(100÷45) = 3,与兜底口径一致)。 > [!NOTE] > **兜底**:一个可用箱型都没配置时,不推荐箱型,只按固定重量计件数:中通每件 45kg、其他物流每件 60kg(向上取整)。 > [!NOTE] > **航邦物流**:与普通物流走同一套拆箱算法,无固定重量特判。 ## 打单 / 电子面单(运单数) 运单数与「白场面单生成」口径 **完全一致**:运单数 = 拆箱得到的箱数 + 特殊花材 **专用箱数**(蓝妖箱、蝴蝶兰箱各至多 1)。中通同样走 45kg 逐箱匹配算法(上例 100kg 中通即 3 单)。**补打**面单不重新拆箱、不算特殊花材。 ## 晚场加单重算 **触发条件**:晚场(18:05 后)加单导致某个打包分组重量变化时,对该分组重算。三种面单的重算单元不同: | 面单类型 | 重算单元 | 说明 | |----|----|----| | 昆明→店 / 仓→店 | **按货架(发货单)** | 同一货架当天所有订单(含晚场)合并装箱,收货人相同。晚场该货架增重 → 按新合计重量把整货架的面单重排。 | | 城市仓 | **按整仓聚合** | 整个城市仓的普通订单混在一起算箱,维持原粒度,不拆到货架级。 | 三者的重算步骤一致,如下: 1. **已出库的面单不动**:已出库面单及其箱子维持原样 2. **估算已出库带走的重量**:按已出库面单的箱容量合计(优先实际设的箱,没设箱按推荐箱,默认装满) 3. **剩余重量重新拆箱**:剩余 = 新总重 − 特殊花材重量 − 已出库容量;拆箱算法同白场(中通 45kg 逐箱匹配、普通最大箱优先) 4. **未出库面单整体承接新推荐**:未出库面单(含已设箱的)按顺序套用新箱型序列,**不保留**原推荐;不够就补新面单,多出来的面单不处理 5. **特殊花材追加**:白场每个 **专用箱** 1 张不变;**晚场又新增了某专用箱的花材,再追加 1 张**(无论白天是否已有同箱——晚场是新装的箱子)。按明细下单时间分白/晚场,同一阶段同一专用箱只算 1 张,重算幂等不重复追加 > [!NOTE] > 推荐变化只打日志,不做留痕/通知;专用箱未配置时晚场跳过追加。 **中通晚场例**:晚场加单后总重 190kg、其中特殊花材 10kg,已出库 2 个 80 箱(按容量估算带走 160kg)→ 剩余 190−10−160 = 20kg → 未出库部分重拆为 1 个能装下 20kg 的最小箱。 ## 供应商端设箱校验(仓库打包) | 入口 | 行为 | |----|----| | 小程序拍照打包 / 修改箱型 | 实际箱型 ≠ 推荐箱型 → **拦截弹窗**(错误码 6001),确认后可继续。 | | PDA 扫码枪打包 | **不拦截**,仅留痕备注「扫码枪打包,实际箱型与推荐箱型不一致」。 | | 货架列表 | 「设箱有变化」的货架可筛选、有角标提示(小程序端)。 | > [!NOTE] > 中通面单同样有箱型推荐,设箱校验对中通 **同样生效**。 ## 已知边界 - 中通面单件数固定 = ceil(总重÷45),单件平均重量 = 总重 ÷ 件数 **≤ 45kg**(如 100kg → 3 件、约 33kg/件),与中通计费口径一致。 - 部分出库按「箱默认装满」估算带走重量,是启发式:若实际没装满,会少算剩余重量、少推荐箱。 - 件数变少时多余面单不回收,仓库按实际情况使用。 - 运营后台暂无「设箱有变化」查阅入口(目前仅小程序端);推荐依据重量/推荐箱容量未落库。 --- # 推荐箱型 / 设箱校验 > 需求速读、白场与晚场完整测试步骤、供应商设箱校验、测试矩阵及可执行 SQL - 类型:测试手册 - 分类:仓库配货 / 打包 - 更新:2026-08-08 - 网页版:https://doc.xfun.fun/box-suggest-test-guide 给第一次接触该需求的测试人员。目标不是只在后台看到一个箱型字段,而是把“配置、生成、打包校验、晚场重算”四条链路完整走通。 依据提交 `0019b5646c3a98e7c813e0cd14a4ba7561ebf89f`,测试场次日期以 `2026-10-27` 为例,执行时替换为当天。 ## 交付状态 - **白场测试数据** 已于 2026-08-08 在测试环境验证 - **供应商设箱** 需使用有货区权限的打包账号 - **晚场加单** 必须使用 type=2 非自营城市仓数据 - **真实令牌** 不写入文档,只在本机临时粘贴 ## 这次到底改了什么 一件花材订单先按箱型容量生成推荐方案;仓库真正设箱时再比较实际箱与推荐箱;城市仓晚场有新单时,还要把未出库包裹按新总量重新规划。 **本提交覆盖四条业务链路** - 后台打包配置新增 `flowerWeight`,单位 kg,0 表示不参与普通推荐。 - 普通物流按箱型容量拆包,中通仍按每 45kg 一件且不推荐普通箱。 - 供应商设箱不符时,小程序先返回 `6001`,确认后带 `forceFlag=1` 重调并留痕。 - 非自营城市仓晚场加单后,type=2 聚合面单按白天加晚场总量重算。 > [!IMPORTANT] > **不要把测试缩成一个列表查询**:后台列表有推荐值,只能证明生成落库成功。它不能证明 6001 二次确认、强制留痕、PDA 绕过、晚场幂等与部分出库扣重正确。 1. **配置容量**:普通箱维护可装花材重量;专用箱容量为 0。 2. **生成推荐**:最大箱先装整箱,余数匹配第一个能装下的最小箱。 3. **实际设箱**:实际箱与推荐箱比较,生成合规、不符或不可比较状态。 4. **晚场重算**:仅城市仓 type=2;已出库不动,其余重排并按需追加。 > [!WARNING] > **以当前代码为唯一验收口径。** > > 旧文档有两处已过时:PDA 现在遇到不符不会返回 6001;白天已有蝴蝶兰且晚场再次新增蝴蝶兰时,当前实现会再追加一张蝴蝶兰箱面单。 ## 先记住这些判定规则 下面是执行测试时最容易混淆的口径。建议先用两分钟看完,再开始造数据。 | 规则 | 输入 | 正确结果 | |----|----|----| | 普通箱拆分 | 80kg,最大容量 87kg | 1 张 110\*60\*60 | | 整箱加余数 | 180kg | 87 + 87 + 6.1,共 3 张 | | 特殊花材 | 普通45kg + 蝴蝶兰2kg + 蓝色妖姬1kg | 1 张普通箱 + 蝴蝶兰箱 + 蓝妖箱 | | 中通 | 100kg | ceil(100/45)=3 张,推荐字段为空 | | 无有效容量 | 所有普通箱 flowerWeight=0 | 回退每 60kg 一件,推荐字段为空 | | 匹配状态 | 未设箱,或无推荐 | `boxMatchStatus=0`,尚不可判定合规 | | 匹配状态 | 实际箱 = 推荐箱 | `boxMatchStatus=1` | | 匹配状态 | 实际箱 ≠ 推荐箱 | `boxMatchStatus=2` | | 晚场状态 0/10 | 未出库 | 一起按新推荐序列覆盖,实际箱不跟随修改 | | 晚场状态 ≥20 | 已出库 | 任何字段都不改,优先按实际箱容量扣减已带走重量 | > [!NOTE] > **18:05 是当前边界:** 白场只取 `create_time < 18:05`,晚场新增判断只认 `create_time > 18:05`。主流程请用 17:00 和 21:00,避免恰好 18:05 的边界空档。 ## 环境准备与完整 SQL 下文 cURL 里的场次日期 `2026-10-27`、API 地址、`usertokenxm`、`wareId` 都是占位,执行前替换成实际值。**令牌不要写进文档或聊天记录。** ### 执行前置 | 检查项 | 要求 | 不满足时 | |----|----|----| | 数据库字段 | `flower_weight`、`suggest_pack_id`、`suggest_pack_info` 已存在 | 停止,先补发布 DDL | | Admin 服务 | 包含本提交,测试接口可调用 | 确认部署分支和实例版本 | | Supplier 服务 | 包含本提交,账号有打包货区权限 | 更换账号或调整 SQL 货位 | | 物流配置 | 供应商脚本的 `@express_id` 是有效物流且不等于 146 | 改成测试环境真实 id | | 配置缓存 | 直接 SQL 改箱型后等待约 100 秒,或后台保存任一配置刷新 | 旧缓存会让结果看似错误 | #### 只读 DDL 自检 ```sql SELECT table_name, column_name, column_type FROM information_schema.columns WHERE table_schema = DATABASE() AND ((table_name = 'xm_bulky_pack_config' AND column_name = 'flower_weight') OR (table_name = 'xm_bulky_wms_express_package' AND column_name IN ('suggest_pack_id', 'suggest_pack_info'))) ORDER BY table_name, ordinal_position; -- 预期恰好返回 3 行 ``` ### 测试数据文件 - **SQL 1** [测试数据-推荐箱型设箱校验-admin.sql](/测试数据-推荐箱型设箱校验-admin.sql):箱型容量 + TESTADM-A/B/C/D 白场后台数据,可重复执行 - **SQL 2** [测试数据-推荐箱型设箱校验.sql](/测试数据-推荐箱型设箱校验.sql):TESTBOX-A/B/C/D 供应商设箱、筛选、角标和 6001 数据 - **SQL 3** [测试数据-推荐箱型设箱校验-晚场.sql](/测试数据-推荐箱型设箱校验-晚场.sql):隔离城市仓的白场 + 21:00 加单,支持 NORMAL / PACKED / PARTIAL / SPECIAL / ZTO > [!WARNING] > **只在测试库执行。**Admin 脚本会把 DDL 默认残留的 `flower_weight=1.00` 清零,并按测试名称清理旧数据。先确认当前连接库名,再执行。 ## 白场后台生成 这组用例证明容量拆分、特殊花材、中通和幂等。TESTADM 数据是 ware_id=0 的 type=1 数据,不能拿去测晚场城市仓重算。 ### 步骤 1 · 执行 Admin 测试 SQL 执行 SQL 1。结果区必须看到 `template_ready=OK`,且 TESTADM-A 到 D 的 `generation_ready` 都是 OK。 **验收**:SQL 执行成功,模板和 4 条发货单自检均为 OK ### 步骤 2 · 触发白场包裹生成 调用一次测试接口。若 SQL 刚修改箱型容量,先等待缓存过期或在后台保存任一箱型。 `POST generateWmsExpressPackage` ```bash curl --url 'https://api.xunmengvip.cn/xm-bulky-admin/test/generateWmsExpressPackage' \ -H 'accept: */*' \ -H 'channel: admin' \ -H 'content-type: application/json' \ -H 'usertokenxm: <粘贴当前 usertokenxm>' \ --data-raw '{"date":"2026-10-27"}' ``` **验收**:接口 HTTP 200 且业务响应成功 ### 步骤 3 · 查询全部序号 列表请求不要传 `serialNum:1`,否则只能看到每组第 1 张,容易误判“没有追加面单”。 `POST wmsExpressPackage/list · sceneType=1` ```bash curl --url 'https://api.xunmengvip.cn/xm-bulky-admin/order/wmsExpressPackage/list' \ -H 'accept: */*' \ -H 'channel: admin' \ -H 'content-type: application/json' \ -H 'usertokenxm: <粘贴当前 usertokenxm>' \ --data-raw '{"roundNo":"2026-10-27","sceneType":1,"zoneList":[],"index":1,"size":100}' ``` **验收**:已核对 TESTADM-A/B/C/D 的全部 serialNum ### 步骤 4 · 对照精确预期 A:1 张 110\*60\*60。B:110\*60\*60、110\*60\*60、小件90\*20。C:普通 110\*45\*45,加蝴蝶兰箱与蓝妖箱各 1 张。D:中通 3 张,推荐 id 全为 0。 **验收**:数量、推荐名称、容量与 packageNum 全部符合 ### 步骤 5 · 重跑验证幂等 先记录 TESTADM 包裹总行数和 id,再调用同一个白场接口一次。行数与 id 必须不变。 **验收**:第二次生成没有重复插入 #### 白场数据库断言 ```sql SELECT p.send_no, p.serial_num, p.package_num, p.suggest_pack_id, p.suggest_pack_info, c.flower_weight AS suggest_capacity, p.total_weight, p.express_status FROM xm_bulky_wms_express_package p LEFT JOIN xm_bulky_pack_config c ON c.id = p.suggest_pack_id WHERE p.send_no LIKE 'TESTADM%' AND p.data_status = 0 ORDER BY p.send_no, p.serial_num; -- 幂等前后记录两次结果,COUNT 与 id 集合都应一致 SELECT COUNT(*) AS row_count, GROUP_CONCAT(id ORDER BY id) AS id_list FROM xm_bulky_wms_express_package WHERE send_no LIKE 'TESTADM%' AND data_status = 0; ``` ## 供应商设箱校验 每次测试会修改包裹状态。建议一个动作验证完就重跑 SQL 2 复位,避免前一个用例影响下一个用例。 > [!WARNING] > 执行 SQL 前先改 `@shelf_a` 到 `@shelf_d`,确保货位在测试打包员权限范围内;把 `@express_id` 改成有效物流 id,且不要用 146。成功打包后,同一 userId 测下一包需间隔 10 秒。 ### 步骤 1 · 执行 Supplier 测试 SQL 记录脚本最后输出的包裹 id,扫码内容就是该 id。T-991 待打包;T-992 合规;T-993 含不符;T-994 无推荐。 **验收**:已保存 T-991 到 T-994 的包裹 id 和箱型 id ### 步骤 2 · 验证货架列表与筛选 `status=0` 时验证 T-991。`status=1` 时应有 T-992/T-993/T-994;T-993 的 `boxMismatchFlag=1`。筛选 2 仅 T-993;筛选 1 返回 T-992 和 T-994。 **验收**:全部、合规、不符三个筛选结果正确 ### 步骤 3 · 验证详情状态 T-991 三行均为 0,但前两行仍有推荐字段;T-992 两行均为 1;T-993 序号1为2、序号2为1;T-994为0。 **验收**:suggestPackId、suggestPackInfo、boxMatchStatus 均符合 ### 步骤 4 · 推荐箱直接成功 重置 SQL 后,对 T-991 序号1传推荐箱,`forceFlag=0`。预期成功,状态10,匹配状态1,不产生强制不符留痕。 **验收**:推荐箱设箱成功且无不符留痕 ### 步骤 5 · 不符时 6001,再确认强制使用 重置 SQL 后,对 T-991 序号2选另一个箱。首次 `forceFlag=0` 必须返回6001且不改状态;立刻用完全相同参数重调,只改 `forceFlag=1`,应成功并记录一次修改留痕。 **验收**:6001、取消不请求、强制重调和留痕全部正确 ### 步骤 6 · 无推荐与 PDA 例外 T-991 序号3可用任意合法箱直接成功。PDA `/pack/finish/scan` 用非推荐箱也应直接成功,不返回6001,但要写“扫码枪打包”不符留痕。PDA 账号必须有物流专员角色。 **验收**:无推荐不拦截;PDA 不拦截但正确留痕 #### 供应商接口请求体模板 ```text // 货架列表。status=0 查待打包;status=1 查已打包 POST /package/shelf/page { "date": "2026-10-27", "userId": <测试账号ID>, "userName": "<测试账号名称>", "status": 1, "boxMatchFilter": 2, "current": 1, "size": 20 } // 首次设箱。codeContent 填 SQL 输出的包裹 id POST /package/pack/finish { "codeContent": "<包裹ID>", "userId": <测试账号ID>, "userName": "<测试账号名称>", "phone": "13800000000", "packId": <实际箱ID>, "packName": "<实际箱名称>", "url": "", "shelfPackage": 0, "forceFlag": 0 } // 收到 code=6001 后,只把 forceFlag 改成 1,其余参数原样重调 ``` #### 强制使用留痕断言 ```sql SELECT package_id, origin_pack_id, origin_pack_name, target_pack_id, target_pack_name, operate_user_id, operate_user_name, remark, create_time FROM xm_bulky_package_box_modify_record WHERE send_no LIKE 'TESTBOX%' AND data_status = 0 ORDER BY id DESC; -- 小程序强制使用预期 remark: -- 实际箱型与推荐箱型不一致,已确认强制使用 -- PDA 不符预期 remark: -- 扫码枪打包,实际箱型与推荐箱型不一致 ``` ## 晚场加单应该这样测 晚场重算只发生在 type=2 非自营城市仓聚合组。TESTADM-A 到 D 都是 type=1,继续用它们触发晚场接口只会得到“没有重算”的正确结果。 > [!WARNING] > **查询用 sceneType=2,不要用 sceneType=4。** > > type=2 聚合包裹的 `send_no` 为空;sceneType=4 按晚场发货单号查,通常看不到这组包裹。也不要传 `serialNum=1`,否则看不到追加行。 ### 主流程:NORMAL ### 步骤 1 · 准备同仓白场与晚场发货单 打开 SQL 3,确认 `@case_name='NORMAL'` 后执行准备区。脚本会选择当天无真实订单占用的非自营城市仓,插入 17:00 的 45kg 白场单和 21:00 的 50kg 晚场单,并输出 wareId。 **验收**:late_fixture_ready=OK,已记下输出的 wareId ### 步骤 2 · 先调用白场生成 虽然数据库已有晚场单,白场入口只会读取 18:05 前的白场单。此时应只有 1 张 `110*45*45`,总重45kg。 **验收**:白场基线为 1 张 110\*45\*45 ### 步骤 3 · 触发晚场重算 调用晚场测试接口。预期总重95kg:原 serial1 改成 `110*60*60`,追加 serial2 `小件100*25`,两张未出库行的 `packageNum=2`。 `POST generateWmsExpressPackageNight` ```bash curl --url 'https://api.xunmengvip.cn/xm-bulky-admin/test/generateWmsExpressPackageNight' \ -H 'accept: */*' \ -H 'channel: admin' \ -H 'content-type: application/json' \ -H 'usertokenxm: <粘贴当前 usertokenxm>' \ --data-raw '{"date":"2026-10-27"}' ``` **验收**:推荐被覆盖、追加 1 张、总重量和件数刷新 ### 步骤 4 · 查询全部 type=2 包裹 填写 SQL 输出的 wareId 后复制请求。接口用于页面核对,精确字段仍建议同时跑下方数据库断言。 `POST wmsExpressPackage/list · sceneType=2` ```bash curl --url 'https://api.xunmengvip.cn/xm-bulky-admin/order/wmsExpressPackage/list' \ -H 'accept: */*' \ -H 'channel: admin' \ -H 'content-type: application/json' \ -H 'usertokenxm: <粘贴当前 usertokenxm>' \ --data-raw '{"roundNo":"2026-10-27","sceneType":2,"zoneList":[],"index":1,"size":100}' ``` **验收**:sceneType=2 能看到 serial1 与 serial2 ### 步骤 5 · 同一晚场接口再调一次 记录行数和 id,再跑一次。第二次不得继续追加,id、serialNum 与推荐都保持不变。 **验收**:晚场第二次执行完全幂等 #### 晚场数据库断言,wareId 取 SQL 输出 ```sql SELECT id, round_no, ware_id, partner_id, serial_num, package_num, total_weight, item_num, express_status, package_id, package_info, suggest_pack_id, suggest_pack_info, data_status, create_time, update_time FROM xm_bulky_wms_express_package WHERE round_no = CURDATE() AND type = 2 AND ware_id = AND partner_id = 0 ORDER BY serial_num, id; -- 幂等前后比较 SELECT COUNT(*) AS row_count, GROUP_CONCAT(id ORDER BY id) AS id_list, GROUP_CONCAT(serial_num ORDER BY serial_num) AS serial_list FROM xm_bulky_wms_express_package WHERE round_no = CURDATE() AND type = 2 AND ware_id = AND partner_id = 0 AND data_status = 0; ``` ### 晚场扩展场景 - **PACKED · 已设箱也重排**:白场45kg先把 serial1 设为实际 110\*45\*45、状态10。晚场后推荐改成110\*60\*60,实际箱不变,因此匹配状态变2。 - **PARTIAL · 部分出库扣容量**:白场80kg的87kg箱设为状态20;晚场再加100kg。剩93kg,追加87kg箱和6.1kg箱;已出库旧行任何字段不变。 - **SPECIAL / ZTO · 专用箱与中通**:同品种跨白晚场要追加专用箱。中通总95kg目标3件,从白场1件追加2件,全部无普通箱推荐。 #### 如何切换晚场扩展场景 重跑 SQL 3 前,把第一段 `@case_name` 改成 `PACKED`、`PARTIAL`、`SPECIAL` 或 `ZTO`。执行准备区,调用白场接口;PACKED/PARTIAL 再执行 SQL 文件里的“白场后置”区;最后调用晚场接口并按文件尾部预期核对。每个场景都要再跑一次晚场接口验证幂等。 ## 完整测试矩阵 P0 是本次发布必测;P1 是边界、风险和兼容性。 | 级别 | ID | 场景 | 关键预期 | |----|----|----|----| | P0 | CFG-01 | flowerWeight 列表、保存与校验 | 0、87.00 可保存;null、负数、1000.01、3位小数失败 | | P0 | DAY-01 | 80kg / 180kg 普通箱拆分 | A 为 1 张最大箱;B 为 87+87+6.1 | | P0 | DAY-02 | 特殊花材独立装箱 | 普通重量先扣特殊重量;专用箱仍有推荐 id | | P0 | DAY-03 | 中通100kg | 3件,推荐 id=0,推荐容量=null | | P0 | DAY-04 | 白场重复执行 | 行数和 id 不变 | | P0 | PACK-01 | 货架筛选与角标 | 不符筛选仅 T-993;合规筛选含无推荐 T-994 | | P0 | PACK-02 | 推荐箱直接设箱 | 成功,状态1,无强制留痕 | | P0 | PACK-03 | 非推荐箱 + forceFlag | 首次6001不落库;force=1成功且仅一条留痕 | | P0 | PACK-04 | 点击“否” | 只关闭弹窗,不发第二次请求 | | P0 | PACK-05 | PDA 非推荐箱 | 不返回6001,直接成功,写扫码枪不符留痕 | | P0 | NIGHT-01 | 45kg + 50kg 主流程 | 49.6箱改87箱,追加13kg箱,总2件 | | P0 | NIGHT-02 | 状态10重排 | 推荐更新,实际箱不变,变为设箱不符 | | P0 | NIGHT-03 | 部分已出库 | 按实际箱容量优先扣重,状态20整行不改 | | P0 | NIGHT-04 | 晚场幂等 | 第二次无更新或追加,id 不变 | | P0 | NIGHT-05 | 特殊花材跨阶段 | 白场蝴蝶兰 + 晚场蝴蝶兰,共2张蝴蝶兰箱 | | P0 | NIGHT-06 | 中通晚场 | 总95kg目标3件,追加2件,均无推荐 | | P1 | EDGE-01 | 0.5 / 6 / 174kg | 最小有效箱、6.1箱、两个87箱且无余箱 | | P1 | EDGE-02 | 中通冷链硬限制 | expressId=146 + packId 12/17,force=1也不能绕过 | | P1 | EDGE-03 | 重算后需求变少 | 多余面单保留,不删除、不作废、不清旧推荐 | | P1 | EDGE-04 | 作废行与序号 | 作废行不参与件数,但新序号从历史最大值继续 | | P1 | RISK-01 | FHH 更小晚场货位 | 观察是否因 key 改变而重复整组生成,属于已知风险 | | P1 | RISK-02 | 物流电子面单数量 | 仅隔离配置下测试,避免真实第三方面单副作用 | ## 怎样算测试完成 每条失败必须能回答:输入是什么、接口返回什么、数据库最终是什么、第二次执行是否变化。 - **保留请求**:接口、请求体、场次、wareId、包裹id。令牌打码,禁止贴完整 JWT。 - **保留响应**:HTTP 状态、业务 code、msg、records 中的推荐与匹配字段。 - **保留前后快照**:id、serialNum、status、实际箱、推荐箱、总重、件数及留痕记录。 #### 测试结果回报模板 ```text 提交:0019b5646c3a98e7c813e0cd14a4ba7561ebf89f 环境:test1 / API 实例:________ 场次:________ 测试账号:________ 白场后台:通过 / 不通过 / 未测 供应商设箱:通过 / 不通过 / 未测 晚场 NORMAL:通过 / 不通过 / 未测 晚场 PACKED:通过 / 不通过 / 未测 晚场 PARTIAL:通过 / 不通过 / 未测 晚场 SPECIAL/ZTO:通过 / 不通过 / 未测 失败用例 ID:________ 请求与响应:________ 数据库前后快照:________ 重跑结果:________ 日志关键字:________ 结论与阻塞:________ ``` ## 看不到数据时先查这里 大多数“接口没值”不是算法问题,而是日期、场景、序号、分组或缓存没有对齐。 ### Admin 列表没有今天的发货单 - 确认请求 `roundNo` 与 SQL 的 `CURDATE()` 是同一天,数据库与应用时区均为 Asia/Shanghai。 - SQL 末尾的 `template_ready`、`generation_ready` 必须为 OK。 - 执行 SQL 只造发货单,不会自动造包裹;必须再调用白场生成接口。 - 查所有包裹时删除 `serialNum:1`;它会隐藏追加序号。 ### 晚场接口成功,但列表还是没有值 - 主流程必须是 `ware_id>0`、`single_delivery_flag=0`、type=2;TESTADM-A 到 D 不满足。 - 使用 `sceneType=2` 并传 wareId,不要用 sceneType=4。 - 白场发货单时间应为17:00,晚场新增应为21:00;恰好18:05不会触发。 - 白场与晚场必须同 wareId、partnerId 和物流分组。 ### 数量对,但推荐箱不对 - 确认箱型配置缓存已刷新,数据库改值后最多等待约100秒。 - 确认普通箱的 flowerWeight 大于0,专用箱名称没有误当普通箱。 - 特殊花材重量会先从普通拆箱重量中扣除。 - 同容量箱按 sort,再按 id 取更靠前的配置。 ### 供应商打包没有返回 6001 - 确认该包裹 `suggest_pack_id>0`,且请求 packId 与推荐 id 不同。 - 确认走的是 `/package/pack/finish` 或 `/finish/change`,PDA scan 入口本来就不返回6001。 - 确认没有误传 `forceFlag=1`。 - 中通冷链箱型硬限制是普通失败,不属于6001。 ### 自动化测试为什么没有全绿 Admin 的推荐拆箱 7 条、晚场规划器 10 条目标单测已通过。Supplier 的 `PackageLogicFinishTest` 当前有 1 失败、1 错误,原因是测试仍断言 PDA 受区域权限限制,并保留了已不再调用的权限 stub;生产代码当前口径是 PDA 必须有物流专员角色,但跳过货区限制。不要把这两个旧断言误判为本需求手工链路失败,手工验收仍需按本页 P0 完整执行。 ## 相关文档 - [推荐箱型 / 设箱校验 · 前端对接](/box-suggest-api) - [推荐箱型 / 设箱校验 · 业务口径](/box-suggest-logic) --- # 包花厂集采开发文档 > 运营端 / 供应商端两服务分工、代码入口、接口清单、状态写入条件、Redis 锁与事务、定时任务、跨服务结算凭证、数据表与只读核查 SQL、已知坑 - 类型:业务口径 - 分类:包花厂集采 - 后端服务:xm-bulky-mall-admin + xm-bulky-mall-supplier - 测试分支:sh/code - 更新:2026-11-03 - 网页版:https://doc.xfun.fun/bulk-purchase-dev-guide ## 结论 集采拆在两个服务里: - **运营端 `xm-bulky-mall-admin/app`**:采购计划(发布、追加、撤回、终止)、自动定采、运营看单 / 改量 / 处理超量 / 取消、包花厂商品管理、需求推进定时任务。 - **供应商端 `xm-bulky-mall-supplier`**:供应商报价、仓库 PDA 作业(签到、打印、扫码、盘点)、超量处理、**唯一的结算记账实现**、自动结算与自动取消任务。 运营后台的「手动结算」不在 admin 里记账,而是签发 60 秒一次性凭证,经 Feign 调 supplier 内部接口,由 `BulkPurchaseSettleServiceImpl` 统一记账。两端操作同一批表,并发靠「Redis 锁 + 带状态条件的 UPDATE」双保险。 业务口径(给产品 / 测试 / 现场看的版本)见 [包花厂集采业务逻辑](/bulk-purchase-logic)。本篇讲代码在哪、怎么串、改的时候要注意什么。 > [!NOTE] > 本文基于 `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` | 货架码前缀 | 枚举值(计划 0~5、供货单 10/20/30/40/90、标签 10/30/90、范围 1~6、超量类型 1/2、补偿方式 1/2/3)见 [包花厂集采业务逻辑](/bulk-purchase-logic) 的「主流程」和各规则章节。补充只在代码里出现的: | 枚举 | 取值 | |----|----| | 报价选中 `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`,避免拿锁后读到旧快照。 > [!WARNING] > 锁超过 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,一步失败不影响其他步。 - 待定采**过了采集截止就不再重试**:此时生成的供货单会立刻被自动取消任务取消。日志 `集采需求卡在待定采且已过采集截止,需人工处理`。 - 自动结算单张独立事务,乐观锁冲突的单留在待结算等下一轮。 ## 跨服务手动结算 ```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) ``` - 操作人来自凭证,supplier **不接受客户端自报的操作人**。 - admin 与 supplier 必须连**同一个 Redis 库**,否则凭证永远「无效或已过期」。 - Feign 异常不打印内容(可能带凭证)。调用超时不代表没结算,先查订单和账变。 ### 记账流程 `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`。 > [!NOTE] > 扫码成功**不写审计**,以标签表和商品库存为准;失败看 `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`,详情按等级扫码数与定采数保留历史超量差额 | --- # 包花厂集采业务逻辑 > 采购计划发布 → 按等级报价 → 自动定采 → 签到打印 → 扫码入库 → 盘点 → 超量处理 → T+3 结算的完整业务口径,附现场操作清单与常见问题排查 - 类型:业务口径 - 分类:包花厂集采 - 端:运营后台 / 供应商小程序 / 仓库 PDA - 后端服务:admin + supplier - 测试分支:sh/code - 更新:2026-11-03 - 网页版:https://doc.xfun.fun/bulk-purchase-logic ## 结论 包花厂集采是一条「运营发计划、供应商报价、系统定采、仓库收货、平台结算」的链路: 1. 运营发布**采购计划**,写清品类、规格、各等级收购扎数和最高价、哪些供应商能报。 2. 供应商按**等级**报单价和可供扎数,截止前可以改报。 3. 报价截止后系统**自动定采**:每个等级取价格最低的前 5 家,依次分到需求量为止,给中选的供应商各生成一张**供货单**。 4. 仓库给供货单**签到 → 打印标签 → 开始盘点 → 扫码**。**每扫一张标签 = 入库 1 扎**,库存在扫码那一刻就加上。 5. **提交盘点**后供货单进入待结算,3 天后按**冻结报价**自动结算;送多了(超量)要先人工处理才能结算。 三条贯穿全链路的口径: - **单位一律是扎**,单价是元/扎。每扎多少枝(`specNum`)只用于展示和标签,**不参与任何乘法**。 - **价格在定采那一刻冻结**到供货单,之后改商品售价、改报价都不影响结算。 - **采购计划和供货单的状态互相独立**。计划「已完成」只表示定采流程结束,货还没收、钱还没结。 开发实现、接口、锁与定时任务见 [包花厂集采开发文档](/bulk-purchase-dev-guide)。 ## 角色与业务对象 | 角色 | 在系统里做什么 | 使用的端 | |----|----|----| | 运营 | 发布 / 追加 / 撤回 / 提前终止采购计划;查看定采结果;处理超量、改结算数、手动结算、取消供货单;管理包花厂商品上架和售价 | 运营后台 | | 供应商(供货方) | 看可报计划、报价、改报;查看自己的供货单 | 供应商小程序 | | 仓库作业人员 | 签到、打印标签、开始盘点、扫码、撤销扫码、提交盘点;也可以处理超量、改结算数、手动结算、取消 | 仓库 PDA | | 发布主体 | 发布计划的内部自营供应商账号。货入它的库存、钱从它的余额扣 | 后台配置 | | 对象 | 含义 | |----|----| | 采购计划(集采需求) | 一次收购安排:品类、规格、各等级收购量和最高价、供应商范围、两类供应商的报价截止和签到截止 | | 报价 | 某供应商对某计划各等级的单价与可供扎数。每次改报存一个新版本,定采只看当前版本 | | 供货单 | 定采中选后生成。**同一计划下一个供应商只有一张供货单**,可包含多个等级 | | 货架码 | 当场次内用于现场找单的编号(`JC` 开头)。跨场次可能重复,不能单靠它判断是否同一张单 | | 标签 | 一张标签 = 一扎。标签归属某张供货单的某个等级,有序号,不能跨单使用 | ## 数量与金额口径 | 字段 | 叫法 | 怎么来的 | |----|----|----| | 收购扎数 | 计划某等级要收多少 | 发布时的收购量 + 报价中追加的扎数 | | 可供扎数 | 供应商承诺能供多少 | 报价时填写;**填 0 表示这个等级只报价、不供货** | | `confirmLabelNum` 定采扎数 | 系统实际分给供应商多少 | 自动定采算出,可能小于供应商的可供扎数 | | `actualLabelNum` 实际产出 | 实际扫进来多少 | 每扫成功一张 +1;提交盘点时按各等级扫码数重新汇总校准 | | `settleLabelNum` 应结算扎数 | 最后按多少扎付钱 | 提交盘点时默认等于扫码数;待结算阶段可按等级人工改 | | `totalAmount` 应结算金额 | 最后付多少钱 | 各等级应结算扎数 × 该等级冻结单价,再求和 | ```text 应结算金额 = Σ(等级应结算扎数 × 等级冻结单价) ``` 例:A 级 3 扎、2.50 元/扎,B 级 2 扎、1.80 元/扎,每扎 20 枝: ```text 3 × 2.50 + 2 × 1.80 = 11.10 元 (不再乘 20 枝) ``` > [!IMPORTANT] > **冻结单价**是定采时保存在供货单等级明细里的报价快照,不是商品当前售价。页面上的「超量金额 / 冻结金额」只是提示数字,**不代表供应商账户里真的冻结了一笔钱**。 ## 主流程 ```text 运营配置品类、规格、等级数量、供应商范围、时间 └─ 发布采购计划(报价中) └─ 供应商按等级报价 / 改报 └─ 两类供应商的报价截止都已过(或运营提前终止) ├─ 没人报价 / 没有可分配的量 ──→ 计划关闭 └─ 自动定采 → 生成供货单 → 计划立即「已完成」 └─ 供货单:待签到 └─ 签到 → 打印标签 → 开始盘点 → 扫码(同步入库) └─ 提交盘点 → 待结算(未扫标签作废) ├─ 有超量 → 按等级确认结算数 → 处理超量 └─ 无超量 / 已处理 → 到期自动结算 或 手动结算 → 已结算 ``` ### 采购计划状态 | 值 | 状态 | 含义 | |----|----|----| | `0` | 草稿 | 还没开放报价,可以改、可以删 | | `1` | 报价中 | 平台层面仍在报价。两类供应商截止时间可能不同,**不代表每家都还能报** | | `2` | 待定采 | 正常只是自动定采过程中的一瞬间;长时间停在这里说明定采失败 | | `3` | 已确认 | 历史状态 / 定采事务中的过渡状态 | | `4` | 已完成 | 自动定采生成供货单后立即完成;**少采也算完成** | | `5` | 已关闭 | 无人报价、提前终止且无人报价、定采后没有任何中选结果 | ### 供货单状态 | 值 | 状态 | 可以做什么 | |----|----|----| | `10` | 待签到 | 签到;运营 / 仓库可取消 | | `20` | 待盘点 | 打印、开始盘点、扫码、撤销扫码、提交盘点;无产出时可取消 | | `30` | 待结算 | 补打 / 重打标签、改应结算扎数、处理超量、手动结算;到期自动结算 | | `40` | 已结算 | 只能查看;不能重复结算、不能改量 | | `90` | 已取消 | 未扫标签已作废,不能打印、扫码、重打 | ### 标签状态 | 值 | 状态 | 含义 | |----|----|----| | `10` | 已生成 / 未扫码 | 可以在本单盘点时扫 | | `30` | 已扫码 | 已计入产出和库存;再扫会被拒绝,不会重复入库 | | `90` | 已作废 | 提交盘点或取消供货单时,未扫的标签自动作废;不能扫、不能重打 | ## 规则:发布采购计划 **发布前必须确定**:发布主体、场次、二级分类、包装规格、毛花 / 成品花、商品规格、开放的等级,以及每个等级的收购扎数、最高价、最小提交量、单家最大提交量。 | 规则 | 说明 | |----|----| | 包装规格 | 二级分类必须配置了有效的 `specNum`,否则不能发布 | | 商品规格 | 分类配置了商品规格时,必须选全(每个规格名选一个值) | | 毛花 / 成品花 | 只是货物类型标识,不改变扎数和金额口径 | | 最高价 | 必须大于 0,最多两位小数 | | 最大提交量 | 不能小于最小提交量,不能大于收购扎数;**不填或填 0 时保存为该等级收购扎数**。历史数据里存的 0 仍按「不限」处理 | | 时间 | 每类供应商(斗南 / 非斗南)的报价截止必须早于自己的签到截止;发布时报价截止不能已经过去 | | 发布后锁定 | 主体、品类、规格等不能再改,只能追加供应商或追加扎数 | ### 供应商范围 | 值 | 范围 | 说明 | |----|----|----| | `1` | 全部供应商 | 用全员标记,不能再追加供应商 | | `2` | 指定基地 | 发布时形成资格快照 | | `3` | 品质优选 | 发布时形成资格快照 | | `4` | 近 30 天销量阈值 | 发布时形成资格快照 | | `5` | 指定供应商 | 发布时形成资格快照 | | `6` | 白名单 | 发布时形成资格快照;之后修改全局白名单**不影响旧计划** | 所有范围的基础条件都是「花材供应商且 `data_status=2`」。范围命中 0 家时不能发布。 ### 产出截止(采集截止) 产出截止决定供货单最晚什么时候必须有产出,过了还没扫过一张就会被自动取消。 ```text 产出截止 = max(场次次日 12:00,最晚签到截止 + 2 小时) ``` - 场次 11-05,最晚签到截止 11-05 20:00 → max(11-06 12:00,11-05 22:00)= **11-06 12:00** - 场次 11-05,最晚签到截止 11-06 11:00 → max(11-06 12:00,11-06 13:00)= **11-06 13:00** > [!WARNING] > 不要口头承诺「次日中午截止」,以计划上的 `collectEndTime` / 供货单上的 `outputEndTime` 为准。 ### 发布后的调整 | 操作 | 允许条件 | 效果 | |----|----|----| | 追加供应商 | 报价中,且范围不是「全部供应商」 | 追加的供应商获得报价资格 | | 追加扎数 | 报价中,且该等级已存在 | 抬高该等级收购扎数;**不会自动抬高单家最大提交量**;已提交的报价不回溯校验 | | 撤回 | 报价中且**没有任何报价** | 回到草稿 | | 提前终止 | 报价中 | 有报价 → 立即进入自动定采;无报价 → 关闭 | | 删除 | 仅草稿 | 删除计划 | > [!CAUTION] > 提前终止不是「停掉倒计时」,有报价时会**马上定采并生成供货单**。操作前要和业务负责人确认仍准备收货。 ## 规则:供应商报价 | 规则 | 说明 | |----|----| | 资格 | 供应商 `data_status=2`;在计划的可报范围内;**发布主体不能给自己的计划报价** | | 等级 | 所有开放的等级都要报价格,不能漏;暂不供的等级可供扎数填 0 | | 可供量 | 至少一个等级大于 0;大于 0 时必须 ≥ 最小提交量、≤ 单家最大提交量、≤ 该等级收购扎数(含追加) | | 价格 | 大于 0、最多两位小数、不高于该等级最高价,且不超过 1000 元/扎 | | 改报 | 报价中用「修改报价」,每次改都存新版本;内容完全没变时不生成新版本 | | 同价排序 | 看当前版本的提交时间,越早越优先 | ### 报价截止提前 5 秒 **供应商实际能提交的最后时刻 = 配置的报价截止 − 5 秒,到达该时刻即拒绝。** 例:配置截止 10:00:00,供应商必须在 09:59:55 **之前**完成请求;09:59:55 已提示「报价已截止,报价和改价须在截止时间前5秒完成提交」。运营端仍按原截止时间(10:00:00)推进定采。 > [!TIP] > 现场提醒供应商提前提交,不要等倒计时归零。 ### 定采前调价 规则上要求:计划处于待定采、自己的报价未被定性、没有供货单、且仍在报价截止前 5 秒之前。正常流程中计划进入待定采时报价截止已过,所以**自然截止后基本不可能调价**,调价诉求应在报价中用「修改报价」完成。 ### 供应商列表 列表只显示**场次日期在服务器当天往前一个月及以后**的计划(包括未来场次)。更早的计划在有权限时仍能打开详情。 | 页签 `filterStatus` | 范围 | |----|----| | `0` / 不传 | 全部可见计划 | | `1` 报价中 | 还没有本人供货单、仍在本人报价窗口内的计划(已报、未报都算) | | `2` 已采用 | 本人有未取消的供货单(含已结算) | | `3` 已截止 | 本人供货单已取消;或本人报价窗口已过 / 计划已结束且没有本人供货单 | ## 规则:自动定采 **触发**:两类供应商的报价截止都已过(系统每 5 分钟检查一次),或运营提前终止且有报价。 **算法(每个等级独立计算)**: 1. 候选 = 该等级可供扎数 > 0 的当前报价(填 0 的只报价不参与)。 2. 排序:**价格低优先 → 同价提交早优先 → 再按供应商 ID 小优先**。 3. 只取前 **5** 家。 4. 从第 1 名开始,每家最多拿自己的可供量,凑满该等级需求就停。 5. 前 5 家合计不够时**如实少采,不找第 6 家补**。 例:某等级需求 1000 扎,前 5 名按价格从低到高为 甲 500、乙 200、丙 500、丁 100、戊 100: ```text 甲 500 + 乙 200 + 丙 300 = 1000 扎,丁、戊一扎不分 ``` 例:需求 1000 扎,前 5 名可供合计只有 800 扎 → 分 800 扎,缺 200 扎,第 6 名也不补。 **结果**: - 同一供应商在多个等级中选 → 合并成一张供货单;供货单保存各等级冻结单价。未中选的等级也可能保留报价明细(可在额度内为这些等级补打标签,见「规则:签到与打印标签」)。 - 有中选结果 → 生成供货单,计划立即「已完成」。 - 没人中选 → 计划「已关闭」。 - 判断结果以供货单为准,不以是否收到「已采用」消息为准。 ## 规则:签到与打印标签 **签到**:待签到的供货单签到后转待盘点。超过签到截止**仍可以签到**,系统记迟到标记;集采签到不触发基地迟到处罚。 **正常 / 增量打印**(待盘点、待结算可用): | 规则 | 说明 | |----|----| | 输入含义 | 每个等级填的是**目标累计张数**,不是本次新增张数 | | 只增不减 | 目标不能小于已生成张数 | | 上限 | **每个等级的目标、以及各等级合计,都不能超过整单定采扎数 × 2** | | 等级范围 | 只能打供货单里已有的等级,不能临时加新等级 | 例:整单定采 50 扎(上限 100 张)。A 级已生成 10 张,改成 12 → 只新增 2 张(序号 11、12)。若 A 已 60 张、B 填 50 张 → 合计 110 张,超过 100 张被拒。 **重打**:按「等级 + 序号区间」重新取回原标签,**复用原码,不新增数量**。区间必须在 1 ~ 已生成张数之间,区间内有作废标签则不能重打。已取消的供货单不能重打。 > [!TIP] > 打印机没出纸但后台已生成标签时,先看已生成张数和序号,用**重打**取回原区间,不要再增量打印。 ## 规则:扫码入库 1. 先点「开始盘点」**占用**这张供货单。同一张单同时只允许一个人盘点;扫码、撤销、提交盘点都必须是占用人本人。暂停或交班时用「释放占用」。 2. 「首码识别」只用于确认标签属于哪张单,**不计数、不入库**。 3. 扫码成功时,以下动作**在同一个事务里**完成:标签置已扫码、该等级扫码数 +1、整单实际产出 +1、发布主体对应商品库存 +1 扎。 > [!IMPORTANT] > **库存在扫码时就增加了,不是等提交盘点。** **扫码被拒**(接口外层成功,但内层 `data.success=false`,本次不计数): | `rejectReason` | 原因 | 现场处理 | |----|----|----| | `2` | 该标签已扫描 | 不要重复计数;返回首次扫码时间和扫码人可核对 | | `3` | 标签已作废 | 作废码不能再用 | | `4` | 该标签属于其他供货单 | 换到正确的供货单扫 | | `5` | 标签不存在 | 核对标签码 | | `6` | 供货单当前状态不允许盘点 | 单据未签到、已提交盘点或已取消 | **撤销扫码**:只能撤销本人扫的、且供货单仍待盘点的标签,同时扣回 1 扎库存。若这批库存已经卖出、剩余库存不够扣,撤销失败。 ## 规则:提交盘点 | 条件 | 说明 | |----|----| | 状态 | 供货单待盘点,且由占用人提交 | | 有产出 | 一张都没扫不能提交 | | 少量备注 | 实际产出 < 定采扎数时必须填盘点备注 | 提交后系统: 1. 按各等级扫码数重算实际产出,默认应结算扎数 = 扫码数,算出金额; 2. 未扫标签全部作废; 3. 供货单转**待结算**,释放占用; 4. **计划结算时间 = 提交盘点时间 + 3 天**; 5. 实际产出 > 定采扎数时标记**未处理超量** `overFlag=1`。 提交后不能再扫码或撤销。要改付款数量,在待结算阶段**按等级改应结算扎数**,不要改扫码数据或重复提交。 ## 规则:超量处理 超量单仍是「待结算」,不单独设异常状态。**有未处理超量的单既不会自动结算,也不能手动结算。** ```text 超量扎数 = max(实际产出 − 定采扎数,0) ``` 处理时选两种类型之一,并填写处理说明。 ### 类型 1:供应商自身超送 供应商自己多送了。必须**先按等级确认应结算扎数**(哪怕决定全额结算,也要显式提交一次),再登记「自身超送」。 > [!WARNING] > 只在处理说明里写「按 40 扎结算」不会改变付款依据。没显式确认过结算数时,系统会拒绝处理并提示「应结算数量还没确认过」。 ### 类型 2:混货 / 错贴标签 当前单扫到了别家的货。处理步骤: 1. 按等级把当前单的应结算扎数改小,**扣减量 = 扫码数 − 应结算数**。 2. 逐条登记被占用方:同一计划下的**其他供应商**、等级、被占用扎数、补偿方式。被占用方必须**有效且已待结算**(未提交盘点、已结算都不行),且其供货单包含该等级。 3. **对平**:每个等级的扣减量必须等于该等级所有补偿明细的被占用扎数合计(三种补偿方式都算),两边任何一个等级对不上都拒绝。 | 补偿方式 | 台账 | 被占用方应结算扎数和金额 | |----|----|----| | `1` 登记补款 | 记录 | **增加**补偿扎数,按被占用方该等级冻结单价重算,随其供货单一起结算 | | `2` 线下补偿 | 记录 | 不变,只记台账 | | `3` 无需补偿 | 记录 | 不变,只记台账 | 例:当前单 A 级扫码 20 扎、定采 11 扎,改应结算为 11 扎(扣减 9 扎)。被占用方原应结算 10 扎: ```text 登记补款 2 扎 + 线下补偿 3 扎 + 无需补偿 4 扎 = 9 扎 (与扣减对平 ✔) 被占用方应结算 = 10 + 2 = 12 扎 (只有登记补款进系统应付) ``` `compensateAmount` 补偿金额可以手填(≥ 0,最多两位小数),不填按被占用方冻结单价 × 扎数计算。它是**台账金额**,不会覆盖任何供货单的应结算金额。 ### 处理之后 - 当前单 `overFlag` 清为 0,记录处理人、时间、说明。 - 运营端**列表**:已处理的单显示超量扎数 0、超量金额 0.00;**详情**仍保留原始超量差额和按冻结价算的历史超量金额。 - 列表提示消失 ≠ 库存退回 ≠ 应结算金额变 0。各端显示不一致时先看处理标识、处理时间和等级结算明细,**不要重复处理**。 - 处理超量**不等于付款**。若计划结算时间已过,下一轮自动结算会接住;没到期则继续等,或手动结算。 - 历史已处理数据不会随新规则自动冲回,发现差异交开发和财务核对。 ## 规则:结算 | 方式 | 条件 | |----|----| | 自动结算 | 待结算 + 计划结算时间已到 + `overFlag=0`,由定时任务执行 | | 手动结算 | 待结算 + `overFlag=0`,填写原因(≤ 255 字);运营后台、仓库端都可操作 | **记账动作**(运营后台、仓库端、自动任务共用同一套): | 对象 | 余额 | 账变类型 | |----|----|----| | 供货供应商 | 增加应结算金额 | `95` 集采收入 | | 发布主体 | 扣减应结算金额(**允许扣成负数**) | `96` 集采支出 | | 供货单 | 置已结算 `status=40`、`settleFlag=1`,记结算时间 / 方式 | — | - **金额为 0** 时也置已结算并留审计,但不产生余额账变,仍会给供应商发结算通知。 - 结算记入的是供应商**平台余额**,不代表银行转账或提现已经完成。 - 结算通知在数据库提交后发送,**消息没收到 ≠ 没结算**,不能因此再结一次。 - 运营后台手动结算失败、超时或无返回时,**先刷新供货单和账变确认结果**,再决定是否重新操作。 ## 规则:取消供货单 | 方式 | 条件 | |----|----| | 手动取消(运营 / 仓库) | 待签到或待盘点,且**实际产出为 0** | | 自动取消 | 待签到或待盘点、产出截止已到、实际产出为 0 | 取消后未扫标签全部作废。 > [!CAUTION] > **已有扫码产出的供货单不能取消**——扫码已经入了库存,取消不会退库存。仍待盘点且确需取消时,先由原扫码人逐张撤销扫码;库存已被卖掉、已提交盘点或已结算的,转技术和业务负责人处理,**不能直接改数据库状态**。 ## 规则:商品与库存 - 商品归**发布主体**。打印时按「发布主体 × 二级分类 × 等级 × 商品规格」找商品映射,没有就自动建品(集采专用建品 `type=3`,不是到场货 `type=1`)。 - 自动建的商品:价格 0、库存 0、**下架**、无图。库存靠扫码增加。 - 入库后需要运营在「包花厂商品」里**确认售价并手动上架**,0 元商品会被上架校验拦截。「已入库但采购端看不到」多半是没上架,不是扫码失败。 - 核对商品要同时看主体、分类、等级、完整规格和映射,不要只按商品名找。 - 改商品售价不影响供货单冻结报价。库存推送异常查推送日志,**不要用再扫一次码来补**。 ## 现场操作清单 ### 到场先确认 1. 环境、当前场次、运营账号、仓库账号;当班业务负责人和技术升级联系人。 2. 当天的采购计划和关联供货单;两类供应商的报价截止、签到截止;供货单的产出截止。 3. 拿一张正常供货单走一遍签到、打印、开始盘点、扫码入口;确认打印机、标签纸、扫码枪可用。 4. 当天待盘点、超量待处理、已到期未结算的单据,先处理会阻塞作业或付款的。 5. 请开发确认运营端需求推进任务、供应商端自动结算 / 自动取消任务的启用状态和最近执行结果。 ### 记录问题时至少写 环境、发生时间、账号角色、场次、采购计划号、供货单号、操作步骤、完整提示。扫码问题补标签码;资金问题补处理前后的应结算扎数和金额。 > [!WARNING] > 不要把登录 token、结算凭证、密码贴进问题记录。 ### 交接时当面核对 现场环境和服务版本、当日场次与截止时间、可用账号和设备、待处理异常单清单、定时任务的频率与负责人、升级联系人、未完成的资金或库存核查。每项遗留都要有单号、当前状态、下一步、负责角色。 ## 常见问题排查 | 现象 | 先核对 | 处理 / 升级 | |----|----|----| | 供应商找不到计划 | 账号、场次、一个月范围、页签、报价资格、本人是否已有供货单 | 切回「全部」并按准确场次找;仍找不到,记计划号和供应商 ID 交开发查资格 | | 倒计时还剩几秒却报已截止 | 实际截止 = 配置时间 − 5 秒;服务器时间 | 以服务端为准,不能承诺补报 | | 报价提示数量超限 | 最小提交量、单家最大提交量、收购量(含追加);不供的等级是否填 0 | 按提示改;追加扎数不会抬高最大提交量 | | 报了价没中选 | 当前有效版本、该等级可供量、价格、该等级前 5 名 | 按**等级**看排序和分配,不能看整单总量 | | 截止后一直待定采 | 两个原截止时间、定采错误日志、是否已过产出截止 | 交技术查任务;已过产出截止的不会自动重试,**不要盲目重跑** | | 计划已完成,供货单却待签到 | 是否刚自动定采 | 正常,供货单独立作业 | | 打印成功没出纸 | 已生成张数、序号、打印机 | 按原序号区间重打,不要增量打印 | | 提示他人正在盘点 | 当前占用人 | 原占用人释放后再开始;无法释放记录账号和单号升级 | | 扫码返回成功但没加数 | `data.success`、`rejectReason` | 区分重复 / 作废 / 跨单,不按外层 code 手工加数 | | 有产出无法取消 | 扫码记录、商品库存、供货单状态 | 待盘点时评估逐张撤销;其他状态升级处理 | | 提交盘点失败 | 占用人、是否有产出、少量备注、状态 | 刷新核对在途扫码,不要手工改状态 | | 混货对不平 | 每个等级「扫码数 − 应结算数」与三种补偿合计 | 补齐漏登的等级或修正扎数;三种方式都参与对平 | | 线下补偿后对方结算数没加 | 补偿方式是 2 / 3 | 正常,只有登记补款会加 | | 处理超量后详情仍有差额 | `overFlag`、处理时间、列表与详情口径 | 详情保留历史差额属正常,不要再扣一次 | | 到期没结算 | 状态、计划结算时间、`overFlag`、任务结果、供应商档案 | 超量先处理;并发冲突查日志后重试;超时先查结果 | | 已结算没收到消息 | 结算字段、95 / 96 账变、消息日志 | 账已成功就查消息,**不要再结算** | | 入库了但商品买不到 | 库存、上下架、价格、主体与规格 | 确认售价并上架,不要重复扫码 | > [!IMPORTANT] > 现场支持可以指导页面正常操作和只读核对。**生产补数据、改余额、删标签、清 Redis 锁、批量重跑任务**都必须另行确认方案,由开发处理。 --- # 冷库库存转代卖 > 运营端转代卖两个接口 + 仓库小程序最终扎数确认,含口径规则与报错文案 - 类型:前端对接 - 分类:冷库暂存花材 - 端:运营后台 / 仓库小程序 - 后端服务:admin / supplier - 更新:2026-07-31 - 网页版:https://doc.xfun.fun/cold-convert-sale-api 客服将客户不想要的冷库暂存花材转为平台代卖。运营后台新增 2 个接口,仓库小程序在现有出库流程上增加最终扎数确认。所有接口均为 POST + JSON,通用约定(登录态、返回结构 R)与现有冷库暂存接口一致。 ## 业务流程与规则 ### 两阶段流程 - **① 运营后台(客服)**:暂存列表操作「转代卖」→ 先调 [预填信息接口](#cold-convert-sale-info) 弹窗展示可转扎数上限 → 客服填写扎数、备注后调 [确认转代卖](#cold-convert-sale),生成一条 **待出库**的转代卖任务并锁定库存。 - **② 仓库小程序**:在暂存出库列表看到该任务(`outType=3`),点击出库时 **必须填写最终出库扎数**(其余照片必传、备注选填等同现有出库)。 - 待出库期间客服可[撤销](#cold-out-cancel)(复用现有撤销接口),撤销后库存解锁:包裹回暂存中、裸花回待发货。 > [!NOTE] > **权限**复用现有权限点:预填信息 = 冷库查看权限,确认转代卖 = 冷库操作权限,不新增权限项。 ### 两种暂存类型的口径差异 | | 裸花暂存(storeType=2) | 包裹暂存(storeType=1) | |----|----|----| | 转出粒度 | **支持部分转出**,按扎数扣减 | **整库存转出**,全部在库包裹一起锁定 | | 可转上限(预填接口 `maxBunchNum`) | 当前剩余暂存扎数,必有值 | 发货单总扎数(打包时点快照),**可能为 null** —— 前端显示「-」且提交时不做前端校验 | | 客服填写扎数校验 | ≤ 剩余暂存扎数 | 快照有值时 ≤ 快照;快照为 null 不校验 | | 仓库出库最终扎数 | ≤ 客服审批扎数,且 ≤ 当前剩余;扣完出清,**有剩余则回「待发货」继续暂存**(需客服重新确认发货) | ≤ 客服审批扎数;包裹全部置已出库 | > [!WARNING] > **不支持转代卖的情况**城市仓货架暂存(ware_id\>0,跨多客户)直接拦截;已存在待出库任务的库存需先完成或撤销。 ## 枚举变更速查 ### outType 出库方式(新增值) | 值 | 含义 | |----|----| | 1 | 关联发货单 | | 2 | 直接出库 | | 3 | 转代卖 **新增** | 历史出库记录、暂存出库列表等所有带 `outType` 的接口天然支持值 3,筛选参数无需改动。任务状态枚举不变:10 待出库 · 20 已出库 · 30 已撤销 ## 运营后台端(客服) ### POST /cold/convert-sale/info · 转代卖预填信息 **新增** 点「转代卖」弹窗前调用,返回暂存类型与可转扎数上限。同时做资格预检——不满足条件(城市仓货架、已有待出库任务、没有在库包裹等)会直接报错,前端 toast 错误信息即可,避免弹窗填完提交才被拒。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |---------------|------|------|--------------------------------| | `coldStockId` | Long | 是 | 冷库库存 id(暂存列表行的 id) | #### 返回 data | 字段 | 类型 | 说明 | |----|----|----| | `storeType` | Integer | 暂存类型 1:包裹 2:裸花 | | `maxBunchNum` | Integer | 可转扎数上限。裸花=当前剩余暂存扎数;包裹=发货单总扎数快照,**可为 null**(前端显示「-」,输入框不做上限校验) | #### 示例 ```text // 请求 { "coldStockId": 1024 } // 返回(裸花) { "code": 200, "data": { "storeType": 2, "maxBunchNum": 15 } } // 返回(包裹,快照查不到) { "code": 200, "data": { "storeType": 1, "maxBunchNum": null } } ``` ### POST /cold/convert-sale · 确认转代卖 **新增** 生成待出库的转代卖任务。包裹暂存会锁定该库存 **全部在库包裹**;实际出库由仓库小程序完成。返回 data 为任务 id。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |---------------|---------|------|---------------------------------------| | `coldStockId` | Long | 是 | 冷库库存 id | | `bunchNum` | Integer | 是 | 转代卖扎数,正整数(≥1) | | `remark` | String | 否 | 备注(转代卖原因或操作说明),≤255 字 | #### 业务报错文案(toast 展示即可) - 「城市仓货架暂存不支持转代卖」 - 「存在待出库任务,请先完成或撤销后再转代卖」(包裹) - 「该库存没有可转代卖的在库包裹」(包裹) - 「转代卖扎数不能大于发货单总扎数X扎」(包裹,快照有值时) - 「转代卖扎数不能大于当前剩余暂存X扎」(裸花) #### 示例 ```text // 请求 { "coldStockId": 1024, "bunchNum": 10, "remark": "客户不要了,转平台代卖" } // 返回 { "code": 200, "data": 88 } // data = 出库任务 id ``` ### POST /cold/out/cancel · 撤销出库任务 **复用** 转代卖任务待出库期间的撤销 **完全复用现有撤销接口**,入参出参不变。撤销后:包裹解锁回「暂存中」,裸花库存回「待发货」;历史出库记录中该任务显示 `outType=3` 及撤销信息。 ### POST /cold/out/records · 历史出库记录 **字段变更** 入参不变(`outType` 筛选传 3 即可查转代卖记录)。返回行新增/调整字段: | 字段 | 类型 | 变更 | 说明 | |----|----|----|----| | `finishBunchNum` | Integer | **新增** | 仓库确认最终出库扎数(转代卖任务,已出库后有值;其他任务为 null) | | `outType` | Integer | 枚举扩展 | 1:关联发货单 2:直接出库 **3:转代卖** | | `outBunchNum` | Integer | 语义扩展 | 原为裸花暂存出库扎数;**转代卖任务为客服填写的转代卖扎数,包裹暂存也有值** | ### POST /cold/stock/page · 暂存列表 **字段变更** 返回行新增字段,用于在列表上给待出库的转代卖库存打标签: | 字段 | 类型 | 变更 | 说明 | |----|----|----|----| | `outTaskType` | Integer | **新增** | 最新任务的出库方式 1:关联发货单 2:直接出库 3:转代卖(无任务时为 null;=3 时前端显示「转代卖」标签) | ## 仓库小程序端 ### POST /cold/mini/out/finish · 点击出库 **字段变更** 入参新增 `bunchNum`。任务为转代卖(`outType=3`)时前端必须让仓库人员填写最终出库扎数;其他任务类型不传。照片必传、备注选填等原有规则不变。 #### 入参新增字段 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `bunchNum` | Integer | 转代卖任务必填 | 最终出库扎数,正整数。裸花任务出库后:扣完 → 库存出清;有剩余 → 剩余扎数回「待发货」继续暂存 | #### 业务报错文案 - 「请填写最终出库扎数」(转代卖任务未填或 ≤0) - 「最终出库扎数不能大于客服审批的转代卖扎数X扎」 - 「最终出库扎数不能大于当前剩余暂存扎数」(裸花,并发或超量) #### 示例 ```text // 转代卖任务出库请求 { "outTaskId": 88, "imageUrls": "https://cdn.example.com/a.jpg,https://cdn.example.com/b.jpg", "remark": "已搬到代卖区", "bunchNum": 8 } ``` ### POST /cold/mini/out/page · 暂存出库列表 **字段变更** 入参不变,转代卖任务会自然出现在待出库/已出库列表中。返回行新增/调整字段: | 字段 | 类型 | 变更 | 说明 | |----|----|----|----| | `finishBunchNum` | Integer | **新增** | 仓库确认最终出库扎数(转代卖任务,已出库后有值) | | `outType` | Integer | 枚举扩展 | 1:关联发货单 2:直接出库 **3:转代卖**(=3 时点击出库需弹出最终扎数输入) | | `outBunchNum` | Integer | 语义扩展 | 转代卖任务为客服填写的转代卖扎数(包裹暂存也有值),可作为最终扎数输入框的默认值/上限提示 | > [!NOTE] > **转代卖任务展示建议**待出库卡片显示「转代卖 · 客服审批 X 扎」;已出库卡片显示「转代卖 · 实际出库 finishBunchNum 扎」。物流相关字段(`linkSendNo`、`expressName`)转代卖任务为空。 对应提交 `3129cb28da 裸花转代卖` · 测试环境已部署(admin + supplier) --- # 质检配货页分页 > 配货列表改分页 + 早班/待补/子货位标记与筛选;含 need/prepared 语义变化与分页破坏性变更说明 - 类型:前端对接 - 分类:仓库配货 / 打包 - 端:仓库小程序 - 后端服务:supplier - 更新:2026-08-17 - 网页版:https://doc.xfun.fun/distribution-page-api 配货列表接口改为轻表分页(全部分区由 7-8s 降至毫秒级),并新增早班货位、待补货位标记与对应筛选。本页覆盖接口的分页方式、新增字段与渲染规则。 供应商端 · 质检配货页 ### POST /zone/info/new · 配货货位列表(分页) 状态:已上测试环境(`supplier/test`)。**有 2 处破坏性变更**:接口改为分页、`need`/ `prepared` 语义变化,见「破坏性变更」。 ## 破坏性变更 —— 先读这两条 > [!NOTE] > **① 接口改为分页,不再一次返回全部货位** > > 请求需传 `pageNo`(从 1 开始)、`pageSize`(默认 20);响应新增 `total`(当前筛选条件下的货位总数)。`shelfSnList` 现在只返回 **当前页**,翻页请自行按 `total` 计算页数。 > > 不传 `pageNo/pageSize` 时后端按第 1 页、每页 20 条兜底 —— 老前端不改会只看到前 20 个货位,务必接入分页。 > [!NOTE] > **② `need` / `prepared` 语义由「扎数」改为「货位数」** > > `need` = 当前筛选条件下 **可配货货位数**(status=0),`prepared` = **配货完成货位数**(status=1)。用于两个 tab 的角标计数。若原来按扎数展示汇总,需改按货位数。 ## 货位卡片视觉状态 列表 **一行一个货位号**(不再拆子货位)。同一张卡片可叠加多种状态,配色与打包页统一,前端直接取用。 | 状态 | 驱动字段 | 视觉 | |----|----|----| | 普通货位 | `restockStatus` 为 0 / 2 | 默认卡片 | | 待补 | `restockStatus === 1` | 卡片正上方居中「待补」橙色徽标 | | 早班 | `earlyShiftFlag === true` | 整卡红底 | | 有子货位 | `hasSubShelf === 1` | 右上角「子」徽标 | | 叠加 | 以上任意组合 | 早班红底 + 待补 + 子 可同时出现 | > [!NOTE] > **颜色值(与打包页一致,请勿改)** > > **待补徽标**:底色 `#FF6A00`,字体 `#FFFFFF`,卡片正上方居中,`restockStatus === 1` 时显示。 > > **早班背景**:整卡背景 `#FF000F` 透明度 **70%**(`rgba(255,0,15,.7)`),`earlyShiftFlag === true` 时染红。 > > **子货位徽标**:`hasSubShelf === 1` 时右上角展示「子」标;点货位进详情看子货位(见「货位级列表 + 子货位详情」)。 ## 响应结构(PrepareShelfSnVo) 顶层计数 + 当前页货位列表。 | 字段 | 类型 | 说明 | |---------------------|-------|------------------------------------------------| | `total` **新增** | long | 当前筛选条件下货位总数,用于分页。 | | `need` **语义变更** | int | 可配货货位数(status=0)。原为扎数。 | | `prepared` **语义变更** | int | 配货完成货位数(status=1)。原为扎数。 | | `shelfSnList` | array | 当前页货位列表,元素为 ShelfSnStatus(下表)。 | 列表元素 `ShelfSnStatus`(一行一货位): | 字段 | 类型 | 说明 | |----|----|----| | `shelfSn` | int | 货位号,卡片主标题。 | | `status` | int | 配货完成状态:`0` 可配货 · `1` 配货完成(货位级:所有子货位都配完才为 1)。 | | `restockStatus` **新增** | int | **待补状态**:`0` 无待补 · `1` 存在待补货(→「待补」徽标)· `2` 补货已完成。徽标仅 `=== 1` 显示。 | | `earlyShiftFlag` **新增** | boolean | **是否早班**。后端已按口径算好,`true` 直接染红,前端无需自行判断时间(口径见「早班口径」)。 | | `hasSubShelf` **新增** | int | **是否有子货位**:`0` 无 · `1` 有(→「子」徽标)。 | | `subShelfSn` | string | 列表层 **恒为空串** `""`(列表是货位级)。子货位明细走详情接口,见「货位级列表 + 子货位详情」。 | | `total` / `prepared` / `notRestock` / `progress` | int / decimal | 该货位进度:应配 / 已配 / 退缺 / 进度(0~1,封顶 1)。跨子货位汇总。 | | `zone` / `zoneShelf` / `segmentName` | string | 分区 / 区-货架 / 分段。既有字段。 | | `deliveryType` | int | 配送方式 `1` 物流 · `2` 自提。既有字段。 | | `limitDistribution` / `wareType` / `wareId` | int | 限制配货标识 / 仓库类型 / 仓库 id。既有字段。 | ## 请求参数(ZoneInfoQo) | 参数 | 类型 | 说明 | |----|----|----| | `date` **必填** | string | 场次日期,如 `2026-08-17`。必填。 | | `type` | int | tab:`0` 可配货 · `1` 配货完成 · `-1` 全部。默认 0。 | | `pageNo` **新增** | int | 页码,从 `1` 开始。默认 1。 | | `pageSize` **新增** | int | 每页条数。默认 20。 | | `onlyRestock` **新增** | boolean | **只看待补**。`true` 仅展示 `restockStatus=1` 的货位;`null`/ `false` 不限。 | | `onlyEarlyShift` **新增** | boolean | **只看早班**。`true` 仅展示早班货位;`null`/ `false` 不限。 | | `deliveryType` | int | 配送方式筛选:`1` 物流 · `2` 自提。既有参数(自提 tab/筛选)。 | | `zone` / `zoneShelf` | string | 分区 / 货架号筛选。既有参数。全部分区即不传 zone。 | | `segmentNameList` | array | 分段筛选,传分段时必须同时传 `zone`。既有参数。 | | `cityFlag` | int | 是否城配区。既有参数。 | ## 货位级列表 + 子货位详情 列表 **一行一个货位号**;一个货位有多个子货位时,只出一张卡片并打「子」标(`hasSubShelf === 1`)。点货位进详情看子货位,走既有详情接口: ### POST /subShelfSn/info · 入参货位号 shelfSn,返回该货位的子货位明细 完成状态是 **货位级**:一个货位只要还有任意子货位没配完,整货位就算未完成(留在「可配货」),全部子货位配完才整体进「配货完成」。 ## 早班口径 —— 后端已算好 > [!NOTE] > **前端直接用 `earlyShiftFlag`,不用自己算时间** > > 后端口径:**`deliveryTime >= 22:00` 或 `deliveryTime <= 01:00`(跨零点)**即早班。此口径与打包页一致,且已在服务端算入 `earlyShiftFlag`,前端 **直接取布尔值染红**即可。 > > 「只看早班」 `onlyEarlyShift` 用的是同一口径,与卡片染红天然一致。 ```text // 渲染时(字段均由后端返回,前端不再自行判断时间): // 早班背景 -> row.earlyShiftFlag === true 背景 rgba(255,0,15,.7) // 待补徽标 -> row.restockStatus === 1 底 #FF6A00 / 字 #FFFFFF // 子货位标 -> row.hasSubShelf === 1 右上角「子」 ``` ## Tab 与筛选映射 | Tab | type | 只看待补 | 只看早班 | 自提 | |--------------|------|-------------|----------------|----------------| | **可配货** | 0 | onlyRestock | onlyEarlyShift | deliveryType=2 | | **配货完成** | 1 | onlyRestock | onlyEarlyShift | deliveryType=2 | 筛选项可自由组合,后端 AND 叠加,且两个 tab 均支持。「配货完成」tab 勾「只看待补」返回的是 **已配货完成但仍有待补记录**的货位(待补与完成是两个独立维度)。 接口 `POST /zone/info/new` · 供应商端 · 测试分支 `supplier/test`。字段口径以后端返回为准;`earlyShiftFlag`/ `restockStatus`/ `hasSubShelf` 均已由服务端算好,前端直接取用。 --- # 分销开票调整优化 > 09-24 调整需求:费用类型勾选分批开票、30/60/90 时间筛选、申请单维度记录与撤销、逾期赔付标识;含核心口径与页面-接口对应 - 类型:前端对接 - 分类:分销开票 - 端:分销小程序 - 后端服务:supplier - 更新:2026-09-27 - 网页版:https://doc.xfun.fun/partner-invoice-adjust-api 对应 2026-09-24《繁花/优花速达开票调整优化》9 条需求的后端实现。全部为新增接口 + 既有接口加可选参数,旧版小程序不受影响。所有接口 POST + JSON,沿用花店登录态,前缀 /invoice。通用约定(响应结构、分页、时间与金额格式)与 上一版文档 完全一致,此处不重复。 ## 接口一览 | 模块 | 路径 | 名称 | 状态 | |----|----|----|----| | 发票中心(新版) | [`/invoice/partner/type/list`](#invoice-partner-type-list) | 可开票类型汇总 | **新增** | | 发票中心(新版) | [`/invoice/partner/flower/supplier/list`](#invoice-partner-flower-supplier-list) | 花材款供应商汇总 | **新增** | | 发票中心(新版) | [`/invoice/partner/apply/preview`](#invoice-partner-apply-preview) | 一键开票预览 | **变更** +invoiceItemTypes、timeRange | | 发票中心(新版) | [`/invoice/partner/apply/submit`](#invoice-partner-apply-submit) | 一键开票提交 | **变更** +invoiceItemTypes、timeRange | | 开票记录(申请单维度) | [`/invoice/partner/application/list`](#invoice-partner-application-list) | 记录列表 | **新增** | | 开票记录(申请单维度) | [`/invoice/partner/application/detail`](#invoice-partner-application-detail) | 申请单详情 | **新增** | | 开票记录(申请单维度) | [`/invoice/partner/application/flower`](#invoice-partner-application-flower) | 花材款发票明细 | **新增** | | 开票记录(申请单维度) | [`/invoice/partner/application/cancel`](#invoice-partner-application-cancel) | 撤销申请单 | **新增** | | 既有接口 | `/invoice/partner/group/list` | 可开票分组列表 | **变更** +timeRange(可选) | | 既有接口 | `/invoice/partner/group/order/list` | 组内订单列表 | **变更** +timeRange(可选) | ## 本版变化(相对 2026-09-07 版) - **发票中心改「按费用类型」展示**:4 类各一行(供应链服务费 / 干线物流费 / 平台服务费 / 花材款),花材款聚合展示供应商家数与合计,不再按供应商平铺分组。数据源换成 [类型汇总接口](#invoice-partner-type-list)。 - **支持勾选费用类型分批开票**:预览 / 提交新增 `invoiceItemTypes`。金额按全量拆分固定,勾选只决定本次开哪些类型(见 [核心口径](#核心口径勾选与金额产品-2026-09-24-拍板))。 - **新增时间筛选 `timeRange`**:30 / 60 / 90 天,按 **确认收货时间**。页面默认选中 30 天并 **显式传 30**;不传为「近 3 个月」旧口径(仅作旧版兼容,新版页面别用)。传其他值报错「时间筛选仅支持30/60/90天」。 - **开票记录改申请单维度**:一次申请一条记录,汇总状态新增「部分开票」。旧的按票据(request)平铺的记录列表不再是记录页数据源。 - **撤销改申请单级**:全部票据未开才可撤,任一已开(或已被百望受理)整单不可撤。不再提供单张票据撤销入口。 - **逾期补开标识**:逾期后商家补开的发票,已开票区仍带逾期标识与「已赔付百分之二」文案(赔付指花店已收到申请金额 2% 的寻梦币补偿)。 - 收据流程本次 **无变化**:收据不参与类型勾选,applyType=2 的「去开收据」流程照旧。 ## 核心口径:勾选与金额(产品 2026-09-24 拍板) > [!NOTE] > **金额永远按全量拆分固定,勾选费用类型只决定本次开哪些类型的票,分批开总和不变。** - 一笔订单的实付被拆成 4 项(花材款 / 供应链服务费 / 干线物流费 / 平台服务费),各项金额由后端全量算法一次算死,**相互独立、勾选不重算**。 - 例:可开票池共 100 元 = 花材款 70 + 物流 10 + 供应链 5 + 平台 15。只勾「花材款」提交 → 本次开 70 元;剩下 30 元留在池子里,下次勾其余类型再开,两次合计仍是 100。 - 所以前端 **底部合计 = 勾选行的 `amount` 直接相加**,不需要再调预览接口算勾选组合的金额(预览接口也支持传勾选,返回值一致)。 - 花材款只能 **整体勾选**,不能只选其中部分供应商。[供应商汇总页](#invoice-partner-flower-supplier-list)是纯查看的下钻页,与勾选状态无关。 - 未勾选任何类型时禁用提交按钮;全不勾传给后端会报「所选费用类型没有可开票金额」。 ## 页面与接口对应(按原型) | 原型页面 | 用到的接口 | |----|----| | 发票中心(类型勾选列表) | [类型汇总](#invoice-partner-type-list)渲染 4 行 + 勾选框;切时间筛选重新请求;底部合计前端累加勾选行 amount | | 平台项行(供应链 / 物流 / 平台服务费)→ 订单列表 | 旧接口 `/partner/group/order/list`:传 `invoiceItemType` + `statusTab: 2` + 与列表页相同的 `timeRange`;**不传 supplierId**(更不能传字符串 "undefined",会报消息解析错误)。返回订单数与金额和类型汇总行一致 | | 花材款 → 供应商汇总页 | 点花材款行进入,[供应商汇总](#invoice-partner-flower-supplier-list)(带同样的 timeRange);纯展示,返回不改变勾选 | | 申请开票(勾选页直接提交,无确认页) | 选抬头 → [提交](#invoice-partner-apply-submit)(invoiceItemTypes + timeRange + invoiceTitleId)。[预览](#invoice-partner-apply-preview)在新版发票流程 **可不调**(已选金额=勾选行 amount 相加即准确值),想做提交前金额校验再调;收据流程(applyType=2)仍用预览 | | 开票记录列表 | [记录列表](#invoice-partner-application-list)(分页);行上展示汇总状态、逾期角标(hasOverdue) | | 申请详情 | [申请单详情](#invoice-partner-application-detail);平台三项行直接给 requestNo / invoiceUrl,花材款行进[花材款明细](#invoice-partner-application-flower) | | 花材款发票明细 | [花材款明细](#invoice-partner-application-flower)三区(待开 / 已开 / 已逾期);单张票所含订单用旧接口 `/partner/request/detail`(传 requestNo) | | 撤销 | 列表 / 详情 `canCancel=true` 时展示按钮 → [撤销申请单](#invoice-partner-application-cancel) | ## 枚举 #### timeRange 时间筛选,按确认收货时间 | 值 | 含义 | |----|----| | 30 | 近 30 天(页面默认,显式传) | | 60 | 近 60 天 | | 90 | 近 90 天 | | 不传 | 近 3 个月(仅旧版兼容) | | 其他值 | 报错 | #### invoiceItemType 费用类型,同旧版 0 花材款 · 1 供应链服务费 · 2 干线物流费 · 3 平台服务费 #### 申请单汇总状态 status 记录列表 / 详情头部 | 值 | 文案 | 含义 | |-----|------------|------------------------------| | 1 | 待商家开票 | 所有票据都还没开 | | 2 | 已开发票 | 所有票据都已开出 | | 3 | 部分开票 | 已开了一部分,其余待开或逾期 | | 4 | 已撤销 | 整单撤销 | #### 详情内类型行状态 typeItems\[\].status | 值 | 文案 | 说明 | |-----|----------|------------------------------------| | 1 | 未开发票 | — | | 2 | 已开发票 | 平台项此时 `invoiceUrl` 非空 | | 3 | 部分开票 | 仅花材款可能出现(部分供应商已开) | | 4 | 已逾期 | 该类型票据全部逾期未开 | #### 逾期与赔付标识 - `hasOverdue`(列表 / 详情):申请单里存在逾期票据,含「逾期后又补开」的。 - `overdueFlag`(花材款明细行):已逾期区恒 true;**已开票区为 true 表示逾期补开**,行上要同时展示逾期标识与 `compensateDesc`。 - `compensateDesc`:逾期时固定「已赔付百分之二」,直接展示后端文案;未逾期为空串。 ## 发票中心(新版) · 前缀 /invoice ### POST /invoice/partner/type/list · 可开票类型汇总 **新增** 新版发票中心的数据源。返回 4 类费用各一行(固定顺序:供应链服务费 → 干线物流费 → 平台服务费 → 花材款),金额为 0 的类型不返回。每行对应一个勾选框,勾选状态由前端维护。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |-------------|------|------|-----------------------------------------| | `timeRange` | int | 否 | 30 / 60 / 90;页面默认选 30 并显式传 30 | #### 响应 data 为数组,可能为空(无可开票订单) | 字段 | 类型 | 说明 | |----|----|----| | `invoiceItemType` | int | 0 花材款 · 1 供应链服务费 · 2 干线物流费 · 3 平台服务费 | | `typeName` | string | 类型名,直接展示 | | `amount` | decimal | 该类型可开票金额合计。勾选后底部合计 = 勾选行 amount 相加 | | `orderCount` | int | 关联订单数(同类型跨供应商去重) | | `supplierCount` | int | 可开票供应商家数;仅花材款 \> 0(如「共5家供应商」),平台项为 0 | #### 示例 ```text // 请求 { "timeRange": 30 } // 响应 { "code": 0, "msg": "success", "data": [ { "invoiceItemType": 1, "typeName": "供应链服务费", "amount": 48.00, "orderCount": 12, "supplierCount": 0 }, { "invoiceItemType": 2, "typeName": "干线物流费", "amount": 96.50, "orderCount": 10, "supplierCount": 0 }, { "invoiceItemType": 3, "typeName": "平台服务费", "amount": 63.50, "orderCount": 12, "supplierCount": 0 }, { "invoiceItemType": 0, "typeName": "花材款", "amount": 1356.00, "orderCount": 12, "supplierCount": 5 } ] } ``` ### POST /invoice/partner/flower/supplier/list · 花材款供应商汇总 **新增** 点花材款行进入的下钻页:按供应商展示可开票金额与订单数。**纯查看**,返回列表页不改变勾选状态;花材款只能整体勾选,此页没有勾选框。传与列表页相同的 timeRange。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |-------------|------|------|----------------------| | `timeRange` | int | 否 | 与类型汇总页保持一致 | #### 响应 data 为数组 | 字段 | 类型 | 说明 | |----------------|---------|--------------------| | `supplierId` | long | 供应商 id | | `supplierName` | string | 供应商名称 | | `amount` | decimal | 该供应商可开票金额 | | `orderCount` | int | 关联订单数 | #### 示例 ```text // 请求 { "timeRange": 30 } // 响应 { "code": 0, "msg": "success", "data": [ { "supplierId": 1021, "supplierName": "云南甲花卉", "amount": 656.00, "orderCount": 6 }, { "supplierId": 1088, "supplierName": "斗南乙鲜花", "amount": 700.00, "orderCount": 7 } ] } ``` ### POST /invoice/partner/apply/preview · 一键开票预览 **变更**:+invoiceItemTypes、timeRange 既有接口加两个可选参数,响应结构不变(见旧文档)。新版发票流程没有确认页,**可以不调**此接口(勾选合计前端累加即可);保留给旧版一键流程、收据流程(applyType=2)、以及可选的提交前金额校验。 #### 请求 新增字段,其余不变 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `applyType` | int | 否 | 新版发票中心传 **1**(仅发票);收据流程照旧传 2 | | `invoiceItemTypes` | int\[\] | 否 | **新增** 勾选的费用类型,如 `[0, 2]`;不传 = 全部(旧版行为) | | `timeRange` | int | 否 | **新增** 30 / 60 / 90,与列表页一致;不传 = 近 3 个月旧口径 | #### 响应 - 结构同旧版:`totalAmount`(勾选类型合计)、`orderCount`(勾选类型去重订单数)、`supplierBreakdown`(只含勾选类型的行)。 - 勾选的类型都没有可开票金额时报错:「所选费用类型没有可开票金额」。 #### 示例 ```text // 请求:只勾了花材款和干线物流费,近30天 { "applyType": 1, "invoiceItemTypes": [0, 2], "timeRange": 30 } ``` ### POST /invoice/partner/apply/submit · 一键开票提交 **变更**:+invoiceItemTypes、timeRange 既有接口加同样两个可选参数。一次提交生成 **一个申请单**(applicationId),内部按勾选类型生成多张票据、各自独立开票。未勾的类型金额留在可开票池,之后可再次勾选提交(新申请单)。 #### 请求 新增字段,其余不变 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `invoiceTitleId` | long | 是 | 发票抬头 ID(同旧版) | | `applyType` | int | 否 | 新版发票中心传 **1**;收据流程照旧传 2(收据不受勾选影响) | | `invoiceItemTypes` | int\[\] | 否 | **新增** 与预览一致,务必传同一份勾选 | | `timeRange` | int | 否 | **新增** 与预览一致 | #### 响应 同旧版 ```text // 请求 { "invoiceTitleId": 88, "applyType": 1, "invoiceItemTypes": [0, 2], "timeRange": 30 } // 响应 { "code": 0, "msg": "success", "data": { "applicationId": 3301, "receiptIds": [] } } ``` ## 开票记录(申请单维度) · 前缀 /invoice ### POST /invoice/partner/application/list · 记录列表 **新增** 新版开票记录页数据源:一次申请一条记录,按申请时间倒序,分页。替代旧的按票据平铺的 `/partner/request/list`。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `pageIndex` | int | 是 | 从 1 起 | | `pageSize` | int | 是 | 建议 10 | | `statusTab` | int | 否 | **新增** 三 Tab 方案:1 开票中(汇总状态为待商家开票或部分开票)2 已完成(已开发票)3 已逾期(存在逾期未开票据);不传返回全部(含已撤销)。已逾期 Tab 下记录的 status 仍是 1 或 3,卡片上的逾期标识用 hasOverdue;逾期没开完的单同时出现在 Tab 1 和 3 | #### 响应 data.records 每行 | 字段 | 类型 | 说明 | |-----------------|----------|----------------------------------------------| | `id` | long | 申请单 ID,后续详情 / 明细 / 撤销都传它 | | `applicationNo` | string | 申请编号 | | `createTime` | datetime | 申请时间 | | `titleName` | string | 发票抬头,卡片直接展示 | | `taxNumber` | string | 税号,个人抬头可能为空 | | `totalAmount` | decimal | 汇总金额(撤销的申请单也保留原金额) | | `status` | int | 1 待商家开票 2 已开发票 3 部分开票 4 已撤销 | | `statusDesc` | string | 状态文案,直接展示 | | `hasOverdue` | boolean | 存在逾期票据(含逾期补开),行上展示逾期角标 | | `canCancel` | boolean | 可撤销:全部票据未开时 true | #### 示例 ```text // 请求 { "pageIndex": 1, "pageSize": 10, "statusTab": 1 } // 响应 { "code": 0, "msg": "success", "data": { "total": 2, "current": 1, "pages": 1, "records": [ { "id": 3301, "applicationNo": "FP20260927001", "createTime": "2026-09-27 10:00:00", "totalAmount": 1452.50, "status": 3, "statusDesc": "部分开票", "hasOverdue": false, "canCancel": false }, { "id": 3288, "applicationNo": "FP20260920007", "createTime": "2026-09-20 15:20:00", "totalAmount": 800.00, "status": 2, "statusDesc": "已开发票", "hasOverdue": true, "canCancel": false } ] } } ``` ### POST /invoice/partner/application/detail · 申请单详情 **新增** 票据信息(抬头 / 税号 / 金额 / 时间)+ 按费用类型的发票明细行。平台三项每行就是一张票,直接给票据编号和发票 URL;花材款是聚合行(张数 = 供应商数),点击进[花材款明细页](#invoice-partner-application-flower)。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |-----------------|------|------|---------------| | `applicationId` | long | 是 | 记录列表的 id | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `id / applicationNo / createTime / totalAmount / status / statusDesc / hasOverdue / canCancel` | — | 同记录列表行 | | `titleName` | string | 发票抬头 | | `taxNumber` | string | 税号 | | `typeItems` | array | 发票明细,按费用类型一行 | | `invoiceItemType` | int | 0 花材款 · 1 供应链服务费 · 2 干线物流费 · 3 平台服务费 | | `typeName` | string | 类型名 | | `amount` | decimal | 该类型申请金额 | | `orderCount` | int | 关联订单数 | | `status` | int | 1 未开发票 2 已开发票 3 部分开票 4 已逾期 | | `statusDesc` | string | 状态文案 | | `pendingCount` | int | 待开票张数。花材款 = 待开供应商数;平台项 0 或 1 | | `invoicedCount` | int | 已开票张数,口径同上 | | `requestNo` | string | 票据编号。**平台项**用它调旧接口 `/partner/request/detail` 看所含订单;**花材款为空**,走花材款明细页 | | `invoiceUrl` | string | 发票文件 URL,平台项已开票时有值,预览 / 下载直接用 | #### 示例 ```text // 请求 { "applicationId": 3301 } // 响应(截取) { "code": 0, "msg": "success", "data": { "id": 3301, "applicationNo": "FP20260927001", "titleName": "昆明某某花店", "taxNumber": "91530100XXXX", "createTime": "2026-09-27 10:00:00", "totalAmount": 1452.50, "status": 3, "statusDesc": "部分开票", "canCancel": false, "hasOverdue": false, "typeItems": [ { "invoiceItemType": 2, "typeName": "干线物流费", "amount": 96.50, "orderCount": 10, "status": 2, "statusDesc": "已开发票", "pendingCount": 0, "invoicedCount": 1, "requestNo": "RQ20260927002", "invoiceUrl": "https://…/rq2.pdf" }, { "invoiceItemType": 0, "typeName": "花材款", "amount": 1356.00, "orderCount": 12, "status": 3, "statusDesc": "部分开票", "pendingCount": 3, "invoicedCount": 2, "requestNo": null, "invoiceUrl": null } ] } } ``` ### POST /invoice/partner/application/flower · 花材款发票明细 **新增** 申请详情里点花材款行进入。按供应商一张票,分三区:待开票 / 已开票(含逾期补开)/ 已逾期。单张票所含订单用旧接口 `/partner/request/detail` 传 requestNo。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |-----------------|------|------|--------| | `applicationId` | long | 是 | 同详情 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `totalAmount` | decimal | 花材款合计金额 | | `supplierCount` | int | 供应商家数(= 票据张数) | | `pendingList / invoicedList / overdueList` | array | 三个分区,行结构相同: | | `requestNo` | string | 票据编号,下钻票据详情用 | | `supplierId` / `supplierName` | — | 供应商 | | `amount` | decimal | 该票金额 | | `orderCount` | int | 关联订单数 | | `invoiceUrl` | string | 发票文件 URL,已开票区有值 | | `overdueFlag` | boolean | 已逾期区恒 true;**已开票区 true = 逾期补开**,行上带逾期标识 | | `compensateDesc` | string | 逾期时「已赔付百分之二」,直接展示;未逾期为空串 | #### 示例 ```text // 请求 { "applicationId": 3301 } // 响应(截取) { "code": 0, "msg": "success", "data": { "totalAmount": 1356.00, "supplierCount": 5, "pendingList": [ { "requestNo": "RQ20260927005", "supplierId": 1090, "supplierName": "丙花卉", "amount": 300.00, "orderCount": 3, "invoiceUrl": null, "overdueFlag": false, "compensateDesc": "" } ], "invoicedList": [ { "requestNo": "RQ20260927003", "supplierId": 1021, "supplierName": "云南甲花卉", "amount": 656.00, "orderCount": 6, "invoiceUrl": "https://…/rq3.pdf", "overdueFlag": false, "compensateDesc": "" }, { "requestNo": "RQ20260910001", "supplierId": 1088, "supplierName": "斗南乙鲜花", "amount": 400.00, "orderCount": 4, "invoiceUrl": "https://…/rq1.pdf", "overdueFlag": true, "compensateDesc": "已赔付百分之二" } ], "overdueList": [ { "requestNo": "RQ20260901009", "supplierId": 1105, "supplierName": "丁园艺", "amount": 0.00, "orderCount": 1, "invoiceUrl": null, "overdueFlag": true, "compensateDesc": "已赔付百分之二" } ] } } ``` ### POST /invoice/partner/application/cancel · 撤销申请单 **新增** 申请单级撤销:整单所有票据都未开(且未被开票平台受理)才可撤,任一已开整单不可撤。前端只在 `canCancel=true` 时展示按钮,仍需处理并发导致的报错。撤销后金额回到可开票池,可重新勾选申请。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |-----------------|------|------|------| | `applicationId` | long | 是 | — | #### 响应与错误文案 - 成功:`{ "code": 0, "data": true }`,刷新列表即可(该单变「已撤销」,金额保留展示)。 - 「申请单不存在」——id 错误或不属于当前花店。 - 「当前状态不可撤销」——已撤销 / 已完成的单。 - 「已有票据开出或逾期,无法撤销申请」「已有票据发起开票,无法撤销申请」——期间商家开了票或百望已受理,提示后刷新详情。 - 「票据状态已变化,请刷新后重试」——并发冲突,重试即可。 ## 旧接口的去留 | 旧接口 | 新版怎么用 | |----|----| | `/partner/group/list`、`/partner/group/order/list` | 保留并支持 `timeRange`(可选)。新版里 `group/order/list` 就是平台项行的订单下钻:`invoiceItemType` + `statusTab: 2` + `timeRange`,不传 supplierId。**timeRange 必须与列表页一致**,不传会落到近 3 个月旧口径,订单数对不上 | | `/partner/request/detail` | **继续使用**:单张票据的所含订单与每单金额,从申请详情(平台项 requestNo)或花材款明细(每行 requestNo)下钻 | | `/partner/request/list` | 保留但新版记录页不再使用,改用 [/partner/application/list](#invoice-partner-application-list) | | `/partner/request/cancel` | 保留但新版不再暴露单张撤销入口,撤销一律走 [/partner/application/cancel](#invoice-partner-application-cancel) | | 收据三接口 `/receipt/application/*`、可开收据 `/partner/receipt/order/list` | 不变,见旧文档 | > [!NOTE] > 联调环境:测试服已部署(bulky-test/purchaser)。所有新接口未登录统一返回 `{ "code": 1, "msg": "当前未登录" }`,请带花店登录态调试;接口行为与文档不符时以 knife4j 在线文档为准并反馈后端。 对应后端分支 xiaofun/xm-bulky-mall-suppiler · 2026-09-24 需求,09-24 已上测(bulky-test/purchaser)。旧版对接文档见 [partner-invoice-api](/partner-invoice-api)。 --- # 花店开票列表新增花材款 > 运营后台花店开票列表:开票类型新增花材款(按申请单聚合一行),按类型区分操作按钮,新增花材款关联供应商及订单弹窗 - 类型:前端对接 - 分类:分销开票 - 端:运营后台 - 后端服务:admin - 状态:已上测 - 测试分支:bulky-test/admin - 更新:2026-10-28 - 网页版:https://doc.xfun.fun/partner-invoice-admin-flower-api ## 总览 运营后台「花店开票列表」原来只展示平台项(供应链服务费 / 干线物流费 / 平台服务费),本次新增 **花材款**:开票类型筛选加「花材款」,列表里花材款 **按申请单聚合成一行**(一行 = 一次申请里所有供应商的花材款),点操作按钮弹出各供应商明细,再下钻看单个供应商的关联订单和发票。 | # | 接口 | 用途 | 何时调用 | |----|----|----|----| | ① | `POST /invoice/manage/partnerPlatform/list` | 花店开票列表 | 进入页面、翻页、切筛选 | | ② | `POST /invoice/manage/partnerPlatform/flower/supplier/list` | 花材款关联供应商及订单 | 花材款行点「查看供应商及关联订单」 | | ③ | `POST /invoice/manage/partnerPlatform/order/list` | 关联订单 | 服务费行点「关联订单」;花材款弹窗里点「查看关联订单」 | | ④ | `POST /invoice/manage/partnerPlatform/sendNo/list` | 关联发货单 | 物流费行点「关联发货单」(不变) | ## 通用约定 - 服务:运营后台 `admin`,路径前缀 `/invoice/manage`;全部 `POST` + JSON,沿用后台登录 token。 - 返回:`{ "code": 0, "msg": "success", "data": … }`;`code != 0` 时 `msg` 直接 toast。 - 分页:入参 `index`(从 1 开始)、`size`;出参 `{ index, size, total, records }`。 - 时间 `yyyy-MM-dd HH:mm:ss`,金额单位元、两位小数。 ## 枚举 | 枚举 | 取值 | |----|----| | `invoiceItemType` 开票类型 | 0 花材款 · 1 供应链服务费 · 2 干线物流费 · 3 平台服务费 | | `partnerId` 所属平台 | 0 寻梦批发 · 3 优花速达 · 140 繁花惠 | | `status` 开票状态 | 0 开票中 · 1 已开票 · 2 已逾期 · 3 已撤销 | ## 页面与接口对应 | 页面元素 | 做法 | |----|----| | 开票类型筛选 | 「全部」不传 `invoiceItemType`;「花材款」传 0;其余传 1 / 2 / 3 | | 列表行类型标签 | 用 `invoiceItemTypeDesc` | | 「供应商 N 家 · 订单 M 笔」 | `supplierCount`、`orderCount`;平台项 `supplierCount` 固定 1 | | 操作栏(按 `invoiceItemType` 区分) | 0 花材款 →「查看供应商及关联订单」调 ②,传 `applicationId`;2 物流费 →「关联发货单」调 ④,传 `id`;1 / 3 服务费 →「关联订单」调 ③,传 `id` | | 花材款弹窗标题区 | ② 返回的 `applicationNo`、`supplierCount`、`orderCount` | | 弹窗供应商行 | 每行都有「查看关联订单」,调 ③ 传该行 `id`;`invoiceUrl` 有值(已开票)时再加「查看发票」,直接打开该 URL | | 关联订单弹窗 | ③ 返回的订单号、场次、金额 | > [!IMPORTANT] > 花材款行的 `id` 为 **null**,弹窗必须用 `applicationId` 调 ②;不要拿 `id` 调 ③ / ④。 ## 接口 ### POST /invoice/manage/partnerPlatform/list · 花店开票列表 **变更**:`invoiceItemType` 支持 0 花材款;不传时花材款与平台项一起返回;响应新增 `applicationId`、`supplierCount` - 平台项(1 / 2 / 3)每张票一行,`id` 为该票 ID。 - 花材款(0)每个申请单一行,`id` 为 null,`requestNo` 为申请单号(INV 开头),金额 / 供应商数 / 订单数为该申请单下各供应商花材款合计(已撤销、已转代开的不计;整单撤销时展示撤销前金额)。 - 花材款行状态沿用申请单状态:申请单已完成 → 已开票;已撤销 → 已撤销;开票中且已过截止时间 → 已逾期;其余 → 开票中。 - 三格汇总金额跟随除状态以外的筛选条件,按每张票自身状态累加,切状态不变。 - 所属平台选「全部」或「寻梦批发」时,花材款会包含普通花店(寻梦批发)的开票申请。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `index` | Integer | 否 | 页码,从 1 开始 | | `size` | Integer | 否 | 每页条数 | | `searchCount` | Boolean | 否 | 是否统计总数 | | `invoiceItemType` | Integer | 否 | 开票类型,见枚举 `invoiceItemType`;不传为全部(含花材款) | | `partnerId` | Integer | 否 | 所属平台,见枚举 `partnerId`;不传为全部 | | `status` | Integer | 否 | 开票状态,见枚举 `status`;不传为全部 | | `requestNo` | String | 否 | 申请编号,精确;花材款可输申请单号(INV 开头)或其下任一供应商票据编号(REQ 开头) | | `titleName` | String | 否 | 发票抬头,模糊 | | `taxNumber` | String | 否 | 税号,精确 | | `purchaserPhone` | String | 否 | 花店手机号,精确 | | `createTimeStart` | LocalDateTime | 否 | 申请时间起 | | `createTimeEnd` | LocalDateTime | 否 | 申请时间止 | #### 请求示例 ```json { "index": 1, "size": 20, "searchCount": true, "invoiceItemType": 0, "partnerId": 140 } ``` #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `invoicingTotalAmount` | BigDecimal | 开票中总金额 | | `invoicedTotalAmount` | BigDecimal | 已开票总金额 | | `overdueTotalAmount` | BigDecimal | 已逾期总金额 | | `total` | Long | 总条数 | | `records[].id` | Long | 票据 ID;**花材款行为 null** | | `records[].applicationId` | Long | **新增** 申请单 ID;花材款行调 ② 用 | | `records[].requestNo` | String | 申请编号;花材款行为申请单号 | | `records[].invoiceItemType` | Integer | 开票类型,见枚举 `invoiceItemType` | | `records[].invoiceItemTypeDesc` | String | 开票类型文案 | | `records[].partnerId` | Integer | 所属平台,见枚举 `partnerId` | | `records[].platformName` | String | 寻梦批发 / 优花速达 / 繁花惠 | | `records[].purchaserId` | Long | 花店 ID | | `records[].purchaserName` | String | 花店名称 | | `records[].purchaserPhone` | String | 花店手机号 | | `records[].titleName` | String | 发票抬头 | | `records[].taxNumber` | String | 税号 | | `records[].requestAmount` | BigDecimal | 申请开票金额 | | `records[].supplierCount` | Integer | **新增** 供应商数;平台项固定 1 | | `records[].orderCount` | Integer | 订单数(去重) | | `records[].status` | Integer | 开票状态,见枚举 `status` | | `records[].statusDesc` | String | 状态文案 | | `records[].invoiceUrl` | String | 发票 URL;**花材款行为 null**,在 ② 里按供应商取 | | `records[].createTime` | LocalDateTime | 申请时间 | | `records[].uploadTime` | LocalDateTime | 上传时间;花材款行为各供应商最近一次上传时间 | | `records[].deadline` | LocalDateTime | 开票截止时间 | #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "index": 1, "size": 20, "total": 2, "invoicingTotalAmount": 512.00, "invoicedTotalAmount": 386.50, "overdueTotalAmount": 0.00, "records": [ { "id": null, "applicationId": 84, "requestNo": "INV202609301091915567936", "invoiceItemType": 0, "invoiceItemTypeDesc": "花材款", "partnerId": 140, "platformName": "繁花惠", "purchaserId": 999102, "purchaserName": "花语花坊", "purchaserPhone": "18758231160", "titleName": "花语花坊(昆明)有限公司", "taxNumber": "91530100MA6P8K2X7L", "requestAmount": 386.50, "supplierCount": 3, "orderCount": 12, "status": 1, "statusDesc": "已开票", "invoiceUrl": null, "createTime": "2026-09-28 09:12:33", "uploadTime": "2026-09-28 09:20:11", "deadline": "2026-10-28 09:12:30" }, { "id": 192, "applicationId": 84, "requestNo": "REQ0220260930999025876041", "invoiceItemType": 1, "invoiceItemTypeDesc": "供应链服务费", "partnerId": 140, "platformName": "繁花惠", "purchaserId": 999102, "purchaserName": "花语花坊", "purchaserPhone": "18758231160", "titleName": "花语花坊(昆明)有限公司", "taxNumber": "91530100MA6P8K2X7L", "requestAmount": 23.00, "supplierCount": 1, "orderCount": 7, "status": 0, "statusDesc": "开票中", "invoiceUrl": null, "createTime": "2026-09-28 09:12:33", "uploadTime": null, "deadline": "2026-10-28 09:12:30" } ] } } ``` #### 错误码 / 失败文案 | msg | 触发场景 | 前端处理 | |----|----|----| | 开票类型参数错误 | `invoiceItemType` 不在 0~3 | 检查传参 | | 开始时间不能大于结束时间 | 时间区间颠倒 | toast | ### POST /invoice/manage/partnerPlatform/flower/supplier/list · 花材款关联供应商及订单 **新增** 花材款行弹窗。列出该申请单下每个供应商的花材款票据;已撤销、已转代开的不展示(整单撤销时展示已撤销的)。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 列表 `records[].applicationId` | #### 请求示例 ```json { "id": 84 } ``` #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `applicationNo` | String | 申请单号 | | `supplierCount` | Integer | 供应商数 | | `orderCount` | Integer | 订单数(去重) | | `supplierList[].id` | Long | 该供应商票据 ID,「查看关联订单」调 ③ 传此值 | | `supplierList[].supplierId` | Long | 供应商 ID | | `supplierList[].supplierName` | String | 供应商名称 | | `supplierList[].amount` | BigDecimal | 待开票金额 | | `supplierList[].orderCount` | Integer | 订单数 | | `supplierList[].status` | Integer | 开票状态,见枚举 `status` | | `supplierList[].statusDesc` | String | 状态文案 | | `supplierList[].invoiceUrl` | String | 发票 URL,仅已开票时有值;有值才展示「查看发票」 | #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "applicationNo": "INV202609301091915567936", "supplierCount": 3, "orderCount": 12, "supplierList": [ { "id": 189, "supplierId": 25563, "supplierName": "昆明斗南花卉产业有限公司", "amount": 186.00, "orderCount": 5, "status": 1, "statusDesc": "已开票", "invoiceUrl": "https://xunmeng-oss.oss-cn-hangzhou.aliyuncs.com/invoice/189.pdf" }, { "id": 190, "supplierId": 18820, "supplierName": "云南花语供应链有限公司", "amount": 128.50, "orderCount": 4, "status": 0, "statusDesc": "开票中", "invoiceUrl": null }, { "id": 191, "supplierId": 30215, "supplierName": "广州花材集采中心", "amount": 72.00, "orderCount": 3, "status": 0, "statusDesc": "开票中", "invoiceUrl": null } ] } } ``` #### 错误码 / 失败文案 | msg | 触发场景 | 前端处理 | |----|----|----| | 信息不存在 | 申请单 ID 不存在(多半是误传了 `id` 而不是 `applicationId`) | 检查传参 | ### POST /invoice/manage/partnerPlatform/order/list · 关联订单 **变更**:原来只支持供应链服务费、平台服务费,现在也支持花材款单个供应商的票据 - 服务费行:传列表 `records[].id`,返回该票关联订单。 - 花材款:传 ② 的 `supplierList[].id`,返回该供应商在本申请单里的订单。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 票据 ID:服务费行用列表 `records[].id`,花材款用 ② 的 `supplierList[].id` | #### 请求示例 ```json { "id": 189 } ``` #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `[].orderSn` | String | 订单编号 | | `[].roundNo` | String | 场次 | | `[].amount` | BigDecimal | 该订单对应金额 | #### 响应示例 ```json { "code": 0, "msg": "success", "data": [ { "orderSn": "20260722999025682363", "roundNo": "2026-07-22", "amount": 2.00 }, { "orderSn": "20260810999025735072", "roundNo": "2026-08-11", "amount": 7.00 } ] } ``` #### 错误码 / 失败文案 | msg | 触发场景 | 前端处理 | |----|----|----| | 该开票记录不是花材款/供应链服务费/平台服务费类型 | 传了物流费票据 ID | 物流费改调 ④ | | 信息不存在 | 票据 ID 不存在 | 检查传参 | ### POST /invoice/manage/partnerPlatform/sendNo/list · 关联发货单 **不变** 仅物流费行(`invoiceItemType` = 2)使用,传列表 `records[].id`。返回 `[].sendNo` 发货单号、`[].roundNo` 场次、`[].expressName` 物流商(多个逗号分隔)、`[].amount` 该发货单物流费合计、`[].orderCount` 订单数。 ## 联调注意 - 测试环境:`bulky-test/admin` 已部署(2026-10-28)。 - 测试库已有繁花惠申请单,例如申请单 ID 84(`INV202609301091915567936`),可用 `invoiceItemType: 0, partnerId: 140` 查到花材款行。 > [!WARNING] > 花材款行和平台项行的 `id` 含义不同:花材款行 `id` 为 null、用 `applicationId`;弹窗里的供应商行 `id` 才是票据 ID。按钮取值别混用。 --- # 繁花惠 / 优花速达开票 > 分销小程序发票中心(分组列表 / 一键开票 / 收据分屏)+ 运营后台平台开票管理,含较 7 月版的变化清单 - 类型:前端对接 - 分类:分销开票 - 端:分销小程序 / 运营后台 - 后端服务:supplier / admin - 更新:2026-09-18 - 网页版:https://doc.xfun.fun/partner-invoice-api 分销小程序发票中心(花店端)+ 运营后台平台开票管理。所有接口均为 POST + JSON,一个接口一张卡片,每张卡片按「用途 → 请求 → 响应 → 示例」排。 ## 接口一览 | 端 | 路径 | 名称 | 状态 | |----|----|----|----| | 小程序 · 发票 | [`/invoice/partner/group/list`](#invoice-partner-group-list) | 可开票分组列表 | **新增** | | 小程序 · 发票 | [`/invoice/partner/group/order/list`](#invoice-partner-group-order-list) | 组内订单列表 | **变更** 默认时间窗 | | 小程序 · 发票 | [`/invoice/partner/apply/preview`](#invoice-partner-apply-preview) | 一键开票预览 | **变更** 新增 receiptPreview | | 小程序 · 发票 | [`/invoice/partner/apply/submit`](#invoice-partner-apply-submit) | 一键开票提交 | **变更** 返回结构 | | 小程序 · 发票 | [`/invoice/partner/request/list`](#invoice-partner-request-list) | 开票记录列表 | **变更** 状态文案 | | 小程序 · 发票 | [`/invoice/partner/request/detail`](#invoice-partner-request-detail) | 开票记录详情 | 不变 | | 小程序 · 发票 | [`/invoice/partner/request/cancel`](#invoice-partner-request-cancel) | 撤销开票记录 | **变更** 新增错误文案 | | 小程序 · 收据 | [`/receipt/application/list`](#receipt-application-list) | 我的收据列表 | **变更** 新增 partnerId | | 小程序 · 收据 | [`/receipt/application/detail`](#receipt-application-detail) | 收据详情 | **变更** 新增 partnerId、itemType | | 小程序 · 收据 | [`/receipt/application/voucher`](#receipt-application-voucher) | 收据凭证预览/下载 | 不变 | | 运营后台 | [`/invoice/manage/partnerPlatform/list`](#invoice-manage-partnerplatform-list) | 平台开票列表 | **变更** 新增平台筛选 | | 运营后台 | [`/invoice/manage/partnerPlatform/sendNo/list`](#invoice-manage-partnerplatform-sendno-list) | 物流费关联发货单 | 不变 | | 运营后台 | [`/invoice/manage/partnerPlatform/order/list`](#invoice-manage-partnerplatform-order-list) | 关联订单列表 | 不变 | | 运营后台 | [`/invoice/manage/request/list`](#invoice-manage-request-list) | 发票申请列表 | **变更** 新增筛选/返回字段 | | 运营后台 | [`/invoice/manage/supplier/request/list`](#invoice-manage-supplier-request-list) | 供应商开票列表 | 不变 | | 运营后台 | [`/logistics/list/costConfig`](#logistics-list-costconfig) | 物流商列表 | **变更** 新增 isInvoiceMarked | | 运营后台 | [`/logistics/add/costConfig`](#logistics-add-costconfig) | 物流商新增/编辑 | **变更** 新增 isInvoiceMarked | ## 本版变化(相对 2026-07-27 版,没接过旧版可跳过) - **新增分组列表接口**。「我的发票」页改用它渲染,不再从一键预览拼分组。 - **一键提交返回结构变了**:`data` 从 long 变为 `{ applicationId, receiptIds }`。成功判定用 `code === 0`。 - **一键预览新增 `receiptPreview`**(收据部分),发票总额 `totalAmount` 不含它。 - **状态文案改口径**:待申请开票 / 待商家开票 / 已开发票 / 已逾期。直接展示后端 `statusDesc`。 - **时间窗统一**:分组列表、组内订单、一键开票池默认都是「确认收货 3 个月内」。 - **发票与收据可分开申请**(2026-09-09):预览 / 提交新增 `applyType`,1 仅发票、2 仅收据,不传维持原一键全量;金额互斥,分开不影响任何计算。 - **收据改由平台出具、与发票分屏**:分销花店的收据页只有列表 / 详情 / 凭证,申请入口在发票中心「可开收据」Tab 的「去开收据」;收据接口新增 `partnerId`、`itemType`。 - **平台项 `supplierId` 不再恒为 0**(挂内部开票公司账号)。判类型一律用 `invoiceItemType`。 - **后台**:平台开票列表加所属平台筛选;发票申请列表加开票类型 / 所属平台筛选与返回(供应商开票列表不变);物流商配置加可开票标识。 ## 通用约定 #### 请求 - 全部 `POST`,`Content-Type: application/json`,无参数也传 `{}`。 - **小程序端**:沿用花店登录态;`purchaserId` 后端从登录态取,**前端不传**。前缀 `/invoice`(发票)、`/receipt`(收据)。 - **后台端**:沿用运营后台登录态。前缀 `/invoice/manage`(开票管理)、`/logistics`(物流商配置)。 - 时间格式与各端现有接口一致(示例按 `yyyy-MM-dd HH:mm:ss`)。金额单位元,两位小数。 #### 响应与分页 | 端 | 响应结构 | 分页入参 | 分页出参 | |----|----|----|----| | 小程序端 | `{ code: 0, msg, data }`,`code === 0` 为成功 | `pageIndex`(从 1 起)、`pageSize` | `data.records`、`data.total`、`data.current`、`data.pages` | | 后台端 | `{ code, msg, data }`,与现有后台一致 | `index`、`size`、`searchCount` | `data.records`、`data.total`、`data.index`、`data.size` | ## 枚举 #### invoiceItemType 开票项目类型,两端通用 | 值 | 名称 | 出具主体 | 票据形式 | |----|----|----|----| | 0 | 花材款 | 可开票供应商,一个供应商一条 | 发票;不可开票供应商的花材走收据 | | 1 | 供应链服务费 | 花序公司 | 发票 | | 2 | 干线物流费 | 捷通公司 | 发票;不可开票物流商的运费走收据 | | 3 | 平台服务费 | 花古德公司 | 发票 | #### partnerId 所属平台,同订单 partner_id 0 寻梦批发 · 3 优花速达 · 140 繁花惠 · 其他值 → 后台文案「其他分销商」 #### 分组内订单状态 status 与筛选 statusTab | status | 文案 | 对应 statusTab | |--------|------------|----------------| | 0 | 待申请开票 | 2 | | 1 | 待商家开票 | 4 | | 2 | 已开发票 | 3 | | 3 | 已逾期 | 5 | | — | 全部 | 1(默认) | #### 开票记录状态 request 维度 | 值 | 小程序文案 | 后台文案 | 说明 | |-----|------------|-----------------|------------------------------| | 0 | 待商家开票 | 开票中 / 待开票 | 小程序 `canCancel=true` | | 1 | 已开发票 | 已开票 | `invoiceUrl` 非空 | | 2 | 已逾期 | 已逾期 | 超 30 天未上传;运营可代上传 | | 3 | 已撤销 | 已撤销 | 小程序「全部」不返回 | | 4 | 已转代开 | 已转代开 | 仅花材款 | #### 其他 - `voucherType`:1 发票 - 收据 `status`:0 申请中 / 1 已开具 - 收据明细 `itemType`:0 花材款 / 2 干线物流费 - 凭证 `actionType`:2 预览 / 3 下载 ## 业务规则(前端需要感知的) #### 页面与接口对应 | 页面 | 用到的接口 | |----|----| | 我的发票(分组卡片) | [可开票分组列表](#invoice-partner-group-list);Tab 切换传 `statusTab`;单独刷某一组用 [组内订单列表](#invoice-partner-group-order-list) | | 「可开票」Tab 底栏「去开票」 | [一键开票预览](#invoice-partner-apply-preview)(applyType=1)取发票总额 → 选抬头 → [一键开票提交](#invoice-partner-apply-submit)(applyType=1) | | 「可开收据」Tab 底栏「去开收据」 | [一键开票预览](#invoice-partner-apply-preview)(applyType=2)取收据总额 → 选抬头 → [一键开票提交](#invoice-partner-apply-submit)(applyType=2) | | 开票记录 | [开票记录列表](#invoice-partner-request-list) → [详情](#invoice-partner-request-detail);`canCancel=true` 可 [撤销](#invoice-partner-request-cancel) | | 收据(独立一屏) | [收据列表](#receipt-application-list) → [详情](#receipt-application-detail) → [凭证预览/下载](#receipt-application-voucher);申请走上面的「去开收据」 | #### 金额拆分 一笔订单各项之和 = 用户实付 | 项 | 取值 | 票据 | |----|----|----| | 花材款(可开票供应商) | 供货价 × 数量,固定不减 | 供应商开发票 | | 花材款(不可开票供应商) | 供货价 × 数量,优惠溢出时倒扣 | 平台开收据,一行「鲜切花」 | | 干线物流费 | min(发货单实际物流成本, 用户物流加价, 剩余额度);取不到成本时为 0 | 可开票物流商 → 捷通公司发票;否则平台收据一行「物流运输」 | | 供应链服务费 | min(打包服务费, 剩余额度) | 花序公司发票 | | 平台服务费 | 剩余残差 | 花古德公司发票 | #### 规则 - 无勾选。申请范围由后端算:确认收货 3 个月内的全部可申请订单,按「订单 × 开票项」剔除已被未撤销申请占用的部分。 - 某项金额为 0 的订单不出现在该分组。线上物流结算数据尚未导入,此时干线物流费为 0、金额进平台服务费。 - 存量:上线前走老流程开过票的分销订单整单锁定,各分组显示“待商家开票”。 - 逾期:超 30 天未上传变“已逾期”,花店得 2% 寻梦币补偿;运营仍可代上传。平台三项不进逾期扣款、不发短信。 - 转代开只对花材款:原记录变“已转代开”,新生成一条“待商家开票”承接。 - 收据不可撤销。发票记录撤销后金额回到可开票池。 - 同一花店在两个平台都有单时,收据按平台各一张,`receiptIds` 多条。 ## 小程序 · 发票 · 前缀 /invoice ### POST /invoice/partner/group/list · 可开票分组列表 **新增** 「我的发票」页数据源。按出具主体 × 费用类型分组:每个可开票供应商一组,平台三项各一组。不分页,每组自带订单列表。无数据返回空数组。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `statusTab` | int | 否 | 1 全部(默认)2 待申请开票 3 已开发票 4 待商家开票 5 已逾期 | | `startTime` | datetime | 否 | 下单开始时间。不传默认确认收货 3 个月内 | | `endTime` | datetime | 否 | 下单结束时间 | #### 响应 data 为数组 | 字段 | 类型 | 说明 | |----|----|----| | `invoiceItemType` | int | 0 花材款 1 供应链服务费 2 干线物流费 3 平台服务费 | | `supplierId` | long | 花材款为供应商 id;平台项为内部开票公司账号 id,未配置时 0 | | `issuerName` | string | 出具主体:供应商名 / 花序公司 / 捷通公司 / 花古德公司 | | `voucherType` | int | 固定 1 发票 | | `groupName` | string | 分组名:「供应商名·鲜切花」或费用类型(如「平台服务费」,不带公司前缀) | | `totalAmount` | decimal | 分组金额合计(当前 Tab 筛选后) | | `orderCount` | int | 订单数(当前 Tab 筛选后) | | `orderList` | array | 组内订单,按下单时间倒序 | | `orderSn` | string | 订单号 | | `roundNo` | string | 场次 | | `amount` | decimal | 该订单在该分组的可开票金额 | | `createTime` | datetime | 下单时间 | | `status` | int | 0 待申请开票 1 待商家开票 2 已开发票 3 已逾期 | | `statusDesc` | string | 状态文案 | | `canApply` | boolean | 是否会进入一键开票范围,等价于 status==0 | #### 示例 ```text // 请求 { "statusTab": 1 } // 响应 { "code": 0, "msg": "success", "data": [ { "invoiceItemType": 0, "supplierId": 1021, "issuerName": "云南甲花卉", "voucherType": 1, "groupName": "云南甲花卉·鲜切花", "totalAmount": 356.00, "orderCount": 2, "orderList": [ { "orderSn": "XM2026090112345", "roundNo": "2026-09-01", "amount": 200.00, "createTime": "2026-09-01 12:00:00", "status": 0, "statusDesc": "待申请开票", "canApply": true }, { "orderSn": "XM2026082898765", "roundNo": "2026-08-28", "amount": 156.00, "createTime": "2026-08-28 09:30:00", "status": 1, "statusDesc": "待商家开票", "canApply": false } ] }, { "invoiceItemType": 1, "supplierId": 0, "issuerName": "花序公司", "voucherType": 1, "groupName": "供应链服务费", "totalAmount": 48.00, "orderCount": 2, "orderList": [ … ] }, { "invoiceItemType": 3, "supplierId": 0, "issuerName": "花古德公司", "voucherType": 1, "groupName": "平台服务费", "totalAmount": 63.50, "orderCount": 2, "orderList": [ … ] } ] } ``` ### POST /invoice/partner/group/order/list · 组内订单列表 **变更**:默认时间窗改为确认收货 3 个月内 单个分组下的订单,不分页。分组卡片已自带 orderList,本接口用于只刷新某一组的场景。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `invoiceItemType` | int | 是 | 0 花材款 1 供应链服务费 2 干线物流费 3 平台服务费 | | `supplierId` | long | 条件 | 花材款必传,取分组列表的 supplierId;平台项不传 | | `statusTab` | int | 否 | 同分组列表 | | `startTime` | datetime | 否 | 下单开始时间。不传默认确认收货 3 个月内 | | `endTime` | datetime | 否 | 下单结束时间 | #### 响应 data 为数组,字段同分组列表的 orderList 元素 | 字段 | 类型 | 说明 | |--------------|----------|-----------------------------------------------| | `orderSn` | string | 订单号 | | `roundNo` | string | 场次 | | `amount` | decimal | 该订单在该分组的可开票金额 | | `createTime` | datetime | 下单时间 | | `status` | int | 0 待申请开票 1 待商家开票 2 已开发票 3 已逾期 | | `statusDesc` | string | 状态文案 | | `canApply` | boolean | 是否会进入一键开票范围 | #### 示例 ```json { "invoiceItemType": 0, "supplierId": 1021, "statusTab": 2 } { "invoiceItemType": 3 } ``` > [!NOTE] > 花材款不传 `supplierId` 报“花材款分组必须指定供应商”;分组不存在返回空数组。 ### POST /invoice/partner/apply/preview · 一键开票预览 **变更**:新增 receiptPreview、applyType 返回发票部分总额与拆分、收据部分预览。`applyType` 控制只算哪一半:「可开票」Tab 传 1,「可开收据」Tab 传 2,不传两半都算。无可开票内容时 `totalAmount` 为 0、`supplierBreakdown` 为空数组、`receiptPreview` 为 null,不报错。用作底栏数据源。 #### 请求 body 可为空 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `applyType` | int | 否 | **新增** 不传 / 0 发票+收据;1 仅发票(收据部分 `receiptPreview` 返回 null);2 仅收据(发票部分 `totalAmount` 0、`supplierBreakdown` 空) | #### 响应 data 为对象 | 字段 | 类型 | 说明 | |----|----|----| | `totalAmount` | decimal | 发票部分总金额,不含收据 | | `orderCount` | int | 发票部分覆盖订单数 | | `supplierBreakdown` | array | 发票拆分明细 | | `invoiceItemType` | int | 0 花材款 1 供应链服务费 2 干线物流费 3 平台服务费 | | `supplierId` | long | 花材款为供应商 id;平台项为内部开票公司账号 id,未配置时 0 | | `supplierName` | string | 供应商名,或「供应链服务费」这类分组名 | | `amount` | decimal | 该项金额 | | `orderCount` | int | 花材款为该供应商关联订单数;平台项固定 1 | | `quotaSufficient` | boolean | 分销订单恒为 true | | `receiptPreview` | object | 收据部分;无可开收据订单时 null | | `totalAmount` | decimal | 收据总金额(多平台合计) | | `orderCount` | int | 收据覆盖订单数 | | `flowerAmount` | decimal | 花材收据金额 | | `flowerRoundText` | string | 花材场次文案,如「2026.06.05-2026.09.01 共十个场次」;金额为 0 时空串 | | `logisticsAmount` | decimal | 物流收据金额 | | `logisticsRoundText` | string | 物流场次文案 | #### 示例 ```json { "code": 0, "msg": "success", "data": { "totalAmount": 467.50, "orderCount": 2, "supplierBreakdown": [ { "invoiceItemType": 0, "supplierId": 1021, "supplierName": "云南甲花卉", "amount": 356.00, "orderCount": 2, "quotaSufficient": true }, { "invoiceItemType": 1, "supplierId": 0, "supplierName": "供应链服务费", "amount": 48.00, "orderCount": 1, "quotaSufficient": true }, { "invoiceItemType": 3, "supplierId": 0, "supplierName": "平台服务费", "amount": 63.50, "orderCount": 1, "quotaSufficient": true } ], "receiptPreview": { "totalAmount": 120.00, "orderCount": 1, "flowerAmount": 120.00, "flowerRoundText": "2026.09.01 共一个场次", "logisticsAmount": 0, "logisticsRoundText": "" } } } ``` > [!NOTE] > 底栏:「可开票」Tab 用 `totalAmount` 置灰「去开票」;「可开收据」Tab 用 `receiptPreview.totalAmount` 置灰「去开收据」。 ### POST /invoice/partner/apply/submit · 一键开票提交 **变更**:data 从 long 变为对象、新增 applyType 只传抬头,范围由后端算(与预览一致)。发票与收据可以分开申请:「去开票」传 `applyType=1` 只提交发票申请(自动拆多条开票记录);「去开收据」传 `applyType=2` 只按平台各开一张收据。不传则两者一次全做。成功后按入口分别跳开票记录页 / 收据列表页。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `invoiceTitleId` | long | 是 | 发票抬头 ID,来自 `/invoice/title/list`。只开收据也要传,收据购买人名称取自抬头 | | `applyType` | int | 否 | **新增** 不传 / 0 发票+收据;1 仅发票;2 仅收据。其他值报「申请类型参数错误」 | #### 响应 data 为对象 | 字段 | 类型 | 说明 | |----|----|----| | `applicationId` | long | 发票申请单 ID;没有可开发票项或 `applyType=2` 时 null | | `receiptIds` | long\[\] | 收据申请 ID 列表,每个平台一张;没有可开收据项或 `applyType=1` 时空数组 | #### 示例 ```text // 去开票 { "invoiceTitleId": 88, "applyType": 1 } { "code": 0, "msg": "success", "data": { "applicationId": 5127, "receiptIds": [] } } // 去开收据 { "invoiceTitleId": 88, "applyType": 2 } { "code": 0, "msg": "success", "data": { "applicationId": null, "receiptIds": [ 903 ] } } ``` #### 业务错误 msg 直接展示 | msg | 含义 | 前端处理 | |----|----|----| | 没有可开票的订单 | applyType 不传 / 1 时没有可申请内容 | 预览为 0 时提前置灰 | | 没有可开收据的订单 | applyType=2 时没有可开收据订单 | 预览 receiptPreview 为 null 时提前置灰 | | 开票申请处理中,请勿重复提交 | 上一次提交还在处理 | 稍后重试 | | 订单xxx已在开票申请中 | 并发兜底 | 重新拉预览 | | 存在已开收据订单,请刷新后重试 | 收据部分被并发抢先 | 重新拉预览 | > [!NOTE] > 不传 applyType 时若发票成功、收据失败,接口整体返回失败,但发票申请已落库;用户重试只会补收据,不会重复申请发票。发票与收据金额互斥(可开票部分进发票,不可开票部分进收据),分开申请不改变任何金额。 ### POST /invoice/partner/request/list · 开票记录列表 **变更**:状态文案 开票记录页数据源,request 维度分页,按申请时间倒序。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |-------------|------|------|----------------------------------------------------| | `pageIndex` | int | 是 | 页码,从 1 开始 | | `pageSize` | int | 是 | 每页条数 | | `status` | int | 否 | 不传全部(不含已撤销);0 待开票 1 已开票 2 已逾期 | #### 响应 data.records 元素 | 字段 | 类型 | 说明 | |----|----|----| | `requestNo` | string | 申请单号 | | `invoiceItemType` | int | 0 花材款 1 供应链服务费 2 干线物流费 3 平台服务费 | | `supplierId` | long | 花材款为供应商 id;平台项为内部开票公司账号 id,未配置时 0 | | `typeDesc` | string | 「供应商名·鲜切花」/ 供应链服务费 / 干线物流费 / 平台服务费 | | `titleName` | string | 票据抬头 | | `taxNumber` | string | 公司税号 | | `requestAmount` | decimal | 申请开票金额 | | `orderCount` | int | 关联订单数 | | `createTime` | datetime | 申请时间 | | `status` | int | 0 待商家开票 1 已开发票 2 已逾期 4 已转代开 | | `statusDesc` | string | 状态文案 | | `invoiceUrl` | string | 发票文件 URL,已开票时非空 | | `canCancel` | boolean | 是否可撤销,仅待商家开票为 true | | `deadline` | datetime | 开票截止时间 | #### 示例 ```json { "pageIndex": 1, "pageSize": 10, "status": null } ``` ### POST /invoice/partner/request/detail · 开票记录详情 **不变** 票据详情页数据源。含列表的全部字段,另加票据类型与所含订单。下载直接用 `invoiceUrl`。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |-------------|--------|------|----------| | `requestNo` | string | 是 | 申请单号 | #### 响应 data 为对象,列表字段之外新增 | 字段 | 类型 | 说明 | |-------------------|----------|----------------------------| | `invoiceTypeDesc` | string | 票据类型,固定「电子发票」 | | `orderList` | array | 所含订单 | | `orderSn` | string | 订单号 | | `roundNo` | string | 场次 | | `amount` | decimal | 该订单在本票据中的开票金额 | | `createTime` | datetime | 下单时间 | #### 示例 ```json { "requestNo": "REQ20260901xxxx" } ``` ### POST /invoice/partner/request/cancel · 撤销开票记录 **变更**:新增错误文案 按单条开票记录撤销,仅 `canCancel=true` 的可撤。撤销后金额回到可开票池。收据不可撤销。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |-------------|--------|------|----------| | `requestNo` | string | 是 | 申请单号 | #### 响应 | 字段 | 类型 | 说明 | |--------|---------|-----------| | `data` | boolean | true 成功 | #### 业务错误 | msg | 前端处理 | |----|----| | 开票记录不存在 | 刷新列表 | | 只有待开票的记录可以撤销 | 刷新列表 | | 开票记录状态已变化,请刷新后重试 | 并发下供应商刚上传了发票或已被撤销,刷新列表 | #### 示例 ```json { "requestNo": "REQ20260901xxxx" } ``` ## 小程序 · 收据 · 前缀 /receipt 分销收据由平台出具,与发票分屏。分销花店的收据页只有「列表 → 详情 → 凭证」,没有可开收据订单列表和勾选申请(`/receipt/order/list`、`/receipt/apply/*` 仅寻梦批发花店使用)。收据 PDF 正文固定两行:「鲜切花」和「物流运输」。 ### POST /receipt/application/list · 我的收据列表 **变更**:新增 partnerId #### 请求 | 字段 | 类型 | 必填 | 说明 | |-------------|----------|------|-------------------| | `pageIndex` | int | 是 | 页码,从 1 开始 | | `pageSize` | int | 是 | 每页条数 | | `status` | int | 否 | 0 申请中 1 已开具 | | `startTime` | datetime | 否 | 申请开始时间 | | `endTime` | datetime | 否 | 申请结束时间 | #### 响应 data.records 元素 | 字段 | 类型 | 说明 | |----|----|----| | `id` | long | 收据申请 ID | | `receiptNo` | string | 电子收款凭证号 | | `partnerId` | int | **新增** 所属平台:0 寻梦批发 3 优花速达 140 繁花惠 | | `applyTime` | datetime | 申请时间 | | `issueTime` | datetime | 开具时间 | | `totalAmount` | decimal | 收据金额 | | `orderCount` | int | 订单数 | | `status` | int | 0 申请中 1 已开具 | | `receiptUrl` | string | 收据 PDF 地址 | | `orderSnList` | string\[\] | 订单编号列表 | #### 示例 ```json { "pageIndex": 1, "pageSize": 10 } ``` ### POST /receipt/application/detail · 收据详情 **变更**:新增 partnerId、itemType #### 请求 | 字段 | 类型 | 必填 | 说明 | |------|------|------|-------------| | `id` | long | 是 | 收据申请 ID | #### 响应 data 为对象 | 字段 | 类型 | 说明 | |----|----|----| | `id` | long | 收据申请 ID | | `receiptNo` | string | 电子收款凭证号 | | `buyerName` | string | 购买人名称(取抬头) | | `buyerCertificateNo` | string | 购买人证照号 | | `partnerId` | int | **新增** 所属平台 | | `totalAmount` | decimal | 收据金额 | | `applyTime` | datetime | 申请时间 | | `issueTime` | datetime | 开具时间 | | `status` | int | 0 申请中 1 已开具 | | `receiptUrl` | string | 收据 PDF 地址 | | `orderList` | array | 订单快照 | | `orderSn` | string | 订单号 | | `roundNo` | string | 场次 | | `receiptAmount` | decimal | 该订单收据金额 | | `supplierCount` | int | 供应商数 | | `confirmTime` | datetime | 确认收货时间 | | `itemList` | array | 明细 | | `itemType` | int | **新增** 0 花材款 2 干线物流费 | | `orderSn` / `roundNo` | string | 同所属订单 | | `supplierId` / `supplierName` | long / string | 分销收据不按供应商拆分,恒为 null,前端忽略 | | `productName` | string | 「鲜切花」或「物流运输」 | | `itemNum` | int | 数量,物流行固定 1 | | `receiptAmount` | decimal | 金额 | #### 示例 ```json { "id": 903 } ``` ### POST /receipt/application/voucher · 收据凭证预览/下载 **不变** 返回 PDF 地址并记录操作日志。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |--------------|------|------|---------------| | `id` | long | 是 | 收据申请 ID | | `actionType` | int | 是 | 2 预览 3 下载 | #### 响应 data 为对象 | 字段 | 类型 | 说明 | |--------------|--------|----------------| | `id` | long | 收据申请 ID | | `receiptNo` | string | 电子收款凭证号 | | `receiptUrl` | string | 收据 PDF 地址 | #### 示例 ```json { "id": 903, "actionType": 2 } ``` ## 运营后台 · 前缀 /invoice/manage、/logistics ### POST /invoice/manage/partnerPlatform/list · 平台开票列表 **变更**:新增 partnerId 筛选与 platformName 数据源为 invoiceItemType 为 1、2、3 的平台开票记录。响应带三格金额汇总;汇总跟随除状态以外的筛选条件,切状态 Tab 不变。 #### 请求 均可选 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `index` | int | 否 | 页码 | | `size` | int | 否 | 每页条数 | | `searchCount` | boolean | 否 | 是否统计总数 | | `invoiceItemType` | int | 否 | null 全部 1 供应链服务费 2 干线物流费 3 平台服务费;2026-10-28 起支持 0 花材款,详见[花店开票列表新增花材款](/partner-invoice-admin-flower-api) | | `partnerId` | int | 否 | **新增** null 全部 0 寻梦批发 3 优花速达 140 繁花惠 | | `status` | int | 否 | null 全部 0 开票中 1 已开票 2 已逾期 3 已撤销 | | `requestNo` | string | 否 | 申请编号,精确 | | `titleName` | string | 否 | 发票抬头,模糊 | | `taxNumber` | string | 否 | 税号,精确 | | `purchaserPhone` | string | 否 | 花店手机号,精确 | | `createTimeStart` | datetime | 否 | 申请时间起 | | `createTimeEnd` | datetime | 否 | 申请时间止 | #### 响应 data 为对象 | 字段 | 类型 | 说明 | |----|----|----| | `invoicingTotalAmount` | decimal | 开票中总金额 | | `invoicedTotalAmount` | decimal | 已开票总金额 | | `overdueTotalAmount` | decimal | 已逾期总金额 | | `total` | long | 总条数 | | `records` | array | 列表 | | `id` | long | 记录 ID,关联单查询用 | | `requestNo` | string | 申请单号 | | `invoiceItemType` | int | 1 供应链服务费 2 干线物流费 3 平台服务费 | | `invoiceItemTypeDesc` | string | 开票类型文案 | | `partnerId` | int | **新增** 所属平台 | | `platformName` | string | **新增** 寻梦批发 / 优花速达 / 繁花惠 / 其他分销商 | | `purchaserId` | long | 花店 ID | | `purchaserName` | string | 花店名称 | | `purchaserPhone` | string | 花店手机号 | | `titleName` | string | 发票抬头 | | `taxNumber` | string | 税号 | | `requestAmount` | decimal | 申请开票金额 | | `orderCount` | int | 关联订单数(按实际订单号数) | | `status` | int | 0 开票中 1 已开票 2 已逾期 3 已撤销 | | `statusDesc` | string | 状态文案 | | `invoiceUrl` | string | 发票 URL | | `createTime` | datetime | 申请时间 | | `uploadTime` | datetime | 上传时间 | | `deadline` | datetime | 开票截止时间 | #### 示例 ```json { "index": 1, "size": 20, "searchCount": true, "invoiceItemType": 2, "partnerId": 140, "status": 0 } ``` > [!NOTE] > 平台项由三家内部开票公司在供应商端自行上传发票;运营侧「代上传」沿用现有代上传接口,待开票和已逾期均可代上传。平台项不支持转代开,不展示该按钮。 ### POST /invoice/manage/partnerPlatform/sendNo/list · 物流费关联发货单 **不变** 仅干线物流费记录(invoiceItemType=2)可调,按发货单号聚合。其他类型报“该开票记录不是物流费类型”。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |------|------|------|-------------------------------| | `id` | long | 是 | 平台开票列表的 records\[\].id | #### 响应 data 为数组 | 字段 | 类型 | 说明 | |---------------|---------|--------------------------| | `sendNo` | string | 发货单号 | | `roundNo` | string | 场次 | | `expressName` | string | 物流商名称,多个逗号分隔 | | `amount` | decimal | 该发货单物流费合计 | | `orderCount` | int | 订单数 | #### 示例 ```json { "id": 8801 } ``` ### POST /invoice/manage/partnerPlatform/order/list · 关联订单列表 **不变** 仅供应链服务费(1)、平台服务费(3)记录可调。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |------|------|------|-------------------------------| | `id` | long | 是 | 平台开票列表的 records\[\].id | #### 响应 data 为数组 | 字段 | 类型 | 说明 | |-----------|---------|----------------| | `orderSn` | string | 订单号 | | `roundNo` | string | 场次 | | `amount` | decimal | 该订单对应金额 | #### 示例 ```json { "id": 8802 } ``` ### POST /invoice/manage/request/list · 发票申请列表 **变更**:新增 2 个筛选、4 个返回字段 既有接口。本次只加字段,其余行为不变。 #### 请求 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `index` | int | 否 | 页码 | | `size` | int | 否 | 每页条数 | | `invoiceItemType` | int | 否 | **新增** null 全部 0 花材款 1 供应链服务费 2 干线物流费 3 平台服务费 | | `partnerId` | int | 否 | **新增** null 全部 0 寻梦批发 3 优花速达 140 繁花惠 | | `requestNo` | string | 否 | 申请单号 | | `purchaserPhone` | string | 否 | 花店手机号 | | `supplierId` | long | 否 | 供应商 ID | | `supplierName` | string | 否 | 供应商名称 | | `status` | int | 否 | 0 待开票 1 已开票 2 已逾期 3 已撤销 4 已转代开 | | `registrationId` | long | 否 | 登记单 ID | | `titleName` | string | 否 | 抬头名称 | | `createTimeStart` | datetime | 否 | 创建时间起 | | `createTimeEnd` | datetime | 否 | 创建时间止 | | `uploadTimeStart` | datetime | 否 | 上传时间起 | | `uploadTimeEnd` | datetime | 否 | 上传时间止 | #### 响应 data.records 元素,只列本次新增字段,其余不变 | 字段 | 类型 | 说明 | |----|----|----| | `invoiceItemType` | int | **新增** 0 花材款 1 供应链服务费 2 干线物流费 3 平台服务费;存量数据为 0 | | `invoiceItemTypeDesc` | string | **新增** 开票类型文案 | | `partnerId` | int | **新增** 所属平台;存量数据已回刷 | | `platformName` | string | **新增** 寻梦批发 / 优花速达 / 繁花惠 / 其他分销商 | #### 示例 ```json { "index": 1, "size": 20, "invoiceItemType": 0, "partnerId": 3 } ``` ### POST /invoice/manage/supplier/request/list · 供应商开票列表 **不变** 既有接口,本次 **请求与返回都没有改动**,前端无需处理。开票类型 / 所属平台的筛选与返回只加在 [发票申请列表](#invoice-manage-request-list) 上,平台三项(供应链服务费 / 干线物流费 / 平台服务费)的运营视图走 [平台开票列表](#invoice-manage-partnerplatform-list)。 > [!NOTE] > 本列表按开票记录的 supplier_id 查询,没有开票类型过滤:花序 / 捷通 / 花古德三个开票公司账号名下的平台项记录也会出现在这里,与真实供应商的花材款记录混排。若运营侧需要区分,另起需求再加字段。 > [!NOTE] > 老的「开票订单列表」不再返回繁花惠 / 优花速达的订单,这两家全部走分销端一键开票,运营侧只做代上传与查看。 ### POST /logistics/list/costConfig · 物流商列表 **变更**:新增 isInvoiceMarked 既有接口。物流商是否为「合作可开票物流商」决定分销订单的干线物流费开发票还是进收据。本次只加一个字段。 #### 请求 新增字段,其余不变 | 字段 | 类型 | 必填 | 说明 | |-------------------|------|------|------------------------------| | `isInvoiceMarked` | int | 否 | **新增** null 全部 0 否 1 是 | #### 响应 data.records 元素,新增字段 | 字段 | 类型 | 说明 | |-------------------|------|--------------------| | `isInvoiceMarked` | int | **新增** 0 否 1 是 | #### 示例 ```json { "index": 1, "size": 20, "isInvoiceMarked": 1 } ``` ### POST /logistics/add/costConfig · 物流商新增/编辑 **变更**:新增 isInvoiceMarked 既有接口,新增与编辑共用。本次只加一个字段。 #### 请求 新增字段,其余不变 | 字段 | 类型 | 必填 | 说明 | |-------------------|------|------|----------------------------------------| | `isInvoiceMarked` | int | 否 | **新增** 0 否 1 是;编辑时不传保持原值 | #### 响应 与现有接口一致。 对应后端分支 xiaofun/xm-bulky-mall-suppiler,applyType 拆分 2026-09-09 开发完成待上测;其余 2026-09-04 已上测(purchaser:bulky-test/purchaser,admin:bulky-test/admin)。接口行为与文档不符时以 knife4j 在线文档为准并反馈后端。 --- # 繁花惠 / 优花速达开票 > 票种与出票方、金额拆分与封顶规则、物流费算法、申请撤销口径、「为什么没看到」速查表、拍板时间线 - 类型:业务口径 - 分类:分销开票 - 更新:2026-09-20 - 网页版:https://doc.xfun.fun/partner-invoice-logic ## 哪些订单能进开票池 - 分销小程序(繁花惠 / 优花速达)的订单,订单状态为 **已完成**; - **确认收货时间在近 3 个月内**(没有确认收货时间的订单不进池,测试造数时注意补 confirm_time); - 发票、收据共用同一个池子,一个订单的钱按下面的规则拆到各票种,互不重复。 ## 票种一览 | 票种 | 内容 | 出票方 | 进入条件 | |----|----|----|----| | 花材发票 | 可开票供应商的花材款,按 **供货价×数量** | 对应供应商 | 该供应商已标记「可开票」 | | 花材收据 | 未标记可开票供应商的花材款 | 平台出具(分平台盖章) | 供应商未标记可开票 | | 干线物流费发票 | 订单的物流费(算法见「物流费专项」) | 捷通公司 | 订单实际使用的 **物流商**已标记「可开票」 | | 物流费收据 | 同上 | 平台出具 | 物流商未标记可开票 | | 供应链服务费发票 | 订单的打包服务费 | 花序公司 | 有剩余额度即生成 | | 平台服务费发票 | 以上都分完后的 **剩余部分** | 花古德公司 | 剩余额度 \> 0 | 两个「可开票」开关都在运营端维护:供应商侧是商品/供应商的可开票标识,物流商侧是物流列表里的「是否可开票」。注意物流商是按 **订单实际发货用的物流商**判断,不是下单页选的那个名字相近的。 ## 金额怎么拆(核心规则) 一切从「这单实际付了多少钱」出发: ```text 可开票总额 = 现金实付 + 寻梦币抵扣 (寻梦币按正常付款算,9-18 拍板) 优惠券 / 满减 / 平台补贴 不算钱,会等比压缩能开的票 ``` 按固定顺序分配,每一步都不能超过还剩的额度: **花材(发票 + 收据)** 按各供应商供货价×数量;可开票供应商进发票,其余进收据 **物流费** 三者取最小:实际运费、订单物流加价、剩余额度(详见「物流费专项」) **供应链服务费** = min(订单打包服务费, 剩余额度) **平台服务费** = 前面全部分完后的剩余,剩多少开多少 **钱不够时的兜底**:实付连花材都覆盖不了时,先从 **收据**里扣,收据扣到 0 还不够,再按比例压低 **花材发票**。所以永远满足恒等式: ```text 花材发票 + 花材收据 + 物流费 + 供应链服务费 + 平台服务费 = 实付(含寻梦币) ``` ### 两个测试期间的真实例子 | 场景 | 数据 | 结果 | |----|----|----| | 实付低于供货价 | 实付145.52,供货价 90(可开票)+60(收据)=150 | 发票 90 全额保住,收据 60−4.48= **55.52**,服务费/物流/平台全为 0 | | 物流加价被剩余额度封顶 | 实付29.90,花材 15+8.8=23.8,加价10.8,运费100 | 物流费 = min(100, 10.8, 剩余6.10) = **6.10** | ## 物流费专项 1. **实际运费**取发货单的实付运费减赔付,**不要求物流结算完成**(9-18 拍板:录了运费就能用); 2. 运费是发货单级的,同一发货单下多笔订单按各自的 **物流加价等比分摊**; 3. 已开过物流票的部分从发货单额度里扣掉,同一发货单不会开两遍; 4. 单笔订单票面 = min(分摊到的实际运费, 该订单物流加价, 剩余额度); 5. 物流商可开票 → 捷通发票;不可开票 → 进收据;运费超过加价的部分本来就开不出,加价超过实付剩余的部分计入不了。 ## 申请、撤销与记录 - **一键申请**:默认发票+收据一起申请;也支持 **只开发票**或 **只开收据**(9-9 拍板可分开)。收据按平台各出一张,两行汇总(花材 / 物流),场次文案如「2026.08.04-2026.09.19 共三个场次」。 - **发票撤销**:待商家开票的可撤;供应商已在开票平台受理或已开出的,撤销入口消失且接口拒绝。撤销后的额度回到池子可重新申请。 - **收据不可撤销**(产品口径),开过收据的订单不再回池。测试期间造废的收据要清数据才能重开。 - **开票记录**按申请单/票据维度展示;平台三项每类合并为一张票据,卡片和详情都显示真实关联订单数(9-18 修正)。 - 分组名不带公司前缀(显示「平台服务费」而非「花古德公司-平台服务费」,9-18 拍板),出票公司在票据详情中展示。 ## 运营端 - **平台开票列表**:只展示供应链服务费 / 干线物流费 / 平台服务费三类,支持按平台(繁花惠/优花速达)筛选,带三格金额汇总; - **发票申请列表**:花材类发票在这里看,新增平台筛选与平台字段; - **物流商管理**:新增/编辑物流商时可设置「是否可开票」(该开关 9-18 修复后已生效); - 平台三项由内部开票公司的供应商账号出票,不进逾期扣款、不发短信。 ## 「为什么没看到」速查表 测试期间实际遇到的全部案例,产品和测试可按这张表自查: | 现象 | 原因 | 怎么让它出现 | |----|----|----| | 整个池子是空的 | 订单没确认收货,或超过 3 个月 | 走完确认收货,测试数据补 confirm_time | | 某供应商花材只在收据里 | 该供应商未标「可开票」 | 运营端标记后重新看 | | 没有物流费 | 发货单没录运费 / 订单物流加价为 0 / 实付剩余为 0 / 物流商未标可开票(注意认准订单实际用的那家) | 四个条件逐一核对 | | 没有服务费 / 平台服务费 | 实付(含寻梦币)扣完花材已经没有剩余 | 下一单不用优惠,实付高于供货价合计 | | 金额比预期小 | 用了优惠/满减,开票总额被实付封顶,按顺序从后往前砍 | 属正常口径,票面合计恒等于实付 | | 订单在别的账号看不到 | 各账号只能看自己的池子 | 用下单账号查看 | | 开过收据的订单不见了 | 收据不可撤销,订单已被占用 | 属正常口径 | ## 本轮测试确认的口径(时间线) | 日期 | 拍板内容 | |-------|----------------------------------------------| | 09-09 | 发票与收据可以分开申请;收据只装不可开票部分 | | 09-18 | 寻梦币按正常付款计入可开票总额(不是优惠券) | | 09-18 | 票据分组名去掉「XX公司-」前缀 | | 09-18 | 平台项卡片显示真实订单数(原固定为 1) | | 09-18 | 物流费不再要求发货单已结算,录入运费即可用 | > [!NOTE] > **一个已知风险**(物流费不等结算的代价):未结算的运费金额没有写保护,结算被驳回时金额会清零,但先开出去的票不会回滚。产品已知悉并接受。 依据测试环境当前部署版本整理(purchaser / admin 均为 9-18 之后的构建)。数据示例均来自本轮真实测试订单。 --- # 开票物流费与繁花包干 > 物流费导入前后 / 更新前后各申请顺序的票据变化、不可开票物流收据、繁花包干城市仓开票逻辑与建议口径 - 类型:业务口径 - 分类:分销开票 - 更新:2026-10-19 - 网页版:https://doc.xfun.fun/partner-invoice-logistics ## 先看结论 - **线上分销发货单的实际运费目前全部为 0**,所以现在每一笔开票申请,物流票据都是 0,用户付的物流加价全部算进了 **平台服务费(花古德)**。 - 系统 **每次都按最新运费重新拆分金额**;已经申请的项目金额固定,没申请的项目跟着最新运费变。 - 因此只要 **一个订单分几次申请、中间运费有变化**,累计票据就不等于用户实付:会 多开,也可能 少开。不可开票物流走收据,问题一样。 - 繁花包干城市仓(繁花到店)订单和普通繁花订单 **走同一套开票逻辑**,没有单独分支;城市仓加价归入平台服务费。 - 建议口径:**订单任意一项第一次申请时,就把当时的物流金额定死(包括 0)**,之后不再随运费变化。`待产品确认` ## 金额怎么拆(复习) 每个分销订单的钱,按下面的顺序依次分配,**各项之和恒等于用户实付**(现金 + 寻梦币): ① 花材款 = 供货价 × 数量(可开票供应商开发票,不可开票的由平台开收据) ② 物流票据 = min( 实际运费分摊, 用户物流加价, 剩余额度 ) → 捷通(可开票物流)/ 收据(不可开票物流) ③ 供应链服务费 = min( 打包服务费, 剩余额度 ) → 花序 ④ 平台服务费 = 剩下的全部 → 花古德 - **实际运费**取发货单的 `实付运费 − 补偿/赔付`(运费对账单导入的那个金额,不要求已结算),再按发货单下各分销订单的物流加价比例分摊到订单。 - **物流加价** = 折进商品单价的物流加价 + 一次性物流加价。 - 运费为 0 或取不到 → 物流票据为 0,物流加价顺延进平台服务费。 - **可开票物流**:运营在后台勾选的物流商出发票(捷通);没勾选的物流商,这部分金额走 **收据**。 ## 物流费导入前 / 导入后 用一笔真实的繁花到店订单举例(`20261011604480968105`):实付 **51.60**,花材供货价 30.30,物流加价 11.80,打包服务费 2.20。 | 时点 | 花材 | 物流票据 | 供应链服务费(花序) | 平台服务费(花古德) | 合计 | |----|----|----|----|----|----| | 导入前(运费 0) | 30.30 | 0 | 2.20 | 19.10 | 51.60 | | 导入后,分摊到本单运费 8 | 30.30 | 8.00 | 2.20 | 11.10 | 51.60 | | 导入后,分摊运费 ≥ 11.80 | 30.30 | 11.80(封顶加价) | 2.20 | 7.30 | 51.60 | 单看任一时点都是平的;问题出在 **不同时点申请了不同的项**。 > [!NOTE] > **剩余额度很小时,运费会挤掉供应链服务费。**例:订单 `20261011361232240029` 实付 76.82,花材 71.30,物流加价 35.20,打包服务费 6.19,花材后剩余只有 5.52。\ > 导入前:物流 0、花序 5.52、花古德 0;导入 8 元运费后:物流 5.52、花序 0、花古德 0。这种订单受影响的是 **花序**,不是花古德。 ## 各种申请顺序下会怎样 仍用上面实付 51.60 的订单。已申请的项金额在申请时写进申请明细,之后不变;未申请的项每次打开页面重新算。 | \# | 场景 | 运费变化后系统表现 | 累计票据 | 结果 | |----|----|----|----|----| | 1 | 运费导入 **之后**才第一次申请(一次或分批都行,期间运费不再变) | 各项按同一运费计算 | 51.60 | 正确 | | 2 | 导入前 **全部类型一起申请**(此时物流 = 0,没有物流项) | 导入后物流项冒出来,显示「待申请」8.00 | 59.60 | 多开 8.00 | | 3 | 导入前只申请了平台服务费 / 供应链服务费 | 同上,物流项可申请 8.00 | 59.60 | 多开 8.00 | | 4 | 按运费 8 **只申请了物流**,后台把运费改成 12(封顶 11.80) | 物流已申请不变 8.00;平台服务费重算为 7.30 | 47.80 | 少开 3.80,永远开不出 | | 5 | 按运费 8 只申请了物流,后台把运费改成 5 | 物流不变 8.00;平台服务费重算为 14.10 | 54.60 | 多开 3.00 | | 6 | 先申请了平台服务费(物流 8 但没勾),后台把运费改成 5 | 平台服务费已按 11.10 开出;物流可申请 5.00 | 48.60 | 少开 3.00 | | 7 | 按运费 8 **所有类型一起申请**,之后运费怎么改 | 全部已固定,物流项也已占用不能再申请 | 51.60 | 正确 | - 场景 2/3 是 **线上现在的常态**:运费全是 0,已申请的订单将来只要导入运费,全部会出现可申请的物流项。 - 可申请池只含 **确认收货 3 个月内**的订单,超过 3 个月的老订单导入运费后也看不到,不会多开,但也开不出物流。 - 运费结算驳回被清零时,已开出的物流票据不会回滚(同理)。 ## 不可开票物流 物流商没被运营勾选为可开票时,「物流票据」那一份钱 **由平台出收据**,其余规则完全一样。 | 时点 | 物流收据 | 这部分钱去哪了 | |----|----|----| | 导入运费前 | 0 | 用户付的物流加价全部在 **花古德的平台服务费发票**里(剩余额度小的订单还会在花序里) | | 导入运费后 | min(运费分摊, 加价) | 平台服务费相应减少;若平台服务费已经开出,物流收据仍可再申请 → 与场景 2/3 一样 **多开** | **导入前已开出的金额(10-16 统计,近 3 个月已完成分销订单,运费全为 0):** | 出票方 | 按现行规则(物流 = 实际运费 = 0) | 对照:若物流收据按物流加价出 | |----|----|----| | 捷通 / 物流收据 | 0 | 886.30 万(收据) | | 花序 · 供应链服务费 | 140.79 万 | 64.79 万 | | 花古德 · 平台服务费 | 879.48 万 | 69.13 万 | 即:现行规则下,不可开票物流的收据是 **0**,物流加价约 886 万全部落在花古德(和少量花序)的发票里。「物流收据按加价出」那一列只是对照方案,`产品未拍板`。 ## 繁花包干城市仓(繁花到店) ### 是什么 - 繁花惠小程序下单、收货点是城市仓的订单(订单 `partner_id=140`、`ware_id>0`),即运营后台显示「(包干)」的繁花订单。 - 它们的 `app_id` 仍是繁花惠,所以 **进同一个分销开票池**,按第二节同一套规则拆分,**代码里没有针对包干的特殊处理**。 ### 金额构成 | 订单上的钱 | 开票归属 | |----|----| | 花材供货价 | 花材款(供应商发票 / 平台收据) | | 物流加价 | 物流票据(受实际运费封顶),超出部分进平台服务费 | | 打包服务费 | 供应链服务费(花序) | | **城市仓加价**、满减、小件分拣费等 | 不单独列项,全部进 **平台服务费(花古德)** | 量级参考(10-10 ~ 10-12 三天已完成订单): | | 订单数 | 实付 | 物流加价 | 城市仓加价 | 打包服务费 | |------------------------|--------|----------|----------|------------|------------| | 繁花到店(包干城市仓) | 800 | 7.38 万 | 2.21 万 | 1.15 万 | 0.41 万 | | 繁花直发 | 3,693 | 34.77 万 | 11.05 万 | 5.27 万 | 1.67 万 | | 优花速达 | 3,381 | 41.97 万 | 17.88 万 | 4.03 万 | 2.01 万 | ### 物流费 - 到店订单的发货单同样 **实际运费为 0**(只有系统估算的「预估运费」,开票不读预估运费),所以第三、四节的问题对它同样成立。 - 抽查 10-11 的 169 张繁花到店发货单,**都没有和城市仓自己的订单混在同一张发货单**。若开启「合并发货」导致混单:运费只会分摊给其中的分销订单(以其物流加价为上限),城市仓订单不参与分摊。 ### 不进这套开票的 - **城市仓包干收益、补短运费**:是平台 / 繁花惠与城市仓之间的结算,不进终端用户的票据。 - **城市仓自己的订单**(花仓酷城市仓、城市仓包干 `partner_id=0`):不是分销小程序订单,走普通大件批发开票流程。 - **分销商物流结算(按场次整笔扣分销商运费)**:9-01 至今繁花 152.52 万、优花速达 219.62 万,是按场次的整笔金额,没有落到发货单上,**开票不读这张表**。如果「导入运费」指的是这里,需要另外确认怎么拆到发货单。 ## 建议口径 `待产品确认` **订单第一次申请任意一项时,把当时算出的物流金额记下来(包括 0)** 不管这次勾了哪些类型,发票侧、收据侧都记。 **以后这个订单再申请其他项,物流都用记下的值,不再读最新运费** 累计票据恒等于实付;第四节 2~6 场景都不会出现多开 / 少开。 **代价** 订单开过任何一项之后,再导入或修改的运费不会体现在票上;那部分钱已经在平台服务费(或花序)里开出去了。 备选:允许按新运费补开物流,但需要对已开的平台服务费红冲重开,流程重、涉及已开发票,不建议。 依据代码:`PartnerInvoiceAmountCalculator`、`PartnerLogisticsCostProvider`、`PartnerInvoiceLogic`、`PurchaserReceiptLogic`(xm-bulky-mall-purchaser,master)。数据取自生产库只读查询。相关文档:[开票逻辑梳理](/partner-invoice-logic)。 --- # 分销开票金额拆分(方案二·售后扣减) > 繁花惠 / 优花速达开票快照的拆分顺序、售后「谁售后扣谁」、优惠券如何被吸收、带数字的例子,以及实开会不会大于实付 - 类型:业务口径 - 分类:分销开票 - 后端服务:purchaser - 状态:已上测 - 测试分支:kpsh - 更新:2026-11-03 - 网页版:https://doc.xfun.fun/partner-invoice-split-v2 ## 结论 - 开票金额在 **确认收货满 5 天** 时按当时数据拆一次并固化(开票快照),之后运费、可开票状态、售后都不再改它。 - 快照时 **各票据项之和恒等于用户实付**(现金 + 寻梦币,已扣优惠券、满减和此前的售后退款),所以快照时实开不会大于实付。 - 唯一会出现「实开 \> 最终实付」的情况:**快照之后才发生的售后**。按「开票后的售后不管」口径不再扣减。 ## 拆分顺序 ```text 用户实付 = 现金实付 + 寻梦币抵扣 (优惠券、满减、快照前的售后退款都已不在其中) ``` 按下表顺序依次分配,后一项只能分前面剩下的钱: | 顺序 | 票据项 | 金额 | 出票方 | |----|----|----|----| | 1 | 花材发票 | 可开票供应商:供货价 × 扎数 − 该供应商承担的售后额 | 对应供应商 | | 2 | 花材收据 | 不可开票供应商:供货价 × 扎数 − 该供应商承担的售后额 | 平台出具 | | 3 | 物流费 | min(实际运费,用户付的物流加价,剩余额度) | 捷通公司发票或收据 | | 4 | 供应链服务费 | min(打包服务费,剩余额度) | 花序公司 | | 5 | 平台服务费 | 剩余全部 | 花古德公司 | ## 售后怎么扣(谁售后扣谁) - 只看 **发货后的售后**:用户 / 客服售后、质检处罚、售后申诉,且退款成功。发货前退款已经减了扎数,不再重复扣。 - 每一笔售后明细扣减额 = min(供应商承担额,退给用户的商品金额),从 **承担的那个供应商** 的花材发票或收据里扣,扣到 0 为止。 - 质检处罚多罚供应商的部分不影响开票;城市仓、到场货账号承担的部分没有对应花材,随剩余额度由平台服务费吸收。 ## 优惠券怎么处理 优惠券不单独列项。它让「用户实付」变少,于是: 1. 先挤占排在最后的平台服务费,然后是供应链服务费、物流费; 2. 这三项挤完仍不够时,倒扣花材收据; 3. 收据也扣到 0 仍不够(实付低于全部供货价)时,按各供应商金额比例下调花材发票,尾差补给金额最大的一项,保证合计正好等于实付。 ## 例子 设定:A 供应商可开票,供货 30 元 × 10 扎 = 300;B 供应商不可开票,供货 20 元 × 5 扎 = 100;用户物流加价 40,实际运费 35,打包服务费 10。 | 情况 | 用户实付 | A 花材发票 | B 花材收据 | 物流费 | 供应链 | 平台 | 合计 | |----|----|----|----|----|----|----|----| | 无券、无售后,原价 500 | 500 | 300 | 100 | 35 | 10 | 55 | 500 | | 用 50 元券 | 450 | 300 | 100 | 35 | 10 | 5 | 450 | | 50 元券 + 售后退用户 70(A 承担 60) | 380 | 240 | 100 | 35 | 5 | 0 | 380 | | 150 元券 + 同上售后 | 280 | 240 | 40 | 0 | 0 | 0 | 280 | | 券极大,实付只剩 200 | 200 | 200 | 0 | 0 | 0 | 0 | 200 | 逐行说明: - 第 2 行:50 元券全部由平台服务费吸收(55 → 5)。 - 第 3 行:A 承担 60,A 发票 300 → 240;实付再少 70,剩余额度只够供应链 5,平台为 0。 - 第 4 行:花材 240 + 100 = 340 已超过实付 280,物流、供应链、平台全为 0,B 收据倒扣 60 → 40;A 发票不动。 - 第 5 行:收据扣到 0 仍超 40,A 发票按比例下调到 200。 ## 测试环境真实订单 | 订单 | 用户实付 | 售后 | 拆分结果 | 合计 | |----|----|----|----|----| | T20261031999108498792 | 188.00 − 37.60 = 150.40 | 退用户 37.60,供应商承担 30 | 花材发票 150 − 30 = 120,物流 13,供应链 2.50,平台 14.90 | 150.40 | | T20261102999108164205 | 216.90(已扣售后 24.10) | 退用户 24.10,供应商承担 15.50 | 花材收据 155 − 15.50 = 139.50,物流 51,供应链 10,平台 16.40 | 216.90 | ## 会不会实开大于实付 | 场景 | 会不会 | 说明 | |----|----|----| | 快照时的订单合计 | 不会 | 平台服务费取剩余,不够依次倒扣收据、下调发票,合计恒等于实付 | | 用了优惠券 | 不会 | 券先由平台项吸收,最终兜底下调发票 | | 快照前发生的售后 | 不会 | 实付已减少,花材按承担额扣减,差额由平台项吸收 | | 快照后才发生的售后 | **会** | 按「开票后的售后不管」不再扣减;例如开票 150 后又退 30,最终实付 120,发票仍为 150 | | 单个供应商维度 | 可能 | 发票按供货价开,券先由平台项吸收;某供应商发票可能高于用户为这几扎券后实付的钱,订单合计不受影响 | > [!IMPORTANT] > 若要完全避免「快照后售后」造成实开大于实付,只能二选一:限制确认收货 5 天后不能再发起售后,或接受这部分差额。 ## 供应商端订单明细 供应商端「发票任务 → 查看订单」: - 商品行显示 供货价 × 扎数 − 售后扣减额,快照之后的售后不计; - 分销订单的合计直接取开票快照金额,因此与商品行之和一致;仅在实付低于全部供货价、发票被按比例下调时,合计会小于各行之和。 --- # 农资优惠券 > 小程序端领券中心 / 券包 / 下单用券,运营后台模板 / 投放活动 / 券包与核销明细;含鉴权约定、枚举、状态流转与请求响应示例 - 类型:前端对接 - 分类:农资商城 - 端:农资小程序 / 运营后台 - 后端服务:supplier / admin - 更新:2026-09-27 - 网页版:https://doc.xfun.fun/peasant-coupon-api 两端接口分开列出:先「小程序端」(supplier 服务,供应商登录态),后「运营后台」(admin 服务)。2026-09-27 更新:领券中心 tab、领取记录、sourceDesc、定向发放重试口径。 ## 小程序端 · 通用约定 共 9 个接口。 - 路径前缀:`/peasant/coupon` - 鉴权:与其他 /peasant/* 接口一致:请求头 Authorization 携带登录 JWT(payload 含 supplierId),服务端解析并校验 Redis 在线态得到 supplierId。未登录/过期返回 code=9 msg=当前未登录。 - `code`:int,0 成功,非 0 失败(1 业务失败、2 参数校验失败、9 未登录、999 系统错误) - `msg`:string,成功固定 success,失败为可直接展示的中文文案 - `data`:业务数据;为 null 时序列化为空字符串 "",失败响应 data 恒为 "" - `serialization`:LocalDateTime 序列化为 "yyyy-MM-dd HH:mm:ss";BigDecimal 序列化为字符串(如 "10.00");分页字段 records/total/size/current/pages ### 枚举 | 枚举 | 取值 | |----|----| | 券实例状态 status | 10 可使用 · 20 已锁定(被待支付订单占用) · 30 已使用 · 40 已过期 · 50 已作废(运营作废) | | 领券中心 tab | 1 可领取(默认) · 2 即将开始(开始时间在未来) | | 我的券包 tab | 1 可使用(status 10/20 且未过期,按 validEnd 升序) · 2 已使用(status 30,按更新时间倒序) · 3 已失效(status 40/50,以及 status 10 但已过期,按更新时间倒序) | | 优惠类型 discountType | 1 满减券(需 thresholdPrice 门槛) · 2 无门槛券 | | 适用商品 productScopeType | 0 全场通用 · 1 指定商品(只对范围内商品金额计算门槛与优惠) | | 获取方式 sourceType | 1 自主领取(sourceDesc=自主领取) · 2 定向发放(sourceDesc=平台发放) · 3 积分兑换(sourceDesc=积分兑换,积分商城上线后出现) | | 有效期类型 validType(影响 validDesc 文案) | 1 固定有效期:"2026.09.18 00:00 至 2026.10.18 23:59" · 2 领取后生效:"领取后N天内有效",validStart=领取时刻,validEnd=领取时刻+N天 | ## 小程序端 · 领券中心 ### POST /peasant/coupon/center/list · 领券中心列表(09-23 新增 tab) 按 tab 返回当前供应商在投放范围内的自主领取活动,不分页。请求体可不传或传 {},等同 tab=1。tab=1 可领取:活动进行中且当前时间在活动起止内、模板进行中且未结束,按开始时间倒序。tab=2 即将开始:活动待生效或进行中但开始时间在未来、活动与模板都未到结束时间,按开始时间升序;此时 claimedCount 恒为 0,remainCount 为预计可领张数,领取按钮应禁用并用 startTime 展示“x月x日 开始”。公共前置:供应商审核通过且类型为种植户/供应商/包花厂,否则返回空数组;供应商需在活动投放范围及模板适用范围内。remainCount = min(活动剩余, 模板剩余, 活动每家限领-已领, 模板每家限领-已领),为 0 时展示“已领完/已达上限”并禁用领取按钮。注意:活动点过“立即开始”后 startTime 会被改成实际开始时刻。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `tab` | int | 否 | 1-可领取(默认) 2-即将开始 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `activityId` | long | 投放活动 id,领取时传此值 | | `activityNo` | string | 活动编号 | | `activityName` | string | 活动名称 | | `templateId` | long | 券模板 id | | `couponName` | string | 券名称 | | `discountType` | int | 1-满减 2-无门槛 | | `thresholdPrice` | string(BigDecimal) | 满减门槛金额,无门槛券为 0 | | `discountPrice` | string(BigDecimal) | 优惠金额 | | `productScopeType` | int | 0-全场 1-指定商品 | | `ruleDesc` | string | 规则文案,如“满100减10”/“无门槛减5” | | `validDesc` | string | 有效期文案 | | `usageDesc` | string | 使用说明 | | `startTime` | datetime | 活动开始时间 | | `endTime` | datetime | 活动结束时间(领取截止) | | `perSupplierLimit` | int | 活动每家限领张数,null 或 0 表示不限 | | `claimedCount` | int | 我在本活动已领取张数 | | `remainCount` | int | 我还可领取张数(>=0) | #### 可能的失败 msg - 当前未登录 #### 请求示例 ```json { "tab": 1 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": [ { "activityId": 12, "activityNo": "ACT20260918001", "activityName": "九月农资满减", "templateId": 5, "couponName": "满100减10", "discountType": 1, "thresholdPrice": "100.00", "discountPrice": "10.00", "productScopeType": 0, "ruleDesc": "满100减10", "validDesc": "领取后30天内有效", "usageDesc": "仅限农资商城下单使用,每单限用一张", "startTime": "2026-09-18 00:00:00", "endTime": "2026-09-30 23:59:59", "perSupplierLimit": 2, "claimedCount": 1, "remainCount": 1 } ] } ``` ### POST /peasant/coupon/claim · 领取优惠券 对某个自主领取活动领 1 张券,成功返回新券码。服务端按 supplierId 做 1 秒防重。校验顺序:账号可领 → 活动存在且为自主领取 → 活动进行中且在时间窗内 → 模板进行中 → 供应商在投放范围 → 事务内加锁校验每家上限与库存并落库。成功后刷新领券中心和券包。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `activityId` | long | 是 | 领券中心返回的 activityId | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | string | 新券码,格式 SIM + 8位模板id + 8位序号 | #### 可能的失败 msg - 活动id不能为空 - 操作频繁,请稍后重试 - 当前账号不可领取 - 活动不存在 - 活动未开始或已结束 - 优惠券已结束 - 不在活动投放范围内 - 已达领取上限 - 优惠券库存不足 - 当前未登录 #### 请求示例 ```json { "activityId": 12 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": "SIM0000000500000012" } ``` ## 小程序端 · 我的券包 ### POST /peasant/coupon/my/list · 我的券包列表 按 tab 分页查询当前供应商的券。tab=1 可使用:status 10/20 且未过期,按 validEnd 升序(快过期在前),status=20 展示“订单占用中”并禁止选用;tab=2 已使用;tab=3 已失效(含 status 10 但已过期、定时任务尚未扫到的券)。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `tab` | int | 否 | 1-可使用 2-已使用 3-已失效,默认 1 | | `current` | long | 否 | 页码,默认 1 | | `size` | long | 否 | 每页条数,默认 20 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `records` | array | 券列表 | | `records.id` | long | 券实例 id,下单时作为 couponInstanceId 传入 | | `records.couponCode` | string | 券码 | | `records.templateId` | long | 券模板 id | | `records.couponName` | string | 券名称(领取时快照) | | `records.discountType` | int | 1-满减 2-无门槛 | | `records.thresholdPrice` | string(BigDecimal) | 满减门槛 | | `records.discountPrice` | string(BigDecimal) | 优惠金额 | | `records.productScopeType` | int | 0-全场 1-指定商品 | | `records.sourceType` | int | 1-自主领取 2-定向发放 3-积分兑换 | | `records.sourceDesc` | string | 来源文案:自主领取 / 平台发放 / 积分兑换(09-27 新增) | | `records.status` | int | 10-可使用 20-已锁定 30-已使用 40-已过期 50-已作废 | | `records.validStart` | datetime | 生效时间 | | `records.validEnd` | datetime | 失效时间 | | `records.orderId` | string | 锁定/核销的订单号,未使用为空字符串 | | `records.useTime` | datetime | 核销时间,未使用为 null | | `records.createTime` | datetime | 领取/到账时间 | | `records.ruleDesc` | string | 规则文案 | | `records.validDesc` | string | 有效期文案 | | `records.usageDesc` | string | 使用说明,无则空字符串 | | `total` | long | 总条数 | | `size` | long | 每页条数 | | `current` | long | 当前页 | | `pages` | long | 总页数 | #### 可能的失败 msg - 当前未登录 #### 请求示例 ```json { "tab": 1, "current": 1, "size": 20 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "records": [ { "id": 1001, "couponCode": "SIM0000000500000012", "templateId": 5, "couponName": "满100减10", "discountType": 1, "thresholdPrice": "100.00", "discountPrice": "10.00", "productScopeType": 0, "sourceType": 1, "sourceDesc": "自主领取", "status": 10, "validStart": "2026-09-18 10:05:32", "validEnd": "2026-10-18 10:05:32", "orderId": "", "useTime": null, "createTime": "2026-09-18 10:05:32", "ruleDesc": "满100减10", "validDesc": "2026.09.18 10:05 至 2026.10.18 10:05", "usageDesc": "仅限农资商城下单使用,每单限用一张" } ], "total": 1, "size": 20, "current": 1, "pages": 1 } } ``` ### POST /peasant/coupon/my/records · 领取记录(09-27 新增) “我的优惠券 - 领取记录”页。分页返回当前供应商领到的全部券,不分状态(含已使用/已过期/已作废),按到账时间 createTime 倒序。data.total 即“共领取 N 张优惠券”。每行展示 couponName、sourceDesc、createTime;右侧状态文案由前端按 sourceType 决定:2 显示“发放成功”,1 和 3 显示“领取成功”。请求体可不传,默认第 1 页 20 条;tab 字段在本接口无效。返回结构与“我的券包列表”完全一致。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `current` | long | 否 | 页码,默认 1 | | `size` | long | 否 | 每页条数,默认 20 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `records` | array | 券列表 | | `records.id` | long | 券实例 id,下单时作为 couponInstanceId 传入 | | `records.couponCode` | string | 券码 | | `records.templateId` | long | 券模板 id | | `records.couponName` | string | 券名称(领取时快照) | | `records.discountType` | int | 1-满减 2-无门槛 | | `records.thresholdPrice` | string(BigDecimal) | 满减门槛 | | `records.discountPrice` | string(BigDecimal) | 优惠金额 | | `records.productScopeType` | int | 0-全场 1-指定商品 | | `records.sourceType` | int | 1-自主领取 2-定向发放 3-积分兑换 | | `records.sourceDesc` | string | 来源文案:自主领取 / 平台发放 / 积分兑换(09-27 新增) | | `records.status` | int | 10-可使用 20-已锁定 30-已使用 40-已过期 50-已作废 | | `records.validStart` | datetime | 生效时间 | | `records.validEnd` | datetime | 失效时间 | | `records.orderId` | string | 锁定/核销的订单号,未使用为空字符串 | | `records.useTime` | datetime | 核销时间,未使用为 null | | `records.createTime` | datetime | 领取/到账时间 | | `records.ruleDesc` | string | 规则文案 | | `records.validDesc` | string | 有效期文案 | | `records.usageDesc` | string | 使用说明,无则空字符串 | | `total` | long | 总条数 | | `size` | long | 每页条数 | | `current` | long | 当前页 | | `pages` | long | 总页数 | #### 可能的失败 msg - 当前未登录 #### 请求示例 ```json { "current": 1, "size": 20 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "records": [ { "id": 4305, "couponCode": "SIM0000000600000001", "templateId": 6, "couponName": "农资采购满减券", "discountType": 1, "thresholdPrice": "200.00", "discountPrice": "10.00", "productScopeType": 0, "sourceType": 1, "sourceDesc": "自主领取", "status": 10, "validStart": "2026-09-23 17:02:29", "validEnd": "2026-10-23 17:02:29", "orderId": "", "useTime": null, "createTime": "2026-09-23 17:02:29", "ruleDesc": "满200减10", "validDesc": "2026.09.23 17:02 至 2026.10.23 17:02", "usageDesc": "全场农资商品可用,每单限用一张" }, { "id": 1, "couponCode": "SIM0000000200000001", "templateId": 2, "couponName": "测试模板2", "discountType": 1, "thresholdPrice": "100.00", "discountPrice": "10.00", "productScopeType": 1, "sourceType": 2, "sourceDesc": "平台发放", "status": 10, "validStart": "2026-09-21 08:27:58", "validEnd": "2026-09-30 08:28:22", "orderId": "", "useTime": null, "createTime": "2026-09-23 11:13:00", "ruleDesc": "满100减10", "validDesc": "2026.09.21 08:27 至 2026.09.30 08:28", "usageDesc": "" } ], "total": 2, "size": 20, "current": 1, "pages": 1 } } ``` ### POST /peasant/coupon/my/count · 可使用张数 返回当前供应商 status=10(不含已锁定)且未过期的券数量,用于“我的”页角标。无请求体。 #### 请求参数 无 #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | long | 可使用张数 | #### 可能的失败 msg - 当前未登录 #### 请求示例 ```json {} ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": 3 } ``` ## 小程序端 · 下单用券 ### POST /peasant/coupon/usable · 下单页可用券 确认订单页调用。购物车下单传 cartIds,商品直接下单传 skuIdPriceList(只用 skuId/buyCount);cartIds 优先。服务端用 sku 当前价(斗南基地用斗南价)×数量算商品金额,对所有可使用券逐张计算:指定商品券只累计范围内商品金额;满减券需适用金额 >= 门槛;优惠上限为 商品总额-0.01(实付最低 0.01 元)。返回 usable/unusable 两组,各按 discount 降序(可默认选中 usable[0])。无可用券时直接返回两个空数组(此时不校验商品参数)。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `cartIds` | array | 否 | 购物车下单:购物车行 id 列表 | | `skuIdPriceList` | array | 否 | 商品直接下单:sku 与数量 | | `skuIdPriceList.skuId` | long | 是 | sku id | | `skuIdPriceList.buyCount` | int | 是 | 购买数量 | | `skuIdPriceList.price` | string(BigDecimal) | 否 | 本接口忽略,可不传 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `usable` | array | 本单可用券,按 discount 降序 | | `usable.…MyCouponDto 全部字段` | - | 同我的券包 records 元素 | | `usable.discount` | string(BigDecimal) | 本单可优惠金额 | | `usable.reason` | string | 可用时为空字符串 | | `unusable` | array | 本单不可用券 | | `unusable.…MyCouponDto 全部字段` | - | 同上 | | `unusable.discount` | string(BigDecimal) | 固定 0 | | `unusable.reason` | string | 不可用原因:无适用商品 / 未满100元 / 订单金额过低 / 优惠券未到生效时间 / 优惠券已过期 / 优惠券不可用 | #### 可能的失败 msg - 请选择商品 - 供应商不存在 - 当前未登录 #### 请求示例 ```json { "cartIds": [ 8801, 8802 ] } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "usable": [ { "id": 1001, "couponCode": "SIM0000000500000012", "couponName": "满100减10", "discountType": 1, "thresholdPrice": "100.00", "discountPrice": "10.00", "productScopeType": 0, "status": 10, "validStart": "2026-09-18 10:05:32", "validEnd": "2026-10-18 10:05:32", "ruleDesc": "满100减10", "validDesc": "2026.09.18 10:05 至 2026.10.18 10:05", "usageDesc": "仅限农资商城下单使用,每单限用一张", "discount": "10.00", "reason": "" } ], "unusable": [ { "id": 1002, "couponCode": "SIM0000000700000003", "couponName": "指定肥料满300减50", "discountType": 1, "thresholdPrice": "300.00", "discountPrice": "50.00", "productScopeType": 1, "status": 10, "validStart": "2026-09-01 00:00:00", "validEnd": "2026-09-30 23:59:59", "ruleDesc": "满300减50", "validDesc": "2026.09.01 00:00 至 2026.09.30 23:59", "usageDesc": "", "discount": "0", "reason": "未满300元" } ] } } ``` ### POST /peasant/order/cart/create · 购物车创建订单(新增 couponInstanceId) 已有接口,公共下单参数新增 couponInstanceId。price 仍传商品合计(券前金额),服务端先校验 price==商品合计,再按 couponInstanceId 重新计算优惠并锁券(10→20),实付 = 商品合计 - 优惠;订单 orderPrice=商品合计,totalPrice=券后实付。券不可用时整单失败。待支付取消/超时关单自动释放券;支付成功后券变已使用。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `couponInstanceId` | long | 否 | 新增:使用的券实例 id(可用券接口 usable[].id),不用券不传 | | `addressId` | long | 是 | 收货地址 id(原有) | | `price` | string(BigDecimal) | 是 | 商品合计金额(券前,原有口径不变) | | `payType` | int | 是 | 1-余额支付 2-微信支付(原有) | | `buyerMemo` | string | 否 | 买家留言(原有) | | `cartIds 等` | - | 是 | 原有字段不变 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | string | 订单号(原有) | #### 可能的失败 msg - 价格有误 - 优惠券不可用:优惠券不存在 - 优惠券不可用:优惠券未到生效时间 - 优惠券不可用:优惠券已过期 - 优惠券不可用:无适用商品 - 优惠券不可用:未满100元 - 优惠券不可用:订单金额过低 - 优惠券不可用,请重新选择(券已被其他订单占用) - 创建订单失败 #### 请求示例 ```json { "addressId": 301, "price": "120.00", "payType": 2, "buyerMemo": "", "couponInstanceId": 1001 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": "NZ20260918100532123456" } ``` ### POST /peasant/order/product/create · 商品直接创建订单(新增 couponInstanceId) 已有接口,用券规则与购物车下单完全一致:price 传商品合计(券前),skuIdPriceList 每项 price 需与 sku 当前价一致,couponInstanceId 传所选券实例 id。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `couponInstanceId` | long | 否 | 新增:使用的券实例 id,不用券不传 | | `addressId` | long | 是 | 收货地址 id | | `price` | string(BigDecimal) | 是 | 商品合计金额(券前) | | `payType` | int | 是 | 1-余额 2-微信 | | `buyerMemo` | string | 否 | 买家留言 | | `skuIdPriceList` | array | 是 | 商品列表 | | `skuIdPriceList.skuId` | long | 是 | sku id | | `skuIdPriceList.price` | string(BigDecimal) | 是 | 单价,需与当前售价一致 | | `skuIdPriceList.buyCount` | int | 是 | 数量 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | string | 订单号 | #### 可能的失败 msg - 价格有误 - 优惠券不可用:xxx(同购物车下单) - 优惠券不可用,请重新选择 - 创建订单失败 #### 请求示例 ```json { "addressId": 301, "price": "120.00", "payType": 1, "buyerMemo": "尽快发货", "skuIdPriceList": [ { "skuId": 5001, "price": "60.00", "buyCount": 2 } ], "couponInstanceId": 1001 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": "NZ20260918100532123457" } ``` ### POST /peasant/order/list 、 /peasant/order/detail · 订单列表 / 订单详情(新增券字段) 已有接口,列表元素与详情均新增 4 个券字段。展示口径:orderPrice=商品合计,couponPrice=券优惠(平台承担),totalPrice=实付(=orderPrice-couponPrice)。退款金额为商品口径,用券订单买家实际到账按比例扣减券优惠,最后一笔退完时清零剩余实付。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `-` | - | 否 | 请求参数不变 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `couponInstanceId` | long | 新增:券实例 id,0 表示未用券 | | `couponCode` | string | 新增:券码,未用券为空 | | `couponName` | string | 新增:券名称快照 | | `couponPrice` | string(BigDecimal) | 新增:券优惠金额,未用券为 0 | | `orderPrice` | string(BigDecimal) | 原有:商品合计(券前) | | `totalPrice` | string(BigDecimal) | 原有:券后实付金额 | #### 可能的失败 msg - 当前未登录 #### 请求示例 ```json { "orderSn": "NZ20260918100532123456" } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "orderId": "NZ20260918100532123456", "orderPrice": "120.00", "totalPrice": "110.00", "couponInstanceId": 1001, "couponCode": "SIM0000000500000012", "couponName": "满100减10", "couponPrice": "10.00", "payType": 2, "orderStatus": 1, "createTime": "2026-09-18 10:05:32" } } ``` ## 运营后台 · 通用约定 共 26 个接口。 - 路径前缀:`/peasantCoupon`(运营后台服务 xm-bulky-admin,三个控制器同挂 @RequestMapping("/peasantCoupon"),仓库内未配置 server.servlet.context-path(Nacos 配置若有前缀由网关/环境决定);所有接口均为 POST + application/json 请求体,与相邻 /peasantOrder、/peasantProduct 控制器一致) - 鉴权:所有接口标注 @AuthToken(默认 PermissionEnum.login):请求头携带 usertokenxm=<登录返回的 JWT>;缺失/失效时 HTTP 仍为 200,响应体 {"code":2,"msg":"登录失效,请重新登录"}(初始密码未改返回 code 19);菜单权限走 xm_bulky_auth_permission.perm=peasantCoupon,后端拦截器不按 perm 拦接口,仅控制前端菜单展示 - `code`:Integer,0 成功;1 业务失败(msg 为业务文案);2 登录超时/失效;4 参数校验失败(msg 形如 "name:券名称不能为空,useType:用途不能为空");6 权限不足;19 请先修改初始密码 - `msg`:String,成功固定 "success",失败为可直接展示的中文文案 - `data`:泛型,成功时的数据;操作类接口成功时 data=true;失败时 data=null - `dateFormat`:LocalDateTime 统一 "yyyy-MM-dd HH:mm:ss"(请求与响应相同) - `decimalFormat`:BigDecimal 全局序列化为字符串,如 "20.00";请求可传数字或字符串 成功示例: ```json { "code": 0, "msg": "success", "data": true } ``` 失败示例: ```json { "code": 1, "msg": "仅进行中的模板可以暂停", "data": null } ``` ### 分页约定 | 字段 | 说明 | |----|----| | 入参 `index` | Integer,页码,不传默认 1 | | 入参 `size` | Integer,页大小,不传默认 10 | | 入参 `searchCount` | Boolean,是否查询总数,默认 true | | 出参 `index` | Integer,当前页 | | 出参 `size` | Integer,页大小 | | 出参 `searchCount` | Boolean,是否查了总数 | | 出参 `total` | Number,总条数 | | 出参 `records` | Array,当前页记录 | | 出参 `other` | Object,扩展数据,本组接口恒为 null | ### 菜单 - **农资优惠券**(route `peasantCoupon`):docs/sql/2026-09-17-peasant-coupon-menu.sql:插入 xm_bulky_auth_permission(pid=“农资商品管理”所在父组的 pid, name='农资优惠券', perm='peasantCoupon', can_index=1),即与“农资商品管理”同级的一个菜单项;前端页面内自行用 Tab 划分 概览 / 券模板 / 投放活动 / 券包与核销明细 四块,后端未再拆子菜单 ### 枚举 | 枚举 | 取值 | |----|----| | 模板/活动状态 status | 0 草稿 · 10 待生效 · 20 进行中 · 30 已暂停 · 40 已结束 · 50 已作废 | | 券实例状态 status(券包) | 10 可使用 · 20 已锁定(下单占用中) · 30 已使用 · 40 已过期 · 50 已作废 | | 模板用途 useType | 1 通用券 · 2 积分商城专用券 | | 优惠类型 discountType | 1 满减券 · 2 无门槛券 | | 适用商品 productScopeType | 0 全场 · 1 指定商品 | | 适用供应商 supplierScopeType(模板) | 0 全部 · 1 指定基地 · 2 指定供应商 | | 有效期类型 validType | 1 固定有效期 · 2 领取后生效 | | 模板 saveMode | 1 保存草稿 · 2 定时生效(startTime 必填;startTime<=当前时间则直接进行中) | | 投放方式 issueType | 1 自主领取 · 2 定向发放 | | 投放对象 targetType(活动) | 0 全部供应商 · 1 指定基地 · 2 指定供应商 | | 活动 saveMode | 1 仅保存(待生效) · 2 保存并启用(保存后立即调用开始) | | 券获取方式 sourceType(券实例) | 1 自主领取 · 2 定向发放 · 3 积分兑换 | | 活动最近记录类型 recordType | 1 领取/发放 · 2 核销 | | 操作日志 bizType | 1 模板 · 2 活动 · 3 券实例 | | 操作日志 action | 1 创建 · 2 编辑 · 3 排期/立即开始(模板定时生效或立即生效、活动立即开始) · 5 暂停 · 6 恢复 · 7 手工结束 · 8 自动结束(模板结束/作废联动,或定时任务到期) · 9 作废 | | 操作日志 operatorType | 0 系统 · 1 平台(运营后台) · 2 供应商 | ### 状态流转 | 对象 | 从 | 动作 | 到 | 接口 | |----|----|----|----|----| | 模板 | 无 | 新建 saveMode=1 | 草稿 | `POST /peasantCoupon/template/save` | | 模板 | 无 | 新建 saveMode=2(startTime>当前时间) | 待生效 | `POST /peasantCoupon/template/save` | | 模板 | 无 | 新建 saveMode=2(startTime<=当前时间) | 进行中 | `POST /peasantCoupon/template/save` | | 模板 | 草稿 | 编辑 saveMode=2 | 待生效 或 进行中(按 startTime 与当前时间比较) | `POST /peasantCoupon/template/update` | | 模板 | 草稿 | 编辑 saveMode=1 | 草稿 | `POST /peasantCoupon/template/update` | | 模板 | 待生效 | 编辑(服务端强制按 saveMode=2 校验;把 startTime 改到过去) | 进行中 | `POST /peasantCoupon/template/update` | | 模板 | 待生效 | 到达 startTime(supplier 侧定时任务,admin 无接口) | 进行中 | `定时任务` | | 模板 | 进行中 | 暂停 | 已暂停 | `POST /peasantCoupon/template/pause` | | 模板 | 已暂停 | 恢复(要求 endTime>当前时间) | 进行中 | `POST /peasantCoupon/template/resume` | | 模板 | 待生效/进行中/已暂停 | 结束(其下待生效/进行中/已暂停的活动同步自动结束,活动日志 action=8) | 已结束 | `POST /peasantCoupon/template/end` | | 模板 | 草稿/待生效/进行中/已暂停 | 作废(要求无任何券实例且 issuedCount=0,需 reason;其下活动同步自动结束) | 已作废 | `POST /peasantCoupon/template/void` | | 模板 | 进行中/已暂停 | 到达 endTime(定时任务) | 已结束 | `定时任务` | | 活动 | 无 | 新建 saveMode=1 | 待生效 | `POST /peasantCoupon/activity/save` | | 活动 | 无 | 新建 saveMode=2(保存后立即开始) | 进行中(开始失败则停留待生效并返回失败文案) | `POST /peasantCoupon/activity/save` | | 活动 | 草稿/待生效 | 编辑(全量字段) | 状态不变 | `POST /peasantCoupon/activity/update` | | 活动 | 进行中/已暂停 | 编辑(仅名称、结束时间、自主领取的投放总量) | 状态不变 | `POST /peasantCoupon/activity/update` | | 活动 | 待生效 | 立即开始(模板须进行中且活动 endTime>当前时间;定向发放桥调 supplier 立即发券,失败回到待生效并写 issueFailMsg) | 进行中 | `POST /peasantCoupon/activity/start` | | 活动 | 待生效 | 到达 startTime(定时任务) | 进行中 | `定时任务` | | 活动 | 进行中 | 暂停 | 已暂停 | `POST /peasantCoupon/activity/pause` | | 活动 | 已暂停 | 恢复(要求 endTime>当前时间) | 进行中 | `POST /peasantCoupon/activity/resume` | | 活动 | 待生效/进行中/已暂停 | 结束 | 已结束 | `POST /peasantCoupon/activity/end` | | 活动 | 草稿/待生效/进行中/已暂停 | 作废(要求 issuedCount=0,需 reason) | 已作废 | `POST /peasantCoupon/activity/void` | | 活动 | 待生效/进行中/已暂停 | 所属模板结束或作废(联动) | 已结束 | `由 /template/end、/template/void 触发` | | 券实例 | 可使用(10) | 人工作废(需 reason) | 已作废(50) | `POST /peasantCoupon/instance/void` | | 券实例 | 可使用/已锁定/已使用/已过期 | 下单锁定、支付核销、退款解锁、到期过期(均由 supplier 侧完成,admin 只读) | 20/30/10/40 | `无(admin 不提供)` | ### 公共模型 #### CouponScopeItemDto | 字段 | 类型 | 说明 | |----|----|----| | `targetId` | Long | 对象 id(商品 id / 基地 id / 供应商 id) | | `targetName` | String | 对象名称快照,保存时服务端补齐,最长 64 字 | #### XmPeasantCouponOpLog | 字段 | 类型 | 说明 | |----|----|----| | `id` | Long | 日志 id | | `bizType` | Integer | 1-模板 2-活动 3-券实例 | | `bizId` | Long | 模板/活动/券实例 id | | `action` | Integer | 动作,见枚举“操作日志 action” | | `beforeStatus` | Integer | 操作前状态(创建时为 0) | | `afterStatus` | Integer | 操作后状态 | | `content` | String | 内容:创建模板 / 编辑差异“名称:旧 → 新;发行总量:100 → 200” / 暂停 / 恢复 / 手工结束 / 作废 / 立即开始 / 定时生效 2026-09-20T10:00 / 模板结束 等,最长 1024 字 | | `reason` | String | 作废原因,其他动作为空串 | | `orderId` | String | 券实例锁定/核销相关订单号,运营端操作为空 | | `operatorType` | Integer | 0-系统 1-平台 2-供应商 | | `operatorId` | Long | 操作人 id(平台=运营账号 userId) | | `operatorName` | String | 操作人昵称 | | `createTime` | String | 操作时间 yyyy-MM-dd HH:mm:ss | ## 运营后台 · 券模板 ### POST /peasantCoupon/template/list · 模板列表 分页查询模板(排除已删除 dataStatus!=0 的数据),按 id 倒序。keyword 对券名称/券编号模糊匹配;useType/discountType/status 精确过滤。列表项不含范围明细与日志,instanceCount 为 null #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `index` | Integer | 否 | 页码,默认 1 | | `size` | Integer | 否 | 页大小,默认 10 | | `searchCount` | Boolean | 否 | 是否查总数,默认 true | | `keyword` | String | 否 | 券名称/券编号,模糊匹配 | | `useType` | Integer | 否 | 用途 1-通用券 2-积分商城专用券 | | `discountType` | Integer | 否 | 优惠类型 1-满减 2-无门槛 | | `status` | Integer | 否 | 状态 0/10/20/30/40/50 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `index` | Integer | 当前页 | | `size` | Integer | 页大小 | | `searchCount` | Boolean | 是否查总数 | | `total` | Number | 总条数 | | `records` | Array | 模板列表 | | `records.id` | Long | 模板 id | | `records.templateNo` | String | 券编号,NZQ+yyyyMMdd+6 位随机数,如 NZQ20260918123456 | | `records.name` | String | 券名称 | | `records.useType` | Integer | 用途 1-通用券 2-积分商城专用券 | | `records.discountType` | Integer | 1-满减券 2-无门槛券 | | `records.thresholdPrice` | String(BigDecimal) | 使用门槛 满 X 元,无门槛券为 0 | | `records.discountPrice` | String(BigDecimal) | 优惠金额 | | `records.productScopeType` | Integer | 适用商品 0-全场 1-指定商品 | | `records.supplierScopeType` | Integer | 适用供应商 0-全部 1-指定基地 2-指定供应商 | | `records.validType` | Integer | 有效期类型 1-固定有效期 2-领取后生效 | | `records.validStart` | String | 固定有效期开始,validType=2 时为 null | | `records.validEnd` | String | 固定有效期结束,validType=2 时为 null | | `records.validDays` | Integer | 领取后有效天数,validType=1 时为 0 | | `records.totalCount` | Integer | 发行总量 | | `records.issuedCount` | Integer | 已发放张数(各活动/积分兑换累计) | | `records.perSupplierLimit` | Integer | 每家供应商上限,0-不限 | | `records.startTime` | String | 模板生效时间(可领取/发放期起点),草稿可为 null | | `records.endTime` | String | 模板结束时间,固定有效期时等于 validEnd | | `records.status` | Integer | 0-草稿 10-待生效 20-进行中 30-已暂停 40-已结束 50-已作废 | | `records.usageDesc` | String | 使用说明 | | `records.creatorId` | Long | 创建人 id | | `records.creatorName` | String | 创建人昵称 | | `records.dataStatus` | Integer | 0-正常,固定 0 | | `records.createTime` | String | 创建时间 | | `records.updateTime` | String | 更新时间 | | `records.ruleDesc` | String | 优惠规则文案:满减“满100减20”,无门槛“无门槛减10” | | `records.validDesc` | String | 有效期文案:固定“2026.09.20 00:00 至 2026.10.20 23:59”,领取后“领取后30天内有效” | | `records.instanceCount` | Long | 已产生券实例数,列表中为 null,详情才返回 | | `records.productScopes` | Array | 列表中为 null | | `records.baseScopes` | Array | 列表中为 null | | `records.supplierScopes` | Array | 列表中为 null | | `records.logs` | Array | 列表中为 null | | `other` | Object | 恒为 null | #### 可能的失败 msg - 登录失效,请重新登录 #### 请求示例 ```json { "index": 1, "size": 10, "keyword": "秋种", "useType": 1, "status": 20 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "index": 1, "size": 10, "searchCount": true, "total": 1, "records": [ { "id": 12, "templateNo": "NZQ20260918305127", "name": "秋种满100减20", "useType": 1, "discountType": 1, "thresholdPrice": "100.00", "discountPrice": "20.00", "productScopeType": 0, "supplierScopeType": 1, "validType": 1, "validStart": "2026-09-20 00:00:00", "validEnd": "2026-10-20 23:59:59", "validDays": 0, "totalCount": 1000, "issuedCount": 236, "perSupplierLimit": 2, "startTime": "2026-09-18 10:00:00", "endTime": "2026-10-20 23:59:59", "status": 20, "usageDesc": "仅限农资商城下单使用,不可叠加", "creatorId": 3, "creatorName": "沈浩", "dataStatus": 0, "createTime": "2026-09-18 09:30:12", "updateTime": "2026-09-18 10:00:00", "ruleDesc": "满100减20", "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59", "instanceCount": null, "productScopes": null, "baseScopes": null, "supplierScopes": null, "logs": null } ], "other": null } } ``` ### POST /peasantCoupon/template/detail · 模板详情 按 id 查模板,附带范围明细(指定商品 productScopes、指定基地 baseScopes、指定供应商 supplierScopes,未选的为空数组)、已产生券实例数 instanceCount、最近 20 条操作日志 logs(id 倒序)。新建/编辑接口成功后也返回本结构 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 模板 id,>=1 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `(模板基础字段)` | PeasantCouponTemplateDto | 与 /template/list records 项字段完全相同 | | `instanceCount` | Long | 已产生券实例数(含各状态),>0 时不可作废 | | `productScopes` | Array | productScopeType=1 时的指定商品;targetId=农资商品 id,targetName=商品名 | | `baseScopes` | Array | supplierScopeType=1 时的指定基地;targetId=xm_bulky_base_config.id,targetName=基地名 | | `supplierScopes` | Array | supplierScopeType=2 时的指定供应商;targetId=供应商 id,targetName=供应商名 | | `logs` | Array | 最近 20 条操作日志,字段见 commonModels.XmPeasantCouponOpLog;全部日志用 /template/logList | #### 可能的失败 msg - 优惠券模板不存在 - id:ID不能为空 - id:ID最小值不能小于1 #### 请求示例 ```json { "id": 12 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "id": 12, "templateNo": "NZQ20260918305127", "name": "秋种满100减20", "useType": 1, "discountType": 1, "thresholdPrice": "100.00", "discountPrice": "20.00", "productScopeType": 0, "supplierScopeType": 1, "validType": 1, "validStart": "2026-09-20 00:00:00", "validEnd": "2026-10-20 23:59:59", "validDays": 0, "totalCount": 1000, "issuedCount": 236, "perSupplierLimit": 2, "startTime": "2026-09-18 10:00:00", "endTime": "2026-10-20 23:59:59", "status": 20, "usageDesc": "仅限农资商城下单使用,不可叠加", "creatorId": 3, "creatorName": "沈浩", "dataStatus": 0, "createTime": "2026-09-18 09:30:12", "updateTime": "2026-09-18 10:00:00", "ruleDesc": "满100减20", "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59", "instanceCount": 236, "productScopes": [], "baseScopes": [ { "targetId": 5, "targetName": "斗南基地" }, { "targetId": 8, "targetName": "晋宁基地" } ], "supplierScopes": [], "logs": [ { "id": 88, "bizType": 1, "bizId": 12, "action": 3, "beforeStatus": 0, "afterStatus": 20, "content": "立即生效", "reason": "", "orderId": null, "operatorType": 1, "operatorId": 3, "operatorName": "沈浩", "createTime": "2026-09-18 10:00:00" }, { "id": 87, "bizType": 1, "bizId": 12, "action": 1, "beforeStatus": 0, "afterStatus": 0, "content": "创建模板", "reason": "", "orderId": null, "operatorType": 1, "operatorId": 3, "operatorName": "沈浩", "createTime": "2026-09-18 10:00:00" } ] } } ``` ### POST /peasantCoupon/template/save · 新建模板 创建模板。saveMode=1 保存为草稿(0);saveMode=2 定时生效:startTime>当前时间进入待生效(10),startTime<=当前时间直接进行中(20)。范围只传 id,名称由服务端补齐。校验顺序:名称非空且<=64字 → useType∈{1,2} → discountType∈{1,2} → discountPrice 0.01~5000 → 满减券 thresholdPrice 1~10000 且 discountPrice=1,saveMode=2 必填 endTime,且 startTime当前时间 → totalCount>=1 → perSupplierLimit>=0 → usageDesc<=500字。无门槛券 thresholdPrice 服务端强制写 0。券编号 NZQ+日期+6 位随机自动生成。成功返回模板详情 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `name` | String | 是 | 券名称,<=64 字 | | `useType` | Integer | 是 | 1-通用券 2-积分商城专用券(2 只能领取后生效,且不能被活动选用) | | `discountType` | Integer | 是 | 1-满减券 2-无门槛券 | | `thresholdPrice` | BigDecimal | 否 | 满 X 元,满减券必填 1~10000;无门槛券传 0 或不传 | | `discountPrice` | BigDecimal | 是 | 优惠金额 0.01~5000,满减券须小于门槛 | | `productScopeType` | Integer | 否 | 0-全场(默认) 1-指定商品 | | `productIds` | Array | 否 | 指定商品 id 列表,productScopeType=1 时必填(农资商品 xm_peasant_product.id) | | `supplierScopeType` | Integer | 否 | 0-全部(默认) 1-指定基地 2-指定供应商 | | `baseIds` | Array | 否 | 指定基地 id 列表(xm_bulky_base_config.id),supplierScopeType=1 时必填 | | `supplierIds` | Array | 否 | 指定供应商 id 列表,supplierScopeType=2 时必填 | | `validType` | Integer | 是 | 1-固定有效期 2-领取后生效 | | `validStart` | String | 否 | yyyy-MM-dd HH:mm:ss,validType=1 必填 | | `validEnd` | String | 否 | yyyy-MM-dd HH:mm:ss,validType=1 必填,同时作为模板 endTime | | `validDays` | Integer | 否 | 领取后有效天数,validType=2 必填且>=1 | | `totalCount` | Integer | 是 | 发行总量 >=1 | | `perSupplierLimit` | Integer | 否 | 每家上限,0-不限(默认 0) | | `startTime` | String | 否 | 模板生效时间,saveMode=2 必填;<=当前时间则立即进行中 | | `endTime` | String | 否 | 模板结束时间,validType=2 且 saveMode=2 时必填;validType=1 时忽略(取 validEnd) | | `usageDesc` | String | 否 | 使用说明 <=500 字 | | `saveMode` | Integer | 否 | 1-保存草稿(默认) 2-定时生效 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | PeasantCouponTemplateDto | 同 /template/detail 返回结构 | #### 可能的失败 msg - name:券名称不能为空 - useType:用途不能为空 - discountType:优惠类型不能为空 - discountPrice:优惠金额不能为空 - validType:有效期类型不能为空 - totalCount:发行总量不能为空 - 券名称不能为空且不超过64字 - 用途不正确 - 优惠类型不正确 - 优惠金额范围 0.01~5000 - 使用门槛范围 1~10000 - 满减券的优惠金额必须小于使用门槛 - 请选择适用商品 - 请选择适用基地 - 请选择适用供应商 - 有效期类型不正确 - 积分商城专用券只能选择领取后生效 - 固定有效期的开始时间必须早于结束时间 - 生效时间不能晚于有效期结束 - 领取后有效天数必须大于0 - 请填写模板结束时间 - 生效时间必须早于结束时间 - 定时生效必须填写生效时间 - 结束时间必须晚于当前时间 - 发行总量必须大于0 - 每家上限不能为负数 - 使用说明不超过500字 #### 请求示例 ```json { "name": "秋种满100减20", "useType": 1, "discountType": 1, "thresholdPrice": 100, "discountPrice": 20, "productScopeType": 0, "supplierScopeType": 1, "baseIds": [ 5, 8 ], "validType": 1, "validStart": "2026-09-20 00:00:00", "validEnd": "2026-10-20 23:59:59", "totalCount": 1000, "perSupplierLimit": 2, "startTime": "2026-09-18 10:00:00", "usageDesc": "仅限农资商城下单使用,不可叠加", "saveMode": 2 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "id": 12, "templateNo": "NZQ20260918305127", "name": "秋种满100减20", "status": 20, "issuedCount": 0, "instanceCount": 0, "baseScopes": [ { "targetId": 5, "targetName": "斗南基地" }, { "targetId": 8, "targetName": "晋宁基地" } ], "productScopes": [], "supplierScopes": [], "logs": [ { "id": 88, "action": 3, "beforeStatus": 0, "afterStatus": 20, "content": "立即生效", "operatorName": "沈浩", "createTime": "2026-09-18 10:00:00" }, { "id": 87, "action": 1, "beforeStatus": 0, "afterStatus": 0, "content": "创建模板", "operatorName": "沈浩", "createTime": "2026-09-18 10:00:00" } ], "其余字段": "同详情" } } ``` ### POST /peasantCoupon/template/update · 编辑模板 请求体与新建相同,另需 id。已结束/已作废不可编辑。草稿/待生效:全量字段可改,按新建规则完整校验并重写范围明细;待生效模板服务端强制按 saveMode=2 校验(startTime/endTime 必填),且若把 startTime 改到当前时间之前则直接进入进行中;草稿传 saveMode=2 等价于排期生效。进行中/已暂停:只接受 name、usageDesc、totalCount、endTime 四个字段(其余字段忽略),校验 name 非空、totalCount>=已发放数、endTime>当前时间、固定有效期时 endTime 不早于 validStart、endTime 不早于其下未结束活动的最晚结束时间;固定有效期模板改 endTime 会同步改 validEnd(新发的券按新有效期)。更新采用当前状态做乐观条件,状态被定时任务改变会返回“模板状态已变更,请刷新后重试”。字段差异写入操作日志。成功返回模板详情 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 模板 id | | `name` | String | 是 | 券名称(所有状态均可改) | | `usageDesc` | String | 否 | 使用说明(所有状态均可改) | | `totalCount` | Integer | 是 | 发行总量,进行中/已暂停时须>=issuedCount | | `endTime` | String | 否 | 结束时间;进行中/已暂停时必填且>当前时间;草稿/待生效固定有效期时忽略(取 validEnd) | | `saveMode` | Integer | 否 | 草稿时 1-仍为草稿 2-排期生效;待生效时服务端固定按 2 处理;进行中/已暂停忽略 | | `(其余字段)` | 同新建 | 否 | useType/discountType/thresholdPrice/discountPrice/productScopeType/productIds/supplierScopeType/baseIds/supplierIds/validType/validStart/validEnd/validDays/perSupplierLimit/startTime 仅草稿/待生效生效;因 @Validated 注解,useType、discountType、discountPrice、validType 即使进行中也必须传值(可原样回传) | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | PeasantCouponTemplateDto | 同 /template/detail 返回结构 | #### 可能的失败 msg - ID不能为空 - 优惠券模板不存在 - 已结束/已作废的模板不能编辑 - 模板状态已变更,请刷新后重试 - 券名称不能为空 - 发行总量不能小于已发放数量236 - 结束时间必须晚于当前时间 - 结束时间不得早于有效期开始 2026-09-20T00:00 - 结束时间不得早于其下活动的结束时间 2026-10-15T23:59:59 - (草稿/待生效时)同新建模板的全部校验文案 #### 请求示例 ```json { "id": 12, "name": "秋种满100减20(加量)", "useType": 1, "discountType": 1, "thresholdPrice": 100, "discountPrice": 20, "validType": 1, "totalCount": 2000, "endTime": "2026-10-31 23:59:59", "usageDesc": "仅限农资商城下单使用,不可叠加" } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "id": 12, "name": "秋种满100减20(加量)", "totalCount": 2000, "endTime": "2026-10-31 23:59:59", "validEnd": "2026-10-31 23:59:59", "status": 20, "logs": [ { "id": 90, "action": 2, "beforeStatus": 20, "afterStatus": 20, "content": "名称:秋种满100减20 → 秋种满100减20(加量);发行总量:1000 → 2000;结束时间:2026-10-20T23:59:59 → 2026-10-31T23:59:59", "operatorName": "沈浩", "createTime": "2026-09-18 11:20:05" } ], "其余字段": "同详情" } } ``` ### POST /peasantCoupon/template/pause · 暂停模板 进行中(20) → 已暂停(30),CAS 更新。暂停后供应商端不可领取/发放,已发出的券不受影响 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 模板 id | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | Boolean | true | #### 可能的失败 msg - 优惠券模板不存在 - 仅进行中的模板可以暂停 #### 请求示例 ```json { "id": 12 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": true } ``` ### POST /peasantCoupon/template/resume · 恢复模板 已暂停(30) → 进行中(20)。前置:endTime 必须晚于当前时间(已到期的只能保持暂停或结束) #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 模板 id | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | Boolean | true | #### 可能的失败 msg - 模板已到结束时间,不能恢复 - 优惠券模板不存在 - 仅已暂停的模板可以恢复 #### 请求示例 ```json { "id": 12 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": true } ``` ### POST /peasantCoupon/template/end · 结束模板 待生效/进行中/已暂停 → 已结束(40),不可逆。其下所有待生效/进行中/已暂停的投放活动同步置为已结束(活动日志 action=8,内容“模板结束”)。已发出的券按各自有效期继续可用。草稿不能结束(请用作废) #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 模板 id | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | Boolean | true | #### 可能的失败 msg - 优惠券模板不存在 - 当前状态不能结束 #### 请求示例 ```json { "id": 12 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": true } ``` ### POST /peasantCoupon/template/void · 作废模板 草稿/待生效/进行中/已暂停 → 已作废(50),需填写原因。前置:该模板未产生任何券实例(instanceCount=0 且 issuedCount=0,CAS 同时校验以防并发领取);已有券的请用“结束”。其下未结束活动同步置为已结束(日志内容“模板作废”)。前端建议:detail.instanceCount>0 时隐藏作废按钮 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 模板 id | | `reason` | String | 是 | 作废原因,非空且<=255 字 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | Boolean | true | #### 可能的失败 msg - reason:作废原因不能为空 - reason:作废原因不能超过255字 - 已产生券实例的模板不能作废,请使用结束 - 优惠券模板不存在 - 当前状态不能作废或已产生券实例 #### 请求示例 ```json { "id": 13, "reason": "配置错误,重新建券" } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": true } ``` ### POST /peasantCoupon/template/logList · 模板操作日志 分页查询某模板的全部操作日志(bizType=1),按 id 倒序。详情里只带最近 20 条,需要看全部时用本接口 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 模板 id | | `index` | Integer | 否 | 页码,默认 1 | | `size` | Integer | 否 | 页大小,默认 10 | | `searchCount` | Boolean | 否 | 是否查总数,默认 true | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `index/size/searchCount/total/other` | 分页信息 | 同 pageEnvelope | | `records` | Array | 日志列表,字段见 commonModels.XmPeasantCouponOpLog | #### 可能的失败 msg - id:ID不能为空 #### 请求示例 ```json { "id": 12, "index": 1, "size": 20 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "index": 1, "size": 20, "searchCount": true, "total": 3, "records": [ { "id": 90, "bizType": 1, "bizId": 12, "action": 2, "beforeStatus": 20, "afterStatus": 20, "content": "发行总量:1000 → 2000", "reason": "", "orderId": null, "operatorType": 1, "operatorId": 3, "operatorName": "沈浩", "createTime": "2026-09-18 11:20:05" }, { "id": 88, "bizType": 1, "bizId": 12, "action": 3, "beforeStatus": 0, "afterStatus": 20, "content": "立即生效", "reason": "", "orderId": null, "operatorType": 1, "operatorId": 3, "operatorName": "沈浩", "createTime": "2026-09-18 10:00:00" }, { "id": 87, "bizType": 1, "bizId": 12, "action": 1, "beforeStatus": 0, "afterStatus": 0, "content": "创建模板", "reason": "", "orderId": null, "operatorType": 1, "operatorId": 3, "operatorName": "沈浩", "createTime": "2026-09-18 10:00:00" } ], "other": null } } ``` ### POST /peasantCoupon/template/options · 模板下拉 活动新建/编辑表单的模板选择器数据:只返回通用券(useType=1)且状态为待生效(10)/进行中(20)的模板,id 倒序,不分页。无请求体(可传 {})。前端可用 totalCount-issuedCount 显示剩余可发、用 startTime/endTime 限制活动时间选择范围、用 supplierScopeType 提示投放对象会与模板适用供应商取交集 #### 请求参数 无 #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | Array | 模板选项列表 | | `data.id` | Long | 模板 id(活动 templateId) | | `data.templateNo` | String | 券编号 | | `data.name` | String | 券名称 | | `data.ruleDesc` | String | 优惠规则文案 | | `data.validDesc` | String | 有效期文案 | | `data.status` | Integer | 10-待生效 20-进行中(活动只能在模板进行中时开始) | | `data.startTime` | String | 模板生效时间,活动开始时间不能早于它 | | `data.endTime` | String | 模板结束时间,活动结束时间不能晚于它 | | `data.totalCount` | Integer | 发行总量 | | `data.issuedCount` | Integer | 已发放 | | `data.perSupplierLimit` | Integer | 模板每家上限,0-不限;定向发放每家张数受其约束 | | `data.supplierScopeType` | Integer | 模板适用供应商 0-全部 1-指定基地 2-指定供应商 | #### 请求示例 ```json {} ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": [ { "id": 12, "templateNo": "NZQ20260918305127", "name": "秋种满100减20", "ruleDesc": "满100减20", "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59", "status": 20, "startTime": "2026-09-18 10:00:00", "endTime": "2026-10-20 23:59:59", "totalCount": 2000, "issuedCount": 236, "perSupplierLimit": 2, "supplierScopeType": 1 }, { "id": 11, "templateNo": "NZQ20260915880213", "name": "新客无门槛10元", "ruleDesc": "无门槛减10", "validDesc": "领取后30天内有效", "status": 10, "startTime": "2026-09-25 00:00:00", "endTime": "2026-12-31 23:59:59", "totalCount": 500, "issuedCount": 0, "perSupplierLimit": 1, "supplierScopeType": 0 } ] } ``` ## 运营后台 · 投放活动 ### POST /peasantCoupon/activity/list · 活动列表 分页查询投放活动,id 倒序。keyword 对活动名/活动编号/模板名/模板编号模糊匹配;issueType/status/templateId 精确过滤(模板详情页“查看活动”可传 templateId)。列表项带 ruleDesc/validDesc(来自模板),不含范围、效果、记录、日志 【10-10 调整】已作废活动返回 voidReason/voidOperatorName/voidTime(与券包明细字段名一致)。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `index` | Integer | 否 | 页码,默认 1 | | `size` | Integer | 否 | 页大小,默认 10 | | `searchCount` | Boolean | 否 | 是否查总数,默认 true | | `keyword` | String | 否 | 活动名/券名/券编号/活动编号 | | `issueType` | Integer | 否 | 投放方式 1-自主领取 2-定向发放 | | `status` | Integer | 否 | 状态 0/10/20/30/40/50 | | `templateId` | Long | 否 | 按模板过滤 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `index/size/searchCount/total/other` | 分页信息 | 同 pageEnvelope | | `records` | Array | 活动列表 | | `records.id` | Long | 活动 id | | `records.activityNo` | String | 活动编号,TFL+yyyyMMdd+6 位随机数 | | `records.name` | String | 活动名称 | | `records.templateId` | Long | 模板 id | | `records.templateNo` | String | 模板券编号快照 | | `records.templateName` | String | 模板券名称快照 | | `records.issueType` | Integer | 投放方式 1-自主领取 2-定向发放 | | `records.targetType` | Integer | 投放对象 0-全部供应商 1-指定基地 2-指定供应商 | | `records.perSupplierLimit` | Integer | 自主领取=每家限领;定向发放=每家发放张数 | | `records.totalCount` | Integer | 投放总量;定向发放为 有效对象数×每家张数(服务端重算) | | `records.issuedCount` | Integer | 已发放/已领取张数 | | `records.targetCount` | Integer | 有效对象数(定向发放),自主领取为 0 | | `records.startTime` | String | 活动开始时间 | | `records.endTime` | String | 活动结束时间 | | `records.status` | Integer | 0-草稿 10-待生效 20-进行中 30-已暂停 40-已结束 50-已作废(admin 新建直接为 10,不产生草稿) | | `records.issueTime` | String | 定向发放实际执行时间,未执行为 null | | `records.issueFailMsg` | String | 定向发放失败原因,成功/未执行为空串;待生效且非空时应在页面提示。业务性失败(库存不足/无有效投放对象)不会自动重试,需运营编辑活动或再点“立即开始” | | `records.creatorId` | Long | 创建人 id | | `records.creatorName` | String | 创建人昵称 | | `records.dataStatus` | Integer | 固定 0 | | `records.createTime` | String | 创建时间 | | `records.updateTime` | String | 更新时间 | | `records.ruleDesc` | String | 券优惠规则文案(取自模板) | | `records.validDesc` | String | 券有效期文案(取自模板) | | `records.baseScopes` | Array | 列表中为 null | | `records.supplierScopes` | Array | 列表中为 null | | `records.effect` | CouponActivityEffectDto | 列表中为 null | | `records.recentRecords` | Array | 列表中为 null | | `records.logs` | Array | 列表中为 null | | `voidReason` | String | 【10-10 新增】作废原因,仅已作废(50)有值,其余为空 | | `voidOperatorName` | String | 【10-10 新增】作废操作人,仅已作废有值 | | `voidTime` | String | 【10-10 新增】作废时间 yyyy-MM-dd HH:mm:ss,仅已作废有值 | #### 请求示例 ```json { "index": 1, "size": 10, "issueType": 2, "status": 20 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "index": 1, "size": 10, "searchCount": true, "total": 1, "records": [ { "id": 31, "activityNo": "TFL20260918417702", "name": "斗南基地秋种定向发券", "templateId": 12, "templateNo": "NZQ20260918305127", "templateName": "秋种满100减20", "issueType": 2, "targetType": 1, "perSupplierLimit": 2, "totalCount": 236, "issuedCount": 236, "targetCount": 118, "startTime": "2026-09-18 12:00:00", "endTime": "2026-10-20 23:59:59", "status": 20, "issueTime": "2026-09-18 12:00:03", "issueFailMsg": "", "creatorId": 3, "creatorName": "沈浩", "dataStatus": 0, "createTime": "2026-09-18 11:50:00", "updateTime": "2026-09-18 12:00:03", "ruleDesc": "满100减20", "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59", "baseScopes": null, "supplierScopes": null, "effect": null, "recentRecords": null, "logs": null } ], "other": null } } ``` ### POST /peasantCoupon/activity/detail · 活动详情 活动基础信息 + 投放范围(baseScopes/supplierScopes)+ 效果指标 effect + 最近领取与核销记录 recentRecords(最近 10 条领取/发放 ∪ 最近 10 条核销,按时间倒序取 10)+ 最近 20 条操作日志。新建/编辑成功后同样返回本结构。效果口径:issuedCount 等来自券实例表按状态聚合;订单指标来自已支付订单(order_status∈1,2,3,5 且 pay_time 非空),整单退款(5)计入 orderSupplierCount 但不计入 orderCount/成本/实付 【10-10 调整】已作废活动返回 voidReason/voidOperatorName/voidTime(与券包明细字段名一致)。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 活动 id | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `(活动基础字段)` | PeasantCouponActivityDto | 与 /activity/list records 项相同 | | `baseScopes` | Array | targetType=1 的指定基地列表,其他为空数组 | | `supplierScopes` | Array | targetType=2 的指定供应商列表,其他为空数组 | | `effect` | CouponActivityEffectDto | 效果指标 | | `effect.issuedCount` | Long | 已发放/领取张数(券实例总数) | | `effect.totalCount` | Integer | 投放总量 | | `effect.issueRate` | String(BigDecimal) | 投放达成率 % = issuedCount/totalCount×100,两位小数,如 "100.00" | | `effect.supplierCount` | Long | 覆盖供应商数(去重) | | `effect.usedCount` | Long | 已核销张数(status=30) | | `effect.useRate` | String(BigDecimal) | 核销转化率 % = usedCount/issuedCount×100 | | `effect.usableCount` | Long | 可使用张数(10) | | `effect.lockedCount` | Long | 已锁定张数(20) | | `effect.expiredCount` | Long | 已过期张数(40) | | `effect.voidCount` | Long | 已作废张数(50) | | `effect.couponCost` | String(BigDecimal) | 估算优惠成本 = Σ关联订单 coupon_price(剔除整单退款) | | `effect.paidAmount` | String(BigDecimal) | 关联订单实付金额合计 | | `effect.driveRatio` | String(BigDecimal)\|null | 消费带动比 = paidAmount/couponCost,成本为 0 时 null,前端显示“—” | | `effect.avgOrderAmount` | String(BigDecimal) | 关联订单客单价 = paidAmount/orderCount,无订单为 0 | | `effect.orderCount` | Long | 关联订单数(剔除整单退款) | | `effect.orderCoverRate` | String(BigDecimal) | 订单关联覆盖率 % = 下单供应商数/覆盖供应商数×100 | | `recentRecords` | Array | 最近领取与核销 10 条 | | `recentRecords.instanceId` | Long | 券实例 id,可跳转 /instance/detail | | `recentRecords.couponCode` | String | 券码 | | `recentRecords.supplierName` | String | 供应商名 | | `recentRecords.recordType` | Integer | 1-领取/发放 2-核销 | | `recentRecords.status` | Integer | 券当前状态 10/20/30/40/50 | | `recentRecords.time` | String | 领取/发放时间(券创建时间)或核销时间 | | `recentRecords.orderId` | String | 锁定/核销订单号,未使用为 null | | `logs` | Array | 最近 20 条操作日志(bizType=2) | | `voidReason` | String | 【10-10 新增】作废原因,仅已作废(50)有值,其余为空 | | `voidOperatorName` | String | 【10-10 新增】作废操作人,仅已作废有值 | | `voidTime` | String | 【10-10 新增】作废时间 yyyy-MM-dd HH:mm:ss,仅已作废有值 | #### 可能的失败 msg - 投放活动不存在 #### 请求示例 ```json { "id": 31 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "id": 31, "activityNo": "TFL20260918417702", "name": "斗南基地秋种定向发券", "templateId": 12, "templateNo": "NZQ20260918305127", "templateName": "秋种满100减20", "issueType": 2, "targetType": 1, "perSupplierLimit": 2, "totalCount": 236, "issuedCount": 236, "targetCount": 118, "startTime": "2026-09-18 12:00:00", "endTime": "2026-10-20 23:59:59", "status": 20, "issueTime": "2026-09-18 12:00:03", "issueFailMsg": "", "creatorId": 3, "creatorName": "沈浩", "dataStatus": 0, "createTime": "2026-09-18 11:50:00", "updateTime": "2026-09-18 12:00:03", "ruleDesc": "满100减20", "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59", "baseScopes": [ { "targetId": 5, "targetName": "斗南基地" } ], "supplierScopes": [], "effect": { "issuedCount": 236, "totalCount": 236, "issueRate": "100.00", "supplierCount": 118, "usedCount": 41, "useRate": "17.37", "usableCount": 180, "lockedCount": 3, "expiredCount": 0, "voidCount": 12, "couponCost": "820.00", "paidAmount": "9865.50", "driveRatio": "12.03", "avgOrderAmount": "246.64", "orderCount": 40, "orderCoverRate": "28.81" }, "recentRecords": [ { "instanceId": 5120, "couponCode": "NZ2609180000512", "supplierName": "云南丰花种植", "recordType": 2, "status": 30, "time": "2026-09-18 15:32:10", "orderId": "PO2609181532001" }, { "instanceId": 5236, "couponCode": "NZ2609180000628", "supplierName": "晋宁兴农合作社", "recordType": 1, "status": 10, "time": "2026-09-18 12:00:03", "orderId": null } ], "logs": [ { "id": 102, "bizType": 2, "bizId": 31, "action": 3, "beforeStatus": 10, "afterStatus": 20, "content": "立即开始", "reason": "", "orderId": null, "operatorType": 1, "operatorId": 3, "operatorName": "沈浩", "createTime": "2026-09-18 12:00:00" }, { "id": 101, "bizType": 2, "bizId": 31, "action": 1, "beforeStatus": 0, "afterStatus": 10, "content": "创建活动", "reason": "", "orderId": null, "operatorType": 1, "operatorId": 3, "operatorName": "沈浩", "createTime": "2026-09-18 11:50:00" } ] } } ``` ### POST /peasantCoupon/activity/targetCount · 有效对象数 定向发放表单联动接口:按当前选择实时计算有效对象数与投放总量(=对象数×每家张数),表单里模板/投放对象/基地/供应商/每家张数变化时调用。口径:审核通过的种植户/供应商/包花厂 ∩ 活动投放范围 ∩ 模板适用供应商范围 ∩ 未达模板每家上限(模板 perSupplierLimit>0 时,已获得数+本次张数 <= 上限)∩ 未在该活动获得过(编辑时传 activityId 排除)。自主领取表单不需要调用 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `templateId` | Long | 是 | 模板 id | | `targetType` | Integer | 否 | 0-全部供应商(默认) 1-指定基地 2-指定供应商 | | `baseIds` | Array | 否 | targetType=1 时的基地 id 列表(为空则对象数为 0) | | `supplierIds` | Array | 否 | targetType=2 时的供应商 id 列表(为空则对象数为 0) | | `perSupplierLimit` | Integer | 否 | 每家发放张数,默认 1,<1 按 1 计 | | `activityId` | Long | 否 | 编辑已有活动时传,排除该活动已发放过的对象 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `targetCount` | Integer | 有效对象数 | | `totalCount` | Integer | 投放总量 = targetCount × 每家张数,用于表单只读回显 | #### 可能的失败 msg - templateId:请选择优惠券模板 - 优惠券模板不存在 #### 请求示例 ```json { "templateId": 12, "targetType": 1, "baseIds": [ 5 ], "perSupplierLimit": 2 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "targetCount": 118, "totalCount": 236 } } ``` ### POST /peasantCoupon/activity/save · 新建活动(10-10 调整:支持草稿) 创建投放活动。【10-10 调整】saveMode=1 存为草稿(0):不会自动开始、供应商端不可见,需编辑时传 saveMode=2 或调 /activity/enable 启用;saveMode=2 存为待生效(10)。校验顺序:模板存在 → 模板为通用券 → 模板状态待生效/进行中 → 名称非空<=64字 → issueType∈{1,2} → targetType=1 须传 baseIds、=2 须传 supplierIds → perSupplierLimit>=1 → startTime=模板 startTime → endTime<=模板 endTime → endTime>当前时间 → 自主领取 totalCount>=perSupplierLimit。定向发放的 totalCount 忽略前端值,服务端按有效对象数×每家张数重算并写 targetCount。saveMode=2 在保存事务提交后:开始时间在未来则保持待生效(10)、startTime 不变,到点由定时任务自动开始;开始时间已到则自动调用“立即开始”,成功返回进行中的详情,失败返回 code=1、msg=“活动已保存为待生效,但启用失败:<原因>”,此时活动已存在(前端应刷新列表而非重复提交)。活动编号 TFL+日期+6 位随机 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `name` | String | 是 | 活动名称 <=64 字 | | `templateId` | Long | 是 | 模板 id,取自 /template/options | | `issueType` | Integer | 是 | 1-自主领取 2-定向发放 | | `targetType` | Integer | 否 | 0-全部供应商(默认) 1-指定基地 2-指定供应商 | | `baseIds` | Array | 否 | targetType=1 必填 | | `supplierIds` | Array | 否 | targetType=2 必填 | | `perSupplierLimit` | Integer | 是 | 自主领取=每家限领;定向发放=每家发放张数;>=1 | | `totalCount` | Integer | 否 | 投放总量,自主领取必填且>=perSupplierLimit;定向发放忽略(服务端重算) | | `startTime` | String | 是 | 活动开始 yyyy-MM-dd HH:mm:ss,不早于模板生效时间(逻辑校验必填) | | `endTime` | String | 是 | 活动结束,不晚于模板结束时间且晚于当前时间(逻辑校验必填) | | `saveMode` | Integer | 否 | 1-仅保存草稿(默认,状态 0,不会自动开始) 2-保存并启用(开始时间未到为待生效 10,已到立即开始 20) | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | PeasantCouponActivityDto | 同 /activity/detail 返回结构 | #### 可能的失败 msg - name:活动名称不能为空 - templateId:请选择优惠券模板 - issueType:投放方式不能为空 - perSupplierLimit:每家张数不能为空 - 优惠券模板不存在 - 只能选择通用券模板 - 只能选择待生效或进行中的模板 - 活动名称不能为空且不超过64字 - 投放方式不正确 - 请选择投放基地 - 请选择投放供应商 - 每家张数必须大于0 - 活动开始时间必须早于结束时间 - 活动开始时间不能早于模板生效时间 2026-09-18T10:00 - 活动结束时间不能晚于模板结束时间 2026-10-20T23:59:59 - 活动结束时间必须晚于当前时间 - 投放总量不能小于每家限领 - 活动已保存为待生效,但启用失败:优惠券模板尚未生效 - 活动已保存为待生效,但启用失败:发放失败 - 活动已保存为待生效,但启用失败:发放调用异常 #### 请求示例 ```json { "name": "斗南基地秋种定向发券", "templateId": 12, "issueType": 2, "targetType": 1, "baseIds": [ 5 ], "perSupplierLimit": 2, "startTime": "2026-09-18 12:00:00", "endTime": "2026-10-20 23:59:59", "saveMode": 2 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "id": 31, "activityNo": "TFL20260918417702", "name": "斗南基地秋种定向发券", "status": 20, "issueType": 2, "targetType": 1, "perSupplierLimit": 2, "targetCount": 118, "totalCount": 236, "issuedCount": 236, "issueTime": "2026-09-18 12:00:03", "issueFailMsg": "", "baseScopes": [ { "targetId": 5, "targetName": "斗南基地" } ], "effect": { "issuedCount": 236, "totalCount": 236, "issueRate": "100.00", "supplierCount": 118, "usedCount": 0, "useRate": "0.00", "usableCount": 236, "lockedCount": 0, "expiredCount": 0, "voidCount": 0, "couponCost": "0", "paidAmount": "0", "driveRatio": null, "avgOrderAmount": "0", "orderCount": 0, "orderCoverRate": "0.00" }, "其余字段": "同详情" } } ``` ### POST /peasantCoupon/activity/update · 编辑活动 请求体同新建,另需 id。已结束/已作废不可编辑。草稿/待生效:全量字段可改(可换模板),按新建规则完整校验,重写范围明细,定向发放重算 targetCount/totalCount;更新以当前状态为乐观条件,状态被定时任务改变返回“活动状态已变更,请刷新后重试”。进行中/已暂停:只接受 name、endTime、(自主领取时)totalCount,校验 name 非空、endTime>当前时间且<=模板 endTime、totalCount>=issuedCount;定向发放进行中不能改总量。【10-10 调整】草稿编辑时传 saveMode=2 则保存后启用(效果同 /activity/enable),传 1 或不传仍为草稿;非草稿状态忽略 saveMode。字段差异写日志。成功返回详情。【09-23 调整】待生效活动编辑成功会清空 issueFailMsg,已到开始时间的定向发放活动会在下一分钟由定时任务自动重发一次 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 活动 id | | `name` | String | 是 | 活动名称 | | `templateId` | Long | 是 | 模板 id(@Validated 必传;进行中/已暂停时忽略,原样回传) | | `issueType` | Integer | 是 | 投放方式(@Validated 必传;进行中/已暂停时忽略) | | `perSupplierLimit` | Integer | 是 | 每家张数(@Validated 必传;进行中/已暂停时忽略) | | `endTime` | String | 是 | 结束时间,进行中/已暂停时必填且>当前时间、<=模板结束时间 | | `totalCount` | Integer | 否 | 自主领取的投放总量,进行中/已暂停时须>=issuedCount | | `targetType/baseIds/supplierIds/startTime` | 同新建 | 否 | 仅草稿/待生效生效 | | `saveMode` | Integer | 否 | 仅草稿生效:2-保存并启用;1/不传-仍为草稿 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | PeasantCouponActivityDto | 同 /activity/detail 返回结构 | #### 可能的失败 msg - ID不能为空 - 投放活动不存在 - 已结束/已作废的活动不能编辑 - 活动状态已变更,请刷新后重试 - 活动名称不能为空 - 结束时间必须晚于当前时间 - 结束时间不能晚于模板结束时间 2026-10-20T23:59:59 - 投放总量不能小于已发放数量120 - (草稿/待生效时)同新建活动的全部校验文案 #### 请求示例 ```json { "id": 32, "name": "全场自主领取(延期)", "templateId": 12, "issueType": 1, "targetType": 0, "perSupplierLimit": 1, "totalCount": 800, "startTime": "2026-09-19 00:00:00", "endTime": "2026-10-15 23:59:59" } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "id": 32, "name": "全场自主领取(延期)", "totalCount": 800, "endTime": "2026-10-15 23:59:59", "status": 20, "logs": [ { "id": 110, "action": 2, "beforeStatus": 20, "afterStatus": 20, "content": "名称:全场自主领取 → 全场自主领取(延期);结束时间:2026-10-10T23:59:59 → 2026-10-15T23:59:59;投放总量:500 → 800", "operatorName": "沈浩", "createTime": "2026-09-18 14:02:11" } ], "其余字段": "同详情" } } ``` ### POST /peasantCoupon/activity/enable · 启用草稿(10-10 新增) 草稿(0) → 待生效(10)。前置:活动为草稿;活动 endTime>当前时间;所属模板为待生效或进行中。启用后:开始时间在未来则停在待生效,到点由定时任务自动开始;开始时间已到则立即执行“立即开始”(startTime 改为当前时间,定向发放同步发券,可能耗时数秒,前端加 loading),开始失败返回 code=1 msg=“活动已保存为待生效,但启用失败:<原因>”,此时活动已是待生效。成功返回活动详情,按返回的 status 刷新行。列表操作建议:草稿显示“启用/编辑/作废”,待生效显示“立即开始/编辑/结束/作废”。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 活动 id | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | PeasantCouponActivityDto | 同 /activity/detail 返回结构 | #### 可能的失败 msg - 投放活动不存在 - 仅草稿状态的活动可以启用 - 活动结束时间已过,请先修改活动时间 - 优惠券模板不是待生效或进行中,不能启用 - 活动状态已变更,请刷新 - 活动已保存为待生效,但启用失败:<原因> #### 请求示例 ```json { "id": 20 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "id": 20, "status": 10, "startTime": "2026-10-10 20:00:00" } } ``` ### POST /peasantCoupon/activity/start · 立即开始 待生效(10) → 进行中(20),并把 startTime 改为当前时间。前置:活动待生效;所属模板必须是进行中(20)(待生效模板需先到生效时间或编辑生效时间);活动 endTime>当前时间。自主领取:直接进行中,供应商端开始可领。定向发放:置进行中后桥调 supplier 侧 /peasant/coupon/activity/issue 同步发券(可能耗时数秒,前端加 loading);发放失败时活动回到待生效并写 issueFailMsg,接口返回 code=1 msg=失败原因,前端提示后刷新详情可见 issueFailMsg。成功时 data 为 true(自主领取)或 supplier 返回的数据。【09-23 调整】发放失败后的自动重试:库存不足、无有效投放对象等业务性失败不再自动重试,活动停在待生效并保留 issueFailMsg,需运营处理后再次点“立即开始”,或编辑活动(编辑会清空 issueFailMsg,定时任务下一分钟自动重发一次);只改模板总量不会清活动的 issueFailMsg。仅“发放异常…”这类系统错误会每 10 分钟自动重试 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 活动 id | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | Boolean\|Object | 自主领取 true;定向发放为 supplier 发放结果数据(前端可忽略,重新拉详情) | #### 可能的失败 msg - 投放活动不存在 - 仅待生效的活动可以开始 - 优惠券模板尚未生效 - 活动结束时间已过 - 活动状态已变更,请刷新 - 发放失败 - 发放调用异常 - (supplier 侧返回的具体发放失败原因,如 无有效发放对象 / 模板剩余数量不足) #### 请求示例 ```json { "id": 31 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": true } ``` ### POST /peasantCoupon/activity/pause · 暂停活动 进行中(20) → 已暂停(30)。自主领取暂停后供应商端不可领取;定向发放已发完的暂停无实际影响 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 活动 id | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | Boolean | true | #### 可能的失败 msg - 投放活动不存在 - 仅进行中的活动可以暂停 #### 请求示例 ```json { "id": 32 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": true } ``` ### POST /peasantCoupon/activity/resume · 恢复活动 已暂停(30) → 进行中(20)。前置:活动 endTime 晚于当前时间 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 活动 id | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | Boolean | true | #### 可能的失败 msg - 活动已到结束时间,不能恢复 - 投放活动不存在 - 仅已暂停的活动可以恢复 #### 请求示例 ```json { "id": 32 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": true } ``` ### POST /peasantCoupon/activity/end · 结束活动 待生效/进行中/已暂停 → 已结束(40),不可逆;已发出的券不受影响 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 活动 id | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | Boolean | true | #### 可能的失败 msg - 投放活动不存在 - 当前状态不能结束 #### 请求示例 ```json { "id": 32 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": true } ``` ### POST /peasantCoupon/activity/void · 作废活动 草稿/待生效/进行中/已暂停 → 已作废(50),需原因。前置:issuedCount=0(从未发放/领取过,CAS 同时校验防并发领取);已有发放的请用“结束”。前端建议:issuedCount>0 时隐藏作废按钮 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 活动 id | | `reason` | String | 是 | 作废原因,非空且<=255 字 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | Boolean | true | #### 可能的失败 msg - reason:作废原因不能为空 - reason:作废原因不能超过255字 - 已发放的活动不能作废,请使用结束 - 投放活动不存在 - 当前状态不能作废或已有发放 #### 请求示例 ```json { "id": 33, "reason": "投放对象选错" } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": true } ``` ### POST /peasantCoupon/activity/logList · 活动操作日志 分页查询某活动的全部操作日志(bizType=2),id 倒序 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 活动 id | | `index` | Integer | 否 | 页码,默认 1 | | `size` | Integer | 否 | 页大小,默认 10 | | `searchCount` | Boolean | 否 | 是否查总数,默认 true | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `index/size/searchCount/total/other` | 分页信息 | 同 pageEnvelope | | `records` | Array | 日志列表,字段见 commonModels.XmPeasantCouponOpLog | #### 可能的失败 msg - id:ID不能为空 #### 请求示例 ```json { "id": 31, "index": 1, "size": 20 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "index": 1, "size": 20, "searchCount": true, "total": 2, "records": [ { "id": 102, "bizType": 2, "bizId": 31, "action": 3, "beforeStatus": 10, "afterStatus": 20, "content": "立即开始", "reason": "", "orderId": null, "operatorType": 1, "operatorId": 3, "operatorName": "沈浩", "createTime": "2026-09-18 12:00:00" }, { "id": 101, "bizType": 2, "bizId": 31, "action": 1, "beforeStatus": 0, "afterStatus": 10, "content": "创建活动", "reason": "", "orderId": null, "operatorType": 1, "operatorId": 3, "operatorName": "沈浩", "createTime": "2026-09-18 11:50:00" } ], "other": null } } ``` ## 运营后台 · 券包与核销明细 ### POST /peasantCoupon/instance/list · 券包列表 分页查询券实例(供应商券包),id 倒序。keyword:券码/券名称/供应商名模糊匹配,来源单号/订单号精确匹配;sourceType/status/templateId/activityId/supplierId 精确过滤。活动页“已发放 N 张”跳转时传 activityId,模板页跳转传 templateId。列表项 logs 为 null #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `index` | Integer | 否 | 页码,默认 1 | | `size` | Integer | 否 | 页大小,默认 10 | | `searchCount` | Boolean | 否 | 是否查总数,默认 true | | `keyword` | String | 否 | 券码/券名称/供应商名(模糊)、来源单号/订单号(精确) | | `sourceType` | Integer | 否 | 获取方式 1-自主领取 2-定向发放 3-积分兑换 | | `status` | Integer | 否 | 券状态 10-可使用 20-已锁定 30-已使用 40-已过期 50-已作废 | | `templateId` | Long | 否 | 按模板过滤 | | `activityId` | Long | 否 | 按活动过滤 | | `supplierId` | Long | 否 | 按供应商过滤 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `index/size/searchCount/total/other` | 分页信息 | 同 pageEnvelope | | `records` | Array | 券实例列表 | | `records.id` | Long | 券实例 id | | `records.couponCode` | String | 券码 | | `records.templateId` | Long | 模板 id | | `records.templateNo` | String | 券编号 | | `records.couponName` | String | 券名称快照 | | `records.useType` | Integer | 1-通用券 2-积分商城专用券 | | `records.discountType` | Integer | 1-满减 2-无门槛 | | `records.thresholdPrice` | String(BigDecimal) | 使用门槛 | | `records.discountPrice` | String(BigDecimal) | 优惠金额 | | `records.productScopeType` | Integer | 适用商品 0-全场 1-指定商品 | | `records.supplierId` | Long | 持有供应商 id | | `records.supplierName` | String | 供应商名 | | `records.baseName` | String | 供应商所属基地名 | | `records.sourceType` | Integer | 获取方式 1-自主领取 2-定向发放 3-积分兑换 | | `records.activityId` | Long | 来源活动 id,积分兑换为 null/0 | | `records.sourceNo` | String | 来源单号(活动编号或积分兑换单号) | | `records.status` | Integer | 10-可使用 20-已锁定 30-已使用 40-已过期 50-已作废 | | `records.validStart` | String | 本券有效期开始 | | `records.validEnd` | String | 本券有效期结束 | | `records.orderId` | String | 当前锁定/核销订单号 | | `records.lockTime` | String | 锁定时间 | | `records.useTime` | String | 核销时间 | | `records.voidReason` | String | 作废原因 | | `records.voidOperatorId` | Long | 作废操作人 id | | `records.voidOperatorName` | String | 作废操作人 | | `records.voidTime` | String | 作废时间 | | `records.dataStatus` | Integer | 固定 0 | | `records.createTime` | String | 领取/发放时间 | | `records.updateTime` | String | 更新时间 | | `records.ruleDesc` | String | 优惠规则文案 | | `records.validDesc` | String | 有效期文案“2026.09.20 00:00 至 2026.10.20 23:59” | | `records.logs` | Array | 列表中为 null | #### 请求示例 ```json { "index": 1, "size": 10, "activityId": 31, "status": 30 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "index": 1, "size": 10, "searchCount": true, "total": 41, "records": [ { "id": 5120, "couponCode": "NZ2609180000512", "templateId": 12, "templateNo": "NZQ20260918305127", "couponName": "秋种满100减20", "useType": 1, "discountType": 1, "thresholdPrice": "100.00", "discountPrice": "20.00", "productScopeType": 0, "supplierId": 2088, "supplierName": "云南丰花种植", "baseName": "斗南基地", "sourceType": 2, "activityId": 31, "sourceNo": "TFL20260918417702", "status": 30, "validStart": "2026-09-20 00:00:00", "validEnd": "2026-10-20 23:59:59", "orderId": "PO2609181532001", "lockTime": "2026-09-18 15:30:02", "useTime": "2026-09-18 15:32:10", "voidReason": null, "voidOperatorId": null, "voidOperatorName": null, "voidTime": null, "dataStatus": 0, "createTime": "2026-09-18 12:00:03", "updateTime": "2026-09-18 15:32:10", "ruleDesc": "满100减20", "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59", "logs": null } ], "other": null } } ``` ### POST /peasantCoupon/instance/detail · 券详情 单张券的全部字段 + 该券的全部业务历史 logs(bizType=3,不限条数,id 倒序),包括供应商端领取、下单锁定、核销、退款解锁、过期、运营作废等,operatorType 区分系统/平台/供应商,orderId 为相关订单号(supplier 侧写入的日志 action/content 以 supplier-core 为准,示例仅示意) #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 券实例 id | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `(券实例字段)` | PeasantCouponInstanceDto | 与 /instance/list records 项相同 | | `logs` | Array | 全部业务历史,字段见 commonModels.XmPeasantCouponOpLog | #### 可能的失败 msg - 优惠券不存在 #### 请求示例 ```json { "id": 5120 } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "id": 5120, "couponCode": "NZ2609180000512", "couponName": "秋种满100减20", "supplierName": "云南丰花种植", "status": 30, "orderId": "PO2609181532001", "useTime": "2026-09-18 15:32:10", "ruleDesc": "满100减20", "validDesc": "2026.09.20 00:00 至 2026.10.20 23:59", "logs": [ { "id": 3302, "bizType": 3, "bizId": 5120, "action": 0, "beforeStatus": 20, "afterStatus": 30, "content": "支付核销", "reason": "", "orderId": "PO2609181532001", "operatorType": 0, "operatorId": 0, "operatorName": "", "createTime": "2026-09-18 15:32:10" }, { "id": 3290, "bizType": 3, "bizId": 5120, "action": 0, "beforeStatus": 10, "afterStatus": 20, "content": "下单锁定", "reason": "", "orderId": "PO2609181532001", "operatorType": 2, "operatorId": 2088, "operatorName": "云南丰花种植", "createTime": "2026-09-18 15:30:02" }, { "id": 3001, "bizType": 3, "bizId": 5120, "action": 1, "beforeStatus": 0, "afterStatus": 10, "content": "定向发放", "reason": "", "orderId": null, "operatorType": 1, "operatorId": 3, "operatorName": "沈浩", "createTime": "2026-09-18 12:00:03" } ], "其余字段": "同列表项" } } ``` ### POST /peasantCoupon/instance/void · 作废券 可使用(10) → 已作废(50),CAS 更新,需填写原因;记录作废人/时间/原因并写券日志(action=9,内容“人工作废”)。已锁定/已使用/已过期/已作废的券不可作废。前端建议只在 status=10 时显示作废按钮 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 券实例 id | | `reason` | String | 是 | 作废原因,非空且<=255 字 | #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `data` | Boolean | true | #### 可能的失败 msg - reason:作废原因不能为空 - reason:作废原因不能超过255字 - 优惠券不存在 - 仅可作废可使用状态的优惠券 #### 请求示例 ```json { "id": 5236, "reason": "供应商账号异常,收回优惠券" } ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": true } ``` ### POST /peasantCoupon/overview · 顶部概览 农资优惠券页面顶部指标卡,全局统计(不分时间段)。无请求体(可传 {})。口径:templateCount=未删除模板总数(含各状态);runningActivityCount=进行中(20)活动数;instanceCount=券实例总数;usedCount=已使用(30)券数;useRate=usedCount/instanceCount×100;couponCost=已支付未整单退款订单(order_status∈1,2,3)的 coupon_price 合计 #### 请求参数 无 #### 响应 | 字段 | 类型 | 说明 | |----|----|----| | `templateCount` | Long | 优惠券(模板)总数 | | `runningActivityCount` | Long | 有效(进行中)活动数 | | `instanceCount` | Long | 已进入券包的券总数 | | `usedCount` | Long | 累计核销张数 | | `useRate` | String(BigDecimal) | 核销率 %,两位小数 | | `couponCost` | String(BigDecimal) | 平台优惠成本(元) | #### 请求示例 ```json {} ``` #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "templateCount": 6, "runningActivityCount": 2, "instanceCount": 1320, "usedCount": 187, "useRate": "14.17", "couponCost": "3560.00" } } ``` --- # 打包页待补 / 早班 / 设箱 > 货架待补状态徽标、早班红底、设箱不符角标的渲染配色与三 tab 筛选映射,含早班跨午夜判断函数 - 类型:前端对接 - 分类:仓库配货 / 打包 - 端:打包小程序 - 后端服务:supplier - 更新:2026-08-19 - 网页版:https://doc.xfun.fun/shelf-restock-api 供应商端 · 打包师傅页面 ### POST /package/shelf/page · 货架打包状态分页 状态:已上测试环境(`supplier/test`)。改动仅新增字段与筛选,原有字段与行为不变。 ## 三种视觉状态一览 同一张货架卡片可叠加多种状态。下面是实际配色的样例,颜色/透明度已按需求锁定,前端直接取用。 | 状态 | 驱动字段 | 视觉 | |----|----|----| | 普通货架 | `restockStatus` 为 0 / 2 | 默认卡片 | | 待补 | `restockStatus === 1` | 卡片正上方居中「待补」橙色徽标 | | 早班 | `earlyShiftFlag === true`(交货时间 ≥22:00 或 ≤01:00) | 整卡红底 | | 设箱不符 | `boxMismatchFlag === 1` | 右上角「不符」红徽标 | | 叠加 | 以上任意组合 | 早班红底 + 待补徽标 + 不符徽标可同时出现 | > [!NOTE] > **颜色值(需求锁定,请勿改)** > > **待补徽标**:底色 `#FF6A00`,字体 `#FFFFFF`,位于卡片正上方居中。 > > **早班背景**:整卡背景 `#FF000F` 透明度 **70%**(即 `rgba(255,0,15,.7)`)。 > > **设箱不符**:沿用现有「不符」红徽标(右上角),由 `boxMismatchFlag` 驱动,本次未改。 ## 响应新增字段(ShelfPackageStatusVo) | 字段 | 类型 | 说明 | |----|----|----| | `restockStatus` **新增** | int | **待补状态**。`0` 无待补 · `1` 存在待补货(→ 展示「待补」徽标)· `2` 补货已完成。徽标只在 `=== 1` 时显示。 | | `earlyShiftFlag` **新增** | boolean | **是否早班**。后端已按口径算好(`deliveryTime >= 22:00` 或 `<= 01:00`),`true` 直接给整卡染红,前端无需自行判断时间,见「早班口径」。 | | `deliveryTime` | string HH:mm:ss | **交货时间**(HH:mm:ss,已含提前 30 分钟)。仅展示用,早班判断请直接取 `earlyShiftFlag`。 | | `boxMismatchFlag` | int | 货位下是否存在设箱不符包裹。`0` 否 · `1` 是(→「不符」徽标)。既有字段。 | | `shelfSn` / `zoneShelf` | int / string | 货架号 / 区-货架,卡片主标题。既有字段。 | | `selfPickup` | int | 是否自提 `1`/ `0`。自提货架单独 tab。既有字段。 | | `preparedInfo` | object | 配货进度 `{ total, prepared, progress }`。既有字段。 | ## 请求新增筛选参数(ShelfPackageStatusPageQo) | 参数 | 类型 | 说明 | |----|----|----| | `restockFilter` **新增** | int | **只看待补**。`null`/ `0` 不限 · `1` 只看待补(后端等价 `restockStatus = 1`)。 | | `onlyEarlyShift` **新增** | boolean | **只看早班**。`true` 只看早班(`deliveryTime >= 22:00` 或 `<= 01:00`);`null`/ `false` 不限。 | | `boxMatchFilter` | int | 设箱筛选。`null`/ `0` 全部 · `1` 设箱合规 · `2` 设箱不符。既有参数。 | | `date` **必填** | string | 场次日期,如 `2026-08-14`。必填。 | | `status` **必填** | int | tab 状态:`0` 可打包/未打包 · `1` 打包完成 · `2` 出库完成 · `3` 自提。必填。 | | `userId` / `userName` **必填** | long / string | 操作人。`current` / `size` 分页。必填。 | ## 早班口径 —— 后端已算好 > [!NOTE] > **前端直接用 `earlyShiftFlag`,不用自己算时间** > > 后端口径:**`deliveryTime >= 22:00` 或 `deliveryTime <= 01:00`(跨零点,含边界)**即早班,已在服务端算入 `earlyShiftFlag`,前端 **直接取布尔值染红**即可。与配货页 `/zone/info/new` 的 `earlyShiftFlag` 是同一口径。 > > 「只看早班」 `onlyEarlyShift` 用的也是同一口径,与卡片染红天然一致;前端不要再按 `deliveryTime` 自行判断,避免阈值分叉。 ```text // 渲染时: // 早班背景 -> row.earlyShiftFlag === true 背景 rgba(255,0,15,.7) // 待补徽标 -> row.restockStatus === 1 底 #FF6A00 / 字 #FFFFFF // 不符徽标 -> row.boxMismatchFlag === 1 ``` ## 三个 Tab 的筛选项映射(需求第 4 点) | Tab | status | 只看待补 | 只看早班 | 设箱合规/不符 | |--------------|--------|-----------------|----------------|--------------------| | **可打包** | 0 | restockFilter=1 | onlyEarlyShift | 无 | | **打包完成** | 1 | restockFilter=1 | onlyEarlyShift | boxMatchFilter=1/2 | | **自提货架** | 3 | restockFilter=1 | 无 | boxMatchFilter=1/2 | 各筛选项可自由组合,后端按 AND 叠加。「只看待补」在所有 tab 都有;「只看早班」仅可打包 / 打包完成两个 tab;「设箱」仅打包完成 / 自提两个 tab(沿用既有 `boxMatchFilter`)。 ## 关于「退货已补货显示绿色」(需求第 1 点) 需求第 1 点(标记退货的已补货花材显示为绿色)属于 **质检配货页面的花材级**展示,不在本打包货架接口范围内,其所需字段由该页面自身接口返回,本次 **不涉及**。本页只覆盖打包页面的第 2、3、4 点。 接口 `POST /package/shelf/page` · 供应商端 · 测试分支 `supplier/test`。字段口径以后端返回为准;`earlyShiftFlag`/ `restockStatus` 均已由服务端算好,前端直接取用。 --- # 供应商入驻协议授权 > 入驻申请页协议弹窗:协议列表 / 注册回传协议版本、712 错误码处理;运营后台供应商详情【协议授权记录】及协议版本正文查看 - 类型:前端对接 - 分类:供应商 - 端:供应商小程序 / App / 运营后台 - 后端服务:supplier / admin - 状态:已上测 - 需求单:XYLZ-3569 - 更新:2026-10-17 - 网页版:https://doc.xfun.fun/supplier-agreement-auth-api ## 总览 | 端 | 接口 | 变更 | 鉴权 | |----|----|----|----| | 供应商端 xm-bulky-supplier | `POST /login/agreement/list` | **新增** 入驻协议 + 隐私政策当前版本及正文 | 免登录 | | 供应商端 xm-bulky-supplier | `POST /login/register` | `入参新增` `agreementVersionIds` | 免登录(同原接口) | | 运营后台 xm-bulky-admin | `POST /supplier/agreementAuth/list` | **新增** 供应商详情右上角【协议授权记录】,返回入驻申请时的授权信息 | 需登录(同现有后台接口) | | 运营后台 xm-bulky-admin | `POST /supplier/agreementAuth/versionContent` | **新增** 查看某次授权对应的协议版本正文 | 需登录 | - 所有接口均为 `POST` + JSON,返回统一结构 `{ "code": 0, "msg": "success", "data": … }`,`code != 0` 时 `msg` 可直接 toast。 - 时间字段格式统一为 `yyyy-MM-dd HH:mm:ss`。 - 协议正文仍由运营后台「协议管理」维护(供应商端:入驻协议、隐私政策)。**版本由后端按正文自动生成**,运营改了正文,`versionId` 就会变。 - 「我已阅读并同意」不调接口,注册时直接提交版本 id。 ## 入驻申请页前端流程 进入入驻申请页 → ① `/login/agreement/list` 拉协议,自动弹窗 → 3 秒倒计时后点「我已阅读并同意」(不调接口,记下全部 versionId) → 勾选复选框,第一步「下一步」 → ② `/login/register` 带上 agreementVersionIds #### 纯前端交互(后端无接口) - **进入页面**:调 ① 后自动弹窗,底部复选框默认未勾选。弹窗标题「请阅读并同意以下协议」,副标题「请仔细阅读以下协议,确认同意后继续入驻。」;tab 按 ① 返回的数组顺序(平台入驻协议在前、隐私政策在后),tab 文字直接用 `name`(不带书名号);正文区可上下滚动,底部提示「协议内容可上下滚动查看」。 - **倒计时**:按钮显示「我已阅读并同意(3s)」,倒计时期间置灰不可点;结束后变为「我已阅读并同意」。每次打开弹窗都从 3 秒重新开始。 - **我已阅读并同意**:关闭弹窗、自动勾选复选框,在页面内存里记下 ① 返回的 **全部** `versionId`(不调接口)。 - **× / 暂不同意**:关闭弹窗,复选框保持未勾选;用户 **可以继续填写入驻资料**,不锁表单、不跳转。 - **点击未勾选的复选框**:不能直接勾上,重新打开协议弹窗,倒计时重新开始,只能通过「我已阅读并同意」完成勾选。 - **点击协议名称**(复选框后的《平台入驻协议》《隐私政策》链接,书名号由前端拼接):打开弹窗并切到对应 tab 查看正文,直接用 ① 已拿到的 `content`,不用再调接口。 - **「下一步」按钮**:放在页面底部右侧(与复选框同一行),使用缩小后的蓝色按钮;**始终可点击**,不因未勾选或资料不全而置灰,点击后按下表反馈。 | 当前情况 | 点击「下一步」后的处理 | |----|----| | 未同意协议 | toast「请先阅读并同意平台入驻协议和隐私政策」,打开协议弹窗(倒计时重新开始) | | 已同意,资料未填写完整或格式错误 | 提示具体问题(如「请填写卖家名称」),并 **滚动定位到第一个错误字段**(按表单从上到下的顺序取第一个) | | 已同意,资料校验通过 | 在页面内保留当前资料,进入第二步「提交申请」(第一步不调后端,第二步提交时才调 ②) | - **手动取消勾选**:恢复未同意状态并 **丢弃记下的 versionId**;再次勾选或点「下一步」都要重新走弹窗(重新调 ① 拿最新版本)。 - **第二步返回第一步**:保留已填写资料、勾选状态和 versionId,不重复弹窗。 - **① 返回空数组**(运营关闭了两份协议):不弹窗、不校验勾选,注册时 `agreementVersionIds` 不传或传 `[]`。 - **收到 712**:见接口 `POST /login/register` 的失败处理。 ## 供应商端接口 ### POST /login/agreement/list **新增** 入驻协议列表。进入入驻申请页时调用;也在 ② 注册返回 712 时重新调用刷新。 #### 请求参数 无,传 `{}` 即可。 #### 响应 data(数组) | 字段 | 类型 | 说明 | |----|----|----| | `agreementId` | Long | 协议 id | | `versionId` | Long | 协议版本快照 id,**② 注册时放进 `agreementVersionIds` 回传** | | `version` | Integer | 版本号(同一协议从 1 递增,可选展示) | | `type` | Integer | 4 = 平台入驻协议;2 = 隐私政策 | | `name` | String | 协议名称,不带书名号,如「平台入驻协议」;直接作 tab 标题,复选框后的链接文字由前端加书名号 | | `content` | String | 协议正文(富文本 HTML,与现有接口 `/home/agreement/content` 格式一致) | #### 响应示例 ```json { "code": 0, "msg": "success", "data": [ { "agreementId": 13, "versionId": 1, "version": 1, "type": 4, "name": "平台入驻协议", "content": "

…

" }, { "agreementId": 15, "versionId": 2, "version": 1, "type": 2, "name": "隐私政策", "content": "

…

" } ] } ``` ### POST /login/register **入参新增** 提交入驻申请(原接口,第二步「提交申请」时调用),原有字段不变,新增一个字段。 #### 新增请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `agreementVersionIds` | Long\[\] | 否 | ① 返回的 **全部** `versionId`(顺序无所谓,最多 10 个)。**传了**:须与当前版本集合完全一致(缺、多、过期都不行),否则返回 712。**不传 / `null` / `[]`**:后端按当前协议版本写授权记录,等同于传了 ① 此刻返回的全部 versionId;版本快照尚不存在时按 ① 同样规则生成;运营关闭了全部协议时不写记录。新版入驻页用户同意后应传,以便协议在填写期间更新时能拦截(712) | ```json { "...原有字段...": "...", "agreementVersionIds": [1, 2] } ``` #### 失败处理 - `code = 712`「协议内容已更新,请重新阅读并同意」:传的版本与当前版本不一致(漏传 / 多传 / 传了旧版本 / 用户停留期间运营改了协议)。**只要传了就会校验**。 - 收到 712 时前端依次: 1. toast 该文案; 2. 从第二步回到第一步,**保留已填写资料**(含第二步已填内容); 3. 取消勾选,丢弃记下的 versionId; 4. 重新调 ① 刷新正文,自动打开弹窗,倒计时从 3 秒重新开始; 5. 用户同意后再次提交 ②,带新的 versionId(验证码未被消耗,可沿用;若已过期按原逻辑重新获取)。 - 712 在校验短信验证码 **之前**返回,不会消耗验证码次数、不会生成供应商。 > [!NOTE] > 注册成功后,后端以 **入驻时间**作为授权同意时间写入授权记录,同时保存协议版本、IP 和 deviceType(不记录弹窗点击时间)。 ## 错误码速查(供应商端) | code | msg | 接口 | 前端处理 | |----|----|----|----| | `712` | 协议内容已更新,请重新阅读并同意 | `/login/register` | toast + 回到第一步(保留资料)+ 取消勾选 + 重新调 ① 刷新并弹窗,倒计时重来 | ## 运营后台接口 ### POST /supplier/agreementAuth/list **新增** 供应商详情页右上角【协议授权记录】按钮,点击打开右侧抽屉「协议授权信息」。返回 **单个对象**:供应商入驻申请时授权的平台入驻协议和隐私政策。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |------|------|------|-----------| | `id` | Long | 是 | 供应商 id | #### 响应 data(对象,可能为 null) | 字段 | 类型 | 说明 | |----|----|----| | `authStatus` | String | 授权状态,固定「已授权」,直接展示为绿色标签 | | `authTime` | String | 授权时间 = 入驻(注册)时间;历史已入驻供应商为补录记录的授权时间 | | `authStage` | String | 授权环节,固定「入驻申请」(含历史已入驻供应商的补录记录) | | `agreements` | Array | 授权协议:平台入驻协议在前、隐私政策在后 | |   `agreementId` | Long | 协议 id | |   `agreementType` | Integer | 4 = 入驻协议;2 = 隐私政策 | |   `agreementName` | String | 协议名称,不带书名号,直接作卡片标题 | |   `versionId` | Long | 协议版本快照 id;**非空时卡片右侧显示「查看协议 \>」**,点击调 [versionContent](#supplier-agreementauth-versioncontent) | |   `version` | Integer | 版本号(可不展示) | #### 响应示例 ```json { "code": 0, "msg": "success", "data": { "authStatus": "已授权", "authTime": "2026-10-16 09:15:32", "authStage": "入驻申请", "agreements": [ { "agreementId": 13, "agreementType": 4, "agreementName": "平台入驻协议", "versionId": 1, "version": 1 }, { "agreementId": 15, "agreementType": 2, "agreementName": "隐私政策", "versionId": 2, "version": 1 } ] } } ``` 无授权记录时: ```json { "code": 0, "msg": "success", "data": null } ``` #### 抽屉布局(对照原型图) - 标题「协议授权信息」,右上角 ×,底部「关闭」按钮;标题下方「供应商:*供应商名称*」(名称取详情页已有数据)。 - 「授权信息」表格三行:授权状态(`authStatus`,绿点 + 绿色标签)、授权时间(`authTime`)、授权环节(`authStage`)。 - 「授权协议」:`agreements` 每项一张卡片,左侧文件图标 + `agreementName`,右侧「查看协议 \>」(`versionId` 非空时显示)。 - 卡片下方灰字「点击查看授权时对应版本的协议正文。」 - `data` 为 null(补录脚本执行前、或协议关闭期间注册的供应商):显示空状态「暂无授权记录」。 #### 失败处理 - 供应商不存在:`code != 0`,msg「信息不存在」。 ### POST /supplier/agreementAuth/versionContent **新增** 点击协议卡片的「查看协议」,展示该次授权对应版本的协议正文(快照,不随运营后续修改变化),可用弹窗或新抽屉展示,标题用 `name`。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------------------------| | `id` | Long | 是 | 协议卡片的 `versionId` | #### 响应 data | 字段 | 类型 | 说明 | |---------------|---------|----------------------------------| | `versionId` | Long | 版本快照 id | | `agreementId` | Long | 协议 id | | `name` | String | 协议名称,不带书名号 | | `version` | Integer | 版本号 | | `content` | String | 正文(富文本 HTML) | | `createTime` | String | 该版本生成时间(即首次下发时间) | ```json { "code": 0, "msg": "success", "data": { "versionId": 1, "agreementId": 13, "name": "平台入驻协议", "version": 1, "content": "

…

", "createTime": "2026-10-20 09:58:11" } } ``` ## 联调注意 - 注册失败(如验证码错误)重试时继续带同一组 `agreementVersionIds` 即可;返回 712 时才需要重新拉 ①。 - 现有接口 `/home/agreement/content` 不受影响,其他页面可以继续使用。 - 历史已入驻供应商由补录脚本按入驻时间各补一条入驻协议、隐私政策的授权记录,挂当前协议版本,运营端显示为「入驻申请」,同样可「查看协议」。补录脚本在代码部署后执行,执行前列表为空。 - 登录页(手机号登录)上的协议勾选不在本次范围内,不产生授权记录。 --- # 供应商申诉调整 > 首页质检问题(缺货+退货+换货)、质检汇总换货扎数与场次卡「已确认」口径及跳转、二级类目编码、状态标签、商品全称、客服只展示 ID、申诉凭证选填 - 类型:前端对接 - 分类:供应商 - 端:供应商小程序 - 后端服务:supplier - 状态:已上测 - 测试分支:supplier/test - 需求单:XYLZ-3640 - 更新:2026-10-29 - 网页版:https://doc.xfun.fun/supplier-appeal-adjust-api 完整接口字段见 [供应商申诉优化 · 前端对接文档](/supplier-punish-center-api);本页集中说明本次涉及的字段、口径与展示规则。 ## 总览 | 接口 | 页面 | 本页涉及的字段 / 规则 | |----|----|----| | `POST /home/supplierPunishCount` | 首页「本月履约情况 —— 质检问题汇总 N 扎」 | `qualityIssueNum / qualityLackNum / qualityReturnNum / qualityExchangeNum` | | `POST /home/qualityIssue/summary` | 质检问题汇总 | 汇总卡 / 场次卡 / 商品行三级 `exchangeNum`;`lackNum / returnNum` 类型集合;`rounds[].punishCount / punishAmount`;`items[].skuDesc / productFullName`;商品行排序;`roundOptions` | | `POST /punish/center/summary` | 我的处罚统计、二级类目卡、「全部类型」下拉 | 二级类目编码表、`categories[].subCategories[]`、`typeOptions` | | `POST /punish/center/list` | 我的处罚 - 处罚明细 | `statusTag / statusTagName`、`skuDesc / productFullName`、`title / typeName`、`reason`(处罚描述)、`latestComplain.operatorId` | | `POST /punish/center/detail` | 处罚详情 | 列表字段 + `operatorId`(下发人)、`complainHistory[].operatorId`(处理人)、`sourceDesc` | | `POST /comment/complain` | 申诉提交 | 凭证 `picUrl` 选填(图片 / 视频),`reason` 必填 | - 全部 `POST` + JSON,返回 `{ "code": 0, "msg": "success", "data": … }`;`code != 0` 时 `msg` 直接 toast。 - 时间格式 `yyyy-MM-dd HH:mm:ss`;金额为保留 2 位小数的数字;扎数为整数。 - 接口路径与入参签名与现有对接文档一致,前端只需按本页处理字段与展示。 ## 首页质检问题 ### POST /home/supplierPunishCount 首页「本月履约情况 —— 质检问题汇总 N 扎」 **读 `qualityIssueNum`**。下列四个字段固定统计 **当月**,与请求中的 `startRoundNo / endRoundNo` 无关;接口其余字段(`stockOut`、`salesReturnNum` 等)口径不同,首页卡片不要用。 #### 请求 ```text // 首页已调用的请求,原样即可 { "startRoundNo": "2026-10-27", "endRoundNo": "2026-10-27" } ``` #### 响应 data(本页相关字段) | 字段 | 类型 | 说明 | |----|----|----| | `qualityIssueNum` | int | 当月质检问题扎数 = `qualityLackNum + qualityReturnNum + qualityExchangeNum`(缺货 + 退货 + 换货)。等于 `/home/qualityIssue/summary` 按月(`statType=1`、不传月份)的 `lackNum + returnNum + exchangeNum` | | `qualityLackNum` | int | 当月缺货扎数,处罚类型 **1 缺货处罚、6 缺货补货、9 缺货不补** 的处罚数量之和;= 汇总页当月 `lackNum` | | `qualityReturnNum` | int | 当月退货扎数,处罚类型 **7、8、11、12** 的处罚数量之和;= 汇总页当月 `returnNum` | | `qualityExchangeNum` | int | 当月换货扎数,取 **换货记录**的换货要求扎数(质检扫码换货 + 运营批量换货,含已完成与未完成,扣除已取消数量);= 汇总页当月 `exchangeNum` | ```text // 响应 data(仅列出本页相关字段) { "stockOut": 3, "...": "...", "qualityIssueNum": 13, "qualityLackNum": 7, "qualityReturnNum": 5, "qualityExchangeNum": 1 } ``` > [!WARNING] > **null 处理**:四个字段同时计算,计算失败时 **四个字段都为 null**,接口仍返回成功、其他字段正常。前端「质检问题汇总」显示「--」(或隐藏数字),入口仍可点击进入汇总页;不要把 null 当 0 展示。 ## 质检问题汇总 ### POST /home/qualityIssue/summary #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `statType` | int | 否 | 1 按月(默认)/ 2 按场次 | | `month` | string | 否 | `2026-10`,不传 = 当月;传了 `roundNo` 时取场次所在月 | | `roundNo` | string | 否 | 按月:「场次质检汇总」下拉选中值,`rounds` 只含该场次,汇总仍为整月;按场次:所选场次,不传取当月最近场次 | #### 扎数口径(三级一致) | 扎数 | 字段 | 口径 | 对应「我的处罚」二级类目 | |----|----|----|----| | 售卖 | `saleNum` | 入场明细售卖扎数 | — | | 缺货 | `lackNum` | 处罚类型 **1、6、9** 的处罚数量 | 11 缺货 | | 退货 | `returnNum` | 处罚类型 **7、8、11、12** 的处罚数量 | 12 退货 | | 换货 | `exchangeNum` | 换货记录的换货要求扎数(质检扫码换货 + 运营批量换货,含已完成与未完成,扣除已取消) | 15 换货配货 | - 汇总卡、场次卡红标、商品行三级都返回 `exchangeNum`,上级 = 下级之和。 - 缺货 / 退货的类型集合与「我的处罚」二级类目 11 / 12 相同。换货未换(类型 25)只计入换货扎数,不计入退货;类型 1「缺货处罚」包含人工下发记录,归缺货并计入缺货扎数。 - 扎数是问题花材数量,包含处罚金额为 0 的记录;二级类目统计的是处罚条数 / 金额,两者度量不同。 #### 响应 data | 字段 | 类型 | 说明 | |----|----|----| | `statType / month / roundNo` | int / string | 实际统计方式、月份、按场次时实际场次(按月时 `roundNo` 为 null) | | `roundCount` | int | 统计范围内场次数 | | `saleNum / lackNum / returnNum / exchangeNum` | int | 汇总卡「售卖 / 缺货 / 退货 / 换货」4 列 | | `roundOptions[]` | string\[\] | 所选月份内有售卖、缺货、退货 **或换货**的场次(倒序),只有换货的场次也在内;场次下拉用 | | `rounds[]` | array | 场次卡(倒序),见下表 | #### rounds\[\] 场次卡 | 字段 | 类型 | 说明 | |----|----|----| | `roundNo` | string | 场次 | | `productKinds / saleNum` | int | 共 X 种商品 / 售卖 Y 扎 | | `lackNum / returnNum / exchangeNum` | int | 红标「缺货 a 扎 · 退货 b 扎 · 换货 c 扎」 | | `punishCount` | int | 该场次 **质检处罚**(category=1)条数,**含待扣款**;等于跳转后 `/punish/center/list {statType:2, roundNo, category:1}` 的 `total` | | `punishAmount` | number | 该场次质检处罚 **已确认金额**:已扣款 / 已退回项的实际处罚(`actualAmount`)之和,**不含待扣款** | | `items[]` | array | 商品行,全部返回由前端折叠(默认 3 行,「展开更多(n)」n = `items.length - 3`) | 卡片底部文案:**「查看处罚明细 —— 共 `punishCount` 条处罚 已确认 · ¥ `punishAmount` \>」**。 #### rounds\[\].items\[\] 商品行 | 字段 | 类型 | 说明 | |----|----|----| | `productId / productName / rankNum` | long / string / int | 商品 ID、商品名、等级(1~5 = A~E,6 = O);行按 `(productId, rankNum)` 区分 | | `skuDesc` | string | 规格属性描述,如「卷边」「不卷边」;无属性时为空串,见 [商品全称](#商品全称-skudesc-productfullname) | | `productFullName` | string | 商品全称,如「高原红A级卷边」,商品列直接展示 | | `saleNum / lackNum / returnNum / exchangeNum` | int | 商品表「售卖 / 缺货 / 退货 / 换货」列 | 商品行排序:售卖扎数倒序 → 换货扎数倒序 → 退货扎数倒序 → 缺货扎数倒序。前端按返回顺序展示即可。 #### 示例 ```text // 请求:按月 { "statType": 1, "month": "2026-10" } // 请求:按场次 { "statType": 2, "roundNo": "2026-10-21" } // 响应 data(按月) { "statType": 1, "month": "2026-10", "roundNo": null, "roundCount": 2, "saleNum": 86, "lackNum": 7, "returnNum": 5, "exchangeNum": 1, "roundOptions": ["2026-10-21", "2026-10-14"], "rounds": [{ "roundNo": "2026-10-21", "productKinds": 6, "saleNum": 52, "lackNum": 7, "returnNum": 5, "exchangeNum": 1, "punishCount": 5, "punishAmount": 120.00, "items": [ { "productId": 399968, "productName": "高原红", "rankNum": 1, "skuDesc": "卷边", "productFullName": "高原红A级卷边", "saleNum": 20, "lackNum": 3, "returnNum": 2, "exchangeNum": 1 }, { "productId": 400017, "productName": "高原红", "rankNum": 1, "skuDesc": "不卷边", "productFullName": "高原红A级不卷边", "saleNum": 20, "lackNum": 4, "returnNum": 3, "exchangeNum": 0 }, { "productId": 401203, "productName": "卡罗拉", "rankNum": 6, "skuDesc": "", "productFullName": "卡罗拉", "saleNum": 12, "lackNum": 0, "returnNum": 0, "exchangeNum": 0 } ] }] } ``` ## 场次卡「查看处罚明细」跳转 质检问题汇总 · 场次卡 → 我的处罚(按场次,定位「质检处罚」Tab) | 跳转后调用 | 入参 | |----|----| | `POST /punish/center/summary` | `{ "statType": 2, "roundNo": "2026-10-21" }` | | `POST /punish/center/list` | `{ "statType": 2, "roundNo": "2026-10-21", "category": 1, "pageIndex": 1, "pageSize": 20 }` | - 页面切到「按场次」、场次下拉选中 `roundNo`、Tab 定位到「质检处罚」(`category=1`)。 - 列表 `total` = 场次卡 `punishCount`;列表中已扣款 / 已退回项的 `actualAmount` 之和 = 场次卡 `punishAmount`。 ## 我的处罚二级类目编码 缺货/退货扎数按类目类型集合统计,只统计质检环节处罚(punish_link=1),人工下发、代卖等其他环节的处罚不计入;条件为 pid=0、data_status=0、handle_status IN (0,1)。换货扎数独立取 exchange_log,19、22 不计扎数。 质检类型(1、6、7、8、9、11、12、13、15)只有质检环节(`punish_link=1`)才归入下表的质检类目,其他环节的同类型处罚归「其他处罚 · 34」,详见 [供应商申诉优化 · 前端对接](/supplier-punish-center-api)。 二级类目 `subCategory` 的 **有效编码仅以下 9 个**,按下表顺序下发(`summary.categories[].subCategories[]` 与 `typeOptions` 均按此顺序): | category | subCategory | 三级类型(sourceType / punishType:typeName) | 卡片标题 title | |----|----|----|----| | 1 质检处罚 | 11 缺货 | 1/1 缺货处罚、1/6 缺货补货、1/9 缺货不补 | 缺货处罚 | | 1 质检处罚 | 12 退货 | 1/7 退货不补、1/8 退货补货、1/11 退货不补转代卖、1/12 退货补货转代卖 | 退货处罚 | | 1 质检处罚 | 14 降级 | 1/13 降1级、1/15 降2级 | 降级处罚 | | 1 质检处罚 | 15 换货配货 | 1/25 换货未换、1/18 换货、1/19 配货错误处罚、1/22 先扫码后配货处罚 | 换货配货处罚 | | 2 迟到处罚 | 21 配货迟到 | 1/14 迟到配货处罚、1/23 代配送货迟到处罚 | 配货迟到处罚 | | 2 迟到处罚 | 22 换货迟到 | 1/20 换货迟到 | 换货迟到处罚 | | 2 迟到处罚 | 23 迟到处罚 | 1/3 迟到处罚 | 迟到处罚 | | 3 其他处罚 | 32 售后处罚 | 2/null 退款支出、3/null 缺货赔付 | 售后处罚 | | 3 其他处罚 | 34 其他处罚 | 1/2 未打勾处罚、1/4 其他处罚、1/16 退款赠花、1/17 超时转代卖,及其他未归类类型 | 其他处罚 | - 二级卡、「全部类型」下拉都按接口下发渲染,**不要写死编码与名称**;列表 / 详情的 `title`(二级名 + 处罚)与 `typeName`(三级名)直接展示。 - 「售后处罚」由后台开关关闭时,`subCategories` 与 `typeOptions` 中都没有 32。 - 筛选回传:点二级卡传 `subCategory`;下拉选三级时原样回传 `sourceType + punishType`。 #### 示例:/punish/center/summary ```text // 请求 { "statType": 1, "month": "2026-10" } // 响应 data(节选) { "statType": 1, "month": "2026-10", "roundNo": null, "roundOptions": ["2026-10-21", "2026-10-14"], "startRoundNo": "2026-10-01", "endRoundNo": "2026-10-31", "actualAmount": 320.00, "totalAmount": 350.00, "appealSuccessAmount": 30.00, "totalCount": 12, "pendingAmount": 15.00, "pendingCount": 1, "categories": [ { "category": 1, "categoryName": "质检处罚", "actualAmount": 240.00, "totalAmount": 260.00, "appealSuccessAmount": 20.00, "totalCount": 8, "subCategories": [ { "subCategory": 11, "subCategoryName": "缺货", "actualAmount": 60.00, "totalAmount": 60.00, "appealSuccessAmount": 0.00, "totalCount": 3 }, { "subCategory": 12, "subCategoryName": "退货", "actualAmount": 120.00, "totalAmount": 140.00, "appealSuccessAmount": 20.00, "totalCount": 3 }, { "subCategory": 14, "subCategoryName": "降级", "actualAmount": 40.00, "totalAmount": 40.00, "appealSuccessAmount": 0.00, "totalCount": 1 }, { "subCategory": 15, "subCategoryName": "换货配货", "actualAmount": 20.00, "totalAmount": 20.00, "appealSuccessAmount": 0.00, "totalCount": 1 } ] }, { "category": 2, "categoryName": "迟到处罚", "actualAmount": 50.00, "totalAmount": 50.00, "appealSuccessAmount": 0.00, "totalCount": 2, "subCategories": [ { "subCategory": 21, "subCategoryName": "配货迟到", "actualAmount": 0.00, "totalAmount": 0.00, "appealSuccessAmount": 0.00, "totalCount": 0 }, { "subCategory": 22, "subCategoryName": "换货迟到", "actualAmount": 20.00, "totalAmount": 20.00, "appealSuccessAmount": 0.00, "totalCount": 1 }, { "subCategory": 23, "subCategoryName": "迟到处罚", "actualAmount": 30.00, "totalAmount": 30.00, "appealSuccessAmount": 0.00, "totalCount": 1 } ] }, { "category": 3, "categoryName": "其他处罚", "actualAmount": 30.00, "totalAmount": 40.00, "appealSuccessAmount": 10.00, "totalCount": 2, "subCategories": [ { "subCategory": 32, "subCategoryName": "售后处罚", "actualAmount": 10.00, "totalAmount": 20.00, "appealSuccessAmount": 10.00, "totalCount": 1 }, { "subCategory": 34, "subCategoryName": "其他处罚", "actualAmount": 20.00, "totalAmount": 20.00, "appealSuccessAmount": 0.00, "totalCount": 1 } ] } ], "typeOptions": [ { "category": 1, "categoryName": "质检处罚", "subCategories": [ { "subCategory": 15, "subCategoryName": "换货配货", "types": [ { "sourceType": 1, "punishType": 25, "name": "换货未换" }, { "sourceType": 1, "punishType": 18, "name": "换货" }, { "sourceType": 1, "punishType": 19, "name": "配货错误处罚" }, { "sourceType": 1, "punishType": 22, "name": "先扫码后配货处罚" } ] } ] }, { "category": 2, "categoryName": "迟到处罚", "subCategories": [ { "subCategory": 23, "subCategoryName": "迟到处罚", "types": [ { "sourceType": 1, "punishType": 3, "name": "迟到处罚" } ] } ] } ], "serverTime": "2026-10-27 10:00:00" } ``` ## 处罚明细 ### POST /punish/center/list #### 本页相关的 records\[\] 字段 | 字段 | 类型 | 卡片展示 | |----|----|----| | `title` | string | 卡片标题,二级名 + 处罚,如「换货配货处罚」「迟到处罚」 | | `subCategory / subCategoryName / typeName` | int / string | 二级 / 三级类型,编码见「我的处罚二级类目编码」 | | `statusTag / statusTagName` | int / string | 卡片右上角标签,见 [状态标签](#状态标签-statustag) | | `punishTime` | string | 处罚时间 | | `productFullName` + `num` | string / int | 商品信息:「`productFullName` · `num` 扎」;需要等级标红时用 `productName` + 等级(`rankNum`)+ `skuDesc` 自行拼 | | `reason` | string | 行名显示为 **「处罚描述」**,值取 `reason` | | `orderSn` | string | 关联单号 | | `deductStatus / deductStatusName` | int / string | 扣款状态 | | `latestComplain.operatorId` | long | 最新申诉的处理客服 ID;`latestComplain.operatorName` 恒为 null | #### 示例 ```text // 请求 { "statType": 2, "roundNo": "2026-10-21", "category": 1, "pageIndex": 1, "pageSize": 20 } // 响应 data { "total": 5, "serverTime": "2026-10-27 10:00:00", "records": [ { "itemKey": "1_922652537", "sourceType": 1, "sourceId": 922652537, "category": 1, "categoryName": "质检处罚", "subCategory": 12, "subCategoryName": "退货", "punishType": 7, "typeName": "退货不补", "title": "退货处罚", "roundNo": "2026-10-21", "punishTime": "2026-10-21 17:16:00", "productName": "高原红", "rankNum": 1, "skuDesc": "卷边", "productFullName": "高原红A级卷边", "num": 2, "orderItemUrl": "https://…", "reason": "花头受损,品质不达标", "orderSn": "T2026102110033425", "punishAmount": 45.00, "returnedAmount": 0.00, "actualAmount": 45.00, "deductStatus": 2, "deductStatusName": "已扣款", "appealStatus": 3, "appealStatusName": "申诉驳回", "statusTag": 4, "statusTagName": "申诉拒绝", "complainType": 3, "canAppeal": true, "nextAppealNo": 2, "appealDeadline": "2026-10-23 11:00:00", "canCancelAppeal": false, "latestComplainId": 88123, "latestComplain": { "id": 88123, "appealNo": 1, "reason": "花头受损为运输导致", "pictures": "[]", "status": 3, "operationContent": "质检照片显示到货即受损", "operatorId": 10086, "operatorName": null, "createTime": "2026-10-22 09:30:00", "handleTime": "2026-10-22 11:00:00" } }, { "itemKey": "1_922652601", "sourceType": 1, "sourceId": 922652601, "category": 1, "categoryName": "质检处罚", "subCategory": 15, "subCategoryName": "换货配货", "punishType": 25, "typeName": "换货未换", "title": "换货配货处罚", "roundNo": "2026-10-21", "punishTime": "2026-10-21 22:00:00", "productName": "卡罗拉", "rankNum": 6, "skuDesc": "", "productFullName": "卡罗拉", "num": 1, "reason": "场次换货未换", "orderSn": "T2026102110039876", "punishAmount": 20.00, "returnedAmount": 0.00, "actualAmount": 20.00, "deductStatus": 2, "deductStatusName": "已扣款", "appealStatus": 0, "appealStatusName": "未申诉", "statusTag": 5, "statusTagName": "已确认", "complainType": 3, "canAppeal": false, "nextAppealNo": null, "appealDeadline": "2026-10-23 22:00:00", "canCancelAppeal": false, "latestComplainId": null, "latestComplain": null } ] } ``` ## 处罚详情 ### POST /punish/center/detail 响应 = 列表项全部字段(含 `statusTag / skuDesc / productFullName`)+ 详情字段。本页相关: | 字段 | 类型 | 说明 | |----|----|----| | `operatorId` | long | 下发处罚人 ID(质检处罚为质检员):处罚取处罚记录、退款支出取售后单、缺货赔付取赔付记录的操作人 ID。展示为「客服:`operatorId`」,**0 或 null 显示「系统」** | | `operatorName` | string | 恒为 null,不展示 | | `sourceDesc` | string | 处罚来源,如「质检处罚」「系统自动处罚」「售后处罚」「售后单」「缺货赔付(订单 T…)」,不含任何姓名;为空不展示 | | `reason` | string | 行名「处罚描述」 | | `complainHistory[].operatorId` | long | 每条申诉的处理客服 ID,显示「客服:ID」,0 或 null 显示「系统」;`operatorName` 恒为 null | ```text // 请求 { "sourceType": 1, "id": 922652537 } // 响应 data(节选) { "sourceType": 1, "sourceId": 922652537, "title": "退货处罚", "typeName": "退货不补", "productName": "高原红", "rankNum": 1, "skuDesc": "卷边", "productFullName": "高原红A级卷边", "num": 2, "reason": "花头受损,品质不达标", "deductStatus": 2, "deductStatusName": "已扣款", "statusTag": 4, "statusTagName": "申诉拒绝", "operatorId": 20231, "operatorName": null, "sourceDesc": "质检处罚", "complainHistory": [ { "id": 88123, "appealNo": 1, "reason": "花头受损为运输导致", "pictures": "[]", "status": 3, "operationContent": "质检照片显示到货即受损", "operatorId": 10086, "operatorName": null, "createTime": "2026-10-22 09:30:00", "handleTime": "2026-10-22 11:00:00", "delTime": null } ], "serverTime": "2026-10-27 10:00:00" } ``` ## 状态标签 statusTag 列表、详情都返回 `statusTag / statusTagName`,前端直接展示 `statusTagName`,不用自行组合扣款 / 申诉状态。判断顺序:先判待扣款,再按申诉状态。 | statusTag | statusTagName | 条件 | |----|----|----| | 0 | (空串,不展示标签) | `deductStatus = 1` 待扣款 | | 1 | 待申诉 | `appealStatus = 0 && canAppeal` | | 2 | 申诉中 | `appealStatus = 1` | | 3 | 申诉成功 | `appealStatus = 2` | | 4 | 申诉拒绝 | `appealStatus = 3`;可二诉时同样显示「申诉拒绝」,另显示「再次申诉」按钮与二诉倒计时 | | 5 | 已确认 | `appealStatus = 0 && !canAppeal`(申诉期已过且未申诉) | - 「申诉状态」筛选下拉仍用 `appealStatus`(0 未申诉 / 1 申诉中 / 2 申诉通过 / 3 申诉驳回),「已确认」不作为筛选项。 - 按钮、倒计时规则仍按 `canAppeal / canCancelAppeal / appealDeadline` 判断。 ## 商品全称 skuDesc / productFullName 返回位置:质检问题汇总 `rounds[].items[]`、我的处罚列表 `records[]`、处罚详情。 | 字段 | 规则 | |----|----| | `skuDesc` | 取商品规格属性值,按原顺序以空格连接;规格中的「等级」与包装 JSON 不展示;无属性时为空串 `""` | | `productFullName` | `productName` + 等级名 + `skuDesc`;等级名仅 `rankNum` 1~5 对应 **A级~E级**,其他取值(如 6 = O 级、0、null) **不拼等级字样** | | productName | rankNum | 规格 | skuDesc | productFullName | |-------------|-----------|--------------------|----------|-----------------| | 高原红 | 1 | 生熟度:卷边 | 卷边 | 高原红A级卷边 | | 高原红 | 1 | 生熟度:不卷边 | 不卷边 | 高原红A级不卷边 | | 卡罗拉 | 2 | 生熟度:中熟,等级:B | 中熟 | 卡罗拉B级中熟 | | 卡罗拉 | 6(O 级) | (空) | (空串) | 卡罗拉 | `productName / rankNum` 仍返回,需要把等级标红时可用 `productName` + 等级(标红)+ `skuDesc` 自行拼接。 ## 客服只展示 ID | 位置 | 字段 | 展示 | |----|----|----| | 处罚详情 · 下发处罚人 | `detail.operatorId` | 「客服:`operatorId`」;0 或 null 显示「系统」 | | 处罚列表 · 最新申诉处理人 | `latestComplain.operatorId` | 「客服:`operatorId`」;0 或 null 显示「系统」 | | 处罚详情 · 申诉记录处理人 | `complainHistory[].operatorId` | 「客服:`operatorId`」;0 或 null 显示「系统」 | - `operatorName`(详情、`latestComplain`、`complainHistory[]`) **恒为 null**,前端不要读取。 - `sourceDesc` 不含客服 / 质检员姓名。 - 适用范围:`/punish/center/*` 与 `/home/qualityIssue/summary`。 ## 提交申诉 ### POST /comment/complain | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | long | 是 | 处罚项 `sourceId` | | `complainType` | int | 是 | 处罚项 `complainType`(3 处罚 / 2 退款支出 / 6 缺货赔付) | | `reason` | string | 是 | 申诉理由,≤255 字 | | `picUrl` | string | 否 | 申诉凭证,图片 / 视频地址的 JSON 数组字符串,**选填**;不传、空串或 `[]` 均可提交。前端上传组件不做必填校验 | | `appealDesc` | string | 否 | 诉求说明 | ```text // 请求:不带凭证 { "id": 922652537, "complainType": 3, "reason": "花头受损为运输导致" } // 请求:带图片和视频 { "id": 922652537, "complainType": 3, "reason": "花头受损为运输导致", "picUrl": "[\"https://img.xm.com/a.jpg\",\"https://img.xm.com/b.mp4\"]" } // 响应 { "code": 0, "msg": "success", "data": true } ``` ## 联调与发版注意 - **前后端需同时发版**:二级类目编码、场次卡 `punishCount / punishAmount` 口径、状态标签、客服 ID 展示均依赖本次后端版本。前端若写死二级类目编码会显示异常,统一按接口下发渲染。 - **首页数字口径**:「质检问题汇总 N 扎」= 缺货 + 退货 + 换货;缺货按类型 1、6、9,退货按类型 7、8、11、12,换货按换货记录。首页与质检问题汇总页的数字以此口径为准,**需告知测试与产品**。 - 场次卡「共 N 条处罚」含待扣款,金额「已确认 · ¥X」只算已扣款 / 已退回项,两者口径不同属正常。 - 自测对照(测试环境 supplier 24562,场次 2026-10-21):缺货 7、退货 5、换货 1;两行高原红分别显示「高原红A级卷边」「高原红A级不卷边」;场次卡 `punishCount` = 跳转后列表 `total`。 - 后端只部署 supplier,运营后台无变化。 --- # 供应商申诉筛选 > 运营后台「供应商申诉」页新增订单号筛选(全部申诉 Tab)与售后单号筛选(售后申诉 Tab),列表与导出共用 POST /order/complain/list - 类型:前端对接 - 分类:供应商 - 端:运营后台 - 后端服务:admin - 状态:已上测 - 测试分支:bulky-test/admin - 需求单:XYLZ-3628 - 更新:2026-10-28 - 网页版:https://doc.xfun.fun/supplier-appeal-filter-api ## 总览 运营后台「供应商申诉」页(商品申诉 / 售后申诉 / 处罚申诉 / 农资售后申诉 四个 Tab)的筛选栏新增两个输入框: - **订单号**:四个 Tab 都展示。 - **售后单号**:仅「售后申诉」Tab 展示,按售后 ID 检索,即列表「关联订单/商品/售后」列里的「售后ID」。 只涉及一个接口,列表与导出共用,**出参不变**,只新增两个入参。 | # | 接口 | 用途 | 何时调用 | |----|----|----|----| | ① | `POST /order/complain/list` | 申诉列表 / 导出(`exportFlag=1`) | 进入页面、切换 Tab、点「搜索」「导出」 | ## 通用约定 - 服务:运营后台 admin;需登录 token,未登录返回 `{ "code": 2, "msg": "登录失效,请重新登录~" }`。 - `POST` + JSON,返回 `{ "code": 0, "msg": "success", "data": … }`;`code != 0` 时 `msg` 直接 toast。 - 分页入参 `index`(页码,从 1 开始,不传默认 1)、`size`(每页条数,不传默认 10)、`searchCount`(是否查总数,默认 true,可不传)。 - 分页出参为 `{ index, size, searchCount, total, records, other }`,记录在 `records`,总数在 `total`;`other` 为附加数据,本接口通常为 null。 ## 枚举 #### 申诉类型 complainType 1 商品申诉 · 2 售后申诉(退款支出) · 3 处罚申诉 · 5 农资售后申诉 · 6 缺货赔付申诉 · 4 为历史保留值,不可传 #### 各 Tab 的类型入参 | Tab | 类型入参 | 展示「订单号」输入框 | 展示「售后单号」输入框 | |----|----|----|----| | 商品申诉 | `complainTypeList: [1, 2, 6]` | 是 | 否 | | 售后申诉 | `complainType: 2` | 是 | 是 | | 处罚申诉 | `complainType: 3`(可带 `punishType`) | 是 | 否 | | 农资售后申诉 | `complainType: 5` | 是(填农资订单号) | 否 | ## 接口 ### POST /order/complain/list · 申诉列表 **变更**:新增入参 `orderSn`、`refundId`,其余入参与出参不变。 #### 请求参数(本次新增) | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `orderSn` | String | 否 | 订单号,**精确匹配**,前后空格自动忽略,最长 64 字,超长提示「订单号不能超过64字」。农资售后申诉 Tab 填农资订单号 | | `refundId` | Long | 否 | 售后ID,**精确匹配**,正整数,小于 1 提示「售后ID参数错误」。仅类型 2、5 生效 | 其余已有入参(`index`、`size`、`complainType`、`complainTypeList`、`punishType`、`status`、`startTime`、`endTime`、`supplierName`、`userId`、`wareFlag`、`wareId`、`wareIdList`、`partnerId`、`partnerIdList`、`supplierTag`、`exportFlag`)含义不变。 #### 请求示例 ```json { "index": 1, "size": 10, "complainType": 2, "orderSn": "20261020664485772268", "refundId": 5181760 } ``` #### 响应 出参结构不变;没有符合条件的记录时返回空分页(`total` 为 0,`records` 为空数组),不报错。 #### 响应示例(无匹配) ```json { "code": 0, "msg": "success", "data": { "index": 1, "size": 10, "total": 0, "records": [] } } ``` #### 规则 - `orderSn` 与 `refundId` 同时填写时是「且」关系,两个都满足才返回。 - 两个新条件与已有条件(状态、时间、供应商、渠道、基地等)也是「且」关系。 - `orderSn`:在当前 Tab 的申诉类型范围内,按关联订单号匹配;处罚申诉里没有订单号的处罚(质检、行政处罚等)按订单号查不到,属正常。 - `refundId`:只在售后申诉(2)和农资售后申诉(5)里匹配。在「商品申诉」Tab 传 `refundId` 不会命中评价或缺货赔付申诉,「处罚申诉」Tab 传则返回空列表。前端只在「售后申诉」Tab 传该字段,其他 Tab 不要透传。 - `orderSn` 和 `refundId` 为空字符串或不传时,等同于不筛选。 - 导出(`exportFlag=1`)使用同一组筛选条件,导出结果与列表一致;无匹配时导出空表。 - 时间区间校验照常进行:开始、结束时间不合法时仍返回参数错误,不会因为订单号查不到而变成空列表。 #### 错误码 / 失败文案 - 订单号不能超过64字 - 售后ID参数错误 ## 联调注意 - 测试环境:运营后台 admin 测试服已部署,分支 `bulky-test/admin`。 - 「售后单号」输入框内容用列表「售后ID」列展示的数字即可,例如 `5181760`。 - 订单号请输入完整订单号,不支持模糊匹配。 --- # 供应商上传发票校验 > 上传接口新增 uploadType:图片/PDF 走 OCR 抬头硬校验,二维码取票链接校验税务局域名;含报错文案速查与二维码解析前端职责 - 类型:前端对接 - 分类:开票 - 端:供应商小程序 - 后端服务:supplier - 更新:2026-09-28 - 网页版:https://doc.xfun.fun/supplier-invoice-upload-api ## 总览:改了什么 - 还是原来的上传接口 `POST /invoice/task/upload`,**新增一个入参 `uploadType`**,前端改动很小。 - **图片/PDF 上传**(原流程):后端会对发票做 OCR 识别,**抬头与申请单不一致直接拦截**,报错文案透出给供应商。数电票原生 PDF 也支持,直接传 PDF 的 OSS 地址即可。 - **新增二维码上传**:供应商开完数电票后拿到的「扫码下载发票」二维码,前端解析出二维码里的链接后传给后端,后端校验链接是否为税务局域名。 - 金额、税号、重复上传均 **不拦截**,只拦抬头(产品拍板口径)。 - 「已开票」状态重新上传发票时,同样会过这套校验。 ## 两种上传方式与前端职责 #### 方式一:发票图片 / PDF(uploadType=1,或不传) - 前端把发票文件(jpg/png/PDF)上传 OSS 拿到 URL,作为 `invoiceUrl` 提交 —— 与现状完全一致。 - 后端 OCR 识别票面抬头(购买方名称),与申请单抬头比对,不一致返回报错。 > [!WARNING] > **PDF 注意:** OSS 上传时必须保留 `.pdf` 扩展名(后端按 URL 后缀区分 PDF 与图片识别通道)。如果上传组件会改文件名,确认生成的 URL 以 `.pdf` 结尾。 #### 方式二:发票二维码(uploadType=2) **新增** - 供应商在税务 App 开票成功后有「扫码下载发票」二维码(往往是截图保存)。 - **前端负责解析二维码**拿到其中的链接文本:拍照/相册选图后用二维码解码(小程序可用 `wx.scanCode` 扫码,或选图后走解码库),把解析出来的 **URL 字符串**作为 `invoiceUrl` 提交。 - 后端只校验该链接是不是税务局域名(规则见下),**不做 OCR**。 > [!NOTE] > **常见误区:** uploadType=2 时 `invoiceUrl` 传的是 **二维码内容里解析出的取票链接**(`https://…chinatax.gov.cn/…`),**不是**二维码截图的 OSS 图片地址。传成图片地址会报「取票链接无效」。 ## 接口定义 ### POST /invoice/task/upload · 入参变更 上传发票(原接口)。鉴权、Header 与现状一致。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 开票申请单 ID | | `invoiceUrl` | String | 是 | uploadType=1:发票图片/PDF 的 OSS 地址;uploadType=2:二维码解析出的税务局取票链接 | | `uploadType` | Integer | 否 | **新增** 1 = 发票图片/PDF(默认,不传按 1 处理);2 = 发票二维码取票链接 | #### 请求示例:图片 / PDF ```json { "id": 123, "invoiceUrl": "https://image-bulky.xunmengvip.com/supplier/invoice/dzfp_2611xxxx.pdf", "uploadType": 1 } ``` #### 请求示例:二维码取票链接 ```json { "id": 123, "invoiceUrl": "https://dppt.yunnan.chinatax.gov.cn/invoice-query/invoice-download?xxx", "uploadType": 2 } ``` #### 响应 ```json { "code": 0, "msg": "success", "data": true } ``` 校验不通过时 `code != 0`,`msg` 为面向供应商的完整文案,**直接 toast 展示即可**,无需按 code 分支。 ## 报错文案速查 | msg 文案 | 触发场景 | 前端处理建议 | |----|----|----| | 发票抬头“××”与申请单抬头“××”不一致,请核对后重新开具 | OCR 识别出的购买方名称与申请单抬头不一致(动态文案,含两边抬头) | toast / 弹窗展示,引导重新开票 | | 发票识别失败,请确认图片清晰完整后重试 | 传的图不是发票(如二维码截图误走图片通道)、图片模糊不完整 | toast,引导重新拍摄/上传 | | 发票识别失败,请稍后重试 | OCR 服务临时异常 | toast,可重试同一文件 | | 取票链接无效,请上传税务局开具的取票二维码 | uploadType=2 且链接不是税务局域名(或误传了图片地址) | toast,检查是否解析对了二维码内容 | | 当前状态不支持上传发票 | 申请单状态既非待开票/逾期,也非已完成重传 | toast 并刷新列表 | | 申请单不存在 | id 错误或不属于当前供应商 | toast 并刷新列表 | ## 校验规则细节 #### 抬头比对(uploadType=1) - 比对前做归一化:**忽略所有空白字符、全角括号()与半角括号 () 视为相同**,其余必须完全一致。 - 例:申请单抬头「云南甲花卉(集团)有限公司」,票面「云南甲花卉(集团) 有限公司」→ 通过;票面「云南乙花卉有限公司」→ 拦截。 #### 取票链接域名(uploadType=2) - 必须是 **https**,且 host 为 `chinatax.gov.cn` 或其子域(可带端口)。 - 合法:`https://inv-veri.chinatax.gov.cn/xxx`、`https://dppt.yunnan.chinatax.gov.cn:8443/xxx` - 非法:`https://chinatax.gov.cn.evil.com/x`、`https://evil.com/chinatax.gov.cn`、`http://…`(非 https)、OSS 图片地址 ## 联调注意 - 测试环境已部署本功能;图片 OCR 校验由后端开关控制(`xm.invoice.ocr-check-enabled`),联调前和后端确认测试环境开关已打开。二维码域名校验 **不走开关**,恒生效。 - OCR 校验对真实「电子发票(普通发票) PDF」已实测通过;测试时请用真实票面文件,随便找张图会命中「发票识别失败」。 - 上传方式的选择建议做成两个入口(对应原型「上传发票图片 / 上传发票二维码」),分别固定 uploadType=1 / 2。 --- # 供应商画像 > 运营后台供应商列表/详情右侧抽屉:基础信息与标签编辑、经营概览与履约售后(近7/30天环比)、处罚明细、品类看板及明细、统计口径文案 - 类型:前端对接 - 分类:供应商 - 端:运营后台 - 后端服务:admin - 状态:已上测 - 测试分支:bulky-test/admin - 更新:2026-10-28 - 网页版:https://doc.xfun.fun/supplier-profile-api ## 总览 交互以原型 `供应商画像-V1.1.html` 为准。所有接口在运营后台服务(admin)下,需登录 token。 | \# | 接口 | 用途 | 何时调 | |----|----|----|----| | ① | `POST /supplier/profile/base` | 头部基础信息 + 标签 | 打开抽屉;保存标签后可用 ⑦ 的返回直接刷新 | | ② | `POST /supplier/profile/metrics` | 经营概览 + 履约与售后(**7 天、30 天一次返回**) | 打开抽屉 | | ③ | `POST /supplier/profile/punishPage` | 处罚明细弹窗(分页) | 点「查看明细」/翻页 | | ④ | `POST /supplier/profile/category` | 品类数据(主营/优选/品质优选)Top3、在售主力花材 Top3、增长最快品类 Top3 | 打开抽屉 | | ⑤ | `POST /supplier/profile/categoryPage` | 品类数据明细 / 在售主力花材明细弹窗(排序 + 分页) | 点「查看明细」/切换/排序/翻页 | | ⑥ | `POST /purchaser/userTag/list`(已有) | 系统标签库 | 打开「编辑标签」弹窗 | | ⑦ | `POST /supplier/profile/tagSave` | 保存标签(已有标签 + 新建标签一次提交) | 点「保存标签」 | ## 通用约定 - 全部 `POST` + JSON,返回 `{ "code": 0, "msg": "success", "data": … }`;`code != 0` 时 `msg` 直接 toast。 - **数据截至**:所有统计截至 **昨天**(今天的场次不算),用出参 `statDate` 展示「数据截至 2026.10.25」。 - **统计窗口**:近 N 天 = `[statDate-N+1, statDate]`;上 N 天 = 紧挨着的前 N 天,不重叠。按 **场次日期**统计。 - 统计数据后端按天缓存,当天内重复打开数字不变(标签、处罚明细不缓存)。 - 金额单位 **元**;比率字段单位为 **%**,后端保留 2 位,**前端展示 1 位**;扎数为整数(JSON 数字)。 - 时间格式 `yyyy-MM-dd HH:mm:ss`,日期 `yyyy-MM-dd`。 - 分页出参统一为 `{ index, size, total, records }`;入参用 `pageNum`,`pageSize` 固定 10(可不传)。 ## 加载时序 点「画像」 → 并发 ① base + ② metrics + ④ category → 7/30 天切换:纯前端切 ② 的 `"7"`/ `"30"`,默认近 7 天 「处罚次数 · 查看明细」 → ③ `period` = 当前履约卡片选中的 7/30 「品类数据 · 查看明细」 → ⑤ `viewType` = 当前分段(main/prefer/quality) → 「在售主力花材 · 查看明细」 → ⑤ `viewType=active` 「编辑标签」 → ⑥ 拉标签库(过滤 type=2) → 前端维护草稿 → 「保存标签」调 ⑦ ## 枚举 #### 标签分类 tagCategory(按分类显示不同颜色徽标) | code | 名称 | 原型样式类 | |--------------|----------|------------------------------------------| | `risk` | 风险预警 | `sp-tag-risk` | | `attention` | 需关注 | `sp-tag-attention` | | `action` | 需行动 | `sp-tag-action` | | `profile` | 客户画像 | `sp-tag-profile` | | `preference` | 偏好标签 | `sp-tag-preference` | | `promotion` | 推广属性 | `sp-tag-promotion` | | `other` | 其他 | `sp-tag-other`(未知 code 也按其他显示) | #### 合作状态 cooperateStatus 直接展示 `cooperateStatusName` 即可:0 未认证 / 1 待审核 / 2 正常 / 3 审核不通过 / 4 已清退 / 5 已冻结 / -100 已删除。 #### 品类视图 viewType `main` 主营品类 · `prefer` 优选 · `quality` 品质优选 · `active` 在售主力花材 #### 等级 rankNum 1~6 对应 A/B/C/D/E/O,可拼在商品名后(如「红玫瑰 A级」)。 ## 接口 ### POST /supplier/profile/base · 基础信息与标签 #### 入参 ```json { "supplierId": 10002 } ``` #### 出参 data | 字段 | 类型 | 说明 | |----|----|----| | `statDate` | date | 数据截至日(昨天) | | `supplierId` / `supplierName` | long / string | 供应商 ID / 名称 | | `loginTel` | string | 手机号 | | `cooperateStatus` / `cooperateStatusName` | int / string | 合作状态,见枚举 | | `enterTime` | datetime | 入驻时间(审核通过时间,无则注册时间),展示到日期 | | `lastSellTime` | datetime \| null | 最近动销时间;null 显示「—」 | | `tagList[]` | array | 全部标签(≤ 8 个):`tagId`、`tagName`、`tagCategory` | ```json { "statDate": "2026-10-25", "supplierId": 10002, "supplierName": "云岭花卉合作社", "loginTel": "18756300314", "cooperateStatus": 2, "cooperateStatusName": "正常", "enterTime": "2024-03-18 10:00:00", "lastSellTime": "2026-10-25 14:26:00", "tagList": [ { "supplierId": 10002, "tagId": 1, "tagName": "玫瑰主供", "tagCategory": "profile" } ] } ``` ### POST /supplier/profile/metrics · 经营概览 + 履约与售后 #### 入参 ```json { "supplierId": 10002 } ``` #### 出参结构 `business`、`service` 都是以 **"7"**、**"30"**(字符串)为 key 的对象。带环比的指标统一为 **指标对象**: | 指标对象字段 | 说明 | |----|----| | `current` | 近 N 天值 | | `previous` | 上 N 天值(展示「上N天xx」) | | `delta` | 环比。**数量/金额类**:百分比(%);**比率类**:百分点差。`previous=0` 的数量/金额类为 `null`,显示「—」 | #### business\["7" \| "30"\](经营概览) | 字段 | 页面文案 | 单位 | delta 类型 | |--------------|---------------|------|------------| | `sellDays` | 近N天动销天数 | 天 | % | | `sellNum` | 近N天销售扎数 | 扎 | % | | `sellAmount` | 近N天销售金额 | 元 | % | #### service\["7" \| "30"\](履约与售后) | 字段 | 页面文案 | 类型 | 说明 | |----|----|----|----| | `afterSaleAmount` | 近N天售后金额 | 指标对象 | 元;delta 为 % | | `afterSaleRate` | 近N天售后率 | 指标对象 | %;delta 为 **百分点**(「↑ 0.3 个百分点」) | | `afterSaleNum` / `afterSaleCount` | 售后率下方「· 1,459 扎 / 86 单」 | number | 近 N 天售后扎数 / 售后单数(无环比) | | `lackRate` | 近N天缺货率 | 指标对象 | %;delta 为百分点 | | `lowerRankRate` | 近N天降级率 | 指标对象 | %;delta 为百分点 | | `punishCount` | 近N天处罚次数 | number | 无环比;旁边「查看明细」 | #### 顶层 `statDate` 数据截至日;`totalPunishCount` 历史累计处罚次数(不随 7/30 变化)。 ```json { "statDate": "2026-10-25", "business": { "7": { "sellDays": { "current": 6, "previous": 5, "delta": 20.0 }, "sellNum": { "current": 9200, "previous": 8100, "delta": 13.6 }, "sellAmount": { "current": 138000.00, "previous": 123000.00, "delta": 12.2 } }, "30": { ... } }, "service": { "7": { "afterSaleAmount": { "current": 4680.00, "previous": 4440.00, "delta": 5.4 }, "afterSaleRate": { "current": 2.70, "previous": 2.90, "delta": -0.2 }, "afterSaleNum": 320, "afterSaleCount": 19, "lackRate": { "current": 0.90, "previous": 1.20, "delta": -0.3 }, "lowerRankRate": { "current": 0.60, "previous": 0.80, "delta": -0.2 }, "punishCount": 4 }, "30": { ... } }, "totalPunishCount": 27 } ``` ### POST /supplier/profile/punishPage · 处罚明细 #### 入参 | 字段 | 必填 | 说明 | |--------------|------|------------------------------------------------------| | `supplierId` | 是 | | | `period` | 否 | 只允许 `7` / `30`,默认 30;传履约卡片当前选中的周期 | | `pageNum` | 否 | 默认 1 | | `pageSize` | 否 | 固定 10,传其他值会报错 | #### records\[\](按处罚时间倒序) | 字段 | 说明 | 页面列 | |----|----|----| | `punishTypeName` | 处罚类型名称 | 处罚类型 | | `punishTime` | 处罚时间 | 处罚时间(左对齐) | | `punishPrice` | 处罚金额(元) | 处罚金额(左对齐) | | `id` / `punishType` / `roundNo` / `orderSn` / `remark` | 处罚 ID / 类型 code / 场次 / 订单号 / 处罚描述 | 原型未展示,按需使用 | ```json { "index": 1, "size": 10, "total": 16, "records": [ { "id": 1, "punishType": 1, "punishTypeName": "缺货处罚", "punishTime": "2026-10-18 09:32:00", "punishPrice": 800.00, "roundNo": "2026-10-18", "orderSn": "...", "remark": "..." } ] } ``` `total` 与指标里的 `punishCount`(同周期)一致;弹窗副标题「共 N 条 · 每页 10 条」用 `total`。 ### POST /supplier/profile/category · 品类看板 #### 入参 ```json { "supplierId": 10002 } ``` #### 出参 data | 字段 | 说明 | |----|----| | `main` / `prefer` / `quality` | 品类数据三个分段的 Top3(近30天销售扎数降序),元素为 **商品对象** | | `active` | 在售主力花材 Top3(近7天销售扎数降序);其中 `sellNum` 为 **近7天**,`afterSaleRate` 为近30天 | | `growth[]` | 增长最快品类 Top3(按 `diffNum` 降序,**含负增长**;近 14 天有销量的品类不足 3 个时不足 3 条):`secondId`、`secondName` 品类名、`recentNum` 近7天、`previousNum` 上7天、`diffNum` 扎数差 | | `mainTotal` / `preferTotal` / `qualityTotal` / `activeTotal` | 各视图完整条数(明细弹窗「共 N 个品类」也可直接用 ⑤ 的 total) | | `statDate` | 数据截至日 | #### 商品对象(④ ⑤ 共用) | 字段 | 说明 | |----|----| | `productId` | 商品 ID | | `productName` / `rankNum` | 商品名称 / 等级 | | `sellNum` | 销售扎数(main/prefer/quality 近30天;active 近7天) | | `afterSaleRate` | 近30天售后率(%) | | `preferFlag` / `qualityFlag` | 当前是否优选 / 品质优选(1 是) | ```json { "statDate": "2026-10-25", "main": [ { "productId": 1, "productName": "红玫瑰", "rankNum": 1, "sellNum": 12800, "afterSaleRate": 2.60, "preferFlag": 1, "qualityFlag": 0 } ], "prefer": [ ... ], "quality": [ ... ], "active": [ ... ], "growth": [ { "secondId": 9, "secondName": "绣球", "recentNum": 1760, "previousNum": 980, "diffNum": 780 } ], "mainTotal": 8, "preferTotal": 6, "qualityTotal": 6, "activeTotal": 6 } ``` ### POST /supplier/profile/categoryPage · 品类明细 #### 入参 | 字段 | 必填 | 说明 | |----|----|----| | `supplierId` | 是 | | | `viewType` | 否 | `main`(默认)/ `prefer` / `quality` / `active` | | `sortBy` | 否 | `sales` 按销售扎数(默认)/ `afterSaleRate` 按售后率;均为降序 | | `pageNum` / `pageSize` | 否 | 同 ③,pageSize 固定 10 | #### 出参 `{ index, size, total, records: [商品对象] }`。`sortBy=sales` 时第 1 页前 3 条与 ④ 的 Top3 一致。 ### POST /purchaser/userTag/list · 系统标签库(已有接口) 无入参。返回全部标签 `[{ id, type, tagCategory, name }]`,**前端过滤 `type == 2`(供应商标签)**。 ### POST /supplier/profile/tagSave · 保存标签 #### 入参 | 字段 | 必填 | 说明 | |----|----|----| | `supplierId` | 是 | | | `tagIdList` | 是 | 保存后该供应商的 **全部**已有标签 id(从标签库选的 + 原来保留的);传 `[]` 表示清空 | | `newTagList` | 否 | 手动新增的标签 `[{ name, tagCategory }]`;name 去首尾空格后 1~5 个字,tagCategory 必选 | ```json { "supplierId": 10002, "tagIdList": [1, 5, 9], "newTagList": [ { "name": "冷链配送", "tagCategory": "other" } ] } ``` #### 出参 供应商对象,取 `supplierTagList[]`(`tagId`、`tagName`、`tagCategory`)刷新抽屉头部标签。 #### 规则 - 覆盖式保存:请求里没有的旧标签会被移除。 - 合计最多 8 个(`tagIdList` + 新标签,去重后)。 - 新标签保存后自动进入系统标签库,下次打开 ⑥ 就能选到。 - 新标签名与库里已有的供应商标签同名时,**直接复用已有标签**(分类以库里为准),不会重复建。 - 只有点「保存标签」才调接口;编辑弹窗内的增删都是前端草稿。 #### 错误文案(直接 toast) 「每个供应商最多 8 个标签」「标签名称最多 5 个字」「标签名称不能为空」「请选择标签分类」「标签不存在或不是供应商标签:{id}」;5 秒内重复提交会报重复提交。 ## 展示规则 - **抽屉标签**:最多显示 7 个,超出显示 `+N`(N = 总数 − 7);无标签显示「暂无标签」;颜色按 `tagCategory`。 - **编辑标签弹窗**:提示「当前供应商已配置标签」「保存后生效」;草稿满 8 个时禁用标签库按钮和新增输入。 - **环比箭头**:`delta > 0` ↑、`< 0` ↓、`= 0` 持平;数量/金额类显示「↑ 11.0%」,比率类显示「↑ 0.3 个百分点」;`null` 显示「—」。涨跌配色由前端定(售后率、缺货率、降级率上涨应用警示色)。 - **7/30 切换**:经营概览、履约与售后两张卡片各自独立切换,默认 **近 7 天**;切换不请求接口。文案里的「近N天/上N天」随之变化(原型顶部固定写「近 30 天 对比上 30 天」的那一行请跟着切换或去掉)。 - **处罚明细弹窗**:标题「近N天处罚明细」,副标题「共 total 条 · 每页 10 条」,提示「处罚记录按处罚时间倒序展示」;高度按满页 10 行固定;处罚时间、金额左对齐。 - **品类数据卡片**:分段 主营品类 / 优选 / 品质优选;优选视图商品名后加「优选」徽标,品质优选视图加「品质优选」徽标,主营不加。 - **品类数据明细弹窗**:标题「品类数据明细」,弹窗内同样三段切换(切换时 `pageNum` 重置为 1,保持当前排序);排序按钮「按近30天销售扎数」「按售后率」;高度固定,切换不跳动。 - **在售主力花材明细**:标题「在售主力花材明细」,无三段切换,`viewType=active`,排序按钮「按近7天销售扎数」「按售后率」。 - 数字千分位:扎数、金额前端格式化(`12,800 扎`、`¥729,300`)。 ## 「统计口径」弹窗文案 抽屉右上角「统计口径」入口,静态文案(与后端实现一致,可直接使用): | 指标 | 口径 | |----|----| | 动销天数 | 统计周期内有交易的场次数 | | 销售扎数 | 同供应商列表/详情「近30天销售扎数」口径(已付款、已发货、已完成订单的花材扎数) | | 销售金额 | 统计周期内供货金额(以供货价计算) | | 售后金额 | 统计周期内已发货订单产生的售后中,供应商承担的部分 | | 售后率 | 售后金额 / 销售金额 | | 缺货率 / 降级率 | 按花材数量统计:缺货或降级扎数 / 统计周期内销售扎数 | | 处罚次数 | 统计处罚类型:缺货处罚、迟到处罚、城市仓处罚、换货未换、缺货补货、缺货不补、退货不补、退货补货、退货转代卖处罚、退货补货转代卖处罚、降一级处罚、降二级处罚、超时转代卖、迟到未配货、配错货处罚、其它 | | 品类数据 | 主营品类:按近30天销售扎数排序,看板展示前 3,弹窗查看更多;优选:当前是优选的商品;品质优选:当前是品质优选的商品 | | 在售主力花材 | 当前上架的商品,按近7天销售扎数排序,看板展示前 3,弹窗查看更多 | | 增长最快品类 | 以 7 天为周期每天滚动,比较品种(不分等级)近 7 天与上 7 天的销售扎数,取增量最大的 3 个;没有增长的品类时也展示,增量为负数 | 统计周期均以昨天为截止日,今天的场次不计入。 ## 联调注意 - 测试环境:运营后台测试服(bulky-test/admin),接口已部署,可直接联调。 - 测试库很多供应商近 60 天没有订单,指标会全是 0、`delta` 为 `null`、品类列表为空——属正常,请用近期有下单的供应商联调;空态请按原型做好「暂无数据」。 - 统计接口当天有缓存:测试同学当天补的订单/处罚,指标次日才变化;处罚明细(③)和标签(①⑦)是实时的,所以当天新增的处罚可能出现「明细条数 \> 指标次数」,次日恢复一致。 - `business`/ `service` 的 key 是字符串 `"7"`、`"30"`,不是数字下标。 - 比率字段后端 2 位小数(如 `2.70`),展示时 `toFixed(1)`。 > [!WARNING] > 供应商不存在时 ①~⑤ 返回 `code != 0`(供应商不存在),toast 后关闭抽屉即可。 --- # 供应商申诉优化 > 首页本月扣款、我的处罚统计/明细/详情(处罚+退款支出+缺货赔付)、质检问题汇总;缺货赔付申诉 complainType=6;运营后台商品申诉 Tab 多类型列表与审核 - 类型:前端对接 - 分类:供应商 - 端:供应商小程序 / 运营后台 - 后端服务:supplier / admin - 状态:已上测 - 测试分支:supplier/test · bulky-test/admin - 需求单:XYLZ-3562 - 更新:2026-10-29 - 网页版:https://doc.xfun.fun/supplier-punish-center-api > [!NOTE] > XYLZ-3640「供应商申诉调整」(首页质检问题、换货扎数、场次卡口径与跳转、二级类目编码、状态标签、商品全称、客服 ID、凭证选填)的集中说明见 [供应商申诉调整 · 前端对接](/supplier-appeal-adjust-api)。 ## 总览 | 端 | 接口 | 用途 | |----|----|----| | 供应商端 xm-bulky-supplier | `POST /home/supplierMoney` | 首页「我的处罚 —— 本月扣款」:字段 `monthPunishAmount` | | 供应商端 xm-bulky-supplier | `POST /home/supplierPunishCount` | 首页「本月履约情况 —— 质检问题汇总」:字段 `qualityIssueNum` | | 供应商端 xm-bulky-supplier | `POST /punish/center/summary` | 我的处罚顶部统计、三大类卡、二级类目卡、「全部类型」下拉 | | 供应商端 xm-bulky-supplier | `POST /punish/center/list` | 我的处罚 - 处罚明细(筛选 + 分页) | | 供应商端 xm-bulky-supplier | `POST /punish/center/detail` | 处罚详情页 | | 供应商端 xm-bulky-supplier | `POST /home/qualityIssue/summary` | 质检问题汇总页(按月 / 按场次汇总 + 场次卡) | | 供应商端 xm-bulky-supplier | `POST /comment/complain` | 提交申诉:支持处罚(3)、退款支出(2)、缺货赔付(6),凭证选填、申诉理由必填 | | 供应商端 xm-bulky-supplier | `POST /comment/complain/cancel` | 撤回申诉(仅处罚申诉) | | 运营后台 xm-bulky-admin | `POST /order/complain/list` | 「商品申诉」Tab 传 `complainTypeList: [1,2,6]`;返回来源标签、缺货赔付信息、可退金额 | | 运营后台 xm-bulky-admin | `POST /order/complain/check` | 支持审核缺货赔付申诉(6);2/6 同意金额上限、驳回必填原因 | - `/home/supplierPunishCount` 除 `qualityIssueNum`/ `qualityLackNum`/ `qualityReturnNum`/ `qualityExchangeNum` 外的字段,以及 `/home/supplierMoney/detail`、`/home/supplierMoney/punish/detail` 供旧版小程序使用;「我的处罚」相关页面使用上表接口。 - 质检问题汇总页 **不提供处罚/申诉操作**,统一在「我的处罚」处理。 ## 通用约定 - 全部 `POST` + JSON,返回 `{ "code": 0, "msg": "success", "data": … }`;`code != 0` 时 `msg` 直接 toast。 - 时间格式 `yyyy-MM-dd HH:mm:ss`;金额均为保留 2 位小数的数字。 - **统计方式**:两个统计页顶部都有「按月 / 按场次」切换,对应入参 `statType`(1 按月,默认;2 按场次),汇总数字按所选条件统计。 - **时间范围**:`month`(`2026-10`)与 `roundNo`(场次 `2026-10-14`),**都不传 = 当月**;传了 `roundNo` 时月份取该场次所在月。`statType=2` 且不传 `roundNo` 时,后端取所选月份最近一个场次,并在出参 `roundNo` 返回。按 **场次日期**归属月份,不是按处罚时间。 - **倒计时**:列表/详情/统计都返回 `serverTime`,剩余时间 = `appealDeadline - serverTime`,本地时钟只用于每秒递减,避免手机时间不准。 - **处罚项**:「我的处罚」把三种来源统一成一条处罚项,用 `sourceType + sourceId` 唯一标识(列表 key 用 `itemKey`)。 | sourceType | 来源 | sourceId 是 | 提交申诉用的 complainType | 申诉规则 | |----|----|----|----|----| | 1 | 处罚(质检、迟到、配货错误等) | 处罚 ID | 3 | 处理后 48h 内首诉;首诉驳回后 24h 内可再诉一次;申诉中可撤回 | | 2 | 退款支出(售后单供应商承担) | 售后单 ID | 2 | 售后通过后 48h 内,只能申诉 1 次,不可撤回 | | 3 | 缺货赔付(供应商承担) | 缺货赔付 ID | 6 | 扣款后 48h 内,只能申诉 1 次,不可撤回 | 是否可申诉、截止时间都由后端算好(`canAppeal` / `appealDeadline`),前端不用自己判断规则。 ## 页面与接口对应 | 原型 | 页面元素 | 接口 / 字段 | |----|----|----| | 02 首页 | 「我的处罚 —— 本月扣款 ¥320.00 \>」 | 首页已调用的 `/home/supplierMoney` 字段 `monthPunishAmount`(不用另调接口);点击进入我的处罚 | | 02 首页 | 「本月履约情况 —— 质检问题汇总 16 扎 \>」 | 首页已调用的 `/home/supplierPunishCount` 字段 `qualityIssueNum`(不用另调接口),与质检问题汇总页按月当月「缺货 + 退货 + 换货」扎数一致;点击进入质检问题汇总 | | 01 质检问题汇总 | 「按月 / 按场次」切换 | ④ 入参 `statType`:1 按月 / 2 按场次 | | 01 质检问题汇总 | 顶部下拉:按月为月份(默认当月),按场次为场次 | 按月传 `month`;按场次传 `roundNo`(选项用 `roundOptions`,首次进入不传,后端取当月最近场次) | | 01 质检问题汇总 | 汇总卡:售卖 N 扎 / 缺货 / 退货 / 换货 / 共 N 个场次 | `saleNum / lackNum / returnNum / exchangeNum / roundCount`,按所选月份或场次统计 | | 01 质检问题汇总 | 「场次质检汇总」场次下拉(按月时) | `roundOptions`;选中后再调 ④ 带 `statType=1, roundNo`,只返回该场次卡片(汇总仍为整月) | | 01 质检问题汇总 | 场次卡(商品行默认 3 行,「展开更多(n)」) | `rounds[]`,n = `items.length - 3` | | 01 质检问题汇总 | 「查看处罚明细 —— 共 5 条处罚 已确认 · ¥120.00 \>」 | `punishCount / punishAmount`;跳我的处罚质检 Tab,summary 带 `statType:2, roundNo`,list 带 `statType:2, roundNo, category:1` | | 03 我的处罚 | 「按月 / 按场次」切换 + 月份 / 场次下拉 | ① 入参 `statType / month / roundNo`;场次下拉用 ① 出参 `roundOptions`;② 明细传同样三个值 | | 03 我的处罚 | 实际处罚、共 N 笔、总处罚、申诉成功 | ① `actualAmount / totalCount / totalAmount / appealSuccessAmount`,按所选月份或场次统计 | | 03 我的处罚 | 三大类卡(质检/迟到/其他) | ① `categories[].actualAmount / totalCount` | | 03 我的处罚 | Tab 下二级类目卡(类型统计·实际扣款) | ① `categories[].subCategories[]` | | 03 我的处罚 | 处罚明细 + 三个筛选(全部类型 / 处罚状态 / 申诉状态) | ② `/punish/center/list` | | 04 处罚详情 | 全部字段 + 申诉记录 + 去申诉 | ③ `/punish/center/detail` | | 申诉页 | 提交申诉 / 撤回 | ⑤ `/comment/complain`、`/comment/complain/cancel` | ## 枚举 #### 类目(category → subCategory → 三级类型) 缺货/退货扎数按类目类型集合统计,只统计质检环节处罚(punish_link=1),人工下发、代卖等其他环节的处罚不计入;条件为 pid=0、data_status=0、handle_status IN (0,1)。换货扎数独立取 exchange_log,19、22 不计扎数。 | category | subCategory | 三级类型(sourceType / punishType:名称) | 卡片标题 title | |----|----|----|----| | 1 质检处罚 | 11 缺货 | 1/1 缺货处罚、1/6 缺货补货、1/9 缺货不补 | 缺货处罚 | | 1 质检处罚 | 12 退货 | 1/7 退货不补、1/8 退货补货、1/11 退货不补转代卖、1/12 退货补货转代卖 | 退货处罚 | | 1 质检处罚 | 14 降级 | 1/13 降1级、1/15 降2级 | 降级处罚 | | 1 质检处罚 | 15 换货配货 | 1/25 换货未换、1/18 换货、1/19 配货错误处罚、1/22 先扫码后配货处罚 | 换货配货处罚 | | 2 迟到处罚 | 21 配货迟到 | 1/14 迟到配货处罚、1/23 代配送货迟到处罚 | 配货迟到处罚 | | 2 迟到处罚 | 22 换货迟到 | 1/20 换货迟到 | 换货迟到处罚 | | 2 迟到处罚 | 23 迟到处罚 | 1/3 迟到处罚 | 迟到处罚 | | 3 其他处罚 | 32 售后处罚 | 2/null 退款支出、3/null 缺货赔付 | 售后处罚 | | 3 其他处罚 | 34 其他处罚 | 1/2 未打勾处罚、1/4 其他处罚、1/16 退款赠花、1/17 超时转代卖及其他未归类类型;以及非质检环节的 1/1、6、7、8、9、11、12、13、15 | 其他处罚 | **质检类型按处罚环节归类**:处罚类型 1、6、7、8、9、11、12、13、15 在质检环节(`punish_link=1`)和其他环节(人工下发、代卖等)都会出现。只有质检环节的按上表归入「质检处罚」下的缺货 / 退货 / 降级;其他环节的同类型处罚统一归「其他处罚 · 其他处罚(34)」,明细、统计、二级类目卡、`typeOptions` 同一口径,质检问题汇总的缺货 / 退货扎数也只算质检环节。前端不用判断,按接口返回的 `category` / `subCategory` 展示即可。 二级类目 `subCategory` 有效编码仅以上 9 个,按表中顺序下发。以上树由 ① 的 `typeOptions` 下发,前端不要写死;「售后处罚」被后台开关关闭时,`typeOptions` 和 `subCategories` 里都不会出现 32。 #### 状态 | 字段 | 取值 | 说明 | |----|----|----| | `deductStatus` 扣款状态 | 1 待扣款 / 2 已扣款 / 3 已退回 | 待扣款 = 质检处罚待运营审核,**不计入**统计金额、不可申诉;已退回 = 申诉成功有退款(含部分退回) | | `appealStatus` 申诉状态 | 0 未申诉 / 1 申诉中 / 2 申诉通过 / 3 申诉驳回 | 取最新一条未撤回的申诉 | ## 供应商端接口 ### POST /home/supplierMoney 首页金额接口,其中 `monthPunishAmount` 用于「本月履约情况 —— 我的处罚 本月扣款 ¥320.00」。 | 字段 | 类型 | 说明 | |----|----|----| | `monthPunishAmount` | number | 本月扣款 = 我的处罚当月实际处罚,与 `/punish/center/summary` 按月不传月份时的 `actualAmount` 一致(不含待扣款)。**计算失败时为 null**,前端显示「--」或隐藏金额,不影响首页其他数据 | ```text // 响应 data(仅列出本页相关字段) { "dayMoney": 126.90, "...": "...", "monthPunishAmount": 320.00 } ``` ### POST /home/supplierPunishCount 首页「本月履约情况 —— 质检问题汇总 N 扎」 **读 `qualityIssueNum`**。四个字段固定统计当月,与请求里的 `startRoundNo`/ `endRoundNo` 无关;其余字段(`stockOut`、`salesReturnNum` 等)口径不同,首页卡片不要用。 | 字段 | 类型 | 说明 | |----|----|----| | `qualityIssueNum` | int | 当月质检问题扎数 = `qualityLackNum` + `qualityReturnNum` + `qualityExchangeNum`,与 `/home/qualityIssue/summary` 按月(`statType=1`、不传月份)的 `lackNum + returnNum + exchangeNum` 一致。**计算失败时为 null**,前端显示「--」,不影响首页其他数据 | | `qualityLackNum` | int | 当月质检缺货扎数,= 质检汇总页按月 `lackNum` | | `qualityExchangeNum` | int | 当月换货要求扎数,等于汇总页 exchangeNum,包含已完成与未完成换货 | | `qualityReturnNum` | int | 当月质检退货扎数,= 质检汇总页按月 `returnNum` | ```text // 响应 data(仅列出本页相关字段) { "stockOut": 3, "...": "...", "qualityIssueNum": 19, "qualityLackNum": 8, "qualityReturnNum": 8, "qualityExchangeNum": 3 } ``` ### POST /punish/center/summary 我的处罚统计,按月或按场次。首页「本月扣款」取 `/home/supplierMoney` 的 `monthPunishAmount`,见接口 `POST /home/supplierMoney`。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `statType` | int | 否 | 统计方式:1 按月(默认)/ 2 按场次 | | `month` | string | 否 | 统计月份 `2026-10`,不传 = 当月 | | `roundNo` | string | 否 | 场次 `2026-10-14`,传了只统计该场次(从质检汇总页跳转时也传它);`statType=2` 不传时取所选月份最近一个有处罚的场次 | #### 响应 data | 字段 | 类型 | 说明 | |----|----|----| | `statType` | int | 实际统计方式(传了 `roundNo` 即为 2) | | `month / startRoundNo / endRoundNo` | string | 实际统计的月份与场次区间(含);按场次时起止都为该场次 | | `roundNo` | string | 按场次时实际统计的场次;所选月份无处罚时为 null | | `roundOptions[]` | array | 所选月份有处罚的场次(倒序),按场次时的场次下拉用 | | `actualAmount` | number | **实际处罚**(顶部大数字、首页本月扣款)= 总处罚 - 申诉成功 | | `totalAmount` | number | 总处罚 | | `appealSuccessAmount` | number | 申诉成功(已退回金额) | | `totalCount` | int | 共 N 笔 | | `pendingAmount / pendingCount` | number / int | 待扣款金额/笔数,**不含在上面四个数里**,需要时可单独展示 | | `categories[]` | array | 固定 3 个一级类目(0 也返回):`category, categoryName, actualAmount, totalAmount, appealSuccessAmount, totalCount, subCategories[]` | | `categories[].subCategories[]` | array | 该一级下全部二级(0 也返回):`subCategory, subCategoryName` + 同样 4 个统计字段 | | `typeOptions[]` | array | 「全部类型」下拉树:`category, categoryName, subCategories[{subCategory, subCategoryName, types[{sourceType, punishType, name}]}]` | | `serverTime` | string | 服务器时间 | #### 示例 ```text // 请求:按月 { "statType": 1, "month": "2026-10" } // 请求:按场次(首次切换不传 roundNo,后端取最近场次) { "statType": 2, "roundNo": "2026-10-14" } // 响应 data(按月) { "statType": 1, "month": "2026-10", "roundNo": null, "roundOptions": ["2026-10-14", "2026-10-12"], "startRoundNo": "2026-10-01", "endRoundNo": "2026-10-31", "actualAmount": 320.00, "totalAmount": 350.00, "appealSuccessAmount": 30.00, "totalCount": 12, "pendingAmount": 0.00, "pendingCount": 0, "categories": [{ "category": 1, "categoryName": "质检处罚", "actualAmount": 240.00, "totalAmount": 260.00, "appealSuccessAmount": 20.00, "totalCount": 9, "subCategories": [ { "subCategory": 11, "subCategoryName": "缺货", "actualAmount": 60.00, "totalAmount": 60.00, "appealSuccessAmount": 0.00, "totalCount": 3 }, { "subCategory": 14, "subCategoryName": "降级", "actualAmount": 40.00, "totalAmount": 40.00, "appealSuccessAmount": 0.00, "totalCount": 2 } ] }], "typeOptions": [{ "category": 1, "categoryName": "质检处罚", "subCategories": [ { "subCategory": 11, "subCategoryName": "缺货", "types": [ { "sourceType": 1, "punishType": 6, "name": "缺货补货" }, { "sourceType": 1, "punishType": 9, "name": "缺货不补" } ] } ] }], "serverTime": "2026-10-21 10:00:00" } ``` ### POST /punish/center/list 处罚明细。按处罚时间倒序。 #### 请求参数(包含 ① 的 statType / month / roundNo) | 字段 | 类型 | 必填 | 说明 / 对应页面操作 | |----|----|----|----| | `statType / month / roundNo` | int / string | 否 | 同 ①,与统计取同一范围;按场次时建议传 ① 返回的 `roundNo` | | `category` | int | 否 | Tab:不传 = 全部,1 质检 / 2 迟到 / 3 其他 | | `subCategory` | int | 否 | 点击二级类目卡 | | `sourceType` + `punishType` | int | 否 | 「全部类型」下拉选中的三级类型,原样回传 `typeOptions` 里的两个值(售后两项 `punishType` 为 null) | | `deductStatus` | int | 否 | 「处罚状态」下拉:1 待扣款 / 2 已扣款 / 3 已退回 | | `appealStatus` | int | 否 | 「申诉状态」下拉:0 未申诉 / 1 申诉中 / 2 申诉通过 / 3 申诉驳回 | | `pageIndex` | int | 否 | 默认 1 | | `pageSize` | int | 否 | 默认 20,最大 50 | #### 响应 data `{ total, records[], serverTime }`,`records[]` 字段: | 字段 | 类型 | 说明 | |----|----|----| | `itemKey` | string | 列表 key,`sourceType_sourceId` | | `sourceType / sourceId` | int / long | 来源与来源 ID,进详情、提交申诉都用它 | | `category / categoryName / subCategory / subCategoryName` | | 类目 | | `punishType / typeName` | | 三级类型(售后处罚 punishType 为 null) | | `title` | string | 卡片标题 = 二级名 + 处罚,如「退货处罚」「换货配货处罚」「迟到处罚」「售后处罚」 | | `roundNo / punishTime` | string | 场次 / 处罚时间 | | `productName / rankNum / num / orderItemUrl` | | 商品信息:名称、等级(1~5 = A~E)、扎数、图片;可能为空 | | `skuDesc / productFullName` | string | 规格属性描述 / 商品名 + A~E级 + 属性值;属性按原顺序用空格拼接,忽略规格中的等级和包装 JSON,例如高原红A级不卷边。质检汇总商品行同样返回 | | `statusTag / statusTagName` | int / string | 统一展示状态,见状态标签表 | | `reason` | string | 处罚描述(行名显示「处罚描述」) | | `orderSn` | string | 关联单号(可复制) | | `punishAmount` | number | 原处罚金额(卡片金额) | | `returnedAmount` | number | 已退回金额 | | `actualAmount` | number | 实际处罚 = 原金额 - 已退回 | | `deductStatus / deductStatusName` | | 扣款状态 | | `appealStatus / appealStatusName` | | 申诉状态 | | `complainType` | int | 提交申诉时传给 `/comment/complain` 的值(3 / 2 / 6) | | `canAppeal` | bool | 当前能否申诉(含再次申诉) | | `nextAppealNo` | int | 可申诉时为第几次申诉(1 首诉 / 2 二诉) | | `appealDeadline` | string | 本轮申诉截止时间(倒计时用);已申诉过的售后处罚为 null | | `canCancelAppeal` | bool | 能否撤回申诉(仅处罚申诉中) | | `latestComplainId` | long | 最新一条申诉 ID,撤回时用 | | `latestComplain` | object | 最新一条申诉:`reason, pictures, status, operationContent`(驳回原因/审核说明) `, returnMoney, operatorId`(处理客服 ID,0 或 null 显示「系统」) `, createTime, handleTime` 等;`operatorName` 恒为 null | #### 示例 ```text // 请求 { "statType": 1, "month": "2026-10", "category": 1, "sourceType": 1, "punishType": 7, "deductStatus": null, "appealStatus": null, "pageIndex": 1, "pageSize": 20 } // 响应 data { "total": 4, "serverTime": "2026-10-21 10:00:00", "records": [{ "itemKey": "1_922652537", "sourceType": 1, "sourceId": 922652537, "category": 1, "categoryName": "质检处罚", "subCategory": 12, "subCategoryName": "退货", "punishType": 7, "typeName": "退货不补", "title": "退货处罚", "roundNo": "2026-10-09", "punishTime": "2026-10-09 17:16:00", "productName": "卡罗拉", "rankNum": 2, "skuDesc": "中熟", "productFullName": "卡罗拉B级中熟", "num": 5, "orderItemUrl": "https://…", "reason": "花材品质不达标", "orderSn": "T2026100910033425", "punishAmount": 45.00, "returnedAmount": 0.00, "actualAmount": 45.00, "deductStatus": 2, "deductStatusName": "已扣款", "appealStatus": 0, "appealStatusName": "未申诉", "statusTag": 1, "statusTagName": "待申诉", "complainType": 3, "canAppeal": true, "nextAppealNo": 1, "appealDeadline": "2026-10-21 17:16:00", "canCancelAppeal": false, "latestComplainId": null, "latestComplain": null }] } ``` ### POST /punish/center/detail #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |--------------|------|------|-----------------------| | `sourceType` | int | 是 | 列表项的 `sourceType` | | `id` | long | 是 | 列表项的 `sourceId` | #### 响应 data = 列表项全部字段 + 以下字段 | 字段 | 说明 | 原型位置 | |----|----|----| | `sourceId` | 处罚编号 | 处罚编号 | | `deductStatusName` | 顶部标签(待扣款/已扣款/已退回) | 扣款状态标签 | | `title` | 处罚判定 | 处罚判定 | | `roundNo` | 场次 | 场次 | | `shelfDisplay` / `zoneShelf` | 货架号,如 `#07-167`(仅处罚有) | 货架号 | | `zoneShelfList` | 换货迟到处罚涉及的多个货位号 | 货架号(多个) | | `num` / `supplierPrice` / `punishProportion` | 处罚数量 / 供货价 / 处罚比例(0-100,%) | 处罚数量、供货价、处罚比例 | | `reason` | 处罚描述 | 处罚描述 | | `remark` | 处罚备注(售后为售后单备注) | 处罚备注 | | `evidencePics` | 相关素材图片:处罚为 `pic_url`(与旧处罚详情 `picUrl` 同格式),售后为买家图片 `pictures` | 相关素材 | | `platformPics` | 售后平台补充图片(仅退款支出) | 相关素材 | | `evidenceVideos` | 售后视频(仅退款支出) | 相关素材 | | `ruleDesc` | 处罚规则说明,为空不展示 | — | | `sourceDesc` | 处罚来源,如「质检处罚」「系统自动处罚」「售后处罚」「售后单」「缺货赔付(订单 T…)」,不含姓名,为空不展示 | — | | `operatorId` / `enterTime` | 下发处罚人 ID(质检处罚为质检员)/ 入场时间;显示「客服:ID」,0 或 null 显示「系统」;`operatorName` 恒为 null | 客服 ID | | `complainHistory[]` | 全部申诉记录,按提交时间正序,`appealNo` 为展示序号;含已撤回记录(`delTime` 非空 = 已撤回);处理人显示 `operatorId`(「客服:ID」,0 或 null 显示「系统」),`operatorName` 恒为 null | 处罚申诉 - 申诉记录(空数组显示「暂无申诉记录」) | | `serverTime` | 服务器时间 | 倒计时 | 处罚不存在、不属于本人、已被运营直接撤销、已取消、处罚金额为 0,或为城市仓处罚、供应商拒不开票处罚时返回失败「处罚不存在」。 ### POST /home/qualityIssue/summary #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `statType` | int | 否 | 统计方式:1 按月(默认)/ 2 按场次 | | `month` | string | 否 | 不传 = 当月;传了 `roundNo` 时取场次所在月 | | `roundNo` | string | 否 | 按月:「场次质检汇总」下拉选中值,`rounds` 只含该场次,汇总仍为整月;按场次:所选场次,汇总与 `rounds` 都只算该场次,不传取当月最近场次 | #### 响应 data | 字段 | 说明 | |----|----| | `statType` | 实际统计方式 | | `month / roundNo` | 统计月份 / 按场次时实际统计的场次(当月无场次时为 null,按月时为 null) | | `roundCount` | 统计范围内场次数(按场次为 1 或 0) | | `saleNum / lackNum / returnNum / exchangeNum` | 统计范围内售卖扎数 / 缺货扎数 / 退货扎数 / 换货扎数(缺货/退货为质检数量,含处罚金额为 0 的) | | `roundOptions[]` | 所选月份内有售卖、缺货、退货或换货的场次(倒序),场次下拉用 | | `rounds[]` | 场次卡(倒序):`roundNo, productKinds`(共 X 种商品) `, saleNum`(售卖 Y 扎) `, lackNum, returnNum, exchangeNum`(红标「缺货a扎·退货b扎·换货c扎」) `, punishCount, punishAmount`(共 N 条处罚 已确认 · ¥X:条数为该场次质检处罚,含待扣款;金额仅为已扣款/已退回项的实际处罚,不含 0 元处罚) `, items[]` | | `rounds[].items[]` | 商品行,按售卖 → 换货 → 退货 → 缺货扎数倒序,全部返回由前端折叠;商品列展示 `productFullName`:`productId, productName, rankNum, skuDesc, productFullName, saleNum, lackNum, returnNum, exchangeNum` | ```text // 请求:按月 { "statType": 1, "month": "2026-10" } // 请求:按场次 { "statType": 2, "roundNo": "2026-10-14" } // 响应 data(按月) { "statType": 1, "month": "2026-10", "roundNo": null, "roundCount": 2, "saleNum": 50, "lackNum": 2, "returnNum": 5, "exchangeNum": 3, "roundOptions": ["2026-10-14", "2026-10-12"], "rounds": [{ "roundNo": "2026-10-14", "productKinds": 5, "saleNum": 32, "lackNum": 2, "returnNum": 3, "exchangeNum": 2, "punishCount": 5, "punishAmount": 120.00, "items": [{ "productId": 1, "productName": "高原红", "rankNum": 1, "saleNum": 8, "lackNum": 1, "returnNum": 1, "exchangeNum": 2, "skuDesc": "卷边", "productFullName": "高原红A级卷边" }] }] } ``` - 「查看处罚明细」跳我的处罚并定位质检 Tab:summary 传 `{statType:2, roundNo}`,list 传 `{statType:2, roundNo, category:1}`。列表 total 等于场次卡 punishCount,已扣款/已退回项 actualAmount 之和等于 punishAmount。 - 缺货/退货扎数按类目类型集合统计有效处罚,只统计质检环节处罚(punish_link=1),人工下发、代卖等其他环节的处罚不计入;申诉成功的不计入。缺货类型为 1、6、9;退货类型为 7、8、11、12;与二级类目 11/12 一致。换货独立取 exchange_log 的 SUM(num),过滤本人、场次区间、status=0、num\>0,包含扫码及批量换货;type 18、19、22、25 的处罚数量不参与扎数统计。只有换货的场次也进入 roundOptions。 ### POST /comment/complain 提交申诉。成功后刷新列表/详情/统计。 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | long | 是 | 处罚项的 `sourceId` | | `complainType` | int | 是 | 处罚项的 `complainType`(3 处罚 / 2 退款支出 / 6 缺货赔付) | | `reason` | string | 是 | 申诉理由,≤255 字 | | `picUrl` | string | 否 | 申诉凭证(图片/视频地址 JSON 数组字符串),三种类型都 **选填**,最多 6 个(前端限制);不传、空串或 `[]` 均可提交,后端统一存 `[]` | | `appealDesc` | string | 否 | 诉求说明 | #### 缺货赔付申诉(complainType=6)可能的失败提示 - 缺货赔付记录不存在 / 缺货赔付未扣款,不能申诉 / 缺货赔付金额异常 - 缺货赔付扣款后72小时内才可以申诉 - 同条缺货赔付记录最多申诉1次 ### POST /comment/complain/cancel 撤回申诉:`{ "id": latestComplainId }`,仅 `canCancelAppeal = true` 时显示按钮(只有处罚申诉支持撤回)。 ## 列表/详情展示规则 客服只展示 ID:详情 `operatorId`(下发人)、列表 `latestComplain.operatorId` 与详情 `complainHistory[].operatorId`(处理人)显示「客服:ID」,0 或 null 显示「系统」;`operatorName` 恒为 null;`sourceDesc` 不含客服或质检员姓名。适用于 `/punish/center/*` 与 `/home/qualityIssue/summary`。 #### 状态标签 列表/详情统一读取 statusTag / statusTagName;按下表优先判断待扣款。申诉状态筛选仍用 appealStatus,不增加「已确认」筛选项。驳回后可二诉时仍显示「申诉拒绝」,同时显示再次申诉按钮及倒计时。 | 条件 | 标签(statusTag / statusTagName) | |----------------------------------|-----------------------------------| | `deductStatus = 1`(优先) | 0 / 空字符串,不展示 | | `appealStatus = 0 && canAppeal` | 1 / 待申诉 | | `appealStatus = 0 && !canAppeal` | 5 / 已确认 | | `appealStatus = 1` | 2 / 申诉中 | | `appealStatus = 2` | 3 / 申诉成功 | | `appealStatus = 3` | 4 / 申诉拒绝 | #### 金额 - 卡片金额显示 `punishAmount`;`returnedAmount > 0` 时加删除线,可附「实扣 ¥ `actualAmount`」(部分退回时实扣不为 0)。 #### 提示条 | 条件 | 提示 | |----|----| | `appealStatus = 0 && canAppeal` | 申诉剩余 hh:mm:ss,超时将无法申诉 | | `appealStatus = 1` | 申诉中,等待客服处理 | | `appealStatus = 2` | 申诉成功,款项已退回 | | `appealStatus = 3 && canAppeal` | 首次申诉已拒绝,可再次申诉 + 二次申诉剩余倒计时 | | `appealStatus = 3 && !canAppeal` | 申诉已拒绝(可展示 `latestComplain.operationContent` 作为原因) | | `deductStatus = 1` | 待运营审核扣款(不可申诉) | #### 按钮 | 条件 | 按钮 | |---------------------------------|----------| | 始终 | 查看详情 | | `canAppeal && appealStatus = 0` | 去申诉 | | `canAppeal && appealStatus = 3` | 再次申诉 | | `canCancelAppeal` | 撤回申诉 | > [!NOTE] > 倒计时归零后按钮自动隐藏即可;以后端为准,用户此时提交会返回超时提示。 ## 运营后台 ### POST /order/complain/list #### 入参(申诉来源相关) | 字段 | 类型 | 说明 | |----|----|----| | `complainTypeList` | int\[\] | 「商品申诉」Tab 传 `[1, 2, 6]`;与 `complainType` 同时传时以本字段为准 | | `complainType` | int | 支持 6(缺货赔付申诉);「售后申诉」Tab 传 2 | #### records\[\] 字段(申诉来源相关) | 字段 | 说明 | 页面列 | |----|----|----| | `appealSourceType` | 1 商品申诉 / 2 退款支出申诉 / 3 缺货赔付申诉(其他类型 null) | 申诉类型/来源 | | `appealSourceStr` | 「商品申诉」「退款支出申诉」「缺货赔付申诉」,直接做彩色标签 | 申诉类型/来源 | | `orderPaidInfo` | 仅 complainType=6:`id, orderSn, reasonType, reasonTypeStr`(缺货赔付/二次售后/其他) `, paidReason, price, xmCoin, deductMoney, refundStatus, createTime, updateTime` | 关联订单/商品/售后 | | `deductMoney` | 仅 2/6:实际扣款金额(2 = 该售后单申诉人承担金额,6 = 赔付金额 + 寻梦币) | — | | `returnedMoney` | 仅 2/6:同一记录已同意退回金额合计 | — | | `maxReturnMoney` | 仅 2/6:同意时最多可退 = deductMoney - returnedMoney | 同意弹窗「最多可退 ¥x」 | - 「关联订单/商品/售后」列:1 用 `orderItemComment`,2 用 `refundInfo`,6 用 `orderPaidInfo`。 - 「退回金额」列:已同意时显示 `returnMoney`。 - 城市仓/分销商名称(`wareName / partnerName / platformStr`)对 2 和 6 都会回填。 - 导出(`exportFlag=1`)包含「申诉来源」「实际扣款金额」两列,类型 6 的「关联订单号」取赔付记录订单号。 ### POST /order/complain/check | 操作 | 请求 | 规则 | |----|----|----| | 同意(2 / 6) | `{ "id": 1, "status": 2, "returnMoney": "10.00", "reason": "…" }` | `returnMoney` 必填且 \> 0,不能超过 `maxReturnMoney`;缺货赔付同意后退回供应商余额(账变「售后承担退回」) | | 驳回(2 / 6) | `{ "id": 1, "status": 3, "reason": "…" }` | **`reason` 必填** | #### 可能的失败提示 - 缺货赔付申诉同意时需要填写退回金额 / 退回金额不能小于等于0 - 退回金额不能超过实际扣款金额,最多可退:x 元 - 驳回申诉必须填写驳回原因 - 缺货赔付记录不存在 / 缺货赔付未扣款,不能退回 - 已有其他已创建、已同意的申诉,无法通过该条申诉 id:x 审核后 12 小时内的「撤销」(`/order/complain/revoke`)也支持类型 6,撤销时会扣回已退金额;`canRevoke` 字段照常返回。 ## 联调注意 - **待扣款**(质检处罚待运营审核)会出现在明细里,但不计入实际处罚/总处罚、不可申诉;运营审核通过后变为已扣款,开始 48h 申诉窗口。 - **不展示**:运营直接撤销的处罚、已取消/删除的处罚、处罚金额为 0 的处罚(含退款支出、缺货赔付)、城市仓处罚、供应商拒不开票处罚。申诉成功的处罚会展示为「已退回」。 - **0 元处罚**不计入统计(实际处罚、共 N 笔、待扣款、各类目/二级类目笔数、首页「本月扣款」、质检场次卡「共 N 条处罚 · ¥X」),与明细列表一致。质检汇总的缺货/退货扎数是质检数量,仍包含 0 元处罚对应的扎数。 - **城市仓处罚、供应商拒不开票处罚**不属于「我的处罚」:下拉选项、统计、明细、首页「本月扣款」、质检场次卡均不包含,详情返回「处罚不存在」。 - **售后处罚**(退款支出、缺货赔付)可由后台 Nacos 开关整体关闭;关闭后统计、明细、下拉里都没有 32,前端无需判断。 - **退款支出**不含极速退款(售后类型 1、2、4、5,未发货退款,供应商不承担)、退运费(7)和质检处罚退款(8,已在处罚中体现);这几类不出现在明细、统计、首页「本月扣款」里。 - 运营端「售后申诉」Tab 用于历史查看;处理统一在「商品申诉」Tab。 - 测试环境:先部署 admin(能审核 6),再部署 supplier(能提交 6)。 --- # 打包负责人物资申领 > 申领提交/撤销/审核 7 个新接口,含凭证字符串格式、canCancel/canAudit 显隐规则,以及出库权限按团队收敛、库房口径改剩余可用两处破坏性变更 - 类型:前端对接 - 分类:仓库配货 / 打包 - 端:梦花仓管小程序 - 后端服务:gallery - 更新:2026-08-25 - 网页版:https://doc.xfun.fun/wms-material-apply-api 梦花仓管小程序 · 进销存 后端模块 `xm-bulky-mall-gallery`(与现有 `/supplier/wms/inventory/*` 同一服务)· 测试分支 `test-gallery` · 2026-08-25 ## 通用约定 跟现有 `/supplier/wms/inventory/*` 一模一样:base `https://api.xunmengvip.cn/xm-bulky-mall-gallery`,全部 POST + JSON body,鉴权、`{ code, msg, data, success }` 包装(`success` 是布尔,等价于 `code === 0`;失败时 HTTP 状态码仍是 200,别用状态码判断成败)、`index`/ `size` 分页都照旧,直接复用现成封装。失败响应的 `msg` 是可以直接 toast 的中文,速查见 08 节。 > [!NOTE] > **⚠️ 唯一的入参怪癖** > > `/apply/sku/stock`、`/apply/detail`、`/apply/cancel` 三个接口复用了通用的 `IdQo`,入参字段名都叫 `id`。其中 **`/apply/sku/stock` 的 `id` 传的是 skuId,不是申领单 ID**,另外两个才是申领单 ID。 ## 角色与状态机 后端从两处判定当前账号的身份,不需要前端传角色来鉴权: | 角色 | 判定依据 | 能做什么 | |----|----|----| | **仓库管理员** | 账号命中配置的仓管角色 | 看全部申领单、同意、拒绝。**不能提交申领**(除非同时也是负责人) | | **打包负责人** | `xm_bulky_team.leader_id` = 当前账号,且团队为启用状态 | 提交申领、撤销自己提交的单、只看自己带的大区的单 | | 两者都不是 | — | 调任意申领接口都会收到 `无物资申领权限` | 一个账号可以同时是仓管和负责人,这种情况下列表默认看全部;如果前端提供了「角色切换」,把当前视角作为 `roleName` 传给 `/apply/page` 即可(见 04 节)。 ### 申领单状态 | status | 名称 | 说明 | |----|----|----| | 10 | 待审核 | 负责人提交后的初始态。此时**库存不动**,不占用、不冻结 | | 20 | 已同意 | 仓管同意,库存此刻才从库房划到大区,`issuedQuantity` 有值 | | 30 | 已拒绝 | 仓管拒绝,`rejectReason` 可能为空字符串 | | 40 | 已撤销 | 申领人本人撤回,仅待审核态可撤 | > [!NOTE] > **待审核不占库存** > > 提交时只校验一次「申领数量 ≤ 库房剩余可用」,之后不做任何预占。所以两张待审核的单可能都显示能发,实际审批时第二张会因为库存不够被拒。**审批失败的 toast 要能正常展示,不要假设提交通过就一定能审批通过。** ## 新增接口 · 负责人侧 ### POST /supplier/wms/apply/sku/stock · 查询物资库房剩余可用库存 **新增** · 调用方:负责人 / 仓管 选好物资后调用,用于在申领数量输入框旁边展示「当前可申领上限」。 | 请求字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | **物资 SKU ID**(不是申领单 ID),取自 `/supplier/wms/inventory/sku/list` 的 `skuId` | ```text // 响应 data { "skuId": 1012, "skuName": "打包纸箱", "specification": "60×40×40", "unitName": "个", "availableStock": 320 // 库房剩余可用 = 全局实物 − 各大区已分配剩余 } ``` ### POST /supplier/wms/apply/create · 提交物资申领 **新增** · 调用方:仅负责人 | 请求字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `skuId` | Long | 是 | 物资 SKU ID | | `teamId` | Integer | 条件 | 申领大区。**负责人只带 1 个大区时可以不传**;带多个大区必传,不传报「请选择申领大区」。传了非自己带的大区报「无权为该大区申领」 | | `quantity` | Integer | 是 | 申领数量,1 ~ 10000,且不能超过 `availableStock` | | `voucherUrls` | String | 是 | **JSON 数组的字符串**,不是数组对象。1 ~ 5 个元素,整串 ≤ 512 字符 | | `remark` | String | 否 | 备注,≤ 255 字符 | > [!NOTE] > **🔴 voucherUrls 是字符串,不是数组** > > 后端字段类型是 `String`,前端要自己 `JSON.stringify` 后再放进 body: > > ```text // ✅ 正确 { "skuId": 1012, "quantity": 50, "voucherUrls": "[\"https://oss/a.jpg\",\"https://oss/b.jpg\"]" } // ❌ 会报「凭证图片格式错误」 { "skuId": 1012, "quantity": 50, "voucherUrls": ["https://oss/a.jpg"] } ``` > > **512 字符是整串长度**,包含引号和逗号。OSS 地址长的话 5 张可能放不下 —— 建议前端上传后存相对路径或短地址,并在提交前自己先量一次 `voucherUrls.length`,超了就地提示,别等后端报错。 响应 `data` 为 `null`,成功即可返回列表页刷新。**接口不返回新建的申领单 ID**,需要跳详情的话得先刷列表拿。 ### POST /supplier/wms/apply/cancel · 撤销物资申领 **新增** · 调用方:仅申领人本人 入参 `{ "id": 申领单ID }`,响应 `data` 为 `null`。 只有 **申领人本人**且单据处于 **待审核**时才能撤销。不满足条件统一返回 `申领单状态已变更,请刷新后重试`(不区分「不是你的单」和「已经被审了」,避免泄露他人单据)。收到这个提示应当刷新列表。 ## 新增接口 · 列表与详情 ### POST /supplier/wms/apply/page · 申领列表 **新增** · 调用方:负责人 / 仓管 | 请求字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `index` | Integer | 否 | 页码,默认 1 | | `size` | Integer | 否 | 页大小,默认 10 | | `status` | Integer | 否 | 10/20/30/40,不传查全部。传其他值报「申领状态不合法」 | | `roleName` | String | 否 | 当前视角角色名。传 `"打包负责人"` 时强制只看自己带的大区 | > [!NOTE] > **roleName 的作用范围** > > - **非仓管账号**:无论传不传 `roleName`,永远只能看到自己带的大区的单。 > - **仓管账号**:默认看全部;只有当它 **同时也是负责人**、且传了 `roleName="打包负责人"` 时,才收窄到自己带的大区。 > - 字符串必须 **精确等于 `打包负责人`**,拼错就当没传。这和现有 `/supplier/wms/inventory/team/list` 的 `roleName` 是同一套约定,小程序当前传的 `"仓库管理员"` 走的就是默认(不收窄)分支,角色切换器的取值直接复用即可。 响应 `records` 单项: ```json { "id": 88, "applyNo": "SL20260825001", // SL + yyyyMMdd + 3位序列 "teamName": "昆明一区", "skuName": "打包纸箱", // 即「物资类别」 "specification": "60×40×40", "unitName": "个", "applyQuantity": 50, "issuedQuantity": 45, // 仅「已同意」有值,其余状态恒为 null "status": 20, "statusDesc": "已同意", // 后端已给中文,直接展示 "createTime": "2026-08-25 09:12:33", "auditTime": "2026-08-25 10:04:51", "canCancel": false, // 撤销按钮显隐 "canAudit": false // 同意/拒绝按钮显隐 } ``` > [!NOTE] > **✅ 按钮显隐直接用 canCancel / canAudit** > > 这两个布尔是后端按「状态 + 当前账号」算好的,**不要在前端用 `status===10 && applicantId===me` 自己推**,列表里也没返回 `applicantId`。同一条单据对不同人返回的值不同,这是预期的。 ### POST /supplier/wms/apply/detail · 申领详情 **新增** · 调用方:仓管 / 本大区负责人 入参 `{ "id": 申领单ID }`。返回值是列表项的 **超集**,在上面所有字段之外多出: | 字段 | 类型 | 说明 | |----|----|----| | `remark` | String | 申领备注,无备注时是空字符串 | | `voucherInfo` | String\[\] | **这里是真数组**,可直接喂给图片预览组件(提交时是字符串,返回时是数组,注意不对称) | | `applicantName` | String | 申领人姓名快照 | | `auditorName` | String | 审核人姓名,未审核时是空字符串 | | `rejectReason` | String | 拒绝原因,未拒绝或仓管没填时是空字符串 | | `cancelTime` | DateTime | 撤销时间,未撤销时为 null | 非仓管账号查非自己大区的单,返回 `申领记录不存在`(不是无权限),前端按「记录不存在」处理即可。 ## 新增接口 · 仓管审核 ### POST /supplier/wms/apply/approve · 同意申领 **新增** · 调用方:仅仓管 | 请求字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `id` | Long | 是 | 申领单 ID | | `issuedQuantity` | Integer | 是 | **实际出库数量**,可以少发但不能多发:必须 ≥1、≤ `applyQuantity`、≤ 当前库房剩余可用 | 审批弹窗建议把 `issuedQuantity` 默认填成 `applyQuantity`,允许仓管改小。响应 `data` 为 `null`。 ### POST /supplier/wms/apply/reject · 拒绝申领 **新增** · 调用方:仅仓管 入参 `{ "id": 申领单ID, "rejectReason": "库存留给昆明二区" }`。`rejectReason` 后端 **不强制**(≤255 字符),如果产品要求必填请前端自己拦。响应 `data` 为 `null`。 > [!NOTE] > **并发下的统一反馈** > > 同意/拒绝/撤销都是 CAS 更新,只有单据仍是「待审核」才会成功。两个仓管同时点,后点的那个会拿到 `申领单状态已变更,请刷新后重试`。**收到这条文案时请自动刷新列表并关闭弹窗**,否则用户会一直点一直报错。 ## 既有接口的行为变化 · 必须跟进 这三处不是新接口,是本次一并收口的老接口。**前端不跟会出报错或数字对不上。** ### POST /supplier/wms/inventory/inbound/create · 新增入库补上了仓管校验 **变更** 该接口此前 **完全没有角色校验**,任何登录账号都能调。现在非仓管调用会返回 `非仓库管理员,无进销存操作权限`。 **团队选择逻辑不变**,仓管依旧可以选库房或任意大区入库。打包负责人此前若能进到这个页面,现在会被拦下,入口按角色隐藏即可。 ### POST /supplier/wms/inventory/outbound/create · 新增出库按来源团队收敛权限 **破坏性变更** | 出库来源 | 谁能操作 | 不满足时的报错 | |----|----|----| | `teamId = 0`(库房) | 仅仓库管理员 | `非仓库管理员,无进销存操作权限` | | `teamId > 0`(大区) | 仓库管理员,或 **该大区的打包负责人** | `无该大区出库权限` | 打包负责人登记本大区物资消耗的能力 **保留**,但不能再从库房直接出货。出库页的团队选择器建议按角色渲染:仓管给全量,负责人只给 `/supplier/wms/inventory/team/list` 传 `roleName="打包负责人"` 后返回的那几个大区。 ### POST /supplier/wms/inventory/stock/page · 库存页「库房」一行的口径变了 **破坏性变更** `teamId = 0` 那一行的 `teamStock`,从「全局实物库存」改成了「**库房剩余可用**」: ```text 库房剩余可用 = 全局实物库存 − Σ(各大区批次剩余) ``` 大区行的 `teamStock` 语义不变(该大区剩余)。响应额外带一个 `availableStock` 字段,值同库房剩余可用,各行都有。 > [!NOTE] > **数字会变小,这是预期的** > > 只要有物资发到过大区,库房那一行的数就会比改版前小。这正是需求要的「发放给大区的部分不再在库房重复展示」。**文案上建议把库房行标成「库房剩余可用」,避免仓管以为库存丢了。** > > 注意 `physicalStock`(全局实物)字段在手机端接口被 `@JsonIgnore` 掉了,响应里没有,前端拿不到也不需要拿。 ## 搭页面时会用到的既有接口 申领相关页面不需要新的下拉数据接口,直接复用现成的两个: | 用途 | 接口 | 说明 | |----|----|----| | 选申领大区 | `/supplier/wms/inventory/team/list` | 传 `{"roleName":"打包负责人"}` 拿到「库房 + 自己带的大区」。**申领页要把 `teamId=0` 的库房项过滤掉**,只留大区。返回只有 1 个大区时可以隐藏选择器、`create` 不传 `teamId` | | 选物资 | `/supplier/wms/inventory/sku/list` | 传 `{"skuName":"纸箱"}` 或 `{"specification":"60"}` 模糊搜。返回 `skuId / skuName / specification / unitName`。**「物资类别」对应 `skuName`,「物资规格」对应 `specification`** | ### 推荐的提交页交互顺序 1. `team/list`(带 roleName)→ 过滤掉库房 → 大区下拉;只有一个就选中并隐藏 2. `sku/list` → 物资类别 / 规格下拉,选中拿到 `skuId` 3. `apply/sku/stock`(`id` 传 skuId)→ 展示「库房剩余可用 N 个」,作为数量输入框的 max 4. 上传凭证 → `JSON.stringify(urls)`,本地先校验张数 ≤5、长度 ≤512 5. `apply/create` → 成功后回列表页刷新 ## 报错文案速查 所有失败响应的 `msg` 都是可以直接 toast 的中文,无需前端翻译。下表用于确认交互分支: | msg | 出现在 | 建议处理 | |----|----|----| | `无物资申领权限` | 全部申领接口 | 既不是仓管也不是负责人,隐藏整个申领入口 | | `非仓库管理员,无进销存操作权限` | approve / reject / 库房出入库 | 隐藏审核按钮 | | `无该大区出库权限` | outbound/create | 出库页团队下拉按角色收窄 | | `请选择申领大区` | create | 负责人带多个大区却没传 teamId,前端应设为必填 | | `无权为该大区申领` | create | teamId 不在自己带的大区里,一般是前端串了数据 | | `物资不存在或已停用` | sku/stock、create | 刷新物资下拉 | | `申领数量超过库房剩余可用库存` | create | 重新拉 sku/stock 刷新上限 | | `申领数量不能大于10000` | create | 前端 max 设 10000 | | `请至少上传1张凭证` | create | 凭证必填 | | `凭证图片最多上传5张` | create | 上传组件限 5 张 | | `凭证图片URL文本不能超过512个字符` | create | 见 03 节说明,提交前本地量长度 | | `凭证图片格式错误` | create | 大概率是传了数组而不是字符串 | | `申领状态不合法` | page | status 只能传 10/20/30/40 | | `申领记录不存在` | detail | 越权查看或单据已删,回列表 | | `申领单状态已变更,请刷新后重试` | cancel / approve / reject | **自动刷新列表并关弹窗** | | `实际出库数量不能超过申领数量` | approve | 审批弹窗 max = applyQuantity | | `实际出库数量超过库房剩余可用库存` | approve | 刷新后重审 | ## 联调注意 - **测试账号要准备两个**:一个是某大区的 `leader_id`(负责人),一个命中仓管角色。同一个账号两种身份都有时,很多分支测不出来。 - **时间字段格式**与现有进销存接口一致,`createTime` / `auditTime` / `cancelTime` 都可能为 null,渲染前判空。 - **提交接口不回 ID**:`create` 的 `data` 是 `null`,需要「提交成功后跳详情」的话,得先刷列表取第一条。 - **voucherInfo 进出不对称**:提交传字符串,详情返回数组。复用同一个表单模型时注意转换。 - **先看库存页再对申领**:库房口径变了,联调时先确认 `stock/page` 库房行的数和你预期的「剩余可用」一致,再测申领链路,否则数字对不上会误判成申领的 bug。 --- # 员工处罚待确认弹窗 > pending-popup 接口:每日 14:00 批次、未登录则登录即弹、多端只弹一次均由后端判定;含调用时机、14 点定时参考实现与弹窗交互说明 - 类型:前端对接 - 分类:仓库配货 / 打包 - 端:仓管小程序 - 后端服务:supplier - 状态:已上测 - 更新:2026-10-09 - 网页版:https://doc.xfun.fun/wms-punish-popup-api ## 总览 - 新增 **1 个接口** `POST /wms-punish-appeal/pending-popup`,无请求体,返回「是否弹窗 + 待处理笔数 + 待确认金额」。 - **「该不该弹」全部由后端判断**(14:00 批次、每批次只弹一次、多端去重)。前端只负责:在约定时机调接口,`showFlag=1` 就弹,否则什么都不做。 - 前端 **不需要**本地记录「今天弹过没有」,也不需要自己判断时间是否过了 14:00,重复调用是安全的。 - 处罚数据口径:当前登录人作为责任人、**待确认**、**不在申诉中**,且 **创建不满 48 小时**(仍在申诉期内)的处罚。`10-12 调整` 超过 48 小时已不能申诉,也会被系统自动确认,因此不再提醒。 ## 接口定义 ### POST /wms-punish-appeal/pending-popup **新增** 查询处罚待确认弹窗。鉴权方式与「我的处罚列表」 `/wms-punish-appeal/list` 相同(Header 带 `Authorization`)。 #### 请求参数 无。Body 可传空对象 `{}` 或不传。 #### 响应 data 字段 | 字段 | 类型 | 说明 | |----|----|----| | `showFlag` | Integer | 是否弹窗:**1 = 弹**,0 = 不弹 | | `pendingCount` | Long | 待处理处罚笔数(showFlag=0 时为 0) | | `pendingAmount` | BigDecimal | 待确认金额,单位元,已保留 2 位小数(showFlag=0 时为 0) | #### 响应示例:需要弹窗 ```json { "code": 0, "msg": "success", "data": { "showFlag": 1, "pendingCount": 3, "pendingAmount": 150.00 } } ``` #### 响应示例:不弹窗 ```json { "code": 0, "msg": "success", "data": { "showFlag": 0, "pendingCount": 0, "pendingAmount": 0 } } ``` > [!NOTE] > 判断是否弹窗 **只看 `showFlag`**,不要用 `pendingCount > 0` 判断:已经弹过的批次后端会返回 showFlag=0、pendingCount=0,即使用户仍有待处理处罚。 > [!WARNING] > 金额 JSON 中是数字,前端展示时请统一格式化为两位小数(`Number(v).toFixed(2)`),例如 `150` 显示为 `¥150.00`。 ## 调用时机(前端要做的) | 时机 | 说明 | |----|----| | ① 登录成功后 | 对应需求「未登录的用户,登录时立即弹」 | | ② 小程序 `onShow`(App 级或首页) | 带着本地 token 冷启动、从后台切回前台都算「进入系统」;也用于补偿 14:00 时小程序在后台、定时器没跑的情况 | | ③ 在线定时:当天 14:00 + 随机 0~60 秒 | 对应需求「已登录的用户 14:00 弹一次」。加随机延迟是为了避免所有在线用户同一秒请求 | #### 参考实现(伪代码,按项目框架调整) ```js // 统一入口:任何时机都只调这个函数,后端负责去重 async function checkPunishPopup() { if (!getToken()) return; const res = await request.post('/wms-punish-appeal/pending-popup', {}); const d = res && res.data; if (d && d.showFlag === 1) { showPunishPopup(d.pendingCount, Number(d.pendingAmount).toFixed(2)); } } // 时机③:在线时到 14:00 触发一次 let popupTimer = null; function scheduleTwoPm() { clearTimeout(popupTimer); const now = new Date(); const target = new Date(now); target.setHours(14, 0, 0, 0); if (now >= target) target.setDate(target.getDate() + 1); // 今天已过 14 点则排到明天 const jitter = Math.floor(Math.random() * 60 * 1000); // 0~60 秒随机打散 popupTimer = setTimeout(() => { checkPunishPopup(); scheduleTwoPm(); }, target - now + jitter); } // 登录成功回调里:checkPunishPopup(); scheduleTwoPm(); // onShow 里: checkPunishPopup(); scheduleTwoPm(); // 切回前台时重排定时器 // 退出登录 / onHide:clearTimeout(popupTimer); ``` > [!NOTE] > 设备本地时间不准没关系:定时器只决定「什么时候问一次」,弹不弹以后端(北京时间)为准。最坏情况是晚一点在下次 onShow 时弹出。 ## 后端判定规则(了解即可) 后端以 **「最近一个已经过去的 14:00」**作为提醒批次,每个用户每个批次最多弹一次:当前 ≥ 今天 14:00 → 批次是今天;当前 \< 今天 14:00 → 批次是昨天。 #### 一个用户的一天(假设一直有 2 笔待确认处罚) | 时间 | 发生了什么 | |----|----| | 09:30 | 登录 → 昨天的批次已经弹过 → 不弹 | | 14:00:23 | 在线定时器触发 → 今天批次未弹 → 弹(显示 2 笔) | | 15:10 | 切回前台 onShow → 今天批次已弹 → 不弹 | | 16:00 | 另一台手机登录同一账号 → 今天批次已弹 → 不弹(多端只弹一次) | | 次日 08:50 | 登录 → 批次仍是「昨天」,已弹过 → 不弹;等到当天 14:00 再弹 | #### 其他情况 - 昨天 14:00 后一直没上线、今天上午才登录 → 昨天批次没弹过 → 登录即弹。 - 14:00 时没有待处理处罚 → 不弹,且 **不占用**本批次;15:00 新来了一笔处罚,下次 onShow 时 → 弹。 - 只有「申诉中」的处罚 → 不弹(员工此时无需操作)。 ## 弹窗交互 | 元素 | 内容 / 行为 | |----|----| | 标题 | 您有处罚待确认 | | 说明 | 您当前有处罚订单待确认,请及时前往处理。逾期未确认,系统将按规则自动确认。 | | 待处理订单 | `{pendingCount}`笔 | | 待确认金额 | ¥ `{pendingAmount}`(两位小数) | | 提示条 | 请尽快确认处罚信息,如有异议可发起申诉 | | 【前往处理】 | 关闭弹窗 → 跳转「我的处罚」列表,**默认筛选待确认**(列表接口 `/wms-punish-appeal/list` 传 `status=0`) | | 【稍后处理】 | 直接关闭,不调接口 | | 底部小字 | 本弹窗仅作消息提醒,具体处罚信息以处罚详情为准 | | 自动关闭 | 弹出后 **3 秒无任何操作**自动关闭;用户在 3 秒内 **触碰弹窗任意位置**即取消倒计时,之后只能手动关闭 | > [!NOTE] > 文案全部前端写死,接口只返回两个数字。弹窗关闭(无论哪种方式)都 **不需要**回调后端:后端在返回 showFlag=1 的同时就已记为「已弹」。 ## 联调注意 - 测试环境已部署,用员工账号(有待确认处罚的责任人)登录即可联调。 - 同一账号同一批次只会弹一次。**想重复测试,调用下面的重置接口即可**,任何时间点重置后都能立即再弹。 #### 测试环境专用:重置弹窗标记 `10-12 新增` ### POST /test/wms-punish-popup/reset - 无请求体,Header 带当前员工的 `Authorization`(与弹窗接口同一个 token),只清 **当前登录账号**的标记。 - 成功返回 `{"code":0,"msg":"success"}`;生产环境调用会报「不允许操作!」,**正式代码里不要调用**。 #### 调试循环 ```text POST /test/wms-punish-popup/reset // 清标记 POST /wms-punish-appeal/pending-popup // → showFlag=1,弹窗 POST /wms-punish-appeal/pending-popup // → showFlag=0(已弹过) POST /test/wms-punish-popup/reset // 再清,下一次又会弹 … ``` 调试期间可以在开发版里临时加一个「重置弹窗」按钮调用它;也可以用 curl: ```bash curl -X POST '<测试环境域名>/test/wms-punish-popup/reset' -H 'Authorization: <员工token>' ``` - 没有 **48 小时内**待确认处罚的账号永远返回 showFlag=0。测试库老数据大多已超过 48 小时,**联调前请先造新数据**:在后台「巡线处罚」给该员工创建一笔处罚(新建默认待确认),48 小时内可反复配合重置接口测试。 - 弹窗只统计 48 小时内的处罚,「我的处罚」列表的待确认筛选会多出已过申诉期、尚未被自动确认的处罚(线上最多多停留约一天;测试环境不跑自动确认,会多出很多)。所以 **列表条数 ≥ 弹窗笔数是正常的**。 --- # 冷库暂存花材接口 > 改版前后接口逐项增删改对照 - 类型:版本差异 - 分类:冷库暂存花材 - 更新:2026-07-15 - 网页版:https://doc.xfun.fun/%E5%86%B7%E5%BA%93%E6%9A%82%E5%AD%98%E8%8A%B1%E6%9D%90%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3-%E6%96%B0%E6%97%A7%E7%89%88%E6%9C%AC%E5%B7%AE%E5%BC%82 | | 旧版 · 首次部署 | 新版 · 当前线上 | |----|----|----| | 接口数 | 26(旧页面误标 28) | 24 | | 依据提交 | `dc00890ece` | `b5f20cfd24` | 变化统计:删除接口 **5** 个 · 新增接口 **3** 个 · **13** 个接口有字段/描述变化 · 枚举变化 **7** 组(删 3 组、改 4 组)。 两版之间经历了一次方案重构:**取消暂存需求提报和次日出库确认**(改为线下沟通 + 联系记录留痕)、**出库由“运营端操作即完成”改为两段式**(客服建待出库任务 → 仓库 PDA 逐包扫码 / 小程序整单确认,期间可撤销)、**冷库状态收敛到冷库明细表**(包裹表仅新增 40 拆箱重打,不再有 15/30 中间态)、裸花登记改为全二维码识别。 ## 运营端(删 4 · 改 9) ### POST /cold/demand/save · 暂存需求提报 **删除** · 运营端 「暂存需求提报」功能整体取消:客服与仓库改为线下沟通,暂存以仓库扫码登记为准,系统不做需求提报。 #### 入参明细(9 个字段) · 返回 R\ | 变化 | 字段 | 类型 | 必填 | 说明 | |------|-------------------|---------|------|-----------------------------------------| | 删除 | purchaserId | Long | 必填 | 客户(采购方)id | | 删除 | sendNo | String | 选填 | 发货单号 | | 删除 | storeType | Integer | 必填 | 暂存类型 1包裹 2裸花 | | 删除 | expectPackageNum | Integer | 选填 | 预计暂存包裹件数 | | 删除 | expectBunchNum | Integer | 选填 | 预计裸花扎数 | | 删除 | shipWay | Integer | 必填 | 发货方式 1指定日期 2客户通知 | | 删除 | planShipDate | String | 选填 | 计划发货日期 yyyy-MM-dd(指定日期时必填) | | 删除 | suggestLocationNo | String | 选填 | 建议暂存货位号 | | 删除 | serviceRemark | String | 选填 | 客服备注 | ### POST /cold/demand/page · 暂存需求提报列表 **删除** · 运营端 同上,需求提报功能整体取消。 #### 入参明细(2 个字段) · 返回 R\\> | 变化 | 字段 | 类型 | 必填 | 说明 | |------|--------------|---------|------|-----------------------------------------| | 删除 | demandStatus | Integer | 选填 | 需求状态 10待仓库登记 20已登记 30已作废 | | 删除 | storeType | Integer | 选填 | 暂存类型 1包裹 2裸花 | ### POST /cold/demand/register · 仓库登记 **删除** · 运营端 同上,需求登记转库存的环节随需求提报一起取消,入库统一走扫码。 #### 入参明细(4 个字段) · 返回 R\ | 变化 | 字段 | 类型 | 必填 | 说明 | |------|------------|---------|------|----------------------------------| | 删除 | demandId | Long | 必填 | 暂存需求id | | 删除 | locationNo | String | 选填 | 实际暂存货位号(空则沿用建议货位) | | 删除 | bunchNum | Integer | 选填 | 裸花实际扎数(裸花暂存必填) | | 删除 | remark | String | 选填 | 备注(空则沿用客服备注) | ### POST /cold/stock/confirm · 次日出库确认 **删除** · 运营端 「次日出库确认」流程取消:合肥客服线下联系客户,系统只保留联系记录留痕(/cold/contact/save),不再驱动状态迁移。 #### 入参明细(5 个字段) · 返回 R\ | 变化 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----|----| | 删除 | coldStockId | Long | 必填 | 冷库库存id | | 删除 | shipConfirm | Integer | 必填 | 确认结果 1今日出库 2继续暂存 3联系不上 4取消暂存 | | 删除 | nextPlanShipDate | String | 选填 | 下次计划发货日期(继续暂存) yyyy-MM-dd | | 删除 | content | String | 选填 | 确认备注/联系内容 | | 删除 | imageUrls | String | 选填 | 图片url(逗号分隔) | ### POST /cold/stock/page · 冷库暂存库存列表 **变更 13 处** · 运营端 #### 接口级变化 | 属性 | 旧版 | 新版 | |------|----------------------------------|--------------------------------------| | 描述 | 客服查看仓库登记后的库存。分页。 | 客服查看仓库扫码入库后的库存。分页。 | #### 返回字段变化 | 变化 | 字段 | 类型 | | 说明 | |----|----|----|----|----| | 新增 | expressName | String | — | 发货物流名称(原发货单物流,直接出库沿用展示) | | 新增 | linkReceiveName | String | — | 关联发货单收货人 | | 新增 | linkReceiveTel | String | — | 关联发货单收货电话 | | 新增 | linkReceiveAddress | String | — | 关联发货单收货地址 | | 新增 | linkShelfSn | Integer | — | 关联发货单主货位号 | | 新增 | linkSubShelfSn | String | — | 关联发货单子货位号 | | 新增 | linkZoneShelf | String | — | 关联发货单区-货位号 | | 新增 | outTaskId | Long | — | 最新出库任务id(outTaskStatus=10 时用于撤销) | | 删除 | financeStatus | Integer | — | 合并发货账务状态 0待处理 10已处理 | | 修改 | purchaserMobile | 说明变更 | 客户手机号 → 客户注册手机号 | | 修改 | linkPurchaserMobile | 说明变更 | 关联发货单客户电话 → 关联发货单客户注册手机号 | | 修改 | outTaskStatus | 说明变更 | 出库任务状态 → 出库任务状态(最新非撤销任务),见枚举 | ### POST /cold/stock/stat · 冷库暂存库存统计 **变更 6 处** · 运营端 #### 接口级变化 | 属性 | 旧版 | 新版 | |----|----|----| | 描述 | 顶部统计卡片。入参同 /cold/stock/page(同筛选条件统计)。 | 顶部统计卡片。**全局口径**,不随列表筛选条件联动(在库客户数/在库包裹数为当前全量,今日出库客户数与小程序首页同口径)。 | #### 入参变化 | 变化 | 字段 | 类型 | 必填 | 说明 | |------|--------------|---------|------|------------| | 删除 | keyword | String | 选填 | 同库存列表 | | 删除 | storeType | Integer | 选填 | 同库存列表 | | 删除 | stockStatus | Integer | 选填 | 同库存列表 | | 删除 | storeRoundNo | String | 选填 | 同库存列表 | | 删除 | planShipDate | String | 选填 | 同库存列表 | ### POST /cold/contact/save · 新增联系记录 **变更 1 处** · 运营端 #### 入参变化 | 变化 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----|----| | 修改 | shipConfirm | 说明变更 | 联系结果 1今日出库 2继续暂存 3联系不上 4取消暂存 → 联系结果 1今日出库 2继续暂存 3联系不上 | ### POST /cold/out/today-send-order · 查询当天发货单 **变更 7 处** · 运营端 #### 接口级变化 | 属性 | 旧版 | 新版 | |----|----|----| | 描述 | 按客户账号 + 当前场次查询。入参仅 coldStockId。 | 默认按冷库库存客户 + 当前场次查询发货单;可按客户注册手机号搜索其他客户的当天发货单。 | | 返回类型 | R\ | R\\> | #### 入参变化 | 变化 | 字段 | 类型 | 必填 | 说明 | |------|--------|--------|------|----------------------------------------------| | 新增 | mobile | String | 选填 | 客户注册手机号,不传时查暂存客户自己的发货单 | #### 返回字段变化 | 变化 | 字段 | 类型 | | 说明 | |----|----|----|----|----| | 新增 | shelfSn | Integer | — | 主货位号 | | 新增 | subShelfSn | String | — | 子货位号 | | 新增 | zoneShelf | String | — | 区-货位号 | | 修改 | purchaserMobile | 说明变更 | 客户手机号 → 客户注册手机号 | ### POST /cold/out/link · 关联当天发货单出库 **变更 1 处** · 运营端 #### 接口级变化 | 属性 | 旧版 | 新版 | |----|----|----| | 描述 | 含拆箱重打。**操作即完成冷库出库**:任务直接落「已出库(20)」,冷库明细置已出库;非拆箱重打时包裹 express_status 30→10(打包完成)并回填当天发货单信息, **随当天发货单走正常物流出库扫码**,重量/运费/面单由正常发货流程采集。整批出库后库存「已出库(50)」;部分出库剩余回「待发货(10)」并清空发货确认。 | 含拆箱重打。常规出库生成待出库任务并锁定所选包裹,实际出库由 PDA 逐包扫码或小程序点击出库完成。拆箱重打时操作即完成出库,不可撤销。 | ### POST /cold/out/direct · 直接出库 **变更 3 处** · 运营端 #### 接口级变化 | 属性 | 旧版 | 新版 | |----|----|----| | 描述 | 不关联当天发货单。**操作即终态**:任务直接落「已出库(20)」,包裹 express_status 30→20(发货完成),实际重量/运费置0走线下对账。整批出库后库存「已出库(50)」;部分出库剩余回「待发货(10)」并清空发货确认。 | 不关联当天发货单。生成待出库任务并锁定所选包裹,实际出库由 PDA 逐包扫码或小程序点击出库完成。发货物流由服务端根据原发货单解析,前端不传。 | #### 入参变化 | 变化 | 字段 | 类型 | 必填 | 说明 | |------|-------------|--------|------|----------------------------------------------| | 删除 | expressId | Long | 必填 | 发货物流id(必须为启用中的物流) | | 删除 | expressName | String | 选填 | 发货物流名称,由服务端按物流id回填,前端可不传 | ### POST /cold/out/cancel · 撤销出库任务 **变更 1 处** · 运营端 #### 接口级变化 | 属性 | 旧版 | 新版 | |----|----|----| | 描述 | **当天反出库**:仅可撤销当天完成的出库任务;拆箱重打不可撤;关联出库的包裹一旦被正常物流扫码发货(express_status=20)不可撤。撤销后包裹退回冷库暂存(30)并还原原场次,冷库明细回暂存中,库存回「待发货(10)」并回滚出库计数。历史遗留的「待出库」任务仍支持旧撤销逻辑。 | 仅可撤销待出库(10)且尚无包裹完成出库的任务。撤销后任务置已撤销(30),释放锁定包裹;无其他待出库任务时,库存回退为待发货(10)。 | ### POST /cold/out/records · 历史出库记录 **变更 10 处** · 运营端 #### 入参变化 | 变化 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----|----| | 修改 | taskStatus | 说明变更 | 任务状态 20已出库 30已撤销(空=全部;10/15为旧流程遗留) → 任务状态 10待出库 20已出库 30已撤销(空=全部) | #### 返回字段变化 | 变化 | 字段 | 类型 | | 说明 | |----|----|----|----|----| | 新增 | linkReceiveName | String | — | 关联发货单收货人 | | 新增 | linkReceiveTel | String | — | 关联发货单收货电话 | | 新增 | linkReceiveAddress | String | — | 关联发货单收货地址 | | 新增 | linkShelfSn | Integer | — | 关联发货单主货位号 | | 新增 | linkSubShelfSn | String | — | 关联发货单子货位号 | | 新增 | linkZoneShelf | String | — | 关联发货单区-货位号 | | 修改 | purchaserMobile | 说明变更 | 暂存客户手机号 → 暂存客户注册手机号 | | 修改 | linkPurchaserMobile | 说明变更 | 关联发货单客户电话 → 关联发货单客户注册手机号 | | 修改 | taskStatus | 说明变更 | 任务状态 20已出库 30已撤销(10/15为旧流程遗留) → 任务状态,见枚举 | ### POST /cold/stock/pending-packages · 未入库包裹清单 **变更 2 处** · 运营端 #### 接口级变化 | 属性 | 旧版 | 新版 | |----|----|----| | 描述 | 发货单下已打包(状态10)但尚未扫入冷库的包裹清单,供客服核对预计/已入/未入件数并催办物流入库。sendNo 与 coldStockId 二选一(传 coldStockId 时取其原发货单号)。 | 发货单下已打包(状态10)但尚未扫入冷库的包裹清单,供客服核对已入/未入件数并催办物流入库。sendNo 与 coldStockId 二选一(传 coldStockId 时取其原发货单号)。 | #### 返回字段变化 | 变化 | 字段 | 类型 | | 说明 | |------|------------------|---------|-----|------------------------------------| | 删除 | expectPackageNum | Integer | — | 客服提报预计暂存件数(无提报单为空) | ## PDA端(增 1 · 改 2) ### POST /cold/pda/out/scan · 扫码出库 **新增** · PDA端 出库改为「两段式」:运营端只生成待出库任务,仓库 PDA 逐包扫码执行实际出库(最后一包扫完任务自动完成)。 #### 入参明细(4 个字段) · 返回 Response\ | 变化 | 字段 | 类型 | 必填 | 说明 | |------|-------------|--------|------|-----------------------------| | 新增 | outTaskId | Long | 必填 | 出库任务id | | 新增 | codeContent | String | 必填 | 包裹码(包裹id) | | 新增 | userId | Long | 注入 | 后端注入,前端不传 | | 新增 | userName | String | 注入 | 后端根据登录人解析,前端不传 | ### POST /cold/pda/scan · 扫码入库 **变更 1 处** · PDA端 #### 入参变化 | 变化 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----|----| | 修改 | userName | 说明变更 | 操作人姓名 → 后端根据登录人解析,前端不传 | | 修改 | userName | 注入变更 | - → inject | ### POST /cold/pda/stock/detail · 库存详情 **变更 3 处** · PDA端 #### 返回字段变化 | 变化 | 字段 | 类型 | | 说明 | |------|-----------------|--------|-----|------------------| | 新增 | purchaserId | Long | — | 客户id | | 新增 | remark | String | — | 客户备注 | | 新增 | scanPackageCode | String | — | 本次扫码的包裹码 | ## 小程序端(删 1 · 增 2 · 改 2) ### POST /cold/mini/customer/search · 客户搜索 **删除** · 小程序端 早期「手动选客户登记裸花」交互的残留:最终方案裸花登记全部由花材二维码识别客户,无手动搜索场景,且该接口可模糊枚举客户手机号,属无消费方的风险面。 #### 入参明细(1 个字段) · 返回 Response\\> | 变化 | 字段 | 类型 | 必填 | 说明 | |------|---------|--------|------|----------------------| | 删除 | keyword | String | 必填 | 关键词(手机号或昵称) | ### POST /cold/mini/out/finish · 点击出库 **新增** · 小程序端 两段式出库的小程序执行入口:裸花任务整单完成,包裹任务批量完成剩余待出库包裹。 #### 入参明细(3 个字段) · 返回 Response\ | 变化 | 字段 | 类型 | 必填 | 说明 | |------|-----------|--------|------|-----------------------------| | 新增 | outTaskId | Long | 必填 | 出库任务id | | 新增 | userId | Long | 注入 | 后端注入,前端不传 | | 新增 | userName | String | 注入 | 后端根据登录人解析,前端不传 | ### POST /cold/mini/package/scan · 包裹扫码入库 **新增** · 小程序端 仓管小程序也支持包裹扫码入库(质检员/区域负责人/仓库管理员,不受物流商配置限制),与 PDA 扫码入库互补。 #### 入参明细(5 个字段) · 返回 Response\ | 变化 | 字段 | 类型 | 必填 | 说明 | |------|-------------|--------|------|------------------------------------------| | 新增 | codeContent | String | 必填 | 扫码内容(包裹id) | | 新增 | coldStockId | Long | 选填 | 累计入库的冷库库存id(连续扫同一客户时传) | | 新增 | locationNo | String | 选填 | 暂存货位号(新建库存时使用) | | 新增 | userName | String | 注入 | 后端根据登录人解析,前端不传 | | 新增 | userId | Long | 注入 | 后端注入,前端不传 | ### POST /cold/mini/out/page · 暂存出库列表 **变更 4 处** · 小程序端 #### 接口级变化 | 属性 | 旧版 | 新版 | |----|----|----| | 描述 | 今日出库任务查询视图(运营端操作即完成,列表以已出库为主)。分页。 | 待出库/已出库任务列表(默认不含已撤销),含包裹扫码进度。分页。 | #### 入参变化 | 变化 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----|----| | 修改 | taskStatus | 说明变更 | 任务状态 20已出库(空=全部;10/15为旧流程遗留) → 任务状态 10待出库 20已出库(空=全部) | #### 返回字段变化 | 变化 | 字段 | 类型 | | 说明 | |----|----|----|----|----| | 新增 | scannedPackageNum | Integer | — | 已扫码出库包裹数(包裹暂存,展示扫码进度;拆箱重打任务恒0,前端按 boxRepackFlag 隐藏) | | 修改 | taskStatus | 说明变更 | 任务状态 20已出库 30已撤销(10/15为旧流程遗留) → 任务状态,见枚举 | ### POST /cold/mini/bare/register · 裸花暂存登记 **变更 5 处** · 小程序端 #### 入参变化 | 变化 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----|----| | 删除 | purchaserId | Long | 选填 | 客户id,由二维码识别,可不传 | | 删除 | locationNo | String | 选填 | 暂存货位号,由二维码识别,可不传 | | 删除 | storeRoundNo | String | 选填 | 暂存场次 yyyy-MM-dd,由二维码识别,可不传 | | 删除 | bunchNum | Integer | 选填 | 裸花扎数,由二维码关联订单汇总,可不传 | | 修改 | userName | 说明变更 | 操作人姓名 → 后端根据登录人解析,前端不传 | | 修改 | userName | 注入变更 | - → inject | ## 枚举字典变化(全局)(删 3 组 · 改 4 组) | 变化 | 枚举 | 旧版 | 新版 | |----|----|----|----| | 删除 | shipWay 发货方式 | 1 指定日期 · 2 客户通知 | (整组删除) | | 删除 | demandStatus 需求状态 | 10 待仓库登记 · 20 已登记 · 30 已作废 | (整组删除) | | 删除 | financeStatus 合并发货账务状态 | 0 待处理 · 10 已处理 | (整组删除) | | 修改 | stockStatus 库存状态 | 0 待入库 · 10 待发货 · 20 待关联 · 50 已出库(30 已关联/40 待出库为旧流程遗留,新逻辑不再写入) | 10 待发货 · 40 待出库 · 50 已出库 | | 修改 | shipConfirm 发货确认/联系结果 | 1 今日出库 · 2 继续暂存 · 3 联系不上 · 4 取消暂存 | 1 今日出库 · 2 继续暂存 · 3 联系不上 | | 修改 | taskStatus 出库任务状态 | 20 已出库 · 30 已撤销(运营端操作即终态;10 待出库/15 出库中为旧流程遗留) | 10 待出库(可撤销) · 20 已出库(不可撤销) · 30 已撤销 | | 修改 | sendStatus 发货单状态 | 0 待发货 · 1 配送中 · 2 已完成 · 3 已取消 · 4 合并新场次发货 | 0 待发货 · 1 配送中 · 2 已完成 · 3 已取消 | --- # 冷库暂存花材 > 运营端 / PDA 端 / 小程序端冷库暂存全部 25 个接口:库存与统计、联系记录、关联发货单与直接出库、转代卖、扫码入库、裸花登记与点击出库 - 类型:前端对接 - 分类:冷库暂存花材 - 端:运营后台 / PDA / 梦花仓管小程序 - 后端服务:admin / supplier-api - 更新:2026-07-30 - 网页版:https://doc.xfun.fun/%E5%86%B7%E5%BA%93%E6%9A%82%E5%AD%98%E8%8A%B1%E6%9D%90%E6%8E%A5%E5%8F%A3%E6%96%87%E6%A1%A3 依据提交 b5f20cfd24 整理,字段以线上 Swagger 为准。运营端、PDA 端和小程序端共 25 个接口,均为 **POST + JSON body**,无 query / path 参数。冷库出库统一由小程序「点击出库」完成,PDA 不再提供扫码出库。新增**转代卖**:客服标记转入代卖(outType=3),仓库点击出库时确认最终扎数。 ## 通用约定 | 端 | 路径前缀 | 服务 | |----|----|----| | 运营端 · 客服后台 | `/cold` | admin | | PDA端 · 物流专员扫码枪 | `/cold/pda` | supplier-api | | 小程序端 · 梦花仓管(质检/区域负责人/仓库管理员) | `/cold/mini` | supplier-api | ### 统一返回结构 运营端包裹 `R`,供应商端包裹 `Response`,结构一致: ```json { "code": 0, "message": "success", "data": { "…": "业务数据" } } ``` ### 分页(两套规则,注意区分) | 端 | 入参 | 返回 | |----|----|----| | 运营端 | `index` 页码=1、`size` 页大小=10、`searchCount`=true,全部选填 | `PageVO{current,size,total,records[]}` | | 小程序端 | `current` 当前页、`size` 每页条数,必填且 ≥1 | `PageVo{current,size,total,records[]}` | ### 鉴权与请求头 - 运营端接口带 `@AuthToken(权限点)`:除登录态外还校验权限点(`coldStorageView` 查看 / `coldStorageOperate` 客服操作 / `coldStorageWarehouse` 仓库操作),每个接口所需权限见接口下方「权限」。 - PDA / 小程序端 `userId`、`userName` 由后端根据登录态填充,**前端无需传**(参数表「必填」列标为「注入」)。后端强制走可校验的 `Authorization`,不再接受历史裸 `token` 头。 - PDA 端限 **物流专员 / 外部物流专员**;小程序端限 **质检员 / 区域负责人 / 仓库管理员**。 | 请求头 | 端 | 必填 | 说明 | |----|----|----|----| | `usertokenxm` | 运营端 | 是 | 登录 JWT | | `channel` | 运营端 | 是 | 固定 `admin` | ## 枚举 | 字段 | 取值 | |----|----| | `storeType` 暂存类型 | 1 包裹 · 2 裸花 | | `stockStatus` 库存状态 | 10 待发货 · 40 待出库 · 50 已出库 | | `shipConfirm` 发货确认/联系结果 | 1 今日出库 · 2 继续暂存 · 3 联系不上 | | `taskStatus` 出库任务状态 | 10 待出库(可撤销) · 20 已出库(不可撤销) · 30 已撤销 | | `outType` 出库方式 | 1 关联发货单 · 2 直接出库 · 3 转代卖 | | `boxRepackFlag` 是否拆箱重打 | 0 否 · 1 是 | | `abnormalFlag` 异常标记 | 0 正常 · 1 异常 | | `sendStatus` 发货单状态 | 0 待发货 · 1 配送中 · 2 已完成 · 3 已取消 | | `logisticsStatus` 是否已发物流单 | 0 否 · 1 是 | ## 运营端 · 客服后台 全部带 @AuthToken,仅需登录态。返回包裹 R。 路径前缀 `/cold`。 ### POST /cold/stock/page · 冷库暂存库存列表 客服查看仓库扫码入库后的库存。分页。 权限:`coldStorageView` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `index` | Integer | 否 | 页码,不传默认1(分页) | | `size` | Integer | 否 | 页大小,不传默认10(分页) | | `searchCount` | Boolean | 否 | 是否查总数,默认true(分页) | | `keyword` | String | 否 | 关键词(手机号/昵称/发货单号/包裹码/货位) | | `storeType` | Integer | 否 | 暂存类型 1包裹 2裸花 | | `stockStatus` | Integer | 否 | 库存状态,见枚举 | | `storeRoundNo` | String | 否 | 暂存场次 yyyy-MM-dd | | `planShipDate` | String | 否 | 计划发货日期 yyyy-MM-dd | ```json { "index": 1, "size": 10, "searchCount": true, "keyword": "138", "storeType": 1, "stockStatus": 10, "storeRoundNo": "2026-07-09", "planShipDate": "2026-07-10" } ``` #### 响应 `R>` | 字段 | 类型 | 说明 | |----|----|----| | `coldStockId` | Long | 冷库库存id | | `purchaserId` | Long | 客户id | | `purchaserNick` | String | 客户昵称 | | `purchaserMobile` | String | 客户注册手机号 | | `storeType` | Integer | 暂存类型 | | `locationNo` | String | 货位号 | | `storeRoundNo` | String | 暂存场次 | | `originSendNo` | String | 原发货单号 | | `expressName` | String | 发货物流名称(原发货单物流,直接出库沿用展示) | | `imageUrls` | String | 入库照片(逗号分隔) | | `planShipDate` | LocalDate | 计划发货日期 | | `packageTotal` | Integer | 总包裹数 | | `packageOut` | Integer | 已出库包裹数 | | `packageRemain` | Integer | 剩余库存包裹数 | | `bunchNum` | Integer | 裸花扎数 | | `stockStatus` | Integer | 库存状态 | | `shipConfirm` | Integer | 最新发货确认 | | `remark` | String | 客户备注 | | `serviceUserName` | String | 关联客服 | | `inUserName` | String | 入库人 | | `inTime` | LocalDateTime | 入库时间 | | `updateTime` | LocalDateTime | 更新时间 | | `linkSendNo` | String | 关联发货单号(最新出库任务) | | `linkPurchaserNick` | String | 关联发货单客户昵称 | | `linkPurchaserMobile` | String | 关联发货单客户注册手机号 | | `linkReceiveName` | String | 关联发货单收货人 | | `linkReceiveTel` | String | 关联发货单收货电话 | | `linkReceiveAddress` | String | 关联发货单收货地址 | | `linkShelfSn` | Integer | 关联发货单主货位号 | | `linkSubShelfSn` | String | 关联发货单子货位号 | | `linkZoneShelf` | String | 关联发货单区-货位号 | | `outTaskStatus` | Integer | 出库任务状态(最新非撤销任务),见枚举 | | `outTaskId` | Long | 最新出库任务id(outTaskStatus=10 时用于撤销) | | `outTaskType` | Integer | 最新任务出库方式 1关联发货单 2直接出库 3转代卖(前端据此显示转代卖标签) | | `abnormalFlag` | Integer | 异常标记 0正常 1异常 | ```json { "code": 0, "message": "success", "data": { "current": 1, "size": 10, "total": 1, "records": [ { "coldStockId": 1001, "purchaserId": 88001, "purchaserNick": "花小九", "purchaserMobile": "138****8888", "storeType": 1, "locationNo": "A-03-12", "storeRoundNo": "2026-07-09", "originSendNo": "SND20260708001", "expressName": "顺丰冷链", "imageUrls": "https://img/1.jpg,https://img/2.jpg", "planShipDate": "2026-07-10", "packageTotal": 5, "packageOut": 2, "packageRemain": 3, "bunchNum": 20, "stockStatus": 10, "shipConfirm": 1, "remark": "易碎轻放", "serviceUserName": "客服小美", "inUserName": "仓管阿强、仓管小李", "inTime": "2026-07-09 09:00:00", "updateTime": "2026-07-09 10:20:00", "linkSendNo": "SND20260709001", "linkPurchaserNick": "花小九", "linkPurchaserMobile": "138****8888", "linkReceiveName": "string", "linkReceiveTel": "string", "linkReceiveAddress": "string", "linkShelfSn": 0, "linkSubShelfSn": "string", "linkZoneShelf": "string", "outTaskStatus": 10, "outTaskId": 3001, "outTaskType": 3, "abnormalFlag": 0 } ] } } ``` ### POST /cold/stock/stat · 冷库暂存库存统计 顶部统计卡片。**全局口径**,不随列表筛选条件联动(在库客户数/在库包裹数为当前全量,今日出库客户数与小程序首页同口径)。 权限:`coldStorageView` #### 请求参数 无(空 body `{}`) ```json {} ``` #### 响应 `R` | 字段 | 类型 | 说明 | |----|----|----| | `customerCount` | Integer | 当前暂存客户数 | | `bareCustomerCount` | Integer | 其中裸花暂存客户数 | | `packageTotal` | Integer | 暂存包裹数 | | `todayWaitOutCustomerCount` | Integer | 今日待出库客户数 | ```json { "code": 0, "message": "success", "data": { "customerCount": 12, "bareCustomerCount": 3, "packageTotal": 5, "todayWaitOutCustomerCount": 4 } } ``` ### POST /cold/contact/page · 联系记录列表 按客户和暂存库存聚合,倒序。非分页。 权限:`coldStorageView` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `coldStockId` | Long | 是 | 冷库库存id | ```json { "coldStockId": 1001 } ``` #### 响应 `R>` | 字段 | 类型 | 说明 | |----|----|----| | `id` | Long | 记录id | | `shipConfirm` | Integer | 联系结果 | | `nextPlanShipDate` | LocalDate | 下次计划发货日期 | | `content` | String | 联系内容 | | `imageUrls` | String | 图片url(逗号分隔) | | `operatorName` | String | 登记人 | | `createTime` | LocalDateTime | 登记时间 | ```json { "code": 0, "message": "success", "data": [ { "id": 1, "shipConfirm": 1, "nextPlanShipDate": "2026-07-11", "content": "已电话确认今日出库", "imageUrls": "https://img/1.jpg,https://img/2.jpg", "operatorName": "客服小美", "createTime": "2026-07-09 10:20:00" } ] } ``` ### POST /cold/contact/save · 新增联系记录 客服登记沟通记录。 权限:`coldStorageOperate` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `coldStockId` | Long | 是 | 冷库库存id | | `shipConfirm` | Integer | 否 | 联系结果 1今日出库 2继续暂存 3联系不上 | | `nextPlanShipDate` | String | 否 | 下次计划发货日期 yyyy-MM-dd | | `content` | String | 是 | 联系内容 | | `imageUrls` | String | 否 | 图片url(逗号分隔) | ```json { "coldStockId": 1001, "shipConfirm": 1, "nextPlanShipDate": "2026-07-11", "content": "已电话确认今日出库", "imageUrls": "https://img/1.jpg,https://img/2.jpg" } ``` #### 响应 `R` `data` = 1(记录id) ### POST /cold/out/stock-packages · 可选出库包裹明细 包裹暂存选择出库包裹。 权限:`coldStorageView` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `coldStockId` | Long | 是 | 冷库库存id | ```json { "coldStockId": 1001 } ``` #### 响应 `R>` | 字段 | 类型 | 说明 | |----|----|----| | `coldStockPackageId` | Long | 冷库包裹明细id(出库时回传) | | `expressPackageId` | Long | 包裹表id | | `packageCode` | String | 包裹码 | | `packageInfo` | String | 商品/规格 | | `originRoundNo` | String | 原场次 | | `locationNo` | String | 货位 | ```json { "code": 0, "message": "success", "data": [ { "coldStockPackageId": 5001, "expressPackageId": 9001, "packageCode": "9001", "packageInfo": "红玫瑰 20扎/件", "originRoundNo": "2026-07-08", "locationNo": "A-03-12" } ] } ``` ### POST /cold/out/today-send-order · 查询当天发货单 默认按冷库库存客户 + 当前场次查询发货单;可按客户注册手机号搜索其他客户的当天发货单。 权限:`coldStorageView` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `coldStockId` | Long | 是 | 冷库库存id | | `mobile` | String | 否 | 客户注册手机号,不传时查暂存客户自己的发货单 | ```json { "coldStockId": 1001, "mobile": "138****8888" } ``` #### 响应 `R>` | 字段 | 类型 | 说明 | |----|----|----| | `sendNo` | String | 发货单号 | | `purchaserId` | Long | 客户id | | `purchaserNick` | String | 客户昵称 | | `purchaserMobile` | String | 客户注册手机号 | | `receiveName` | String | 收货人 | | `receiveTel` | String | 收货电话 | | `receiveAddress` | String | 收货地址 | | `shelfSn` | Integer | 主货位号 | | `subShelfSn` | String | 子货位号 | | `zoneShelf` | String | 区-货位号 | | `actualTotalWeight` | Integer | 发货单实际重量(g) | | `sendStatus` | Integer | 发货单状态 0待发货 1配送中 2已完成 3已取消 | | `logisticsStatus` | Integer | 是否已发物流单 0否 1是 | | `canLink` | Boolean | 是否允许关联出库 | | `linkTip` | String | 不可关联/货架提示 | ```json { "code": 0, "message": "success", "data": [ { "sendNo": "SND20260709001", "purchaserId": 88001, "purchaserNick": "花小九", "purchaserMobile": "138****8888", "receiveName": "张三", "receiveTel": "138****8888", "receiveAddress": "浙江省杭州市西湖区…", "shelfSn": 0, "subShelfSn": "string", "zoneShelf": "#15-112", "actualTotalWeight": 3200, "sendStatus": 0, "logisticsStatus": 0, "canLink": true, "linkTip": "" } ] } ``` ### POST /cold/out/link · 关联当天发货单出库 含拆箱重打。生成待出库任务并锁定所选包裹(撤销窗口),实际出库由小程序点击出库完成。**拆箱重打同样走两段式**:任务待出库期间可撤销,仓库点击出库时才完结原包裹(明细置30、包裹表置40);且**必须全部包裹一起出库**——勾选数不等于该库存全部在库包裹数时报错,不允许部分重打。 权限:`coldStorageOperate` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `coldStockId` | Long | 是 | 冷库库存id | | `coldStockPackageIds` | List | 否 | 本次出库的冷库包裹明细id列表(包裹暂存必填,裸花不填) | | `boxRepackFlag` | Integer | 否 | 是否拆箱重打 0否 1是(重打必须勾选全部在库包裹) | | `sendNo` | String | 是 | 关联的当天发货单号 | ```json { "coldStockId": 1001, "coldStockPackageIds": [ 5001, 5002 ], "boxRepackFlag": 0, "sendNo": "SND20260709001" } ``` #### 响应 `R` `data` = 3001(出库任务id) ### POST /cold/out/direct · 直接出库 不关联当天发货单。生成待出库任务并锁定所选包裹,实际出库由小程序点击出库完成。发货物流由服务端根据原发货单解析,前端不传。 权限:`coldStorageOperate` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `coldStockId` | Long | 是 | 冷库库存id | | `coldStockPackageIds` | List | 否 | 本次出库的冷库包裹明细id列表(包裹暂存必填,裸花不填) | | `outRemark` | String | 否 | 出库备注 | ```json { "coldStockId": 1001, "coldStockPackageIds": [ 5001, 5002 ], "outRemark": "走顺丰冷链" } ``` #### 响应 `R` `data` = 3001(出库任务id) ### POST /cold/out/cancel · 撤销出库任务 仅可撤销待出库(10)且尚无包裹完成出库的任务。撤销后任务置已撤销(30),释放锁定包裹;无其他待出库任务时,库存回退为待发货(10)。 权限:`coldStorageOperate` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `outTaskId` | Long | 是 | 出库任务id | | `cancelReason` | String | 是 | 撤销原因 | ```json { "outTaskId": 3001, "cancelReason": "客户临时取消" } ``` #### 响应 `R` `data` 为 `null`。 ### POST /cold/stock/abnormal · 库存异常标记 库存数量与现场不一致时标记异常。 权限:`coldStorageWarehouse` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `coldStockId` | Long | 是 | 冷库库存id | | `abnormalFlag` | Integer | 是 | 异常标记 0正常 1异常 | | `abnormalRemark` | String | 是 | 异常说明 | ```json { "coldStockId": 1001, "abnormalFlag": 0, "abnormalRemark": "实际少2件" } ``` #### 响应 `R` `data` 为 `null`。 ### POST /cold/stock/plan-ship · 变更计划发货日期 客户要求变更发货日期,记录变更日志。 权限:`coldStorageOperate` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `coldStockId` | Long | 是 | 冷库库存id | | `planShipDate` | String | 是 | 计划发货日期 yyyy-MM-dd | | `remark` | String | 否 | 变更说明 | ```json { "coldStockId": 1001, "planShipDate": "2026-07-10", "remark": "易碎轻放" } ``` #### 响应 `R` `data` 为 `null`。 ### POST /cold/convert-sale/info · 转代卖预填信息 转代卖弹窗预填。裸花返回当前剩余暂存扎数上限;包裹返回发货单总扎数(打包时点快照,取原发货单序号1包裹的 item_num,**不受退款影响**),可为 null,前端显示「-」且提交时不校验上限。 权限:`coldStorageView` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `coldStockId` | Long | 是 | 冷库库存id | ```json { "coldStockId": 1001 } ``` #### 响应 `R` | 字段 | 类型 | 说明 | |----|----|----| | `storeType` | Integer | 暂存类型 1包裹 2裸花 | | `maxBunchNum` | Integer | 可转扎数上限(裸花=剩余暂存扎数;包裹=发货单总扎数快照,可为null) | ```json { "code": 0, "message": "success", "data": { "storeType": 1, "maxBunchNum": 20 } } ``` ### POST /cold/convert-sale · 确认转代卖 客服联系客户确认后标记转代卖(相关退款由客服手动操作订单处理)。生成**待出库转代卖任务(outType=3)**,沿用两段式:待出库期间可用「撤销出库任务」撤销,实际出库由小程序点击出库完成。**裸花**:扎数≤当前剩余暂存扎数,支持部分转出(剩余扎数继续暂存);**包裹**:整条库存全部在库包裹一起锁定转出,扎数≤发货单总扎数快照(查不到不校验),实际扎数由仓库出库时确认;存在其他待出库任务或城市仓货架暂存(wareId>0)时拦截。转代卖不走物流发货,任务物流字段为空。 权限:`coldStorageOperate` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `coldStockId` | Long | 是 | 冷库库存id | | `bunchNum` | Integer | 是 | 转代卖扎数 ≥1 | | `remark` | String | 否 | 备注(转代卖原因或操作说明,≤255字) | ```json { "coldStockId": 1001, "bunchNum": 20, "remark": "易碎轻放" } ``` #### 响应 `R` `data` = 3001(出库任务id) ### POST /cold/out/records · 历史出库记录 查看已关联、已直接出库、已撤销的出库操作记录。分页,按出库任务生成时间倒序。 权限:`coldStorageView` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `index` | Integer | 否 | 页码,不传默认1(分页) | | `size` | Integer | 否 | 页大小,不传默认10(分页) | | `searchCount` | Boolean | 否 | 是否查总数,默认true(分页) | | `keyword` | String | 否 | 关键词(手机号/昵称/发货单号/货位) | | `storeType` | Integer | 否 | 暂存类型 1包裹 2裸花 | | `outType` | Integer | 否 | 出库方式 1关联发货单 2直接出库 3转代卖 | | `taskStatus` | Integer | 否 | 任务状态 10待出库 20已出库 30已撤销(空=全部) | | `startDate` | String | 否 | 生成起始日期 yyyy-MM-dd | | `endDate` | String | 否 | 生成截止日期 yyyy-MM-dd | ```json { "index": 1, "size": 10, "searchCount": true, "keyword": "138", "storeType": 1, "outType": 1, "taskStatus": 10, "startDate": "string", "endDate": "string" } ``` #### 响应 `R>` | 字段 | 类型 | 说明 | |----|----|----| | `outTaskId` | Long | 出库任务id | | `coldStockId` | Long | 冷库库存id | | `purchaserNick` | String | 暂存客户昵称 | | `purchaserMobile` | String | 暂存客户注册手机号 | | `storeType` | Integer | 暂存类型 | | `locationNo` | String | 货位号 | | `storeRoundNo` | String | 暂存场次 | | `outType` | Integer | 出库方式 1关联发货单 2直接出库 3转代卖 | | `linkSendNo` | String | 关联发货单号 | | `linkPurchaserNick` | String | 关联发货单客户昵称 | | `linkPurchaserMobile` | String | 关联发货单客户注册手机号 | | `linkReceiveName` | String | 关联发货单收货人 | | `linkReceiveTel` | String | 关联发货单收货电话 | | `linkReceiveAddress` | String | 关联发货单收货地址 | | `linkShelfSn` | Integer | 关联发货单主货位号 | | `linkSubShelfSn` | String | 关联发货单子货位号 | | `linkZoneShelf` | String | 关联发货单区-货位号 | | `outPackageNum` | Integer | 出库包裹数 | | `outBunchNum` | Integer | 出库裸花扎数(转代卖任务=客服填写扎数,包裹暂存也有值) | | `finishBunchNum` | Integer | 仓库确认最终出库扎数(转代卖任务) | | `boxRepackFlag` | Integer | 是否拆箱重打 0否 1是 | | `expressName` | String | 发货物流(转代卖为空) | | `outRemark` | String | 出库备注 | | `taskStatus` | Integer | 任务状态,见枚举 | | `operatorName` | String | 客服操作人 | | `operatorMobile` | String | 客服操作人手机号 | | `operateTime` | LocalDateTime | 出库任务生成时间 | | `finishUserName` | String | 实际出库人 | | `finishTime` | LocalDateTime | 实际出库时间 | | `finishImageUrls` | String | 出库照片(逗号分隔url) | | `finishRemark` | String | 出库备注(仓库出库时填写) | | `cancelUserName` | String | 撤销人 | | `cancelTime` | LocalDateTime | 撤销时间 | | `cancelReason` | String | 撤销原因 | ```json { "code": 0, "message": "success", "data": { "current": 1, "size": 10, "total": 1, "records": [ { "outTaskId": 3001, "coldStockId": 1001, "purchaserNick": "花小九", "purchaserMobile": "138****8888", "storeType": 1, "locationNo": "A-03-12", "storeRoundNo": "2026-07-09", "outType": 1, "linkSendNo": "SND20260709001", "linkPurchaserNick": "花小九", "linkPurchaserMobile": "138****8888", "linkReceiveName": "string", "linkReceiveTel": "string", "linkReceiveAddress": "string", "linkShelfSn": 0, "linkSubShelfSn": "string", "linkZoneShelf": "string", "outPackageNum": 2, "outBunchNum": 20, "finishBunchNum": 3, "boxRepackFlag": 0, "expressName": "顺丰冷链", "outRemark": "走顺丰冷链", "taskStatus": 10, "operatorName": "客服小美", "operatorMobile": "139****6666", "operateTime": "2026-07-09 11:00:00", "finishUserName": "仓管阿强", "finishTime": "2026-07-09 15:30:00", "finishImageUrls": "https://img/out1.jpg,https://img/out2.jpg", "finishRemark": "已装车", "cancelUserName": "string", "cancelTime": "string", "cancelReason": "客户临时取消" } ] } } ``` ### POST /cold/stock/pending-packages · 未入库包裹清单 发货单下已打包(状态10)但尚未扫入冷库的包裹清单,供客服核对已入/未入件数并催办物流入库。`sendNo` 与 `coldStockId` 二选一(传 coldStockId 时取其原发货单号)。 权限:`coldStorageView` #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `sendNo` | String | 否 | 发货单号(与冷库库存id二选一) | | `coldStockId` | Long | 否 | 冷库库存id(包裹暂存,传则取其原发货单号) | ```json { "sendNo": "SND20260709001", "coldStockId": 1001 } ``` #### 响应 `R` | 字段 | 类型 | 说明 | |----|----|----| | `sendNo` | String | 发货单号 | | `storedPackageNum` | Integer | 已入冷库包裹数 | | `pendingPackageNum` | Integer | 未入库包裹数 | | `pendingList` | List | 未入库包裹清单 | | `pendingList[].expressPackageId` | Long | 包裹表id | | `pendingList[].packageCode` | String | 包裹码 | | `pendingList[].packageInfo` | String | 商品/规格 | | `pendingList[].originRoundNo` | String | 原场次 | | `pendingList[].zoneShelf` | String | 货架 | | `pendingList[].totalWeight` | BigDecimal | 包裹重量(g) | | `pendingList[].expressStatus` | Integer | 包裹状态 10打包完成待入冷库 | ```json { "code": 0, "message": "success", "data": { "sendNo": "SND20260709001", "storedPackageNum": 1, "pendingPackageNum": 1, "pendingList": [ { "expressPackageId": 9001, "packageCode": "9001", "packageInfo": "红玫瑰 20扎/件", "originRoundNo": "2026-07-08", "zoneShelf": "#15-112", "totalWeight": 5200, "expressStatus": 10 } ] } } ``` ## PDA端 · 物流专员扫码枪 限**物流专员 / 外部物流专员**角色(外部物流专员仅能操作已配置物流商的包裹)。userId 由后端从登录态注入,前端无需传。返回包裹 Response。**PDA 只负责入库**,出库统一走小程序「点击出库」。 路径前缀 `/cold/pda`。 ### POST /cold/pda/scan · 扫码入库 扫描包裹码执行冷库暂存入库。连续扫同一客户时回传上次的 coldStockId。**type=2 取消暂存**:扫错时对同一包裹码再扫一次即撤销当前暂存(已锁定待出库的包裹需先在运营端撤销出库任务;全部取消后整条库存作废)。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `codeContent` | String | 是 | 扫码内容(包裹id) | | `type` | Integer | 否 | 扫码类型 1暂存(默认) 2取消暂存(扫错撤销) | | `coldStockId` | Long | 否 | 累计入库的冷库库存id(连续扫码同一客户时传) | | `locationNo` | String | 否 | 暂存货位号(新建库存时使用) | | `userName` | String | 注入 | 后端根据登录人解析,前端不传 | | `userId` | Long | 注入 | 后端注入,前端不传 | ```json { "codeContent": "9001", "type": 0, "coldStockId": 1001, "locationNo": "A-03-12" } ``` #### 响应 `Response` | 字段 | 类型 | 说明 | |----|----|----| | `coldStockId` | Long | 冷库库存id(继续扫码回传) | | `purchaserId` | Long | 客户id | | `purchaserNick` | String | 客户昵称 | | `purchaserMobile` | String | 客户手机号 | | `storeType` | Integer | 暂存类型 | | `locationNo` | String | 货位 | | `storeRoundNo` | String | 场次 | | `remark` | String | 客户备注 | | `expressName` | String | 发货物流名称 | | `inUserName` | String | 入库人姓名(多人扫码顿号分隔) | | `packageTotal` | Integer | 已暂存包裹总数 | | `scanPackageCode` | String | 本次扫码的包裹码 | | `packageSummary` | List | 已扫包裹汇总(按型号聚合) | | `packageSummary[].packageInfo` | String | 包裹型号/规格 | | `packageSummary[].num` | Integer | 该型号已扫数量 | | `packageList` | List | 已入库包裹明细 | | `packageList[].packageCode` | String | 包裹码(包裹id) | | `packageList[].packageInfo` | String | 名称/规格 | | `packageList[].serialNum` | Integer | 序号 | | `packageList[].originRoundNo` | String | 原场次 | ```json { "code": 0, "message": "success", "data": { "coldStockId": 1001, "purchaserId": 88001, "purchaserNick": "花小九", "purchaserMobile": "138****8888", "storeType": 1, "locationNo": "A-03-12", "storeRoundNo": "2026-07-09", "remark": "易碎轻放", "expressName": "顺丰冷链", "inUserName": "仓管阿强、仓管小李", "packageTotal": 5, "scanPackageCode": "9001", "packageSummary": [ { "packageInfo": "红玫瑰 20扎/件", "num": 2 } ], "packageList": [ { "packageCode": "9001", "packageInfo": "红玫瑰 20扎/件", "serialNum": 1, "originRoundNo": "2026-07-08" } ] } } ``` ### POST /cold/pda/stock/detail · 库存详情 查看当前累计已扫码包裹。返回结构同「扫码入库」(ColdScanResultVo)。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `coldStockId` | Long | 是 | 冷库库存id | ```json { "coldStockId": 1001 } ``` #### 响应 `Response` | 字段 | 类型 | 说明 | |----|----|----| | `coldStockId` | Long | 冷库库存id | | `purchaserId` | Long | 客户id | | `purchaserNick` | String | 客户昵称 | | `purchaserMobile` | String | 客户手机号 | | `storeType` | Integer | 暂存类型 | | `locationNo` | String | 货位 | | `storeRoundNo` | String | 场次 | | `remark` | String | 客户备注 | | `expressName` | String | 发货物流名称 | | `inUserName` | String | 入库人姓名(多人扫码顿号分隔) | | `packageTotal` | Integer | 已暂存包裹总数 | | `scanPackageCode` | String | 本次扫码的包裹码 | | `packageSummary` | List | 已扫包裹汇总(按型号聚合) | | `packageSummary[].packageInfo` | String | 包裹型号/规格 | | `packageSummary[].num` | Integer | 该型号已扫数量 | | `packageList` | List | 已入库包裹明细 | | `packageList[].packageCode` | String | 包裹码 | | `packageList[].packageInfo` | String | 名称/规格 | | `packageList[].serialNum` | Integer | 序号 | | `packageList[].originRoundNo` | String | 原场次 | ```json { "code": 0, "message": "success", "data": { "coldStockId": 1001, "purchaserId": 88001, "purchaserNick": "花小九", "purchaserMobile": "138****8888", "storeType": 1, "locationNo": "A-03-12", "storeRoundNo": "2026-07-09", "remark": "易碎轻放", "expressName": "顺丰冷链", "inUserName": "仓管阿强、仓管小李", "packageTotal": 5, "scanPackageCode": "9001", "packageSummary": [ { "packageInfo": "红玫瑰 20扎/件", "num": 2 } ], "packageList": [ { "packageCode": "9001", "packageInfo": "红玫瑰 20扎/件", "serialNum": 1, "originRoundNo": "2026-07-08" } ] } } ``` ## 小程序端 · 梦花仓管(质检/区域负责人/仓库管理员) 限**质检员 / 区域负责人 / 仓库管理员**角色。userId 由后端从登录态注入,前端无需传。分页入参 current/size 必填。返回包裹 Response。 路径前缀 `/cold/mini`。 ### POST /cold/mini/home · 首页统计 冷库暂存首页统计卡片。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `date` | String | 否 | 业务日期/场次 yyyy-MM-dd(默认当天) | ```json { "date": "2026-07-09" } ``` #### 响应 `Response` | 字段 | 类型 | 说明 | |----|----|----| | `customerCount` | Integer | 当前暂存客户数 | | `bareCustomerCount` | Integer | 其中裸花暂存客户数 | | `packageTotal` | Integer | 暂存包裹合计 | | `todayOutCustomerCount` | Integer | 今日暂存出库客户数 | ```json { "code": 0, "message": "success", "data": { "customerCount": 12, "bareCustomerCount": 3, "packageTotal": 5, "todayOutCustomerCount": 4 } } ``` ### POST /cold/mini/in/page · 暂存入库列表 今日新增入库(暂存中)。分页。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `current` | Long | 是 | 当前页 ≥1(分页) | | `size` | Long | 是 | 每页条数(分页) | | `date` | String | 否 | 暂存场次 yyyy-MM-dd | | `keyword` | String | 否 | 关键词(手机号/昵称/货位) | | `storeType` | Integer | 否 | 暂存类型 1包裹 2裸花(空=全部) | ```json { "current": 1, "size": 10, "date": "2026-07-09", "keyword": "138", "storeType": 1 } ``` #### 响应 `Response>` | 字段 | 类型 | 说明 | |----|----|----| | `coldStockId` | Long | 冷库库存id | | `purchaserNick` | String | 客户昵称 | | `purchaserMobile` | String | 客户手机号 | | `storeType` | Integer | 暂存类型 | | `locationNo` | String | 货位 | | `storeRoundNo` | String | 场次 | | `originSendNo` | String | 原发货单号 | | `imageUrls` | String | 入库照片(逗号分隔) | | `packageTotal` | Integer | 总包裹数 | | `packageOut` | Integer | 已出库 | | `bunchNum` | Integer | 裸花扎数 | | `stockStatus` | Integer | 库存状态 | | `remark` | String | 客户备注 | | `inUserName` | String | 入库人(多人扫码顿号分隔) | | `updateTime` | LocalDateTime | 更新时间 | ```json { "code": 0, "message": "success", "data": { "current": 1, "size": 10, "total": 1, "records": [ { "coldStockId": 1001, "purchaserNick": "花小九", "purchaserMobile": "138****8888", "storeType": 1, "locationNo": "A-03-12", "storeRoundNo": "2026-07-09", "originSendNo": "SND20260708001", "imageUrls": "https://img/1.jpg,https://img/2.jpg", "packageTotal": 5, "packageOut": 2, "bunchNum": 20, "stockStatus": 10, "remark": "易碎轻放", "inUserName": "仓管阿强、仓管小李", "updateTime": "2026-07-09 10:20:00" } ] } } ``` ### POST /cold/mini/current/page · 当前暂存列表 仍在冷库、未完全出库。入参 / 返回同「暂存入库列表」(ColdStockListVo)。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `current` | Long | 是 | 当前页 ≥1(分页) | | `size` | Long | 是 | 每页条数(分页) | | `date` | String | 否 | 暂存场次 yyyy-MM-dd | | `keyword` | String | 否 | 关键词(手机号/昵称/货位) | | `storeType` | Integer | 否 | 暂存类型(空=全部) | ```json { "current": 1, "size": 10, "date": "2026-07-09", "keyword": "138", "storeType": 1 } ``` #### 响应 `Response>` | 字段 | 类型 | 说明 | |----|----|----| | `coldStockId` | Long | 冷库库存id | | `purchaserNick` | String | 客户昵称 | | `purchaserMobile` | String | 客户手机号 | | `storeType` | Integer | 暂存类型 | | `locationNo` | String | 货位 | | `storeRoundNo` | String | 场次 | | `originSendNo` | String | 原发货单号 | | `imageUrls` | String | 入库照片(逗号分隔) | | `packageTotal` | Integer | 总包裹数 | | `packageOut` | Integer | 已出库 | | `bunchNum` | Integer | 裸花扎数 | | `stockStatus` | Integer | 库存状态 | | `remark` | String | 客户备注 | | `inUserName` | String | 入库人(多人扫码顿号分隔) | | `updateTime` | LocalDateTime | 更新时间 | ```json { "code": 0, "message": "success", "data": { "current": 1, "size": 10, "total": 1, "records": [ { "coldStockId": 1001, "purchaserNick": "花小九", "purchaserMobile": "138****8888", "storeType": 1, "locationNo": "A-03-12", "storeRoundNo": "2026-07-09", "originSendNo": "SND20260708001", "imageUrls": "https://img/1.jpg,https://img/2.jpg", "packageTotal": 5, "packageOut": 2, "bunchNum": 20, "stockStatus": 10, "remark": "易碎轻放", "inUserName": "仓管阿强、仓管小李", "updateTime": "2026-07-09 10:20:00" } ] } } ``` ### POST /cold/mini/out/page · 暂存出库列表 待出库/已出库任务列表(默认不含已撤销),含包裹扫码进度。分页。**outType=3 转代卖任务**:outBunchNum 为客服填写的转代卖扎数(包裹暂存也有值),点击出库时需确认最终扎数。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `current` | Long | 是 | 当前页 ≥1(分页) | | `size` | Long | 是 | 每页条数(分页) | | `date` | String | 否 | 业务日期 yyyy-MM-dd | | `keyword` | String | 否 | 关键词(手机号/昵称/货位) | | `storeType` | Integer | 否 | 暂存类型 1包裹 2裸花(空=全部) | | `taskStatus` | Integer | 否 | 任务状态 10待出库 20已出库(空=全部) | ```json { "current": 1, "size": 10, "date": "2026-07-09", "keyword": "138", "storeType": 1, "taskStatus": 10 } ``` #### 响应 `Response>` | 字段 | 类型 | 说明 | |----|----|----| | `outTaskId` | Long | 出库任务id | | `coldStockId` | Long | 冷库库存id | | `purchaserNick` | String | 客户昵称 | | `purchaserMobile` | String | 客户手机号 | | `storeType` | Integer | 暂存类型 | | `outType` | Integer | 出库方式 1关联发货单 2直接出库 3转代卖 | | `linkSendNo` | String | 关联发货单号 | | `todayShelfNo` | String | 当天货架 | | `expressName` | String | 发货物流(转代卖为空) | | `locationNo` | String | 货位 | | `storeRoundNo` | String | 场次 | | `outPackageNum` | Integer | 出库包裹数 | | `packageTotal` | Integer | 总包裹数(展示 出库数/总数) | | `outBunchNum` | Integer | 出库裸花扎数(转代卖任务=客服填写扎数) | | `finishBunchNum` | Integer | 仓库确认最终出库扎数(转代卖任务,已出库后有值) | | `boxRepackFlag` | Integer | 是否拆箱重打 0否 1是 | | `taskStatus` | Integer | 任务状态,见枚举 | | `operatorName` | String | 客服操作人 | | `operatorMobile` | String | 客服操作人手机号 | | `operateTime` | LocalDateTime | 操作时间 | | `finishImageUrls` | String | 出库照片(逗号分隔url,已出库任务) | | `finishRemark` | String | 出库备注(已出库任务) | | `finishTime` | LocalDateTime | 出库完成时间(已出库任务) | ```json { "code": 0, "message": "success", "data": { "current": 1, "size": 10, "total": 1, "records": [ { "outTaskId": 3001, "coldStockId": 1001, "purchaserNick": "花小九", "purchaserMobile": "138****8888", "storeType": 1, "outType": 1, "linkSendNo": "SND20260709001", "todayShelfNo": "A-03", "expressName": "顺丰冷链", "locationNo": "A-03-12", "storeRoundNo": "2026-07-09", "outPackageNum": 2, "packageTotal": 5, "outBunchNum": 20, "finishBunchNum": 3, "boxRepackFlag": 0, "taskStatus": 10, "operatorName": "客服小美", "operatorMobile": "139****6666", "operateTime": "2026-07-09 11:00:00", "finishImageUrls": "https://img/out1.jpg,https://img/out2.jpg", "finishRemark": "已装车", "finishTime": "2026-07-09 15:30:00" } ] } } ``` ### POST /cold/mini/out/finish · 点击出库 冷库出库唯一执行入口。裸花任务整单完成;包裹任务将任务内待出库包裹批量完成;**拆箱重打任务**在此完结原包裹(明细置30、包裹表置40)。**转代卖任务(outType=3)必须填 bunchNum 确认最终出库扎数**(预填客服填写值):裸花按最终扎数扣减库存,扣完置已出库,有剩余回待发货继续暂存;裸花最终扎数不能大于当前剩余暂存扎数。**出库照片必传(至少一张)**,备注可选。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `outTaskId` | Long | 是 | 出库任务id | | `imageUrls` | String | 是 | 出库照片(逗号分隔url,至少一张) | | `remark` | String | 否 | 出库备注 | | `bunchNum` | Integer | 否 | 最终出库扎数(转代卖任务必填,正整数;其他任务忽略) | | `userId` | Long | 注入 | 后端注入,前端不传 | | `userName` | String | 注入 | 后端根据登录人解析,前端不传 | ```json { "outTaskId": 3001, "imageUrls": "https://img/1.jpg,https://img/2.jpg", "remark": "易碎轻放", "bunchNum": 20 } ``` #### 响应 `Response` `data` = "success"(固定 success) ### POST /cold/mini/bare/scan · 裸花二维码识别 扫描花材二维码,带出客户、场次、货架和收货地址,供「裸花暂存登记」预填与二次确认。**暂存扎数改为登记时手填**,返回的 bunchNum 为订单总扎数,仅供参考。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `codeContent` | String | 是 | 花材二维码内容 | ```json { "codeContent": "60001-1-20-1" } ``` #### 响应 `Response` | 字段 | 类型 | 说明 | |----|----|----| | `enterItemId` | Long | 入场花材id | | `purchaserId` | Long | 客户id | | `purchaserNick` | String | 客户昵称 | | `purchaserMobile` | String | 客户手机号 | | `roundNo` | String | 场次 | | `locationNo` | String | 货架号 | | `bunchNum` | Integer | 订单花材总扎数(仅参考,暂存扎数由登记时手填) | | `receiveAddress` | String | 收货地址 | | `originSendNo` | String | 原发货单号 | ```json { "code": 0, "message": "success", "data": { "enterItemId": 60001, "purchaserId": 88001, "purchaserNick": "花小九", "purchaserMobile": "138****8888", "roundNo": "2026-07-09", "locationNo": "A-03-12", "bunchNum": 20, "receiveAddress": "浙江省杭州市西湖区…", "originSendNo": "SND20260708001" } } ``` ### POST /cold/mini/bare/register · 裸花暂存登记 扫花材二维码登记裸花入冷库。**客户 / 场次 / 货位由二维码识别**(见「裸花二维码识别」),**暂存扎数必填、由仓库人员手填**(客户可能只暂存部分花材,不取识别结果的订单总扎数),**填写扎数大于货架现有扎数(识别结果 bunchNum)时直接报错**,同客户同场次同货架重复登记会拦截。暂存登记时入库照片必填;特殊备注选填(如需记录暂存花材的品种、等级、数量)。**type=2 取消暂存**:扫错时对同一花材码再扫一次即作废当前登记(无需扎数和照片;已生成出库任务的需先在运营端撤销)。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `codeContent` | String | 是 | 花材二维码内容(同「裸花二维码识别」) | | `type` | Integer | 否 | 扫码类型 1暂存(默认) 2取消暂存(扫错撤销,无需扎数和照片) | | `bunchNum` | Integer | 否 | 暂存扎数(暂存登记必传,正整数,不能大于货架现有扎数) | | `imageUrls` | String | 否 | 入库照片(逗号分隔url,暂存登记必传) | | `remark` | String | 否 | 特殊备注(选填,如品种、等级、数量) | | `userName` | String | 注入 | 后端根据登录人解析,前端不传 | | `userId` | Long | 注入 | 后端注入,前端不传 | ```json { "codeContent": "60001-1-20-1", "type": 0, "bunchNum": 20, "imageUrls": "https://img/1.jpg,https://img/2.jpg", "remark": "易碎轻放" } ``` #### 响应 `Response` `data` = 1001(冷库库存id) ### POST /cold/mini/package/scan · 包裹扫码入库 扫描包裹码执行冷库暂存入库。连续扫同一客户时回传上次的 coldStockId。质检员、区域负责人和仓库管理员不按物流商限制。**type=2 取消暂存**:扫错时对同一包裹码再扫一次即撤销当前暂存(已锁定待出库的包裹需先在运营端撤销出库任务;全部取消后整条库存作废)。 #### 请求参数 | 字段 | 类型 | 必填 | 说明 | |----|----|----|----| | `codeContent` | String | 是 | 扫码内容(包裹id) | | `type` | Integer | 否 | 扫码类型 1暂存(默认) 2取消暂存(扫错撤销) | | `coldStockId` | Long | 否 | 累计入库的冷库库存id(连续扫同一客户时传) | | `locationNo` | String | 否 | 暂存货位号(新建库存时使用) | | `userName` | String | 注入 | 后端根据登录人解析,前端不传 | | `userId` | Long | 注入 | 后端注入,前端不传 | ```json { "codeContent": "9001", "type": 0, "coldStockId": 1001, "locationNo": "A-03-12" } ``` #### 响应 `Response` | 字段 | 类型 | 说明 | |----|----|----| | `coldStockId` | Long | 冷库库存id(继续扫码回传) | | `purchaserId` | Long | 客户id | | `purchaserNick` | String | 客户昵称 | | `purchaserMobile` | String | 客户手机号 | | `storeType` | Integer | 暂存类型 | | `locationNo` | String | 货位 | | `storeRoundNo` | String | 场次 | | `remark` | String | 客户备注 | | `expressName` | String | 发货物流名称 | | `inUserName` | String | 入库人姓名(多人扫码顿号分隔) | | `packageTotal` | Integer | 已暂存包裹总数 | | `scanPackageCode` | String | 本次扫码的包裹码 | | `packageSummary` | List | 已扫包裹汇总(按型号聚合) | | `packageSummary[].packageInfo` | String | 包裹型号/规格 | | `packageSummary[].num` | Integer | 该型号已扫数量 | | `packageList` | List | 已入库包裹明细 | | `packageList[].packageCode` | String | 包裹码(包裹id) | | `packageList[].packageInfo` | String | 名称/规格 | | `packageList[].serialNum` | Integer | 序号 | | `packageList[].originRoundNo` | String | 原场次 | ```json { "code": 0, "message": "success", "data": { "coldStockId": 1001, "purchaserId": 88001, "purchaserNick": "花小九", "purchaserMobile": "138****8888", "storeType": 1, "locationNo": "A-03-12", "storeRoundNo": "2026-07-09", "remark": "易碎轻放", "expressName": "顺丰冷链", "inUserName": "仓管阿强、仓管小李", "packageTotal": 5, "scanPackageCode": "9001", "packageSummary": [ { "packageInfo": "红玫瑰 20扎/件", "num": 2 } ], "packageList": [ { "packageCode": "9001", "packageInfo": "红玫瑰 20扎/件", "serialNum": 1, "originRoundNo": "2026-07-08" } ] } } ``` --- # 冷库暂存花材信息流 > 已打包 / 未打包冷库暂存业务方案:暂存、关联、出库、发货信息闭环(含小程序 / PDA 界面示意) - 类型:方案说明 - 分类:冷库暂存花材 - 更新:2026-07-14 - 网页版:https://doc.xfun.fun/%E5%B7%B2%E6%89%93%E5%8C%85%E6%9C%AA%E6%89%93%E5%8C%85%E5%86%B7%E5%BA%93%E6%9A%82%E5%AD%98%E8%8A%B1%E6%9D%90%E4%BF%A1%E6%81%AF%E6%B5%81%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88 > [!IMPORTANT] > **核心结论:** 冷库暂存业务统一以“冷库库存”为核心数据。已打包货物按包裹管理,未打包裸花按客户管理;合肥客服负责客户沟通、次日出库客户确认、关联发货单、直接出库及继续暂存沟通记录,暂存需求由客服与仓库线下沟通(系统不做需求提报),实际暂存入库由仓库扫码登记:扫包裹码=包裹暂存,扫花材码=裸花暂存。 ## 需求背景 目前客户存在指定日期发货、隔天发货、临时通知发货,以及新订单与历史暂存货物合并发货等需求。现有冷库暂存主要依赖人工沟通及线下记录,容易造成库存不清、流程不统一和信息不同步。 ### 客户发货诉求 - 指定日期发货。 - 隔天发货。 - 客户临时通知发货。 - 新订单与历史暂存货物合并发货。 ### 当前主要问题 - 冷库库存无法统一管理。 - 已打包与未打包暂存流程不统一。 - 新订单无法关联历史暂存货物。 - 物流、区域负责人、客服之间信息不同步。 - 易发生漏发、错发、未按要求打包等问题。 ## 设计目标 - 支持冷库库存统一管理。 - 支持已打包包裹暂存。 - 支持未打包裸花暂存。 - 支持跨场次发货。 - 支持历史暂存货物关联新发货单。 - 支持直接发出冷库库存包裹。 - 实现冷库库存可追踪。 - 实现全流程消息同步。 ## 业务原则 | 业务类型 | 管理维度 | 说明 | |----|----|----| | 已打包暂存 | 包裹 | 已打包货物以包裹为管理对象,扫码数量即为实际库存数量。 | | 未打包暂存(裸花) | 客户 | 裸花暂存以客户为管理对象,不记录包裹。 | ## 冷库暂存总体业务流程 冷库暂存业务统一以冷库库存作为后续业务处理依据。 客户提出暂存需求 → 合肥客服线下与仓库沟通暂存 → 仓库/物流/区域负责人执行冷库入库并登记 → **系统生成冷库库存(核心)** → 合肥客服次日出库前查看冷库库存并联系客户确认 客户确认发货后分两条路径: - **路径一:关联当天发货单**——与当天新订单货物一起发货,并生成今日出库任务。 - **路径二:直接发出冷库库存**——不关联当天发货单,直接生成冷库库存出库任务。 两条路径之后:生成今日出库任务 → 梦花仓管今日出库 → 物流/区域负责人执行 → 完成发货 > [!NOTE] > 合肥客服以系统中的冷库库存为依据,在次日出库前联系客户确认是否发货。客户确认后,可根据实际业务选择关联当天发货单或直接发出冷库库存;客户沟通后仍需继续暂存的,由客服登记沟通记录并保留暂存状态。 ## 场景一:已打包暂存(包裹维度) ### 客服沟通与仓库登记 适用于客户要求部分包裹暂存、全部包裹暂存、指定日期发货或发货当天通知。 #### 客服线下沟通 客服与仓库人员线下沟通暂存事宜(暂存客户、大致件数、发货要求),系统不做需求提报。 #### 仓库登记与同步 - 仓库按实际入库扫包裹码登记暂存,扫码即确定为包裹暂存。 - 登记后同步梦花仓管小程序、区域负责人、物流人员。 新增入口:梦花仓管 / 冷库暂存 / 今日新增入库。 ### 冷库入库 物流人员将需要暂存的包裹拉至冷库,并通过扫描包裹码执行“冷库暂存”。 #### 系统记录 - 包裹编号。 - 入库时间。 - 入库人员。 - 暂存状态。 扫码数量即为实际库存数量。 ### 到达发货日期:存在新的发货单 由合肥客服在当天发货单中关联冷库暂存客户。 #### 关联操作 - 选择暂存客户。 - 选择是否拆箱。 - 指定包裹,支持部分包裹。 确认后,系统自动生成“今日暂存花出库任务”。区域负责人、物流人员可在梦花仓管 / 冷库暂存 / 今日出库中查看场次、包裹及对应今天货架,随后物流拉包裹并正常发货。 > [!NOTE] > **拆箱逻辑:** 若选择拆箱重新打包,原包裹状态从“已打包”流转为“已打包无需出库”。原包裹不进入物流出库,原花材参与当天重新打包,新包裹正常发货;原打包记录保留,不影响打包绩效。 ### 到达发货日期:当天没有新的发货单 1. 合肥客服在运营端提交冷库暂存库存出库。 2. 选择暂存客户与指定包裹,支持部分包裹。 3. 确认后生成冷库出库任务。 4. 梦花仓管 / 冷库暂存 / 今日出库展示待出库包裹,无需关联当天货架。 5. 物流扫码出库并交物流公司。 状态流转:已打包 → 已出库。 ## 场景二:未打包暂存(裸花) ### 客服沟通与仓库登记 适用于客户要求不打包,裸花直接进入冷库。 #### 客服线下沟通 客服与仓库人员线下沟通裸花暂存事宜,系统不做需求提报。仓库或现场负责人扫花材二维码完成裸花入库登记(扫码即确定为裸花暂存)后,同步至梦花仓管 / 冷库暂存 / 今日新增入库。 ### 冷库入库 质检或区域负责人拉花至冷库,拍照确认后提交。系统新增冷库暂存客户。 > [!NOTE] > 裸花不记录包裹,仅记录客户。 ### 到达发货日期:存在新的发货单 合肥客服在当天发货单关联冷库暂存客户,裸花场景仅支持选择客户。 梦花仓管今日出库展示暂存客户与今天货架。现场拉花后进行打包,进入当天箱子并正常出库;若箱子数量不足,现场新增箱子面单。 ### 到达发货日期:当天没有新的发货单 客服在运营端冷库暂存库存中选择暂存客户并确认。梦花仓管今日出库展示客户信息,现场拉花、打包、扫码出库并交物流公司,无需对应当天货架。 ## 角色职责 | 角色 | 职责 | |----|----| | 合肥客服 | 与客户沟通暂存需求并线下告知仓库(系统不做需求提报);负责次日需要出库客户的信息确认;按客户确认结果关联当天发货单、直接生成冷库出库任务,或登记继续暂存沟通记录。 | | 仓库 | 根据线下沟通及现场实际情况扫码登记冷库暂存入库(扫包裹码=包裹暂存,扫花材码=裸花暂存),维护暂存货位、入库数量、入库照片、入库人等信息。 | | 物流人员 | 冷库入库、包裹扫码、冷库出库。 | | 区域负责人 | 查看区域冷库暂存花材入库与出库信息,跟进异常事项。 | | 打包人员 | 重新打包、扫码打包。 | | 系统 | 信息同步、状态流转。 | ## 小程序端功能设计(质检/区域负责人) 小程序端用于质检、区域负责人查看冷库暂存信息,并完成裸花暂存登记。页面入口为底部导航“冷库暂存”。完整交互原型见[原型版](/已打包未打包冷库暂存花材信息流解决方案-原型版)。 ![小程序暂存出库列表](/assets/cold-storage/mini-outbound.png) *暂存出库:查看待出库、已出库的包裹暂存与裸花暂存客户。* ![小程序暂存入库列表](/assets/cold-storage/mini-inbound.png) *暂存入库:查看今日新增入库与当前暂存记录。* ![小程序当前暂存列表](/assets/cold-storage/mini-current.png) *当前暂存:按客户维度查看仍在冷库中的暂存货物。* ![PDA包裹暂存扫码入库](/assets/cold-storage/pda-scan-inbound.png) *PDA 扫码入库:物流专员扫描第一个包裹码后带出当前客户信息和已扫包裹型号数量;每次扫码即提交暂存入库,同场次同货架记录自动合并。* ### 冷库暂存首页 #### 筛选与查询 - 支持按客户手机号、客户昵称、暂存货位号检索。 - 支持按日期查看,默认展示当天业务日期。 - 支持按暂存类型筛选:全部类型、包裹暂存、裸花暂存。 - 支持按业务状态切换:暂存出库、暂存入库、当前暂存。 #### 统计卡片 - 暂存客户:当前存在冷库暂存记录的客户数。 - 其中裸花暂存客户:当前裸花暂存客户数。 - 暂存包裹合计:当前冷库暂存包裹总数。 - 今日暂存出库客户:今日生成或完成出库任务的客户数。 ### 暂存出库列表 用于质检、区域负责人查看今日需要出库或已出库的冷库暂存任务。 | 字段 | 说明 | |----------------|---------------------------------------------------------| | 客户信息 | 客户姓名、手机号,手机号支持一键拨打。 | | 暂存类型 | 包裹暂存或裸花暂存。 | | 暂存货位号 | 冷库内暂存货位,例如 \#01-005。 | | 暂存场次 | 原暂存入库场次日期。 | | 出库数量 | 包裹暂存展示出库包裹数/总包裹数;裸花暂存展示裸花扎数。 | | 拆箱重打 | 包裹暂存支持展示是否拆箱重打。 | | 任务状态 | 待出库、已出库。 | | 客服与操作时间 | 展示客服姓名、手机号、出库任务生成或操作时间。 | ### 暂存入库与当前暂存列表 用于查看已进入冷库的客户暂存信息,包括包裹暂存与裸花暂存。 - 包裹暂存展示:客户信息、暂存货位号、暂存场次、出库包裹数/总包裹数、客户备注、入库人、更新时间。 - 裸花暂存展示:客户信息、暂存货位号、暂存场次、裸花扎数、客户备注、入库人、更新时间。 - 暂存入库列表展示状态为“暂存中”的入库记录。 - 当前暂存列表展示仍在冷库内、尚未完全出库的库存记录。 ### 裸花暂存登记 裸花暂存由质检或区域负责人在小程序端登记,用于客户要求“不打包,裸花直接入冷库”的场景。登记时通过扫描花材二维码识别客户和花材信息,确认无误后提交暂存登记。 | 区域 | 字段 | 要求 | |----|----|----| | 扫码识别 | 花材二维码 | 扫描现场花材二维码,系统识别对应客户、场次、货架号和花材数量。 | | 扫码结果 | 场次、货架号、花材总扎数 | 展示二维码对应的场次、货架和花材数量,便于现场核对。 | | 扫码结果 | 客户昵称、手机号、收货地址 | 展示客户基础信息,用于确认暂存对象无误。 | | 确认提交 | 扫码带出信息 | 扫描成功后展示确认信息;确认无误后提交裸花暂存登记。 | | 系统记录 | 入库人、登记时间 | 系统自动记录当前操作人和提交时间。 | > [!TIP] > 裸花暂存登记页的交互原型见[原型版](/已打包未打包冷库暂存花材信息流解决方案-原型版)。 > [!NOTE] > 裸花暂存仍按客户维度管理,不拆分为包裹库存;当前版本不支持裸花部分出库。 ## PDA 端功能设计(物流专员) PDA 端用于物流专员对已打包包裹执行扫码入库,形成包裹维度冷库库存。 ### 包裹暂存扫码入库 - 入口:PDA / 冷库暂存。 - 物流专员扫描第一个包裹码后,系统识别并带出当前客户信息、暂存类型、暂存货位号、暂存场次及客服备注。 - 页面展示当前客户信息,以及已扫包裹的型号、数量汇总,方便物流专员核对。 - 每次扫描包裹码后即进行暂存提交,包裹状态变更为冷库暂存,并记录入库人、入库时间。 - 支持连续扫码,同一客户、同场次、同货架下的包裹自动累计。 - 同场次同货架已有暂存记录时,后续扫码入库记录合并到同一暂存库存中,更新包裹数量和型号数量汇总。 ### PDA 页面字段 | 区域 | 字段 | 说明 | |----|----|----| | 客户信息 | 客户姓名、手机号、暂存货位号、暂存场次、客户备注、发货物流、入库人 | 扫描第一个包裹码后自动带出,用于核对暂存客户与现场货位。 | | 已扫包裹汇总 | 包裹型号、型号数量、已扫总数 | 按包裹型号聚合展示已扫入库数量。 | | 已包裹信息 | 包裹名称、规格、序号 | 展示已扫码进入冷库的包裹明细。 | | 扫码输入 | 包裹码 | 支持外接扫码枪或 PDA 内置扫码能力;每次扫码成功后即提交暂存入库。 | | 合并规则 | 场次、货架号 | 同场次同货架的包裹暂存记录自动合并,累计包裹数量和型号数量。 | ### 扫码校验 - 包裹码不存在:提示“未找到包裹信息”。 - 包裹不属于当前暂存客户:提示“包裹客户与暂存客户不一致”。 - 包裹已入库:提示“该包裹已冷库暂存,请勿重复扫码”。 - 包裹已出库或已取消:不允许入库。 - 同场次同货架已有暂存库存:允许继续扫码并合并到已有记录。 - 包裹所属场次与当前任务不同但属于历史暂存场景时,允许跨场次入库并记录原场次。 ## 运营端客服功能设计 运营端客服是冷库暂存客户沟通与出库处理角色。合肥客服收集客户暂存需求后与仓库线下沟通(系统不做需求提报),不负责实际入库登记;第二天出库前,客服确认需要出库的客户信息,并根据客户确认结果完成关联发货单、直接出库或继续暂存沟通记录。 ### 功能菜单 运营端建议在客服或仓库业务菜单下新增“冷库暂存”一级菜单,客服侧重点处理客户沟通与出库确认。 ```text 冷库暂存 ├── 冷库暂存库存 │ ├── 待发货 │ ├── 待关联 │ ├── 已关联 │ ├── 待出库 │ └── 已出库 ├── 联系记录 └── 历史出库记录 ``` - 冷库暂存库存:客服查看仓库已登记库存,并进行关联当天发货单出库、直接出库、联系记录登记。 - 联系记录:按客户维度查看沟通历史,也可从列表操作栏进入抽屉页登记。 - 历史出库记录:查看已关联、已直接出库、已撤销的出库操作记录。 > [!TIP] > 冷库暂存库存列表、关联当天发货单出库、直接出库、联系记录四个页面的交互原型见[原型版](/已打包未打包冷库暂存花材信息流解决方案-原型版)(原始 HTML 方案,保留可点击的原型)。 ### 客服暂存沟通(线下,系统不做需求提报) 花店找到客服后,客服与仓库人员 **线下沟通**暂存事宜,系统不提供需求提报功能。暂存类型由仓库入库扫码确定:**扫包裹码=包裹暂存,扫花材码=裸花暂存**;入库数量、货位、照片和入库人均以仓库扫码登记为准。客户的发货要求(计划发货日期、客户通知发货等)由客服在入库后于冷库库存上维护。 ### 冷库库存管理 运营端新增“冷库暂存库存”页面,作为客服查看仓库登记结果、处理次日出库确认和出库任务的统一入口。 #### 列表筛选 - 客户手机号、客户昵称、发货单号、包裹码、暂存货位号。 - 暂存类型:全部、包裹暂存、裸花暂存。 - 库存状态:待发货、待关联、已关联、待出库、已出库。 - 暂存场次、计划发货日期、客服、入库人。 #### 列表字段 - 客户信息、暂存类型、暂存货位号、暂存场次、计划发货日期。 - 包裹数/裸花扎数、已出库数量、剩余库存数量。 - 客户备注、客服、入库人、入库时间、更新时间。 - 关联发货单、出库任务状态、异常标记。 ### 次日出库客户信息确认 合肥客服在第二天出库前,基于“待发货”冷库库存联系客户确认是否发货。 - 支持标记客户确认结果:确认今日发货、继续暂存、联系不上、取消暂存。 - 确认今日发货后,状态变更为待关联。 - 继续暂存时,可维护下次计划发货日期或保持客户通知发货。 - 沟通后客户仍需继续暂存时,必须登记沟通记录,记录客户意向、下次联系时间或计划发货日期。 - 联系不上时保留待发货状态,并记录联系备注。 - 每次确认动作记录操作人、操作时间、确认结果、确认备注。 ### 关联当天发货单出库 客户确认与当天新订单合并发货时,合肥客服点击“关联当天发货单出库”。系统根据暂存类型执行不同逻辑。 - 系统根据客户账号和当前场次自动查询当天发货单。按客户账号唯一性判断,一个客户一个场次只会有一个发货单。 - 关联发货单信息需展示发货单中重量,用于判断是否满足截单生成货架号规则。 - 若发货单中重量 \< 6kg 且发货单不是已发货状态,不允许关联,并提示“发货单不足 6kg 且未发货,不允许关联”。 - 若发货单中重量 \< 6kg 但已提前发货,允许关联;因提前发货不会生成货架号,分区货架展示为“提前发货未生成货架号”。 - 若发货单中重量 \< 6kg 且已发货,但存在供应商不会送货等业务问题,由客服在处理时联系解决,系统不阻断关联。 - 包裹暂存:弹窗展示暂存包裹明细,客服选择本次出库包裹,支持选择部分包裹;确认后生成出库任务。 - 裸花暂存:关联逻辑一致,但不选择包裹,按客户维度整单出库;记录展示该发货单对应的裸花扎数。 - 确认出库前必须出现二次确认弹窗,提示关联单号、发货单客户昵称/电话、本次出库明细。 - 确认出库后,列表“关联单据”处展示发货单号、当天发货单客户昵称和电话,用于和首列暂存客户对比,快速发现客户不一致问题。 - 暂存详情需记录本次出库明细,包括出库时间、操作人、关联发货单、包裹明细或裸花扎数。 - 任务生成后状态进入待出库;在未实际出库前,支持撤销关联出库,并记录撤销人、撤销时间、撤销原因。 ### 物流费结算与运费退补 冷库暂存合并新场次发货会涉及暂存场次与发出场次两套物流费用,需要在系统中明确费用归属、运费退补记录和暂存场次特殊完结条件。 - 暂存场次已向客户收取物流费,但该场次实际未发货,实付给物流公司的费用为 0。 - 发出场次物流费用初始按当天发货单收取;当历史暂存货物与当天发货单合并发出后,发出场次实付给物流公司的费用通常会大于预收物流费。 - 发出场次产生的物流费差额需通过当前系统已有“运费退补”能力向客户补收运费,并更新退补记录。 - 暂存场次物流费的退还必须在发出场次发货单运费填报完成,并更新退补记录后才允许进行。 - 发出场次发货单运费填报完成,并更新退补记录后,暂存场次发货单状态更新为“合并新场次发货”,作为特殊完结状态参与正常供应商结算。 ### 直接出库 当天没有新的发货单,但客户要求发出冷库库存时,合肥客服点击“直接出库”。直接出库弹窗与关联当天发货单出库弹窗分开处理。 - 包裹暂存:展示暂存包裹明细,客服选择本次直接出库包裹,支持选择部分包裹。 - 裸花暂存:不选择包裹,按客户维度整单出库,记录本次直接出库扎数。 - 发货物流沿用原发货单的物流,弹窗内仅展示不可修改。 - 确认出库前必须出现二次确认弹窗,提示暂存客户、本次出库包裹或裸花扎数、发货物流。 - 确认后生成梦花仓管今日出库任务,无需关联当天货架,不展示关联单号。 - 任务生成后状态进入待出库;在未实际出库前,支持撤销直接出库,并记录撤销人、撤销时间、撤销原因。 ### 联系记录 联系记录不作为顶部全局按钮,放在列表操作栏。点击“联系记录”后打开抽屉页,客服可查看历史沟通记录并新增记录。 - 联系记录按客户和暂存库存聚合展示。 - 新增记录时需填写联系结果、联系内容;继续暂存时建议填写下次联系时间或计划发货日期。 - 记录字段包括登记时间、登记人、联系结果、联系内容。 - 历史记录按登记时间倒序展示。 ### 异常处理 - 库存数量与现场不一致:允许标记库存异常,填写异常说明并同步区域负责人。 - 包裹扫码缺失:客服可查看未入库包裹清单并催办物流入库。 - 客户要求变更发货日期:允许维护计划发货日期并记录变更日志。 - 拆箱重打:必须记录原包裹状态为“已打包无需出库”,新包裹按当天订单正常发货。 - 仓库未完成入库登记:客服仅可查看待登记状态并催办仓库,不允许直接生成出库任务。 - 关联发货单不足 6kg 且未发货:系统禁止关联,并提示客服该发货单截单时可能自动取消。 ## 状态设计 ### 包裹状态 待打包 → 已打包 → 冷库暂存 → 已关联订单 → 待出库 → 已出库 > [!NOTE] > 新增“已打包无需出库”,用于拆箱重新打包的历史包裹状态完结。 ### 裸花状态 待入库 → 冷库暂存 → 已关联订单 → 待打包 → 已打包 → 已出库 ### 暂存场次发货单状态 已收物流费 → 冷库暂存未发货 → 等待发出场次运费填报 → 退补记录已更新 → 合并新场次发货 > [!NOTE] > “合并新场次发货”为暂存场次发货单的特殊完结状态。只有发出场次发货单运费填报完成,并更新退补记录后,暂存场次发货单才允许更新为该状态,并按正常规则参与供应商结算。 ## 信息同步 1. 仓库完成暂存入库登记后,同步运营端客服、物流、区域负责人。 2. 客服关联后,同步物流、区域负责人。 3. 今日出库任务同步物流、区域负责人。 4. 打包存在拆箱时,弹窗提醒。 5. 小程序裸花暂存登记后,同步运营端客服、区域负责人。 6. PDA 包裹扫码入库后,同步运营端冷库库存与小程序当前暂存列表。 ## 系统改造 ### 运营端新增 - 发货单关联冷库客户。 - 冷库暂存库存。 - 冷库库存查询。 - 继续暂存沟通记录。 - 关联当天发货单出库。 - 关联发货单中重量展示与不足 6kg 关联校验。 - 直接出库。 - 发出场次运费退补联动。 - 未出库撤销。 ### WMS 新增 - 冷库包裹状态。 - 裸花暂存管理。 - 跨场次关联能力。 - PDA 包裹扫码入库。 ### 冷库库存管理 冷库库存作为整个冷库业务的统一管理入口,主要用于查看仓库登记后的当前库存、待关联客户、待发货客户,支持客服在次日出库前联系客户确认是否发货,并可关联当天发货单出库、直接生成冷库库存出库任务、登记继续暂存沟通记录,或在未实际出库前撤销出库任务。 | 状态 | 说明 | |--------|----------------------------| | 待发货 | 等待客户通知或指定日期。 | | 待关联 | 已确认发货,待关联发货单。 | | 已关联 | 已关联当天发货单。 | | 待出库 | 已生成今日出库任务。 | | 已出库 | 已完成发货。 | ### 梦花仓管新增菜单 ```text 冷库暂存 ├── 今日新增入库 ├── 今日出库 ├── 冷库库存 └── 裸花暂存登记 ``` ## 注意事项 **跨场次限制:** 冷库业务涉及历史场次货物参与当前场次发货,需要全面梳理现有系统关于场次的限制。重点确认打包、发货、出库、包裹、箱号是否限制当前场次;若限制影响业务流转,应同步解除相关限制。 > [!NOTE] > **裸花暂不支持部分出库:** 当前裸花以客户维度管理,暂不支持部分鲜花继续留库或部分鲜花提前发货。若支持部分出库,需要维护裸花库存拆分、数量扣减、再次关联等复杂逻辑,后续根据业务需求再评估。 **物流费结算:** 暂存场次已收客户物流费但实付物流公司为 0;发出场次合并暂存货物后实付物流公司费用可能大于预收物流费,需通过运费退补补收差额并更新退补记录。暂存场次物流费退还必须等发出场次发货单运费填报完成,并更新退补记录后再处理。 **不足 6kg 发货单关联:** 截单生成货架号时,发货单中重量 \< 6kg 会被系统自动取消。若暂存花材在截单前关联到不足 6kg 且未发货的发货单,会导致后续货架、出库和发货异常,因此系统必须限制关联。 ## 验收标准 1. 暂存需求由客服与仓库线下沟通,系统不做需求提报;暂存类型由入库扫码确定(扫包裹码=包裹暂存,扫花材码=裸花暂存)。 2. 支持仓库/物流完成冷库入库登记,并形成库存记录。 3. 支持跨场次关联历史冷库暂存客户。 4. 支持关联部分包裹(已打包场景)。 5. 支持拆箱重新打包,并新增“已打包无需出库”状态,不影响打包绩效。 6. 支持直接发出冷库库存,无需关联当天发货单。 7. 梦花仓管新增“今日新增入库”“今日出库”“冷库库存”功能,相关角色可实时查看任务。 8. 冷库暂存业务支持指定日期发货及客户通知发货两种模式。 9. 裸花暂存暂不支持部分出库。 10. 冷库业务支持跨场次流转,不受原有场次限制。 11. 合肥客服可根据冷库库存完成第二天出库客户信息确认。 12. 冷库库存可作为统一业务入口,支持关联当天发货单或直接生成冷库出库任务。 13. 小程序端支持暂存出库、暂存入库、当前暂存列表查看,并支持裸花暂存登记。 14. PDA 端支持物流专员扫描包裹码完成包裹冷库暂存入库。 15. 运营端客服支持冷库库存查询、次日出库确认、关联当天发货单、直接生成出库任务、继续暂存沟通记录及异常处理。 16. 包裹暂存关联当天发货单出库时,支持选择部分包裹,并在暂存详情记录本次出库包裹明细。 17. 裸花暂存关联当天发货单出库时,不选择包裹,按客户维度全量出库,并记录本次出库扎数。 18. 关联当天发货单出库后,列表关联单据展示发货单号、当天发货单客户昵称和电话。 19. 关联当天发货单出库和直接出库均需二次确认;未实际出库前支持撤销,并记录撤销原因。 20. 联系记录从列表操作栏进入抽屉页登记,记录按登记时间倒序展示。 21. 关联当天发货单信息需展示发货单中重量;发货单不足 6kg 且未发货时不允许关联,并展示明确提示。 22. 发货单不足 6kg 但已提前发货时允许关联,分区货架展示为“提前发货未生成货架号”。 23. 发出场次运费不足时支持通过现有运费退补能力向客户补收运费,并更新退补记录。 24. 暂存场次物流费退还必须在发出场次发货单运费填报完成,并更新退补记录后进行。 25. 发出场次发货单运费填报完成,并更新退补记录后,暂存场次发货单状态更新为“合并新场次发货”,并可正常参与供应商结算。 需求文档更新时间:2026-07-09 08:34