文件
hl-api-changelog/changelogs-v2/2026-10/04_8714_团期核单重做8类tab明细+公摊拆账-修改接口-管理后台.md
T
2026-10-04 14:35:39 +08:00

41 KiB
原始文件 Blame 文件历史

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 判定)
email 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. 注意事项

  1. 确认路径别调错:确认核单是 POST …/settlement/panel/confirm;POST …/settlement/confirm(无 panel 段)是团期结算财务复核(旧财务主链,权限与语义都不同),两者不是一回事。
  2. 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。
  3. 特有列是并集:LineVO 出参包含全部 8 类特有列的并集,非本 tab 类别的列恒为 null——按 category 只读本类列,不要对 null 列做渲染兜底之外的逻辑。
  4. 全量替换:PUT 暂存必须提交整 tab 全量行;漏传 = 删除。
  5. 金额/ID 按字符串处理:所有金额、ID 均为 JSON 字符串,禁转 number(精度丢失)。
  6. teamNo 可空:未付订金的子订单 teamNo 为 null,列表渲染需做空值兜底;alloc-preview 回执的 splits.teamNo 本期恒 null(试算不查团号),不要用它做展示。
  7. 预览行识别:GET tab 首读的带出预览行 lineId=null,不是已保存数据,编辑/删除交互应只对 lineId 非空的行开放。
  8. editable 驱动写交互:editable=false(CONFIRMED)时禁用全部写按钮;硬调写口会吃 589568。
  9. 付款方式不走字典:paymentMethod 是固定 3 值(SIGNED/COMPANY_PAID/CASH_PAID),前端硬编码映射即可;mealType / expenseType 走对应数据字典。
  10. 指定报名校验在服务端:Σ拆账=行总额(589750)、至少一户(589751)、户属本团(589572)都由服务端强校验,前端可做预校验提升体验,但不能替代服务端回执处理。
  11. 删除行必带 category + expectedVersion(Query 参数),不是请求体。

13. 关联 / 联系人