--- schema: "hl-changelog/v2" ticket: "8813" title: "团期核单明细带出/暂存合并:已暂存 tab 也持续带出未暂存预览行,新增 bizKey 溯源" consumer: "admin" author: "yst" change_type: "修改接口" backend_status: "merged" gateway_status: "not_required" frontend_status: "implemented" frontend_owner: "hl-admin(claude)" frontend_ref: "b09fb93a03a60f35acbb7e707b519cb888b65e90" target_release: "" verified_at: "2026-10-10" status_note: "后端 PR #8817 已合并 dev-v3(squash 4734715671),含 Flyway 迁移 V20261009_747(8 张 order_group_settlement_* 明细表加 biz_key 列),需部署 order-v3 后生效;前端必须同步改造:进入 tab 整份编辑 lines 数组(含 lineId=null 待带出行),暂存时原样透传 sourceType/bizKey,否则已暂存 tab 会出现旧暂存行与重新带出行双显双计 / 预览行整份同权编辑+底色标识,sourceType/bizKey 纯透传,PUT 回执 warnMessage toast,589756 重读全量;checkpoint 全量+生产构建过" updated_at: "2026-10-10" base: "dev-v3" --- # 团期核单明细带出/暂存合并 + bizKey 溯源(管理后台) > ⚠️ **修改接口(前端必须同步上线)**:团期核单 8 类 tab 的「带出」语义从「暂存过的 tab 不再带出」改为「**始终合并返回**」——已暂存行照原样显示、未暂存的带出预览行继续带入(lineId=null)。前端交互改为「整份 lines 编辑 + 暂存时透传 sourceType/bizKey」。**新旧前端混跑会出现双显双计**,务必同步上线。 ## 1. 接口背景 团期核单 8 类费用 tab(住宿/门票/餐食/车辆/导游/摄影师/其他支出/其他收入)里,大量明细行是系统从订单、资源准备、批次成本里**自动带出**的预览行(如各订单的房费、门票费、团车公摊),运营只需确认或微调后暂存。 旧行为的问题:一旦某个 tab 暂存过,系统就**不再带出**该 tab 的后续预览行——后续新报名的订单费用、新产生的批次成本就「消失」了,运营完全感知不到漏了钱。 本次(#8813)改为: - **带出与暂存合并**:无论何时进入 tab,已暂存的行照常显示;**尚未被暂存覆盖的带出预览行继续带入**(lineId=null、带 sourceType/bizKey 标识),运营一眼看到「还有 N 条带出成本没纳入」。 - **bizKey 溯源**:每条行新增 bizKey(来源业务键),暂存时原样回传,后端据此判断「这条暂存行对应哪条带出成本」,已暂存的预览行不再重复带出,防止重复计入。 - **漏带提醒**:暂存时如果漏掉了某些带出行,响应里给 warnMessage / missedCarryOverCount 做 WARN 提示(不阻断)。 ## 2. 变更清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 分类 tab 明细查询 | GET | /v3/admin/order/group-batch/{groupBatchId}/settlement/{category} | ⚠️ 行为变化 | **暂存过的 tab 也持续带出未暂存预览行**;行出参加 bizKey;sourceType 正确区分 CARRY_OVER/BATCH_COST/MANUAL | | 2 | 整 tab 暂存 | PUT | /v3/admin/order/group-batch/{groupBatchId}/settlement/{category} | ⚠️ 行为变化 | 入参行可透传 sourceType/bizKey;响应新增 warnMessage/missedCarryOverCount;新增硬拦错误码 589756/589757 | | 3 | 新增单行 | POST | /v3/admin/order/group-batch/{groupBatchId}/settlement/{category}/lines | 修改 | 入参同样可透传 sourceType/bizKey(接受待带出行时使用),同 589756 校验 | category 8 值:hotels / activities / meals / vehicles / guide-fees / photographer-fees / other-expenses / other-incomes。 DDL:8 张 order_group_settlement_* 明细表加 biz_key 列(迁移 V20261009_747),Flyway 部署时自动执行,**前端无需任何动作**。 ## 3. 接口详情 | 项 | 说明 | |---|---| | 服务 | hl-order-service-v3(端口 8086) | | 使用场景 | 管理后台 → 团期详情 → 核单 Tab:8 类费用明细查看/编辑/暂存 | | 认证 | 管理后台登录态(JWT);网关既有路由 /v3/admin/**,无新增网关配置 | | 权限码 | 沿用核单既有权限:group-batch:audit:view(GET)、group-batch:audit:edit(PUT / lines 新增) | | 幂等性 | PUT 整 tab 暂存仍走 main 主行锁 + expectedVersion CAS(过期仍吃 589573);新增单行天然幂等(无去重键) | | 限流 | 无特殊限流 | ## 4. 接口入参 ### 4.1 路径参数(三个端点共有) | 参数 | 位置 | 类型 | 必填 | 说明 | |------|------|------|------|------| | groupBatchId | Path | Long | 是 | 团期 ID | | category | Path | String | 是 | 费用类别 8 值之一(见 §6) | ### 4.2 PUT 整 tab 暂存请求体(GroupSettleTabSaveReqVO,仅列变化点) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | expectedVersion | Number(int) | 是 | 乐观锁版本号,取 GET 响应里的 version;过期返 589573 | | lines | Array | 是 | **整份明细行数组(全量替换语义)**,行结构见 4.3 | ### 4.3 行结构 GroupSettleLineSaveReqVO(PUT 与 POST lines 共用) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | lineId | String(Long) | 否 | 已暂存行带原 lineId;**带出预览行(GET 里 lineId=null 的行)暂存时不传**,由后端建档 | | itemName / spec / quantity / unitPrice / totalAmount / remark 等业务字段 | — | 按原契约 | 无变化,沿用 #8714 契约 | | **sourceType** | String | 否 | **新增可透传**:带出预览行暂存时**原样回传** GET 给的值(CARRY_OVER/BATCH_COST);手加行**不传**(后端默认 MANUAL) | | **bizKey** | String | 否 | **新增可透传**:带出预览行暂存时**原样回传** GET 给的 bizKey;手加行不传 | > **铁律**:sourceType/bizKey 前端**不解析、不构造、不修改**,只做「GET 拿到什么 → PUT 原样回传什么」的透传。 ## 5. 出参字段 ### 5.1 GET 分类 tab 明细(GroupSettleTabRespVO,仅列变化点) | 字段 | 类型 | 说明 | |------|------|------| | status / version / editable / summary 等 | — | 无变化;editable 仍 = status==DRAFT | | lines[] | Array | **行为变化**:现在 = 已暂存行 ∪ 未暂存带出预览行(合并返回,不再互斥) | | lines[].lineId | String(Long) 或 null | 已暂存行=实际行 ID;**带出预览行=null**(前端据此区分并可渲染「待带出」样式) | | lines[].sourceType | String | **现在会正确区分**:CARRY_OVER(订单带出)/ BATCH_COST(批次成本带出)/ MANUAL(手加) | | **lines[].bizKey** | **String 或 null** | **新增**:来源业务键。带出行有值,手加行=null。**前端不解析**,仅透传 | 金额与 ID 字段一律以 **JSON 字符串**返回(防 JS 精度丢失)。 bizKey 格式参考(仅供理解,**不需要解析**): - 单价型:`D1|驯鹿拉车体验|成人票|59`(slot|名称|规格|单价) - 总额型:`TOTAL:VEHICLE:SHARED:BUS` / `TOTAL:{类别}:L{序号}` ### 5.2 PUT 暂存响应(GroupSettleTabWriteRespVO,仅列变化点) | 字段 | 类型 | 说明 | |------|------|------| | version / lineIds 等既有字段 | — | 无变化 | | **warnMessage** | **String 或 null** | **新增**:暂存漏了某些带出行时的提示文案,如「有 2 条带出成本未纳入本次暂存」;无遗漏时为 null | | **missedCarryOverCount** | **Number(int)** | **新增**:漏带的带出行条数;0 = 无遗漏 | > WARN 只是提醒,**暂存本身成功**(HTTP 200)。前端建议 toast 展示 warnMessage,不打断流程。 ## 6. 枚举 / 数据字典 ### 6.1 category(路径段,8 值) hotels(住宿)/ activities(门票·游玩)/ meals(餐食)/ vehicles(车辆)/ guide-fees(导游)/ photographer-fees(摄影师)/ other-expenses(其他支出)/ other-incomes(其他收入)。中文名走数据字典 settlement_category。 ### 6.2 sourceType(行来源,3 值) | 值 | 含义 | 出现场景 | |---|---|---| | CARRY_OVER | 订单带出(从各子订单费用明细带出) | 带出预览行 / 由带出行暂存落库的行 | | BATCH_COST | 批次成本带出(团级公摊成本,如团车) | 同上 | | MANUAL | 手加行 | 前端手工新增的行(默认,不传即 MANUAL) | ## 7. 错误码 | 错误码 | 触发场景 | 前端处理 | |---|---|---| | **589756** | **明细行来源标识与本团带出成本不匹配**:提交的 sourceType/bizKey 不在本 tab 当前带出预览集内(如过期预览行、伪造 bizKey) | 硬拦。提示用户刷新 tab 重新加载最新带出集后再暂存 | | **589757** | **存在重复的来源成本行**:同一次提交里出现两条相同 bizKey 的行 | 硬拦。前端去重逻辑自查(正常整份编辑不会触发) | | 589573 | expectedVersion 过期(并发暂存冲突) | 沿用原处理:刷新重载 | ## 8. 示例 ### 8.1 典型:tab 已暂存过,新带出预览行继续带入 GET /v3/admin/order/group-batch/1024/settlement/hotels ```json { "code": 0, "data": { "status": "DRAFT", "editable": true, "version": 3, "lines": [ { "lineId": "88001", "sourceType": "CARRY_OVER", "bizKey": "D1|驯鹿拉车体验|成人票|59", "itemName": "驯鹿拉车体验-成人票", "quantity": "10", "unitPrice": "59.00", "totalAmount": "590.00" }, { "lineId": "88002", "sourceType": "MANUAL", "bizKey": null, "itemName": "临时加床费", "quantity": "1", "unitPrice": "200.00", "totalAmount": "200.00" }, { "lineId": null, "sourceType": "CARRY_OVER", "bizKey": "D2|星空蒙古包|标间|480", "itemName": "星空蒙古包-标间", "quantity": "5", "unitPrice": "480.00", "totalAmount": "2400.00" } ] } } ``` > 第 1 条是已暂存的带出行(有 lineId + bizKey);第 2 条是手加行(bizKey=null);第 3 条是**新带出的预览行**(lineId=null),等待运营确认。 ### 8.2 边界:暂存整份提交,带出行透传 sourceType/bizKey,漏带 1 条给 WARN PUT /v3/admin/order/group-batch/1024/settlement/hotels ```json { "expectedVersion": 3, "lines": [ { "lineId": "88001", "sourceType": "CARRY_OVER", "bizKey": "D1|驯鹿拉车体验|成人票|59", "itemName": "驯鹿拉车体验-成人票", "quantity": "10", "unitPrice": "59.00", "totalAmount": "590.00" }, { "lineId": "88002", "itemName": "临时加床费", "quantity": "1", "unitPrice": "200.00", "totalAmount": "200.00" } ] } ``` 响应(漏了 8.1 里那条 lineId=null 的带出行,**暂存仍成功**): ```json { "code": 0, "data": { "version": 4, "warnMessage": "有 1 条带出成本未纳入本次暂存", "missedCarryOverCount": 1 } } ``` > 手加行(第 2 条)不传 sourceType/bizKey;带出行(第 1 条)原样透传。下次再 GET,那条漏掉的预览行仍会带出(lineId=null),不会丢。 ### 8.3 业务失败:bizKey 不在带出集触发 589756 暂存时提交了一条 bizKey=D9|不存在的项目|票|1(不在本 tab 当前带出预览集): ```json { "code": 589756, "msg": "明细行来源标识与本团带出成本不匹配" } ``` ## 9. 业务边界 **适用**: - 核单 DRAFT 状态下的 8 类 tab 查看/编辑/暂存/单行新增。 - 「暂存过但仍想看到新带出成本」的场景——本次核心。 **不适用**: - 核单 CONFIRMED 后:editable=false,带出行不再带入(tab 已锁定),写口全部被既有门禁拦截。 - 前端自行拼装 bizKey:bizKey 由后端生成,前端伪造会吃 589756。 **特殊边界**: - 带出预览行**不落库**,只有暂存后才成为正式行;所以 GET 里 lineId=null 的行在「刷新页面」后可能因业务数据变化而增减,属正常。 - 同 tab 多次暂存:已暂存的带出行(bizKey 已落库)不会重复带出。 ## 10. 修改前后对比 ### 行为级 | 场景 | 修改前 | 修改后 | |---|---|---| | tab 已暂存过,再次进入 | **不再带出任何预览行**,新产生的订单费用/批次成本不可见 | **已暂存行 + 未暂存带出预览行合并返回**,新成本持续可见 | | 带出预览行标识 | sourceType 区分不可靠 | sourceType 正确区分 CARRY_OVER/BATCH_COST/MANUAL,且带 **bizKey** 溯源键 | | 暂存漏掉带出行 | 静默漏掉,运营无感知 | 响应给 warnMessage + missedCarryOverCount(WARN 不阻断) | | 伪造/过期 bizKey | 无校验 | 589756 硬拦 | | 同次提交 bizKey 重复 | 无校验 | 589757 硬拦 | ### 字段级 | 字段 | 修改前 | 修改后 | |---|---|---| | GET 出参 lines[].bizKey | 无 | **新增**(String,带出行有值,手加行 null) | | PUT/POST 入参 lines[].sourceType / bizKey | 不接收 | **新增可透传**(带出预览行暂存时原样回传) | | PUT 响应 warnMessage / missedCarryOverCount | 无 | **新增** | ## 11. 影响评估 / 回滚 **兼容性**:⚠️ **前端必须同步上线,属破坏性行为变化**。 - 新后端 + 旧前端:已暂存 tab 会同时出现「旧暂存行(bizKey=null)+ 重新带出的同类预览行」,**双显双计**,金额虚高。 - 新前端 + 旧后端:bizKey 无处可取,退化为旧行为(可接受但不解决问题)。 **前端改造要点**: 1. 进入 tab 拿到的**完整 lines 数组(含 lineId=null 待带出行)整份编辑**,不再区分「带出区/暂存区」两个数据源。 2. 暂存时把带出预览行的 sourceType/bizKey **原样透传**回去,整份提交。 3. 渲染上可用 lineId==null 标「待带出」样式;用 missedCarryOverCount>0 toast warnMessage。 4. 收到 589756 提示刷新重载(多半是预览集已变)。 **回滚方案**:后端回滚本 PR 即恢复旧行为(biz_key 列保留无害);前端若已上线透传逻辑,旧后端会忽略多余字段,不报错。建议前后端同窗口发布。 ## 12. 注意事项 - bizKey 前端**不解析、不构造、不修改**,纯透传。格式见 §5.1 仅作理解参考。 - 金额/ID 一律字符串处理。 - editable 判定未变:仍 = status==DRAFT。 - 部署依赖:Flyway 迁移 V20261009_747 随 order-v3 部署自动执行,**联调前先确认测试服已部署本 PR 之后的构建**。 ## 13. 关联 / 联系人 - Issue: https://git.1814.love/wx/HL/issues/8813 - PR: https://git.1814.love/wx/HL/pulls/8817 - Commit: https://git.1814.love/wx/HL/commit/4734715671 - 后端负责人:yst(腰苏图) - 前置契约:#8714 团期核单重做 8 类 tab(本仓 changelogs-v2/2026-10/04_8714_团期核单重做8类tab明细+公摊拆账-修改接口-管理后台.md)