41 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 8714 | 团期核单重做为8类tab明细结构+公摊/指定报名拆账(旧/audit端点下线) | admin | yst | 修改接口 | merged | not_required | implemented | mmg | 4a9a34637c6d7a669479547703186948f18fdcc9 | v2.1 | 2026-10-04 | 整批 6 PR 累积的结构重做,8 tab 出参结构变化 + 15 个新接口 + 旧 /audit/* 端点下线 404 + teamNo 新增,前端需按本契约重对接;sharedCostByType 键值从 BatchCostType 4 值切到 settlement_category 8 值。已合 dev-v3 未部署测试服。前端已交付(2026-10-04):核单页整页重对接——settlement 族 9 端点封装(8 tab GET/PUT、panel、sub-orders、lines 增删、panel/confirm、alloc-preview、invoice)+8 类 tab 明细录入(整 tab PUT 全量+CAS)+公摊/指定报名拆账(全手改 Σ=行总额客户端预校验+试算)+DRAFT→CONFIRMED 确认流+开票/结算门禁,旧 /audit/* 调用与四表常量全部移除,sharedCostByType 消费点本就读 costTypeDesc 字典零适配,提交 4a9a34637。 | 2026-10-04 | dev-v3 |
order-v3 groupbatch:团期核单重做为 8 类 tab 明细结构 + 公摊/指定报名拆账(管理后台)
⚠️ 修改接口(整体重做,破坏性):团期核单从旧「四表模型(统一科目行 + 逐户用量 + 逐户分摊)」重做为对齐常规订单核单的「8 类费用 tab 明细结构」,新增公摊 / 指定报名拆账能力。旧
/audit/*6 个端点全部下线 404(开发期无向后兼容),由 8 个分类 tab GET/PUT + 面板族 7 个新端点替代。费用类别从硬编码 AuditCategory 切换为数据字典settlement_category(8 值)。前端原核单页需按本契约整体重对接。
1. 接口背景
团期核单(#7868 初版)采用「统一科目行 + 逐户用量 + 逐户分摊」四表模型,与单订单核单的 8 类费用 tab 结构不一致:运营要在两套交互之间切换,且旧模型只有「均分 / 指定比例」粗粒度分摊,无法表达「这笔车费全团公摊、那笔房费只摊给指定几户」的真实业务。
#8714 整批 6 个 PR 把团期核单推倒重做:
- 存储:旧四表
order_batch_audit/_item/_detail/_alloc已 DROP,新建 10 张表(1 主表order_group_settlement_main+ 8 张分类明细表 + 1 张拆账表order_group_settlement_alloc)。 - 结构:对齐常规订单核单的 8 类费用 tab 明细结构——住宿 / 门票·游玩 / 餐食 / 车辆 / 导游 / 摄影师 / 其他收入 / 其他支出,每类一个 tab,每 tab 内是明细行数组。
- 拆账:每行支持两种分摊方式——公摊 SHARED(按全团人数均摊 / 按户均摊)与指定报名 DESIGNATED(勾选子订单子集 + 逐户比例 + 可手改金额),强校验 Σ拆账 = 明细行总额。
- 状态机:从旧多态简化为 DRAFT(录入中)→ CONFIRMED(已确认) 两态;确认后不可逆,不提供 un-confirm。
- 费用类别:废弃硬编码 AuditCategory,改走数据字典
settlement_category(8 值),类别中文名由字典下发。
本批已合并 dev-v3,尚未部署测试服(backend_status: merged),前端可先做接口层适配,联调待部署后进行。
2. 变更清单
| # | 接口 | 变更点 | 类型 |
|---|---|---|---|
| 1 | GET …/settlement/{hotels/activities/meals/vehicles/guide-fees/photographer-fees/other-incomes/other-expenses} |
8 个分类 tab 读端点路径不变、实现整体替换:出参从旧科目行结构改为 GroupSettleTabRespVO(明细行 + 拆账 splits + 版本号) | ⚠️ 出参结构重做 |
| 2 | PUT …/settlement/{同上 8 个路径} |
新增 8 个整 tab 暂存写端点(全量替换语义 + expectedVersion CAS) | ✨ 新增接口 |
| 3 | GET …/settlement/panel |
新增核单面板:状态主行 + 8 类合计 + 在团户视图(已摊成本/收入/毛利) | ✨ 新增接口 |
| 4 | GET …/settlement/sub-orders |
新增在团子订单列表(指定报名勾选数据源) | ✨ 新增接口 |
| 5 | POST …/settlement/lines |
新增单条明细行新增 | ✨ 新增接口 |
| 6 | DELETE …/settlement/lines/{lineId} |
新增明细行删除(级联删拆账) | ✨ 新增接口 |
| 7 | POST …/settlement/panel/confirm |
新增确认核单(确认后不可逆;注意路径是 panel/confirm 不是 /settlement/confirm,后者被团期结算财务复核占用) | ✨ 新增接口 |
| 8 | POST …/settlement/alloc-preview |
新增拆账试算不落库(前端录入期预览,P2 可选) | ✨ 新增接口 |
| 9 | POST …/settlement/invoice |
新增按子订单开票(GB-ADM-054 自旧 /audit/invoice 迁移,门禁改为核单 CONFIRMED) |
✨ 新增接口 |
| 10 | GET/PUT …/audit、POST …/audit/allocate、POST …/audit/reallocate、GET …/audit/export、POST …/audit/invoice |
全部下线,调旧路径 404(开发期无向后兼容) | ⚠️ 删除接口 |
| 11 | 出参 VO | panel.households[] / sub-orders[] / tab lines[].splits[] 新增 teamNo 团号字段(#8779) |
✨ 字段新增 |
| 12 | sharedCostByType(GroupReturnDetailRespVO / GroupBatchSettlementSummaryRespVO) |
costType 键值从 BatchCostType 4 值(BUS/LEADER/PHOTOGRAPHER/OTHER)切到 settlement_category 8 值;costTypeDesc 走数据字典 |
⚠️ 键值域变化 |
| 13 | 错误码 | 新增 589750-589755 拆账段;旧 589569/589570 废弃(语义分别由 589753/589750 承接) | ⚠️ 错误码调整 |
路径前缀统一为 /v3/admin/order/group-batch/{groupBatchId}/settlement,下文用 … 代指。
3. 接口详情
| 项 | 说明 |
|---|---|
| 服务 | hl-order-service-v3(端口 8086) |
| 路径前缀 | /v3/admin/order/group-batch/{groupBatchId}/settlement |
| 使用场景 | 管理后台 → 团期详情 → 核单 Tab:8 类费用明细录入、公摊/指定报名拆账、确认核单、按子订单开票 |
| 认证 | 管理后台登录态(JWT);网关既有路由 /v3/admin/**,无新增网关配置 |
| 权限码 | group-batch:audit:view(8 tab GET / panel / sub-orders / alloc-preview);group-batch:audit:edit(8 tab PUT / lines 新增删除);group-batch:audit:allocate(确认核单);group-batch:audit:invoice(开票)。缺码一律 589507 |
| 幂等性 | 写口不加 @Idempotent:并发与重复提交由 main 主行锁 + expectedVersion CAS 兜底(过期必吃 589573);开票防重由发票域自带 SETNX 承接 |
| 限流 | 无特殊限流 |
4. 接口入参
4.1 路径 / Query 参数
| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| groupBatchId | Path | Long | ✅ | 团期 ID,全部端点共有 |
| lineId | Path | Long | ✅ | 明细行 ID,仅 DELETE …/lines/{lineId} |
| category | Query | string | ✅ | 费用类别 8 值之一,仅 DELETE …/lines/{lineId}(决定从哪张分类表删) |
| expectedVersion | Query | int | ✅ | 乐观锁版本(≥0),仅 DELETE …/lines/{lineId};过期返 589573 |
4.2 PUT …/settlement/{tab} 整 tab 暂存请求体(GroupSettleTabSaveReqVO)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| expectedVersion | int | ✅ | 乐观锁版本(GET tab/panel 返回的 version 原样回传);与库值不符返 589573,本次请求零写入 |
| lines | array | ✅ | 整 tab 全量行数组(≤500 行)。全量替换语义:库里存在但未提交的行 = 删除;空数组 [] = 清空本 tab |
lines[] 元素(GroupSettleLineSaveReqVO)公共列:
| 字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| lineId | Long | 可空 | - | 已有行 ID;空 = 新增行 |
| allocMode | string | ✅ | SHARED / DESIGNATED |
分摊方式:SHARED=公摊 / DESIGNATED=指定报名 |
| allocRule | string | 条件必填 | PER_HEAD_AVG / PER_ORDER_AVG |
公摊口径;SHARED 缺省按类别默认(导游/摄影师/其他收入=按户均摊,其余=按人数均摊);DESIGNATED 必须为 null |
| allocGroup | string | 可空 | ≤32 字 | 分摊分组键(如 BUS/SUV;同组内单独均摊) |
| budgetAmount | number | 可空 | ≥0,2 位小数 | 预算额/带出值(只对比不参与拆账计算) |
| actualAmount | number | ✅ | ≥0,2 位小数 | 实际总额(拆账基准) |
| changeReason | string | 条件必填 | ≤255 字 | 改价原因(实际总额与库值不同且偏离带出值时必填) |
| paymentMethod | string | 可空 | SIGNED / COMPANY_PAID / CASH_PAID |
付款方式:签单 / 对公已付 / 现金已付(固定 3 值枚举,不走数据字典) |
| voucherUrls | string[] | 可空 | ≤9 张 | 凭证图 URL 数组 |
| remark | string | 可空 | ≤255 字 | 备注 |
| splits | array | 条件必填 | - | 拆账明细:仅 DESIGNATED 行必填且 ≥1;SHARED 行服务端自动重算,提交内容被忽略 |
splits[] 元素(SplitSaveReqVO,仅指定报名行):
| 字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|
| orderId | Long | ✅ | 须属本团在团户 | 承担子订单 ID;不属本团返 589572;同户重复出现被拒 |
| ratio | number | 可空 | ≥0,6 位小数 | 拆分权重(不强制合计为 1,服务端按权重归一化) |
| amount | number | 可空 | ≥0,2 位小数 | 手改金额(非空优先,不再参与权重分剩余);全部手改时 Σamount 必须分毫不差 = 行 actualAmount,否则 589750 |
| note | string | 可空 | ≤255 字 | 备注(提前离团等) |
lines[] 各类特有列(只填本 tab 类别的列,其余留空;字段定义同 §5.2 各类特有列表):
- 住宿 HOTEL:hotelId / roomTypeId / dayNumber(≥1) / stayDate / hotelName / roomTypeName / roomCount(≥0) / unitPrice
- 门票·游玩 TICKET:dayNumber / dayDate / scenicName / specName / ticketCount(≥0) / ticketUnitPrice
- 餐食 MEAL:mealType / mealDate / mealName / restaurantId / restaurantName / quantity(≥0) / unitPrice
- 车辆 VEHICLE:serviceStartDate / serviceEndDate / vehicleId / vehiclePlate / vehicleModelName / driverId / driverName / dailyPrice
- 导游 GUIDE / 摄影师 PHOTOGRAPHER:staffId / staffName / workDays(≥0,1 位小数) / perDayFee
- 其他收入 OTHER_INCOME:incomeDate / projectName / projectCategory / specification / quantity(2 位小数) / unitPrice
- 其他支出 OTHER_EXPENSE:expenseType / expenseDate / projectName
4.3 POST …/settlement/lines 新增单条明细行(GroupSettleLineCreateReqVO)
继承 4.2 行字段(lineId 传了也被忽略),额外两个字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| category | string | ✅ | 费用类别 8 值之一(决定落哪张分类明细表;非法值 589753) |
| expectedVersion | int | ✅ | 乐观锁版本(同 4.2) |
4.4 POST …/settlement/panel/confirm 确认核单(GroupSettleConfirmReqVO)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| expectedVersion | int | ✅ | 乐观锁版本(GET panel/tab 返回的 version 原样回传) |
确认后不可逆:不提供 un-confirm,前端须二次确认弹窗提示「确认后不可修改」。确认后全部写口拒绝(589568)。
4.5 POST …/settlement/alloc-preview 拆账试算(GroupSettleAllocPreviewReqVO,不落库)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| category | string | ✅ | 费用类别 8 值之一 |
| lines | array | ✅ | 待试算明细行数组(≤500 行,结构同 4.2 lines[]);无 expectedVersion(纯试算不写库) |
4.6 POST …/settlement/invoice 按子订单开票(GroupBatchInvoiceReqVO)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| orderId | Long | ✅ | 子订单 ID(须为本团在团户,否则 589572;该户已有有效发票 589571) |
| titleType | string | ✅ | PERSONAL 个人 / COMPANY 单位 |
| title | string | ✅ | 发票抬头(≤128 字,公司全称或个人姓名) |
| taxNo | string | 条件必填 | 税号(≤32 字,titleType=COMPANY 时必填,跨字段约束由 Service 判定) |
| string | ✅ | 收件邮箱(合法邮箱格式,≤128 字) |
发票类型不由前端选:服务端固定增值税普通发票。本请求不带 expectedVersion(开票不写核单主行,version 不变)。
5. 出参字段
通用约定:金额一律字符串输出(DECIMAL 防精度),ID 一律字符串输出(Long 防 JS 精度丢失),日期 yyyy-MM-dd,日期时间 yyyy-MM-dd HH:mm:ss。
5.1 GET …/settlement/{tab} 分类 tab 出参(GroupSettleTabRespVO)
| 字段 | 类型 | 说明 |
|---|---|---|
| groupBatchId | string | 团期 ID |
| category | string | 本 tab 类别值(activities tab 恒 TICKET) |
| categoryName | string | 类别中文名(数据字典 settlement_category) |
| status | string | 核单状态:DRAFT 录入中 / CONFIRMED 已确认 |
| version | int | 乐观锁版本(PUT/DELETE/confirm 回传 expectedVersion 用) |
| editable | boolean | 是否可编辑(= status == DRAFT;false 时前端禁用全部写交互) |
| budgetTotal | string 或 null | 本类 Σ 带出值/预算额(全行为空则 null) |
| actualTotal | string | 本类 Σ 实际总额 |
| allocatedTotal | string | 本类已拆账金额(Σ splits.amount) |
| lines | array | 明细行数组(无行时 [];首读未落库时返回「两层带出预览」行:lineId=null、splits=[],不落库) |
首读即建行:任何团期状态(含未返团)首读都返回真实结构,不再是旧版 NOT_STARTED 空壳。
5.2 lines[] 明细行(LineVO)——公共列
| 字段 | 类型 | 说明 |
|---|---|---|
| lineId | string 或 null | 明细行 ID(带出预览行为 null) |
| confirmStatus | string | 行确认态:UNCONFIRMED / CONFIRMED(核单确认时批量翻 CONFIRMED) |
| allocMode | string | SHARED 公摊 / DESIGNATED 指定报名 |
| allocRule | string 或 null | PER_HEAD_AVG 按全团人数均摊 / PER_ORDER_AVG 按户均摊;DESIGNATED 恒 null |
| allocGroup | string 或 null | 分摊分组键(如 BUS/SUV) |
| budgetAmount | string 或 null | 带出值/预算额(只对比不参与计算) |
| actualAmount | string | 实际总额(拆账基准) |
| changeReason | string 或 null | 改价原因 |
| sourceType | string | MANUAL 手工 / CARRY_OVER 第 1 层带出 / BATCH_COST 共享成本带出 |
| paymentMethod | string 或 null | SIGNED 签单 / COMPANY_PAID 对公已付 / CASH_PAID 现金已付 |
| voucherUrls | string[] | 凭证图 URL 数组(无则 []) |
| remark | string 或 null | 备注 |
| splits | array | 本行拆账结果(未拆账为 []),结构见 5.3 |
各类特有列(出参为全类别并集,非本 tab 的特有列恒为 null,按 category 取本类列即可):
| 类别 | 特有列 |
|---|---|
| HOTEL 住宿 | hotelId / roomTypeId / dayNumber / stayDate / hotelName / roomTypeName / roomCount / unitPrice(元/间夜) |
| TICKET 门票·游玩 | dayNumber / dayDate / scenicName / specName / ticketCount / ticketUnitPrice |
| MEAL 餐食 | mealType / mealDate / mealName / restaurantId / restaurantName / quantity(份数) / unitPrice(元/份) |
| VEHICLE 车辆 | serviceStartDate / serviceEndDate / vehicleId / vehiclePlate / vehicleModelName / driverId / driverName / dailyPrice(日单价) |
| GUIDE 导游 / PHOTOGRAPHER 摄影师 | staffId / staffName / workDays(1 位小数) / perDayFee(日费) |
| OTHER_INCOME 其他收入 | incomeDate / projectName / projectCategory / specification / quantity(2 位小数) / unitPrice |
| OTHER_EXPENSE 其他支出 | expenseType / expenseDate / projectName |
5.3 lines[].splits[] 拆账明细(SplitVO)
| 字段 | 类型 | 说明 |
|---|---|---|
| orderId | string | 子订单 ID |
| teamNo | string 或 null | 团号(order_main.team_no,订金支付成功后生成;未付订金为 null)。⚠️ alloc-preview 试算回执的 splits.teamNo 本期恒 null 未填充 |
| householdName | string | 户名(客户姓名快照) |
| ratio | string | 拆账比例(6 位小数;公摊=计算快照,指定报名=用户比例/归一化权重快照) |
| peopleCount | int 或 null | 该户参与人数快照 |
| amount | string | 拆账金额 |
| roundingBearer | boolean | 尾差承担户标记(公摊尾差整笔记 eligible 集合中 order_id 最小的户) |
| note | string 或 null | 备注(提前离团等) |
5.4 PUT …/settlement/{tab} 暂存回执(GroupSettleTabWriteRespVO)
| 字段 | 类型 | 说明 |
|---|---|---|
| version | int | 写入后的乐观锁版本(已 +1) |
| status | string | 核单状态(恒 DRAFT;CONFIRMED 时写口已被 589568 拦截) |
| lines | array | 写入后本 tab 全量明细行(结构同 5.2,含重算后的 splits),前端直接整页替换 |
5.5 GET …/settlement/panel 面板出参(GroupSettlePanelRespVO)
| 字段 | 类型 | 说明 |
|---|---|---|
| groupBatchId | string | 团期 ID |
| status | string | DRAFT / CONFIRMED(确认后不可逆) |
| version | int | 乐观锁版本 |
| confirmedAt | string 或 null | 确认时间(DRAFT 为 null) |
| confirmedByName | string 或 null | 确认人姓名(DRAFT 为 null) |
| editable | boolean | 是否可编辑(= status == DRAFT) |
| readyToAllocate | boolean | 各户第 1 层核单是否全部定稿 |
| blockingOrderIds | string[] | 第 1 层核单未定稿的子订单 ID;空数组 = 全部定稿 |
| categories | array | 8 类合计(恒 8 条,按类别序),元素见下表 |
| households | array | 在团子订单列表(仅排除已取消),元素见下表 |
categories[](CategorySummaryVO):
| 字段 | 类型 | 说明 |
|---|---|---|
| category | string | 费用类别 8 值 |
| categoryName | string | 类别中文名(字典 settlement_category) |
| budgetAmount | string 或 null | 该类 Σ 带出值/预算额(全行为空则 null) |
| actualAmount | string | 该类 Σ 实际总额 |
| allocatedAmount | string | 该类已拆账金额(Σ splits.amount) |
| unallocatedAmount | string | 该类未拆账金额(actualAmount − allocatedAmount) |
households[](HouseholdVO):
| 字段 | 类型 | 说明 |
|---|---|---|
| orderId | string | 子订单 ID |
| orderNo | string | 订单号 |
| teamNo | string 或 null | 团号(未付订金为 null) |
| householdName | string | 户名 |
| peopleCount | int | 人数 |
| roomCount | int 或 null | 房间数 |
| allocatedCost | string | 已摊成本(该户 7 个支出类 Σ splits.amount) |
| allocatedIncome | string | 已摊收入(该户其他收入 Σ splits.amount) |
| grossProfit | string | 毛利(应收口径 − 已摊成本 + 已摊收入) |
5.6 GET …/settlement/sub-orders 在团子订单出参(GroupSettleSubOrderRespVO[])
| 字段 | 类型 | 说明 |
|---|---|---|
| orderId | string | 子订单 ID |
| orderNo | string | 订单号 |
| teamNo | string 或 null | 团号(未付订金为 null) |
| householdName | string | 户名 |
| peopleCount | int | 人数(公摊按人摊的分子预览) |
| roomCount | int 或 null | 房间数 |
在团口径 = 仅排除已取消。用途:指定报名勾选弹窗的数据源。
5.7 写端点回执
POST …/settlement/lines(GroupSettleLineCreateRespVO):
| 字段 | 类型 | 说明 |
|---|---|---|
| lineId | string | 新增明细行 ID |
| version | int | 写入后的乐观锁版本(已 +1) |
DELETE …/settlement/lines/{lineId}(GroupSettleLineDeleteRespVO):
| 字段 | 类型 | 说明 |
|---|---|---|
| version | int | 写入后的乐观锁版本(已 +1);该行拆账已同事务级联软删 |
POST …/settlement/panel/confirm(GroupSettleConfirmRespVO):
| 字段 | 类型 | 说明 |
|---|---|---|
| status | string | 恒 CONFIRMED |
| version | int | 确认后的乐观锁版本(已 +1) |
| confirmedAt | string | 确认时间 |
| confirmedByName | string | 确认人姓名 |
POST …/settlement/alloc-preview(GroupSettleAllocPreviewRespVO):
| 字段 | 类型 | 说明 |
|---|---|---|
| lines | array | 试算后的明细行(结构同 5.2 LineVO;confirmStatus 恒 UNCONFIRMED、lineId 原样回显或 null、sourceType 恒 MANUAL;splits 为重算预览,splits[].teamNo 本期恒 null)。无任何写库 |
POST …/settlement/invoice(GroupSettleInvoiceRespVO):
| 字段 | 类型 | 说明 |
|---|---|---|
| status | string | 核单状态(恒 CONFIRMED,开票门禁) |
| version | int | 核单乐观锁版本(开票不改主行,原值) |
| invoiceId | string | 新发票 ID |
6. 枚举 / 数据字典
category(费用类别,数据字典 settlement_category,8 值)
| 值 | 中文 | 路径段 | 支出/收入 |
|---|---|---|---|
HOTEL |
住宿 | hotels | 支出 |
TICKET |
门票·游玩 | activities(旧名沿用,出参 category 恒 TICKET) | 支出 |
MEAL |
餐食 | meals | 支出 |
VEHICLE |
车辆 | vehicles | 支出 |
GUIDE |
导游 | guide-fees | 支出 |
PHOTOGRAPHER |
摄影师 | photographer-fees | 支出 |
OTHER_INCOME |
其他收入 | other-incomes | 收入 |
OTHER_EXPENSE |
其他支出 | other-expenses | 支出 |
中文名以后端下发的 categoryName 为准(字典驱动),前端不要硬编码。
allocMode(分摊方式)
| 值 | 含义 | 配套约束 |
|---|---|---|
SHARED |
公摊(全团 eligible 户分摊) | allocRule 可省(按类别默认);splits 由服务端重算,提交被忽略 |
DESIGNATED |
指定报名(勾选子订单子集承担) | allocRule 必须 null;splits 必填 ≥1(未选返 589751);Σ拆账=行总额(不等返 589750) |
allocRule(公摊口径,仅 SHARED 有效)
| 值 | 含义 | 类别默认 |
|---|---|---|
PER_HEAD_AVG |
按全团人数均摊(eligible 户按人数加权,人数 0 的户不参与) | 住宿/门票·游玩/餐食/车辆/其他支出默认 |
PER_ORDER_AVG |
按户均摊(eligible 户等权,不看人数) | 导游/摄影师/其他收入默认 |
公摊尾差规则:非尾差户按 ROUND_HALF_UP 到分,尾差 = 总额 − Σ其余户份额,整笔记 eligible 集合中 order_id 最小的户(splits 里 roundingBearer=true);尾差户金额为负时全组改 ROUND_DOWN 重算,尾差恒非负。
status(核单状态)/ confirmStatus(行确认态)
| 字段 | 值 | 含义 |
|---|---|---|
| status | DRAFT |
录入中(可编辑) |
| status | CONFIRMED |
已确认(不可逆,全部写口拒 589568) |
| confirmStatus | UNCONFIRMED / CONFIRMED |
明细行确认态,随核单确认批量翻 CONFIRMED |
sourceType(明细行来源)
| 值 | 含义 |
|---|---|
MANUAL |
手工录入 |
CARRY_OVER |
第 1 层(子订单核单)带出 |
BATCH_COST |
共享成本带出 |
paymentMethod(付款方式,固定 3 值枚举,不走数据字典)
| 值 | 含义 |
|---|---|
SIGNED |
签单 |
COMPANY_PAID |
对公已付 |
CASH_PAID |
现金已付 |
其他字典(特有列内引用)
| 字段 | 字典 | 取值 |
|---|---|---|
| mealType(餐食) | meal_type |
BREAKFAST / LUNCH / DINNER / SELF |
| expenseType(其他支出) | expense_type |
FUEL / TOLL / PARKING / RENTAL / MAINTENANCE / OTHER |
7. 错误码
| 错误码 | 文案({0} 为动态定位串) | 触发场景 |
|---|---|---|
| 589507 | 无操作权限(当前角色未授予团期权限,或该团期不在您名下) | 缺 group-batch:audit:* 对应权限码 |
| 589567 | 该团期尚未进入整团核单:{0} | 尚未建核单主记录时直接调写口/确认(正常链路首读 GET 即建行,不会遇到) |
| 589568 | 整团核单当前状态不允许该操作:{0} | ① CONFIRMED 后任何写操作;② 确认核单时在团子订单第 1 层核单未全部定稿({0} 带未定稿订单号清单,与 panel 的 blockingOrderIds 对应) |
| 589571 | 已开票冲突:{0} | 该子订单已有有效发票仍重复开票 |
| 589572 | 所选订单不属于本团期的整团核单范围 | 指定报名 splits / 开票 orderId 不属本团在团户 |
| 589573 | 整团核单数据已被他人修改(当前版本 {0},提交版本 {1}),请刷新后重试 | expectedVersion 与库值不一致(CAS 并发冲突),本次请求零写入,前端须重新 GET 读回全量再提交 |
| 589750 | 拆账合计与明细行总额不一致({0}),请核对后再提交 | 指定报名行 Σ拆账 ≠ actualAmount;{0} 为四数文案「{行名}拆账合计 {实} / 明细总额 {应},差额 {差}」 |
| 589751 | 指定报名须至少选择一个承担子订单 | DESIGNATED 行 splits 为空 |
| 589752 | 全团人数为 0,无法按人数均摊;请改用按户均摊或先维护出行人 | SHARED + PER_HEAD_AVG 时全团人数为 0 |
| 589753 | 核单拆账入参非法:{0} | 金额/单价/数量为负、未知费用类别等入参层非法 |
| 589754 | 在团子订单已变化({0}),已拆账数据失效,请重新暂存各分类完成拆账后再确认 | 确认核单前置校验:子订单退团/转入后未重新暂存拆账 |
| 589755 | 核单无明细行,不可确认,请先在各分类 Tab 录入明细后再确认 | 零明细行尝试确认 |
废弃错误码(保留占位不再抛出,前端可清理映射):589569(金额非法 → 由 589753 承接)、589570(四数对平 → 由 589750 承接)、589517(导出超限,导出端点已下线)。
8. 示例
8.1 典型:GET 住宿 tab(已录入两行,一行公摊一行指定报名)
请求:
GET /v3/admin/order/group-batch/1934567890123456789/settlement/hotels
响应 200:
{
"code": 0,
"data": {
"groupBatchId": "1934567890123456789",
"category": "HOTEL",
"categoryName": "住宿",
"status": "DRAFT",
"version": 3,
"editable": true,
"budgetTotal": "8600.00",
"actualTotal": "8400.00",
"allocatedTotal": "8400.00",
"lines": [
{
"lineId": "1934567890123456790",
"confirmStatus": "UNCONFIRMED",
"allocMode": "SHARED",
"allocRule": "PER_HEAD_AVG",
"allocGroup": null,
"budgetAmount": "4800.00",
"actualAmount": "4600.00",
"changeReason": "酒店涨价已与对方确认",
"sourceType": "CARRY_OVER",
"paymentMethod": "SIGNED",
"voucherUrls": ["https://oss.example.com/voucher/1.jpg"],
"remark": "含早餐",
"hotelId": "1934567890123400001",
"roomTypeId": "1934567890123400002",
"dayNumber": 2,
"stayDate": "2026-10-03",
"hotelName": "图嘎营地",
"roomTypeName": "蒙古包",
"roomCount": 5,
"unitPrice": "380.00",
"splits": [
{ "orderId": "1934567890123450001", "teamNo": "26-0001", "householdName": "张三",
"ratio": "0.300000", "peopleCount": 3, "amount": "1380.00", "roundingBearer": true, "note": null },
{ "orderId": "1934567890123450002", "teamNo": "26-0002", "householdName": "李四",
"ratio": "0.700000", "peopleCount": 7, "amount": "3220.00", "roundingBearer": false, "note": null }
]
},
{
"lineId": "1934567890123456791",
"confirmStatus": "UNCONFIRMED",
"allocMode": "DESIGNATED",
"allocRule": null,
"allocGroup": null,
"budgetAmount": "3800.00",
"actualAmount": "3800.00",
"changeReason": null,
"sourceType": "MANUAL",
"paymentMethod": "COMPANY_PAID",
"voucherUrls": [],
"remark": "升级房型差价",
"hotelId": "1934567890123400001",
"roomTypeId": "1934567890123400003",
"dayNumber": 2,
"stayDate": "2026-10-03",
"hotelName": "图嘎营地",
"roomTypeName": "豪华蒙古包",
"roomCount": 1,
"unitPrice": "3800.00",
"splits": [
{ "orderId": "1934567890123450002", "teamNo": "26-0002", "householdName": "李四",
"ratio": "1.000000", "peopleCount": 7, "amount": "3800.00", "roundingBearer": false, "note": "李四家要求升级" }
]
}
]
}
}
8.2 边界:PUT 整 tab 暂存(指定报名混合「比例 + 手改金额」+ 未付订金 teamNo=null)
请求:
PUT /v3/admin/order/group-batch/1934567890123456789/settlement/vehicles
Content-Type: application/json
{
"expectedVersion": 3,
"lines": [
{
"lineId": null,
"allocMode": "DESIGNATED",
"allocRule": null,
"actualAmount": 3000.00,
"paymentMethod": "CASH_PAID",
"serviceStartDate": "2026-10-02",
"serviceEndDate": "2026-10-04",
"vehiclePlate": "蒙E·12345",
"vehicleModelName": "考斯特",
"driverName": "巴特尔",
"dailyPrice": 1500.00,
"splits": [
{ "orderId": "1934567890123450001", "ratio": 1.000000, "amount": null, "note": null },
{ "orderId": "1934567890123450002", "ratio": null, "amount": 2000.00, "note": "手改固定 2000" }
]
}
]
}
响应 200(李四家手改 2000 优先,张三家按权重吸收剩余 1000 并承担尾差;该两户未付订金故 teamNo=null):
{
"code": 0,
"data": {
"version": 4,
"status": "DRAFT",
"lines": [
{
"lineId": "1934567890123456800",
"confirmStatus": "UNCONFIRMED",
"allocMode": "DESIGNATED",
"allocRule": null,
"actualAmount": "3000.00",
"sourceType": "MANUAL",
"splits": [
{ "orderId": "1934567890123450001", "teamNo": null, "householdName": "张三",
"ratio": "0.333333", "peopleCount": 3, "amount": "1000.00", "roundingBearer": true, "note": null },
{ "orderId": "1934567890123450002", "teamNo": null, "householdName": "李四",
"ratio": null, "peopleCount": 7, "amount": "2000.00", "roundingBearer": false, "note": "手改固定 2000" }
]
}
]
}
}
读法:回执 lines 是写入后本 tab 全量回读(含重算后的 splits),前端直接整页替换;version 已 +1,下次写操作回传 4。
8.3 业务失败
8.3a 指定报名拆账合计 ≠ 行总额(589750):
PUT /v3/admin/order/group-batch/1934567890123456789/settlement/hotels
{ "expectedVersion": 4, "lines": [ { "allocMode": "DESIGNATED", "actualAmount": 3800.00,
"splits": [ { "orderId": "1934567890123450001", "amount": 3000.00 } ] } ] }
{ "code": 589750,
"message": "拆账合计与明细行总额不一致(图嘎营地蒙古包拆账合计 3000.00 / 明细总额 3800.00,差额 800.00),请核对后再提交" }
8.3b 调旧 /audit 路径(已下线,404):
GET /v3/admin/order/group-batch/1934567890123456789/audit
HTTP/1.1 404 Not Found
旧 GET/PUT …/audit、POST …/audit/allocate、POST …/audit/reallocate、GET …/audit/export、POST …/audit/invoice 六个端点全部已删除,网关/服务均不再路由,调用一律 404。前端若保留旧核单页代码必须整体替换。
9. 业务边界
适用:
- 管理后台团期详情的整团核单:8 类费用明细录入 / 暂存 / 拆账 / 确认 / 按子订单开票。
- 公摊(全团分摊)与指定报名(部分户承担)两种成本核算口径的录入与试算。
不适用:
- 逐子订单独立应收应付:拆账只影响成本/毛利核算口径,不生成逐户应收应付单;财务主链仍一团一张报账单(biz_no = 团号 batch_no),走既有
POST …/settlement/finalize→POST …/settlement/confirm(财务复核,与本次新增的 panel/confirm 是两个不同端点)。 - 确认后修改:CONFIRMED 不可逆,不提供 un-confirm;录错只能走后端人工处理。
- 旧四表模型的科目行/逐户用量交互:已随旧表 DROP 彻底移除。
特殊边界:
- 首读即建行:任何团期状态(含未返团)GET tab/panel 都会建核单主行并返回真实结构;库里无明细行时 tab 返回「两层带出预览」行(lineId=null、splits=[],不落库)——前端不要把预览行当已保存行渲染删除/编辑按钮(lineId 为 null 即预览行)。
- 全量替换语义:PUT 整 tab 是覆盖写——库里存在但未提交的行会被删除;前端编辑后必须提交整 tab 全量行,不能只传改动行。
- CAS 冲突处理:任何写口返 589573 时,前端必须重新 GET 读回全量(含新 version)再让用户基于最新数据重改,不能本地 version+1 重试。
- 确认前置:确认核单要求 ① 有明细行(否则 589755);② 在团子订单第 1 层核单全部定稿(否则 589568,未定稿清单见 panel
blockingOrderIds/readyToAllocate,前端可在确认按钮上据此置灰);③ 在团户集合与已落拆账一致(退团/转入后须重新暂存,否则 589754)。 - 开票门禁:仅 CONFIRMED 后可开票;一户一票(589571)。
- eligible 户:公摊的 eligible 集合 = 在团户(仅排除已取消);PER_HEAD_AVG 时人数为 0 的户不参与分摊。
10. 修改前后对比
字段级对比
| 项 | 原来(旧 /audit 契约) | 现在(#8714 重做后) |
|---|---|---|
| 核单数据载体 | 统一科目行 + 逐户用量 + 逐户分摊(四表) | 8 类分类明细行 + 行内 splits 拆账(10 表) |
| 费用类别 | 硬编码 AuditCategory | 数据字典 settlement_category 8 值,categoryName 字典下发 |
| 核单状态机 | DRAFT → ALLOCATED → CHECKED 等多态 | DRAFT → CONFIRMED 两态,确认后不可逆 |
| 分摊方式 | 均分 / 指定比例(粗粒度) | SHARED 公摊(按人/按户两口径)/ DESIGNATED 指定报名(比例 + 手改金额混合) |
| 尾差承担 | 旧决策(order_id 最小户) | 沿用:eligible 集合 order_id 最小户,roundingBearer 标记 |
| 写口并发控制 | expectedVersion(order_batch_audit.version) | expectedVersion(order_group_settlement_main.version),同 CAS 语义、同 589573 |
| 出参 ID / 金额 | 数值型 | 全部字符串化(Long 防精度丢失、DECIMAL 防精度) |
| 团号 | 出参无 teamNo | panel.households[] / sub-orders[] / tab splits[] 新增 teamNo(#8779) |
| sharedCostByType.costType | BatchCostType 4 值:BUS / LEADER / PHOTOGRAPHER / OTHER |
settlement_category 8 值(HOTEL/TICKET/MEAL/VEHICLE/GUIDE/PHOTOGRAPHER/OTHER_INCOME/OTHER_EXPENSE),costTypeDesc 走数据字典 |
行为级对比
| 行为 | 原来 | 现在 |
|---|---|---|
| 打开核单页 | 返团后才可查,未返团 NOT_STARTED 空壳 | 任何团期状态首读即建行,返回真实结构;无明细行给带出预览(不落库) |
| 保存 | 整单保存(一次 PUT 全科目) | 按 tab 保存(8 个 PUT 各自全量替换本类)+ 单行新增/删除端点 |
| 分摊计算 | 提交核算时整团重算 | 每次暂存/新增/删除行即重算该行 splits 并落库;另有不落库试算 alloc-preview |
| 确认/提交核算 | allocate → reallocate → 验团多步 | 一次 panel/confirm(8 表全量复核后锁 CONFIRMED,不可逆) |
| 开票 | POST …/audit/invoice |
POST …/settlement/invoice(门禁从旧状态改为 CONFIRMED) |
| 导出 | GET …/audit/export |
端点下线,本期无导出 |
| 旧 /audit 6 端点 | 正常服务 | 全部删除,调用 404 |
| 下游 D2 汇总(return-detail / settlement summary) | 共享成本读旧四表 | 改读新 8 表明细(PR-5),sharedCostByType 键值域同步切换 |
| 验团(结算)门禁 | 看旧核单主表状态 | 切看新核单主表 CONFIRMED(PR-5) |
11. 影响评估 / 回滚
- 破坏兼容:是,整体重做。旧
/audit/*端点已删(404),旧四表已 DROP,8 个 tab GET 出参结构完全变化。前端原团期核单页(科目行交互)必须整体重写对接,不存在渐进迁移路径。 - 前端必须同步上线:是。后端部署后旧前端核单页全部 404 / 解析失败。建议前后端同批上线;后端当前已合 dev-v3 未部署测试服,前端可先行开发,联调窗口在部署后。
- 跨页面影响:消费
GroupReturnDetailRespVO/GroupBatchSettlementSummaryRespVO的sharedCostByType[].costType渲染页(核团详情、结算汇总)须同批适配——键值从 BatchCostType 4 值切到 settlement_category 8 值,展示名直接用后端下发的costTypeDesc(字典驱动),不要再做 4 值硬编码映射。 - workaround 清理点:旧核单页对 589569/589570 的错误码映射可删(已废弃);对旧多态状态机(DRAFT/ALLOCATED/CHECKED 等)的分支渲染可删;「未返团空壳」占位逻辑可删(现在任何状态都返回真实结构)。
- 回滚方案:后端回滚 = 需恢复旧四表 DDL + 旧代码(代价大,实际不可回滚——旧表已 DROP,数据不迁移)。本批按「开发期推倒重做、无向后兼容」交付,前端务必在同批部署窗口前完成适配。
12. 注意事项
- 确认路径别调错:确认核单是
POST …/settlement/panel/confirm;POST …/settlement/confirm(无 panel 段)是团期结算财务复核(旧财务主链,权限与语义都不同),两者不是一回事。 - activities = TICKET:门票·游玩 tab 的路径段是历史沿用的
activities,但出参/入参的 category 值恒为TICKET。路由映射写死:hotels→HOTEL、activities→TICKET、meals→MEAL、vehicles→VEHICLE、guide-fees→GUIDE、photographer-fees→PHOTOGRAPHER、other-incomes→OTHER_INCOME、other-expenses→OTHER_EXPENSE。 - 特有列是并集:LineVO 出参包含全部 8 类特有列的并集,非本 tab 类别的列恒为 null——按
category只读本类列,不要对 null 列做渲染兜底之外的逻辑。 - 全量替换:PUT 暂存必须提交整 tab 全量行;漏传 = 删除。
- 金额/ID 按字符串处理:所有金额、ID 均为 JSON 字符串,禁转 number(精度丢失)。
- teamNo 可空:未付订金的子订单 teamNo 为 null,列表渲染需做空值兜底;
alloc-preview回执的 splits.teamNo 本期恒 null(试算不查团号),不要用它做展示。 - 预览行识别:GET tab 首读的带出预览行
lineId=null,不是已保存数据,编辑/删除交互应只对 lineId 非空的行开放。 - editable 驱动写交互:
editable=false(CONFIRMED)时禁用全部写按钮;硬调写口会吃 589568。 - 付款方式不走字典:paymentMethod 是固定 3 值(SIGNED/COMPANY_PAID/CASH_PAID),前端硬编码映射即可;mealType / expenseType 走对应数据字典。
- 指定报名校验在服务端:Σ拆账=行总额(589750)、至少一户(589751)、户属本团(589572)都由服务端强校验,前端可做预校验提升体验,但不能替代服务端回执处理。
- 删除行必带 category + expectedVersion(Query 参数),不是请求体。
13. 关联 / 联系人
- Issue(主):wx/HL#8714
- Issue(teamNo 跟进):wx/HL#8779
- PR 序列(6 PR + 1 跟进,按合并序):
- PR-1 建表迁移:wx/HL#8716 | commit https://git.1814.love/wx/HL/commit/3bad2ad276
- PR-2 DO/Mapper/枚举骨架:wx/HL#8718 | commit https://git.1814.love/wx/HL/commit/ee90112a2d
- PR-3 公摊/指定报名拆账计算器:wx/HL#8722 | commit https://git.1814.love/wx/HL/commit/34d10981c0
- PR-4 8 类 tab + 面板/暂存/确认/开票 Service 与接口:wx/HL#8737 | commit https://git.1814.love/wx/HL/commit/788b9c1493
- PR-5 下游改造(D2 汇总读新 8 表 + 验团门禁切新主表):wx/HL#8764 | commit https://git.1814.love/wx/HL/commit/b9f22e3adc
- PR-6 旧代码清理 + DROP 旧四表:wx/HL#8778 | commit https://git.1814.love/wx/HL/commit/30f83ada81
- teamNo 补字段(#8779):wx/HL#8780 | commit https://git.1814.love/wx/HL/commit/d759fba111
- 后端负责人:@yst