diff --git a/changelogs-v2/2026-10/04_8714_团期核单重做8类tab明细+公摊拆账-修改接口-管理后台.md b/changelogs-v2/2026-10/04_8714_团期核单重做8类tab明细+公摊拆账-修改接口-管理后台.md new file mode 100644 index 00000000..9919fea4 --- /dev/null +++ b/changelogs-v2/2026-10/04_8714_团期核单重做8类tab明细+公摊拆账-修改接口-管理后台.md @@ -0,0 +1,657 @@ +--- +schema: "hl-changelog/v2" +ticket: "8714" +title: "团期核单重做为8类tab明细结构+公摊/指定报名拆账(旧/audit端点下线)" +consumer: "admin" +author: "yst" +change_type: "修改接口" +backend_status: "merged" +gateway_status: "not_required" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "整批 6 PR 累积的结构重做,8 tab 出参结构变化 + 15 个新接口 + 旧 /audit/* 端点下线 404 + teamNo 新增,前端需按本契约重对接;sharedCostByType 键值从 BatchCostType 4 值切到 settlement_category 8 值。已合 dev-v3 未部署测试服。" +updated_at: "2026-10-04" +base: "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: + +```json +{ + "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 + +```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): + +```json +{ + "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 + +```json +{ "expectedVersion": 4, "lines": [ { "allocMode": "DESIGNATED", "actualAmount": 3800.00, + "splits": [ { "orderId": "1934567890123450001", "amount": 3000.00 } ] } ] } +``` + +```json +{ "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. 关联 / 联系人 + +- Issue(主):https://git.1814.love/wx/HL/issues/8714 +- Issue(teamNo 跟进):https://git.1814.love/wx/HL/issues/8779 +- PR 序列(6 PR + 1 跟进,按合并序): + - PR-1 建表迁移:https://git.1814.love/wx/HL/pulls/8716 | commit https://git.1814.love/wx/HL/commit/3bad2ad276 + - PR-2 DO/Mapper/枚举骨架:https://git.1814.love/wx/HL/pulls/8718 | commit https://git.1814.love/wx/HL/commit/ee90112a2d + - PR-3 公摊/指定报名拆账计算器:https://git.1814.love/wx/HL/pulls/8722 | commit https://git.1814.love/wx/HL/commit/34d10981c0 + - PR-4 8 类 tab + 面板/暂存/确认/开票 Service 与接口:https://git.1814.love/wx/HL/pulls/8737 | commit https://git.1814.love/wx/HL/commit/788b9c1493 + - PR-5 下游改造(D2 汇总读新 8 表 + 验团门禁切新主表):https://git.1814.love/wx/HL/pulls/8764 | commit https://git.1814.love/wx/HL/commit/b9f22e3adc + - PR-6 旧代码清理 + DROP 旧四表:https://git.1814.love/wx/HL/pulls/8778 | commit https://git.1814.love/wx/HL/commit/30f83ada81 + - teamNo 补字段(#8779):https://git.1814.love/wx/HL/pulls/8780 | commit https://git.1814.love/wx/HL/commit/d759fba111 +- 后端负责人:@yst