diff --git a/changelogs-v2/2026-09/18_7932_团期核团核算开票导出与节点下钻-新增接口-管理后台.md b/changelogs-v2/2026-09/18_7932_团期核团核算开票导出与节点下钻-新增接口-管理后台.md new file mode 100644 index 00000000..2a7da649 --- /dev/null +++ b/changelogs-v2/2026-09/18_7932_团期核团核算开票导出与节点下钻-新增接口-管理后台.md @@ -0,0 +1,1276 @@ +--- +schema: "hl-changelog/v2" +ticket: "7932" +title: "团期核团:核团面板、保存、提交核算、重新核算、开票、导出核单与核单节点下钻" +consumer: "admin" +author: "jw(GIT)" +change_type: "新增接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "新增团期详情「核团验团」Tab 所需 7 个接口(GB-ADM-050/051/052/052b/054/055/056),全部挂在 /v3/admin/order/group-batch/{groupBatchId}/ 下,网关已有路由覆盖。权限用新增的 group-batch:audit:view/edit/allocate/invoice 四个码(不是 group-batch:finance:*)。验团归档 /settle 的行为变更见同日另一份 changelog(18_7932_团期验团归档前置核团-修改接口-管理后台.md)。后端 PR #7940/#7944/#7943/#7955 已合并 dev-v3(aed07cc3c)并部署 TEST。" +updated_at: "2026-09-18" +base: "dev-v3" +--- + +# order-v3: ✨ 团期核团——核团面板、保存、提交核算、重新核算、开票、导出核单、核单节点下钻 + +> **存放目录**: `changelogs-v2/{YYYY-MM}/`(管理后台,二期 order-v3) +> +> **服务**: hl-order-service-v3(order-v3) +> **PR**: #7940(050 读接口)、#7944(051/052/052b + 验团改造)、#7943(056 节点下钻)、#7955(054 开票、055 导出、权限码切换) +> **Issue**: #7932 +> **日期**: 2026-09-18 +> **影响范围**: 团期详情「核团验团」Tab(原型 `BatchAuditPanel`) + +--- + +## ⚠️ 关键变化 + +1. **新增 7 个接口**,支撑「核团验团」Tab:看面板 → 改用量 / 改价保存 → 提交核算(定稿)→ 需要时重新核算 → 开票 → 导出核单;另有一个核单按行程节点逐户下钻。 +2. **路径、错误码、并发字段都和原型 / 旧文档 `12-核算与验团.html` 不一样**,以本文为准(见第四节对照表):路径是 `/v3/admin/order/group-batch/{groupBatchId}/...`,错误码是 589xxx,并发用 `expectedVersion`。 +3. **权限用新的四个码 `group-batch:audit:*`**,**不要**再用 `group-batch:finance:*` 控制核团按钮显隐(见第四节权限表)。 +4. **验团按钮不在本文**:验团仍调既有 `POST .../settle`,但它现在要求核团先完成定稿,这是**已上线接口的行为变更**,见同日修改接口 changelog。 + +--- + +## 一、背景 + +团期出团回来后,运营要把一个团 8 个科目(住宿 / 车辆 / 景区娱乐 / 用餐 / 导游 / 摄影 / 其他支出 / 其他收入)的成本按固定分摊口径落到每一户,算出逐户成本与毛利,定稿后再验团归档,期间可按户开票、导出核单。 + +- 成本金额从第 1 层订单核单与第 2 层团期共享成本**带出**为默认值;运营主要改的是逐户用量(房间数、人数、是否参加、乘车分组),需要时也可以改单价 / 总额(偏离带出值须写改价原因)。 +- 收入是**应收口径且已扣本户实退**,不是实收现金。 +- 状态机:未开始 `NOT_STARTED` →(已返团首次打开面板)录入中 `DRAFT` →(提交核算)已核算 `ALLOCATED` →(验团)已验团 `CHECKED`;`ALLOCATED` 可经「重新核算」退回 `DRAFT`;`CHECKED` 可经「验团反确认」退回 `ALLOCATED`。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 核团面板(GB-ADM-050) | GET | `/v3/admin/order/group-batch/{groupBatchId}/audit` | 新增 | 已返团首次打开会自动带出成本、建草稿 | +| 2 | 保存核团录入(GB-ADM-051) | PUT | `/v3/admin/order/group-batch/{groupBatchId}/audit` | 新增 | 科目行 + 逐户用量全集保存,金额服务端重算 | +| 3 | 提交核算(GB-ADM-052) | POST | `/v3/admin/order/group-batch/{groupBatchId}/audit/allocate` | 新增 | 录入中 → 已核算,写逐户定稿 | +| 4 | 重新核算(GB-ADM-052b) | POST | `/v3/admin/order/group-batch/{groupBatchId}/audit/reallocate` | 新增 | 已核算 → 录入中,清空定稿(已开票的团拒绝) | +| 5 | 核团按户开票(GB-ADM-054) | POST | `/v3/admin/order/group-batch/{groupBatchId}/audit/invoice` | 新增 | 已核算 / 已验团时给本团一户申请普票 | +| 6 | 导出核单(GB-ADM-055) | GET | `/v3/admin/order/group-batch/{groupBatchId}/audit/export` | 新增 | CSV 附件,**不是** Result 信封 | +| 7 | 核单按行程节点逐户下钻(GB-ADM-056) | POST | `/v3/admin/order/group-batch/{groupBatchId}/settlement/node-lines` | 新增 | 只读;nodeIds 取自团期行程汇总 | + +--- + +## 三、接口详情 + +### 1. 核团面板(GB-ADM-050) `GET /v3/admin/order/group-batch/{groupBatchId}/audit` + +**VO**: `Result` + +#### 使用场景 + +打开团期详情「核团验团」Tab 时调用,一次拿全面板数据:两个状态(核团 / 团期)、三个合计、科目行、逐户用量、逐户分摊、提示、人员结算,以及「提交核算」按钮能否点(`readyToAllocate` / `blockingOrderIds`)。每次写接口成功后也建议重新调一次刷新面板。 + +**注意这是「读接口带写」**:团期已返团(出行完毕 / 核单中 / 已验团)且还没有核团记录时,第一次调用会自动带出成本、建一份「录入中」草稿;未返团只返回 `NOT_STARTED`,不建任何数据。 + +**权限**:`group-batch:audit:view`。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long(传字符串) | ✅ | 团期 ID | 团期详情里的 `groupBatchId` | + +无 Query、无请求体。 + +#### 出参 + +金额字段一律是**字符串**(如 `"1200.00"`),ID 一律是**字符串**;时间格式 `yyyy-MM-dd HH:mm:ss`。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| auditStatus | String | 核团状态:`NOT_STARTED` 未开始 / `DRAFT` 录入中 / `ALLOCATED` 已核算 / `CHECKED` 已验团 | +| batchStatus | String | 团期状态透传(如 `REVIEWING` 核单中、`SETTLED` 已验团) | +| auditId | String | 核团 ID;`NOT_STARTED` 为 null | +| subCount | Integer | 参与核算户数(录入中 = 当前在团户数;已定稿 = 定稿快照) | +| peopleCount | Integer | 参与核算总人数(口径同上) | +| totalCost | String | 整团成本 | +| totalRevenue | String | 整团收入(应收口径,已扣实退) | +| grossProfit | String | 整团毛利 = 收入 − 成本 | +| readyToAllocate | Boolean | 是否满足提交核算前置(所有在团户第 1 层核单已定稿);false 时「提交核算」置灰 | +| blockingOrderIds | String[] | 第 1 层核单还没定稿的子订单 ID;空数组 = 可提交 | +| primaryPayeeName | String | 主报账人姓名(录入中取团期当前主报账人;已定稿取定稿快照),可为 null | +| allocatedAt | String | 提交核算时间,可为 null | +| checkedAt | String | 验团时间,可为 null | +| checkNote | String | 验团意见,可为 null | +| version | Integer | 乐观锁版本;**下一次写请求原样回传为 `expectedVersion`** | +| items[] | Object[] | 科目行,见下 | +| items[].itemId | String | 科目行 ID | +| items[].category | String | 科目:`HOUSE` 住宿 / `VEHICLE` 车辆 / `ACTIVITY` 景区娱乐 / `MEAL` 用餐 / `GUIDE` 导游 / `PHOTO` 摄影 / `OTHER_EXPENSE` 其他支出 / `OTHER_INCOME` 其他收入 | +| items[].itemName | String | 科目名 | +| items[].dayNo | Integer | 第几天 / 第几晚,无日归属为 null | +| items[].unitPrice | String | 单价(单价型 `HOUSE`/`ACTIVITY`/`MEAL` 才有值,否则 null) | +| items[].totalAmount | String | 总额(总额型 `VEHICLE`/`GUIDE`/`PHOTO`/`OTHER_EXPENSE`/`OTHER_INCOME` 才有值,否则 null) | +| items[].allocRule | String | 分摊口径:`PER_ROOM_NIGHT` 按各户用房数 / `PER_HEAD_CHECKED` 勾选参加后按人数 / `PER_VEHICLE_GROUP` 按乘车分组内户数均分 / `PER_ORDER_AVG` 按户平均 | +| items[].allocGroup | String | 分摊分组(车科目 `BUS` / `SUV`),可为 null | +| items[].budgetAmount | String | 带出源金额,只供对比(改价是否「偏离」就是跟它比) | +| items[].changeReason | String | **读接口恒为 null**(改价原因只进团期时间线,不回显) | +| items[].seq | Integer | 排序 | +| details[] | Object[] | 逐户逐项用量,见下 | +| details[].detailId | String | 用量行 ID | +| details[].itemId | String | 科目行 ID | +| details[].orderId | String | 子订单 ID | +| details[].quantity | Number | 用量(房 = 间数;景娱 / 餐 = 人数;总额型为 null)。**数字,不是字符串** | +| details[].participated | Boolean | 是否参加(false = 本户该科目记 0,只影响单价型科目) | +| details[].allocGroup | String | 本户分组(车科目 `BUS` / `SUV`),可为 null | +| details[].amount | String | 本户该科目金额(服务端按分摊口径算,前端不要自己算) | +| details[].note | String | 例外备注 | +| allocs[] | Object[] | 逐户分摊(录入中 = 实时试算;已核算 / 已验团 = 定稿快照),见下 | +| allocs[].orderId | String | 子订单 ID | +| allocs[].contactName | String | 联系人 | +| allocs[].peopleCount | Integer | 本户人数 | +| allocs[].roomCount | Integer | 本户用房数 | +| allocs[].revenueAmount | String | 本户收入 = 应收 − 本户实退 + 其他收入分摊 | +| allocs[].costAmount | String | 本户成本合计 | +| allocs[].grossProfit | String | 本户毛利 | +| allocs[].roundingBearer | Boolean | 是否承担均分科目的尾差(分摊除不尽的几分钱记在这一户);可为 null | +| allocs[].costBreakdown | Object | 本户成本构成,键 = `HOUSE`/`VEHICLE`/`ACTIVITY`/`MEAL`/`GUIDE`/`PHOTO`/`OTHER_EXPENSE`(7 个成本科目,不含其他收入),值为金额字符串 | +| allocs[].note | String | 本户备注 | +| warnings[] | Object[] | 结构化提示,**不阻断**,见下 | +| warnings[].code | String | 提示码,见第六.5节 | +| warnings[].level | String | 级别,目前只有 `WARN`;以后可能加 `INFO`,按字符串兼容 | +| warnings[].itemId | String | 关联科目行 ID,可为 null | +| warnings[].orderId | String | 关联子订单 ID,可为 null | +| warnings[].message | String | 提示文案,可直接展示 | +| staffSettlements[] | Object[] | 人员结算(第 1 层导游 / 摄影费用行,只展示不可改) | +| staffSettlements[].orderId | String | 所属子订单 ID | +| staffSettlements[].staffId | String | 人员 ID | +| staffSettlements[].staffName | String | 人员姓名 | +| staffSettlements[].staffRole | String | `GUIDE` 导游 / `PHOTOGRAPHER` 摄影 | +| staffSettlements[].payableAmount | String | 应付金额 | +| staffSettlements[].settleStatus | String | 转账状态,可为 null | +| staffSettlements[].settledAt | String | 转账日期 `yyyy-MM-dd`,可为 null | +| staffSettlements[].transferRef | String | 转账凭证号,可为 null | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2100857935637663745/audit +Authorization: Bearer +``` + +无请求体。 + +#### 响应示例 + +(TEST 真实响应,2026-09-18,已返团团期首次打开;`items` 11 行 / `details` 33 行只保留前几行) + +```json +{ + "code": 200, + "message": "成功", + "data": { + "auditStatus": "DRAFT", + "batchStatus": "REVIEWING", + "auditId": "2100858806698135554", + "subCount": 3, + "peopleCount": 6, + "totalCost": "3467.00", + "totalRevenue": "16200.00", + "grossProfit": "12733.00", + "readyToAllocate": false, + "blockingOrderIds": ["2100857936799485953"], + "primaryPayeeName": null, + "allocatedAt": null, + "checkedAt": null, + "checkNote": null, + "version": 0, + "items": [ + { + "itemId": "2100858806698135555", + "category": "HOUSE", + "itemName": "D1 阿尔山成悦大酒店 标准间", + "dayNo": 1, + "unitPrice": "300.00", + "totalAmount": null, + "allocRule": "PER_ROOM_NIGHT", + "allocGroup": null, + "budgetAmount": "1250.00", + "changeReason": null, + "seq": 1 + }, + { + "itemId": "2100858806698135559", + "category": "VEHICLE", + "itemName": "车辆(团期共享·整团大巴)", + "dayNo": null, + "unitPrice": null, + "totalAmount": "1000.00", + "allocRule": "PER_VEHICLE_GROUP", + "allocGroup": "BUS", + "budgetAmount": "1000.00", + "changeReason": null, + "seq": 2 + } + ], + "details": [ + { + "detailId": "2100858806698135556", + "itemId": "2100858806698135555", + "orderId": "2100857935461502977", + "quantity": 1.00, + "participated": true, + "allocGroup": null, + "amount": "300.00", + "note": null + } + ], + "allocs": [ + { + "orderId": "2100857935461502977", + "contactName": "核团甲", + "peopleCount": 2, + "roomCount": 1, + "revenueAmount": "5400.00", + "costAmount": "1055.68", + "grossProfit": "4344.32", + "roundingBearer": true, + "costBreakdown": { + "HOUSE": "300.00", + "VEHICLE": "333.34", + "ACTIVITY": "289.00", + "MEAL": "100.00", + "GUIDE": "0.00", + "PHOTO": "0.00", + "OTHER_EXPENSE": "33.34" + }, + "note": null + } + ], + "warnings": [ + { + "code": "CATEGORY_AMOUNT_MISMATCH", + "level": "WARN", + "itemId": "2100858806698135555", + "orderId": null, + "message": "科目「D1 阿尔山成悦大酒店 标准间」按单价 × 用量算得 1200.00,与第 1 层核单实际成本 1250.00 不一致" + } + ], + "staffSettlements": [] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +未返团(团期还没到「出行完毕」):只返回两个状态,其余为空,**不建数据**(按代码整理的形态,`batchStatus` 为示意值)。 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "auditStatus": "NOT_STARTED", + "batchStatus": "RECRUITING", + "auditId": null, + "subCount": null, + "peopleCount": null, + "totalCost": null, + "totalRevenue": null, + "grossProfit": null, + "readyToAllocate": false, + "blockingOrderIds": [], + "version": null, + "items": [], + "details": [], + "allocs": [], + "warnings": [], + "staffSettlements": [] + }, + "success": true +} +``` + +已返团但两层都没有可带出的成本(TEST 真实:团期 B):`items=[]`、`details=[]`,`totalCost="0.00"`,`allocs` 仍按户列出收入。运营通过 GB-ADM-051 新增科目行。 + +读第 1 层数据失败时不报错,降级为 `warnings[]` 里的 `SOURCE_UNAVAILABLE` / `ALLOC_TRIAL_FAILED` 提示。 + +#### 错误响应 + +```json +{ "code": 589507, "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", "data": null, "success": false } +``` + +| code | 触发条件 | +|------|----------| +| 589507 | 缺 `group-batch:audit:view`(被拒时不建任何数据) | +| 589500 | 团期不存在 | +| 401 | 未登录(网关拦截) | + +#### 业务边界 + +- 两个人同时第一次打开同一团:只会建一份草稿,两边拿到同一个 `auditId`(TEST 并发实测)。 +- 已核算 / 已验团时读的是定稿快照,第 1 层之后再改也不会变;若第 1 层核单在定稿后被重开,会出 `SOURCE_FINALIZED_REOPENED` 提示。 +- 在团户与录入不一致(例如定稿前有户退团)会出 `HOUSEHOLD_SET_CHANGED` 提示。 +- `items[].changeReason` 永远是 null,不要拿它回显改价原因。 + +--- + +### 2. 保存核团录入(GB-ADM-051) `PUT /v3/admin/order/group-batch/{groupBatchId}/audit` + +**VO**: `GroupBatchAuditSaveReqVO` → `Result` + +#### 使用场景 + +「核团验团」Tab 录入中状态下点「保存」:提交**全部**科目行和**全部**逐户用量(全集对账:带 `itemId` 的覆盖、不带的新增、库里有而本次没提交的删除)。服务端重算每户金额、`version + 1`。 + +**权限**:`group-batch:audit:edit`。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long(传字符串) | ✅ | 团期 ID | | +| expectedVersion | Body | Integer | ✅ | ≥ 0 | 取自 GB-ADM-050 或上一次写接口返回的 `version` | +| items | Body | Object[] | ✅ | ≤ 500 行;没有传 `[]` | 科目行全集 | +| items[].itemId | Body | String | 否 | 须为本团现有科目行 | 空 = 新增 | +| items[].category | Body | String | ✅ | 8 个科目枚举之一 | 决定计价方式 | +| items[].itemName | Body | String | ✅ | ≤ 128 字 | 科目名 | +| items[].dayNo | Body | Integer | 否 | - | 第几天 / 第几晚 | +| items[].unitPrice | Body | String/Number | 条件 | ≥ 0,整数位 ≤ 10、小数 ≤ 2 | **只有单价型(HOUSE/ACTIVITY/MEAL)填**,此时 totalAmount 必须空 | +| items[].totalAmount | Body | String/Number | 条件 | ≥ 0,整数位 ≤ 10、小数 ≤ 2 | **只有总额型填**,此时 unitPrice 必须空 | +| items[].changeReason | Body | String | 条件 | ≤ 256 字 | 改了单价 / 总额、且改后整项金额 ≠ `budgetAmount` 时**必填** | +| items[].allocRule | Body | String | ✅ | 须与科目匹配;`PER_HEAD_AVG` 不可用 | 分摊口径 | +| items[].allocGroup | Body | String | 否 | ≤ 32 字 | 车科目 `BUS` / `SUV` | +| items[].seq | Body | Integer | ✅ | - | 排序 | +| details | Body | Object[] | ✅ | ≤ 20000 行;没有传 `[]` | 逐户用量全集,按 (itemId, orderId) 对账 | +| details[].itemId | Body | String | ✅ | 须是本次 items 里带 itemId 的行 | | +| details[].orderId | Body | String | ✅ | 须为本团在团户,否则 589572 | | +| details[].quantity | Body | Number | 否 | ≥ 0,整数位 ≤ 8、小数 ≤ 2 | 房 = 间数、景娱 / 餐 = 人数;总额型不填;空 = 缺省用量 | +| details[].participated | Body | Boolean | ✅ | - | false = 本户该科目记 0 | +| details[].allocGroup | Body | String | 否 | ≤ 32 字 | 空 = 随科目行 | +| details[].note | Body | String | 否 | ≤ 256 字 | 例外备注 | + +**没有 `details[].amount`**:逐户金额服务端算,传了也收不到。 + +#### 出参 + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | String | 团期 ID | +| auditStatus | String | 写入后的核团状态(保存后恒为 `DRAFT`) | +| auditStatusText | String | 中文名(录入中 / 已核算 / 已验团) | +| batchStatus | String | 团期状态透传 | +| readyToAllocate | Boolean | 写入后重新判定的提交核算前置 | +| blockingOrderIds | String[] | 第 1 层未定稿的子订单 ID | +| version | Integer | 写入后的新版本(已 +1),下次写原样回传 | +| warnings[] | Object[] | 结构同 GB-ADM-050 `warnings[]`(这里主要是 `CATEGORY_AMOUNT_MISMATCH`) | + +#### 请求示例 + +(TEST 实测请求的结构,只列一行科目与一行用量;实际须提交全集) + +```http +PUT /v3/admin/order/group-batch/2100857935637663745/audit +Authorization: Bearer +Content-Type: application/json + +{ + "expectedVersion": 1, + "items": [ + { + "itemId": "2100858806698135595", + "category": "OTHER_EXPENSE", + "itemName": "其他支出(逐户核单带出)", + "dayNo": null, + "unitPrice": null, + "totalAmount": "120.00", + "changeReason": "杂费追加 20 元已与财务确认", + "allocRule": "PER_ORDER_AVG", + "allocGroup": null, + "seq": 11 + } + ], + "details": [ + { + "itemId": "2100858806698135591", + "orderId": "2100857936799485953", + "quantity": 1, + "participated": false, + "allocGroup": null, + "note": null + } + ] +} +``` + +#### 响应示例 + +(TEST 真实响应,2026-09-18,把一户的用餐改为不参加) + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "2100857935637663745", + "auditStatus": "DRAFT", + "auditStatusText": "录入中", + "batchStatus": "REVIEWING", + "readyToAllocate": false, + "blockingOrderIds": ["2100857936799485953"], + "version": 1, + "warnings": [ + { + "code": "CATEGORY_AMOUNT_MISMATCH", + "level": "WARN", + "itemId": "2100858806698135555", + "orderId": null, + "message": "科目「D1 阿尔山成悦大酒店 标准间」按单价 × 用量算得 1200.00,与第 1 层核单实际成本 1250.00 不一致" + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +写接口,无空数据形态。任何一条校验失败整次保存回滚,科目行、用量、合计、版本都不变(TEST 已核对零写入)。 + +```json +{ "code": 589573, "message": "核团数据已被他人修改(当前版本 3,提交版本 2),请刷新后重试", "data": null, "success": false } +``` + +#### 错误响应 + +(TEST 真实响应:改了总额、偏离带出值但没填原因) + +```json +{ "code": 100001, "message": "参数非法: 科目「其他支出(逐户核单带出)」改价后金额 120.00 偏离带出值 100.00,请填写改价原因", "data": null, "success": false } +``` + +| code | 触发条件 | +|------|----------| +| 589507 | 缺 `group-batch:audit:edit` | +| 589500 | 团期不存在 | +| 589567 | 还没有核团记录(先调 GB-ADM-050 生成草稿) | +| 589568 | 不在录入中(已核算须先重新核算;已验团不可改),message 写明「当前 X,需要 Y」 | +| 589569 | 科目填法不对:单价型填了总额 / 总额型填了单价 / 两者同填或同空 / 科目与分摊口径不匹配 / 枚举值非法 | +| 589570 | 有金额的均分科目没有可分摊的户(如车科目分组里没有一户),message 带科目与金额 | +| 589572 | `details[].orderId` 不是本团在团户 | +| 589573 | `expectedVersion` 与当前版本不一致(别人刚保存过),message 带两个版本号 | +| 100001 | 改价偏离带出值未填原因;`itemId` 不属本团 / 重复提交;用量引用了不在本次 items 里的科目行;同一 (itemId, orderId) 重复 | +| 400 | 请求体注解校验失败(必填缺失、长度 / 金额超限、负数),message 为具体字段提示 | + +#### 业务边界 + +- **全集语义**:漏传的科目行 / 用量行会被删除。前端必须把面板上的全部行带回来,不能只传改动的行。 +- 新增的科目行(不带 itemId)没法在同一次请求里被 details 引用,服务端会给每个在团户自动补一行缺省用量(参加、缺省用量、分组随科目行);要改这些户的用量,保存后按 GB-ADM-050 返回的新 itemId 再保存一次。 +- 改价判定:单价或总额与库里不同 = 改价。改后整项金额仍等于 `budgetAmount` 不用填原因;只改用量不算改价。改价会记一条团期时间线(含改前改后金额与原因)。 +- 每次成功保存 `version + 1`,前端要用回执里的新 `version` 覆盖本地值。 + +--- + +### 3. 提交核算(GB-ADM-052) `POST /v3/admin/order/group-batch/{groupBatchId}/audit/allocate` + +**VO**: `GroupBatchAuditAllocateReqVO` → `Result` + +#### 使用场景 + +录入无误后点「确认核算 / 提交核算」:按库里当前的录入逐户分摊并定稿,状态 `DRAFT → ALLOCATED`。定稿后录入只读,要改须先「重新核算」。 + +**权限**:`group-batch:audit:allocate`。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long(传字符串) | ✅ | 团期 ID | | +| expectedVersion | Body | Integer | ✅ | ≥ 0 | 取自 GB-ADM-050 / 上次写接口的 `version` | + +不接受任何金额、用量入参(按库里的录入定稿)。 + +#### 出参 + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | String | 团期 ID | +| auditStatus | String | 恒为 `ALLOCATED` | +| subCount | Integer | 参与核算户数(定稿快照) | +| peopleCount | Integer | 参与核算总人数 | +| totalCost | String | 整团成本 | +| totalRevenue | String | 整团收入(应收口径,已扣实退) | +| grossProfit | String | 整团毛利 | +| allocatedAt | String | 提交核算时间 | +| roundingDiff | String | 各均分科目尾差之和 | +| version | Integer | 写入后的新版本(已 +1),重新核算时原样回传 | +| allocs[] | Object[] | 逐户定稿,结构同 GB-ADM-050 `allocs[]`(含 `roundingBearer`) | + +#### 请求示例 + +```http +POST /v3/admin/order/group-batch/2100857935637663745/audit/allocate +Authorization: Bearer +Content-Type: application/json + +{ "expectedVersion": 3 } +``` + +#### 响应示例 + +(TEST 真实响应,2026-09-18;三户中只列第一户) + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "2100857935637663745", + "auditStatus": "ALLOCATED", + "subCount": 3, + "peopleCount": 6, + "totalCost": "3437.00", + "totalRevenue": "16200.00", + "grossProfit": "12763.00", + "allocatedAt": "2026-09-18 16:21:19", + "roundingDiff": "0.01", + "version": 4, + "allocs": [ + { + "orderId": "2100857935461502977", + "contactName": "核团甲", + "peopleCount": 2, + "roomCount": 1, + "revenueAmount": "5400.00", + "costAmount": "1062.34", + "grossProfit": "4337.66", + "roundingBearer": true, + "costBreakdown": { + "HOUSE": "300.00", + "VEHICLE": "333.34", + "ACTIVITY": "289.00", + "MEAL": "100.00", + "GUIDE": "0.00", + "PHOTO": "0.00", + "OTHER_EXPENSE": "40.00" + }, + "note": null + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +写接口,无空数据形态。被拒时零写入,状态仍为录入中。 + +```json +{ "code": 100502, "message": "提交核算处理中,请勿重复提交", "data": null, "success": false } +``` + +#### 错误响应 + +(TEST 真实响应:有一户第 1 层核单未定稿) + +```json +{ "code": 589568, "message": "核团当前状态不允许该操作:提交核算需要所有在团订单第 1 层核单已定稿,未定稿订单:2100857936799485953", "data": null, "success": false } +``` + +| code | 触发条件 | +|------|----------| +| 589507 | 缺 `group-batch:audit:allocate` | +| 589500 | 团期不存在 | +| 589567 | 还没有核团记录 | +| 589568 | 不在录入中;或本团没有在团户;或有户第 1 层核单未定稿(message 列出订单 ID) | +| 589569 | 库里有取值非法的科目行 | +| 589570 | 逐户合计与整团合计对不平,message 带「Σ逐户成本 / 整团成本,Σ逐户收入 / 整团收入」四个数 | +| 589573 | 版本不一致 | +| 100502 | 同一团期 5 秒内重复提交(防双击,第二次被拒,不是返回上一次结果) | + +#### 业务边界 + +- 先看 GB-ADM-050 的 `readyToAllocate`,false 时按钮置灰并提示 `blockingOrderIds`,避免盲提交。 +- 均分科目除不尽时,尾差记在该科目可分摊户中订单 ID 最小的一户(`roundingBearer=true`),逐户合计精确等于整团合计。 +- 用餐 / 景娱按「是否参加」计;车辆、导游、摄影、其他支出、其他收入不看「是否参加」。 + +--- + +### 4. 重新核算(GB-ADM-052b) `POST /v3/admin/order/group-batch/{groupBatchId}/audit/reallocate` + +**VO**: `GroupBatchAuditReallocateReqVO` → `Result` + +#### 使用场景 + +已核算后发现要改:点「重新核算」,状态 `ALLOCATED → DRAFT`,清空逐户定稿(旧定稿整组写进团期时间线留底),录入恢复可编辑。**本团任一子订单已开票时拒绝**,须与财务走人工处理。 + +**权限**:`group-batch:audit:allocate`。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long(传字符串) | ✅ | 团期 ID | | +| expectedVersion | Body | Integer | ✅ | ≥ 0 | 取自 GB-ADM-050 / 提交核算回执的 `version` | + +#### 出参 + +结构同 GB-ADM-051 回执。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | String | 团期 ID | +| auditStatus | String | 恒为 `DRAFT` | +| auditStatusText | String | 录入中 | +| batchStatus | String | 团期状态透传 | +| readyToAllocate | Boolean | 提交核算前置 | +| blockingOrderIds | String[] | 第 1 层未定稿的子订单 ID | +| version | Integer | 新版本(已 +1) | +| warnings[] | Object[] | 恒为空数组 | + +#### 请求示例 + +```http +POST /v3/admin/order/group-batch/2100857935637663745/audit/reallocate +Authorization: Bearer +Content-Type: application/json + +{ "expectedVersion": 4 } +``` + +#### 响应示例 + +(TEST 真实响应,2026-09-18) + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "2100857935637663745", + "auditStatus": "DRAFT", + "auditStatusText": "录入中", + "batchStatus": "REVIEWING", + "readyToAllocate": true, + "blockingOrderIds": [], + "version": 5, + "warnings": [] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +写接口,无空数据形态。被拒时零写入,定稿保持不变。 + +```json +{ "code": 100502, "message": "重新核算处理中,请勿重复提交", "data": null, "success": false } +``` + +#### 错误响应 + +(TEST 真实响应:本团有一户已开票) + +```json +{ "code": 589571, "message": "已开票冲突:本团子订单 2100857936799485953 已开票,禁止重新核算,请先与财务确认发票处理后走人工调整", "data": null, "success": false } +``` + +| code | 触发条件 | +|------|----------| +| 589507 | 缺 `group-batch:audit:allocate` | +| 589500 | 团期不存在 | +| 589567 | 还没有核团记录 | +| 589568 | 不在已核算(录入中无需重算;已验团须先验团反确认),message 写明当前 / 需要 | +| 589571 | 本团有子订单已开票,message 列出订单 ID | +| 589573 | 版本不一致 | +| 100502 | 同一团期 5 秒内重复提交 | + +#### 业务边界 + +- 已验团(CHECKED)不能直接重新核算:先调既有 `POST .../settle/reopen`(验团反确认)退回已核算,再重新核算。 +- 清空的旧定稿不会丢,写在团期时间线里(事件 `BATCH_AUDIT_REALLOCATE`)。 + +--- + +### 5. 核团按户开票(GB-ADM-054) `POST /v3/admin/order/group-batch/{groupBatchId}/audit/invoice` + +**VO**: `GroupBatchInvoiceReqVO` → `Result` + +#### 使用场景 + +核团「开票」弹窗:已核算或已验团时,为本团某一户申请发票。复用系统既有订单发票,发票类型由服务端固定为**增值税普通发票**,开票内容固定「旅游服务费」,金额在后续开票(出票)环节按本户应收计算。 + +**权限**:`group-batch:audit:invoice`。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long(传字符串) | ✅ | 团期 ID | | +| orderId | Body | String | ✅ | 须为本团在团户 | 子订单 ID | +| titleType | Body | String | ✅ | `PERSONAL` / `COMPANY` | 个人 / 单位抬头 | +| title | Body | String | ✅ | ≤ 128 字 | 抬头(公司全称或个人姓名) | +| taxNo | Body | String | 条件 | ≤ 32 字 | `titleType=COMPANY` 时必填 | +| email | Body | String | ✅ | 邮箱格式,≤ 128 字 | 收件邮箱 | + +**不传 `expectedVersion`、不传 `invoiceType`**(开票不改核团版本;发票类型服务端固定)。 + +#### 出参 + +结构同 GB-ADM-051 回执;`version` 为原值(开票不改版本)。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | String | 团期 ID | +| auditStatus | String | `ALLOCATED` 或 `CHECKED`(开票时的核团状态) | +| auditStatusText | String | 已核算 / 已验团 | +| batchStatus | String | 团期状态透传 | +| readyToAllocate | Boolean | 提交核算前置 | +| blockingOrderIds | String[] | 第 1 层未定稿的子订单 ID | +| version | Integer | 当前版本(不变) | +| warnings[] | Object[] | 恒为空数组 | + +#### 请求示例 + +```http +POST /v3/admin/order/group-batch/2100857935637663745/audit/invoice +Authorization: Bearer +Content-Type: application/json + +{ + "orderId": "2100857935461502977", + "titleType": "PERSONAL", + "title": "核团甲", + "taxNo": null, + "email": "guest@example.com" +} +``` + +#### 响应示例 + +(TEST 实测:财务角色为一户开个人抬头发票 → 200,auditStatus/version 不变;已验团态开票同样 200) + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "2100857935637663745", + "auditStatus": "ALLOCATED", + "auditStatusText": "已核算", + "batchStatus": "REVIEWING", + "readyToAllocate": true, + "blockingOrderIds": [], + "version": 9, + "warnings": [] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +写接口,无空数据形态。被拒时不产生发票申请。 + +```json +{ "code": 589568, "message": "核团当前状态不允许该操作:当前「录入中」,需要「已核算」或「已验团」;请先提交核算后再开票", "data": null, "success": false } +``` + +#### 错误响应 + +(TEST 真实响应:选了别的团的订单) + +```json +{ "code": 589572, "message": "所选订单不属于本团期的核团范围", "data": null, "success": false } +``` + +| code | 触发条件 | +|------|----------| +| 589507 | 缺 `group-batch:audit:invoice` | +| 589500 | 团期不存在 | +| 581514 | 单位抬头没填税号(沿用发票域错误码) | +| 589572 | 订单不是本团在团户 | +| 589567 | 还没有核团记录 | +| 589568 | 核团不在已核算 / 已验团 | +| 589571 | 该户已有有效发票(message 带订单 ID) | +| 400 | 请求体校验失败(抬头类型非法、邮箱格式错、超长等) | +| 其他发票域码 | 订单状态不满足开票条件等,message 原样透出 | + +#### 业务边界 + +- 一户只能有一张有效发票;已开票的团不能再「重新核算」(见接口 4)。 +- 前端不需要、也不能选发票类型(专票所需的开户行 / 账号等本接口不收)。 +- 税号、邮箱按原样保存,不做脱敏展示改造。 + +--- + +### 6. 导出核单(GB-ADM-055) `GET /v3/admin/order/group-batch/{groupBatchId}/audit/export` + +**VO**: `text/csv 附件(AuditCsvExport,不套 Result 信封)` + +#### 使用场景 + +核团 Tab「导出核单」按钮。按浏览器下载文件处理(`responseType: 'blob'`),文件名从 `Content-Disposition` 取。录入中也可以导出,此时导出的是试算值:文件第一行与文件名都带「未定稿」。 + +**权限**:`group-batch:audit:view`。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long(传字符串) | ✅ | 团期 ID | | + +#### 出参 + +成功时 HTTP 200,响应头 `Content-Type: text/csv; charset=utf-8`,`Content-Disposition: attachment; filename=""; filename*=UTF-8''`;正文 UTF-8 带 BOM、行尾 CRLF。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| 第 1 行(仅录入中) | 文本 | `未定稿:以下为录入中的试算值,提交核算后以定稿为准` | +| 说明行 | 文本 | `说明:收入为应收口径(已扣实退);金额单位:元` | +| 表头 | 14 列 | `子订单号,联系人,人数,用房数,住宿,车辆,景区娱乐,用餐,导游,摄影,其他支出,成本合计,收入,毛利` | +| 逐户行 | 14 列 | 按订单 ID 升序,数字与 GB-ADM-050 面板一致 | +| 合计行 | 14 列 | 首列「合计」;成本合计 / 收入 / 毛利 = 整团合计 | +| 文件名 | String | `核单-<期号>.csv`;录入中为 `核单-<期号>-未定稿.csv` | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2100857935637663745/audit/export +Authorization: Bearer +``` + +无请求体。 + +#### 响应示例 + +(TEST 真实响应,2026-09-18,已核算团期;响应头如下,正文是 CSV 而不是 JSON) + +```json +{ + "httpStatus": 200, + "Content-Type": "text/csv;charset=utf-8", + "Content-Disposition": "attachment; filename=\"%E6%A0%B8%E5%8D%95-Q202610202100857901416419330.csv\"; filename*=UTF-8''%E6%A0%B8%E5%8D%95-Q202610202100857901416419330.csv", + "decodedFileName": "核单-Q202610202100857901416419330.csv" +} +``` + +CSV 正文: + +```text +说明:收入为应收口径(已扣实退);金额单位:元 +子订单号,联系人,人数,用房数,住宿,车辆,景区娱乐,用餐,导游,摄影,其他支出,成本合计,收入,毛利 +2100857935461502977,核团甲,2,1,300.00,333.34,289.00,100.00,0.00,0.00,40.00,1062.34,5400.00,4337.66 +2100857936304635906,核团乙,3,2,600.00,333.33,289.00,150.00,0.00,0.00,40.00,1412.33,8100.00,6687.67 +2100857936799485953,核团丙,1,1,300.00,333.33,289.00,0.00,0.00,0.00,40.00,962.33,2700.00,1737.67 +合计,,6,4,1200.00,1000.00,867.00,250.00,0.00,0.00,120.00,3437.00,16200.00,12763.00 +``` + +#### 空数据 / 降级响应 + +录入中导出(TEST 真实):文件名 `核单-Q202610242100859128720101378-未定稿.csv`,第一行为「未定稿:以下为录入中的试算值,提交核算后以定稿为准」,其余同上。 + +```json +{ + "httpStatus": 200, + "Content-Type": "text/csv;charset=utf-8", + "decodedFileName": "核单-Q202610242100859128720101378-未定稿.csv" +} +``` + +#### 错误响应 + +失败时**不下载文件**,而是返回普通 JSON 错误(HTTP 200 + `Result` 信封)。前端用 blob 接收时,要先判断 `Content-Type` 是否为 `application/json`,是则解析出 message 提示。 + +(TEST 真实响应:团期还没有核团记录) + +```json +{ "code": 589567, "message": "该团期尚未进入核团:请先打开核团面板生成核算草稿后再导出", "data": null, "success": false } +``` + +| code | 触发条件 | +|------|----------| +| 589507 | 缺 `group-batch:audit:view` | +| 589500 | 团期不存在 | +| 589567 | 还没有核团记录(导出不会像面板那样自动建草稿) | +| 589568 | 录入中且按当前录入试算失败,message 带失败原因 | +| 589517 | 户数超过 2000 行上限 | + +#### 业务边界 + +- 只读,不建数据、不改状态。 +- 联系人等文本以 `=` `+` `-` `@` 开头时会前置单引号,防止 Excel 当公式执行。 + +--- + +### 7. 核单按行程节点逐户下钻(GB-ADM-056) `POST /v3/admin/order/group-batch/{groupBatchId}/settlement/node-lines` + +**VO**: `GroupBatchSettlementNodeLinesReqVO` → `Result` + +#### 使用场景 + +团期行程汇总(GB-ADM-018)某一项点开「核单明细」:把该项的 `days[].nodes[].nodeIds` 原样传进来,返回全团每户在这些节点上的核单门票行。只读、无副作用;用 POST 只是因为 nodeIds 可能很多。 + +**权限**:`group-batch:audit:view`(与核团面板同码)。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long(传字符串) | ✅ | 团期 ID | | +| nodeIds | Body | String[] | ✅ | 1~200 个;重复自动去重 | 取自 GB-ADM-018 `days[].nodes[].nodeIds` | + +不接受 `orderIds`:本团户由服务端按 groupBatchId 自己查。 + +#### 出参 + +**注意**:本接口的金额字段是**数字**(不是字符串),与核团其余接口不同。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| groupBatchId | String | 团期 ID | +| nodeName | String | 项目名(核单行景点名众数,没有核单行时回退行程节点名),可为 null | +| lineCount | Integer | 命中核单行数(只算 `coverage=OK`) | +| confirmedCount | Integer | 其中已逐行确认的行数 | +| totalSellAmount | Number | 全团该项客户成交合计(只算 `OK` 行) | +| totalActualCost | Number | 全团该项实际成本合计(只算 `OK` 行) | +| householdCount | Integer | 请求节点涉及的户数 | +| settledHouseholdCount | Integer | 其中已核单的户数;合计只覆盖这些户,前端要摆在合计旁边 | +| unmatchedNodeIds | String[] | 不属于本团的节点 ID(已忽略、不报错),正常为空 | +| rows[] | Object[] | 逐户明细,按 dayNumber、orderId 升序;每个 (户, 节点) 至少一行 | +| rows[].orderId | String | 子订单 ID | +| rows[].orderNo | String | 子订单编号 | +| rows[].customerName | String | 联系人 | +| rows[].coverage | String | `OK` 有核单行 / `PENDING` 该户还没核单(不是没花钱)/ `NOT_APPLICABLE` 该节点类型核单不按节点归集(置灰「无核单明细」,**不要显示成 ¥0**)/ `MISSING` 已核单但找不到该节点的行 | +| rows[].nodeId | String | 行程节点 ID | +| rows[].nodeType | String | 节点类型(如 `SCENIC`、`RESTAURANT`) | +| rows[].dayNumber | Integer | 第几天 | +| rows[].dayDate | String | 行程日 `yyyy-MM-dd` | +| rows[].settlementId | String | 核单行 ID;仅 `OK` 有值 | +| rows[].ticketCount | Integer | 购票数量;仅 `OK` 有值 | +| rows[].sellPrice | Number | 客户成交单价;仅 `OK` 可能有值 | +| rows[].totalAmount | Number | 行金额;仅 `OK` 可能有值 | +| rows[].plannedCost | Number | 计划成本;仅 `OK` 有值 | +| rows[].actualCost | Number | 实际成本;仅 `OK` 有值 | +| rows[].paymentMethod | String | `SIGNED` / `COMPANY_PAID` / `CASH_PAID`;仅 `OK` 有值 | +| rows[].settlementConfirmStatus | String | `UNCONFIRMED` / `CONFIRMED`;仅 `OK` 有值 | +| rows[].terminated | Boolean | 该户是否出行中终止(服务端不过滤,前端结合 endDayNumber 解释) | +| rows[].endDayNumber | Integer | 终止截断天,未终止为 null | + +`coverage` 不是 `OK` 时,所有金额字段都是 **null**(不是 0)。 + +#### 请求示例 + +```http +POST /v3/admin/order/group-batch/2100857935637663745/settlement/node-lines +Authorization: Bearer +Content-Type: application/json + +{ "nodeIds": ["2100857935742521346", "2100857935755104258", "2100859129223335939"] } +``` + +#### 响应示例 + +(TEST 真实响应,2026-09-18;6 行只列 2 行,最后一个 nodeId 属于别的团) + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "2100857935637663745", + "nodeName": "海拉尔国家森林公园", + "lineCount": 3, + "confirmedCount": 3, + "totalSellAmount": 0, + "totalActualCost": 87.00, + "householdCount": 3, + "settledHouseholdCount": 3, + "unmatchedNodeIds": ["2100859129223335939"], + "rows": [ + { + "orderId": "2100857935461502977", + "orderNo": "HL20260918160218408", + "customerName": "核团甲", + "coverage": "OK", + "nodeId": "2100857935742521346", + "nodeType": "SCENIC", + "dayNumber": 1, + "dayDate": "2026-10-20", + "settlementId": "2100858609570041858", + "ticketCount": 1, + "sellPrice": null, + "totalAmount": null, + "plannedCost": 29.00, + "actualCost": 29.00, + "paymentMethod": "CASH_PAID", + "settlementConfirmStatus": "CONFIRMED", + "terminated": false, + "endDayNumber": null + }, + { + "orderId": "2100857935461502977", + "orderNo": "HL20260918160218408", + "customerName": "核团甲", + "coverage": "NOT_APPLICABLE", + "nodeId": "2100857935755104258", + "nodeType": "RESTAURANT", + "dayNumber": 1, + "dayDate": "2026-10-20", + "settlementId": null, + "ticketCount": null, + "sellPrice": null, + "totalAmount": null, + "plannedCost": null, + "actualCost": null, + "paymentMethod": null, + "settlementConfirmStatus": null, + "terminated": false, + "endDayNumber": null + } + ] + }, + "success": true +} +``` + +#### 空数据 / 降级响应 + +传入的节点全都不属于本团(TEST 真实):`rows=[]`,节点列进 `unmatchedNodeIds`,不报错。 + +```json +{ + "code": 200, + "message": "成功", + "data": { + "groupBatchId": "2100857935637663745", + "rows": [], + "unmatchedNodeIds": ["2100859129223335939"] + }, + "success": true +} +``` + +#### 错误响应 + +(TEST 真实响应:传了 201 个节点) + +```json +{ "code": 400, "message": "nodeIds 一次最多 200 个", "data": null, "success": false } +``` + +| code | 触发条件 | +|------|----------| +| 400 | `nodeIds` 为空(「nodeIds 不能为空」)或超过 200 个 | +| 589507 | 缺 `group-batch:audit:view` | +| 589500 | 团期不存在 | + +#### 业务边界 + +- 合计只累加 `coverage=OK` 的行;有 `PENDING` 户时合计偏小是正常的,要同时展示 `settledHouseholdCount / householdCount`。 +- 本接口金额来自订单核单,和行程汇总里的参考价不同源,对不上是正常的。 + +--- + +## 四、契约约束与正确调用方式 + +### 权限码(jw 2026-09-18 裁决) + +| 权限码 | 管哪些接口 | 默认授予角色 | +|--------|------------|--------------| +| `group-batch:audit:view` | GB-ADM-050 面板、055 导出、056 节点下钻 | 团期管理员、财务、管理员 | +| `group-batch:audit:edit` | GB-ADM-051 保存 | 团期管理员、管理员 | +| `group-batch:audit:allocate` | GB-ADM-052 提交核算、052b 重新核算 | 团期管理员、管理员 | +| `group-batch:audit:invoice` | GB-ADM-054 开票 | 财务、管理员 | + +- 缺码统一返回 **589507**。超级管理员全部放行。 +- **核团按钮显隐不要再用 `group-batch:finance:view` / `group-batch:finance:advance`**(旧文档写法已作废)。 +- **验团 / 验团反确认(`POST .../settle`、`.../settle/reopen`)不走上面四个码**,仍按角色:超级管理员 / 管理员 / 财务。团期管理员能核算但不能验团。 + +### 与原型 / 旧文档 `12-核算与验团.html` 的出入(以本文为准) + +| 项 | 原型 / 旧文档 | 实际 | +|----|---------------|------| +| 路径 | `/admin/group-batch/items/{batchId}/audit*` | `/v3/admin/order/group-batch/{groupBatchId}/audit*` | +| 路径参数 | `batchId` | `groupBatchId` | +| 错误码 | 595xxx | 589xxx(595022→589567、595025→589568、595023→589569、595024→589570、595026→589571、595034→589572、595001→589500、595006→589573) | +| 并发字段 | `expectedUpdateTime` | `expectedVersion`(整数,取 GB-ADM-050 的 `version`) | +| 提示 | `warnings: string[]` | `warnings: [{code, level, itemId, orderId, message}]` | +| 权限 | `group-batch:finance:*` | `group-batch:audit:*` 四码 | +| 开票入参 | 含 `expectedVersion`、`invoiceType` | 两个都不传,发票类型服务端固定普票 | +| 导出 | 定稿后才能导 | 录入中也能导,首行与文件名标「未定稿」 | +| 无成本源的团 | 预置科目模板 | 首次打开 `items=[]`,经 GB-ADM-051 新增科目行 | +| `items[].changeReason` | 回显改价原因 | 读接口恒 null,原因只进时间线 | +| 提交核算回执 | 无版本 | 带 `version`,重新核算时回传 | +| 验团 | 独立 `/audit/check` | 没有这个接口;验团走既有 `POST .../settle`(见修改接口 changelog) | + +### ✅ 正确 / ❌ 错误 用法 + +| 场景 | 做法 | +|------|------| +| ✅ 每次写请求 | 带上最近一次拿到的 `version` 作为 `expectedVersion`,成功后用回执里的新 `version` 覆盖 | +| ✅ 收到 589573 | 提示「数据已被他人修改」,重新调 GB-ADM-050 刷新后再操作 | +| ✅ 保存 | 提交全部科目行 + 全部用量行 | +| ❌ 只提交改动过的行 | 漏掉的行会被删掉 | +| ❌ 前端自己算逐户金额 / 合计 | 以服务端 `amount` / `allocs` 为准 | +| ❌ 单价型科目传 `totalAmount` | 589569 | +| ✅ 错误提示 | 589567/589568/589570/589571/589573 的 message 自带定位信息(当前状态、差额、订单号、版本号),直接展示 message | + +--- + +## 五、数据库行为 + +- 无新表、无 Flyway(核团四张表已由 #7868 建好);新增 4 个权限码的种子数据(user-service)。 +- GB-ADM-050:已返团且无核团记录时,首次读取会建一份录入中草稿(主行 + 带出的科目行 + 逐户用量);未返团不写。 +- GB-ADM-051:覆盖 / 新增 / 删除科目行与用量行,重算金额与三个合计,版本 +1;改价写一条团期时间线。 +- GB-ADM-052:写逐户定稿,状态改已核算,版本 +1,写一条时间线。 +- GB-ADM-052b:清空逐户定稿,状态回录入中,版本 +1,旧定稿整组写进时间线。 +- GB-ADM-054:写一条订单发票申请(复用既有订单发票),不改核团数据与版本。 +- GB-ADM-055 / 056:只读。 +- 任何接口被拒时整笔回滚,零写入(TEST 逐条核对)。 + +--- + +## 六、边界行为 + +- 未登录 → 401(网关拦截)。 +- 缺权限码 → 589507,且不产生任何写入(包括 GB-ADM-050 不会建草稿)。 +- 团期未返团 → GB-ADM-050 返回 `NOT_STARTED`;其余写接口 / 导出返回 589567。 +- 并发编辑 → 后提交者 589573;并发双击提交核算 / 重新核算 → 第二次 100502。 +- 已验团 → 保存 / 提交核算 / 重新核算均 589568;开票仍可。 +- 第 1 层数据读取失败 → GB-ADM-050 不报错,出 `SOURCE_UNAVAILABLE` 提示。 + +## 六.5、枚举 / 数据字典 + +### auditStatus(核团状态) + +**所属字段**: GB-ADM-050 / 051 / 052 / 052b / 054 出参 `auditStatus` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `NOT_STARTED` | 未开始 | 团期未返团,只在 GB-ADM-050 出现 | +| `DRAFT` | 录入中 | 可保存、可提交核算 | +| `ALLOCATED` | 已核算 | 定稿;可重新核算、开票、验团 | +| `CHECKED` | 已验团 | 只读;可开票;验团反确认退回已核算 | + +### category(科目) + +**所属字段**: `items[].category` | **类型**: `String` + +| 值 | 中文 | 计价 | 默认分摊口径 | +|----|------|------|--------------| +| `HOUSE` | 住宿 | 单价 | `PER_ROOM_NIGHT` | +| `VEHICLE` | 车辆 | 总额 | `PER_VEHICLE_GROUP` | +| `ACTIVITY` | 景区娱乐 | 单价 | `PER_HEAD_CHECKED` | +| `MEAL` | 用餐 | 单价 | `PER_HEAD_CHECKED` | +| `GUIDE` | 导游 | 总额 | `PER_ORDER_AVG` | +| `PHOTO` | 摄影 | 总额 | `PER_ORDER_AVG` | +| `OTHER_EXPENSE` | 其他支出 | 总额 | `PER_ORDER_AVG` | +| `OTHER_INCOME` | 其他收入 | 总额 | `PER_ORDER_AVG`(计入收入) | + +### allocRule(分摊口径) + +**所属字段**: `items[].allocRule` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `PER_ROOM_NIGHT` | 按房 | 单价 × 本户房间数(不参加记 0) | +| `PER_HEAD_CHECKED` | 勾选按人 | 单价 × 本户人数(不参加记 0) | +| `PER_VEHICLE_GROUP` | 按乘车组 | 总额 ÷ 同分组户数,不看是否参加 | +| `PER_ORDER_AVG` | 按户均分 | 总额 ÷ 在团户数,不看是否参加 | +| `PER_HEAD_AVG` | 按人均分 | 一期不可用,保存传入返回 589569 | + +### warnings[].code(提示码) + +**所属字段**: `warnings[].code` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `CATEGORY_AMOUNT_MISMATCH` | 带出金额不一致 | 单价 × 用量与第 1 层实际成本不一致,可能需要改价 | +| `SOURCE_FINALIZED_REOPENED` | 第 1 层已重开 | 定稿后第 1 层核单被重开或重新定稿 | +| `HOUSEHOLD_SET_CHANGED` | 在团户变化 | 在团户与核团录入不一致 | +| `ALLOC_TRIAL_FAILED` | 试算失败 | 按当前录入无法试算(如对不平),面板逐户为空 | +| `SOURCE_UNAVAILABLE` | 第 1 层读取失败 | 第 1 层核单事实读取失败 | +| `ITEM_CODE_INVALID` | 科目行枚举非法 | 库里科目行取值非法 | + +### coverage(节点下钻覆盖状态) + +**所属字段**: GB-ADM-056 `rows[].coverage` | **类型**: `String` + +| 值 | 中文 | 说明 | +|----|------|------| +| `OK` | 有核单行 | 金额字段有值 | +| `PENDING` | 未核单 | 该户还没提交核单,不是没花钱 | +| `NOT_APPLICABLE` | 无核单明细 | 该节点类型不按节点归集,置灰,不显示 ¥0 | +| `MISSING` | 缺行 | 已核单但没有该节点的行(多为核单后行程改动) | + +--- + +## 七、不影响范围 + +- **仅影响**: 团期详情「核团验团」Tab 的 7 个新接口。 +- **零影响**: + - 订单侧第 1 层核单(核团只读取、不回写) + - 团期共享成本录入 / 列表 / 汇总(`.../settlement/cost`、`.../settlement/summary`) + - 既有订单发票接口 + - 网关配置(沿用已有 `/v3/admin/**` 路由) + +--- + +## 八、测试环境已验证 + +被测版本:hl-order-service-v3 = dev-v3(#7932 PR-1~PR-3b,合并提交 `aed07cc3c`);user-service 权限码种子 `V20260918.932` 已于 2026-09-18 17:58 执行。经网关实测,造数:团期 A(3 户,走完整链路)、B(无成本源)、C(并发首读)、D(判权)、E(无核团行)。 + +``` +AC-1 GET 050 首读:库中 0 行 → 建 DRAFT 主行 + 11 科目行 + 33 用量行,响应与库逐行一致 ✓ + 并发首读(两请求同时):只建 1 行,两边 auditId 相同 ✓ +AC-2 PUT 051 改一户用餐不参加 → 该户该项 50.00→0.00,其余户不变,version 0→1 ✓ +AC-3 改总额 100→120 不填原因 → 100001 零写入;填原因 → 200,时间线记 BATCH_AUDIT_PRICE_OVERRIDE ✓ +AC-17 同一 expectedVersion 写两次 → 第二次 589573「当前版本 3,提交版本 2」,零写入 ✓ +AC-10 单价型传总额 / 两者同填 / 两者同空 / 总额型传单价 → 均 589569,零写入 ✓ +AC-13 用量里带他团订单 → 589572,零写入 ✓ +C1 一户第 1 层未定稿提交核算 → 589568 列出订单号,零写入;定稿后 readyToAllocate=true ✓ +AC-4/7 POST 052 → ALLOCATED,Σ逐户成本 3437.00 = 整团,Σ收入 16200.00 = 整团,尾差户标记正确 ✓ +AC-8 已核算 PUT → 589568;POST 052b → DRAFT,旧定稿 3 户写进时间线,之后可再保存 ✓ +AC-9 一户已开票时 052b → 589571 列出订单号,定稿不清空 ✓ +AC-12 已验团时保存 / 提交 / 重算 → 均 589568 ✓ +AC-15 GET 055:已核算导出 6 行、合计行 = 整团三数、逐户与库一致;录入中文件名与首行带「未定稿」 ✓ + 无核团行导出 → 589567 ✓ +AC-13 054 财务开个人抬头 → 200,发票为增值税普通发票 / 旅游服务费 / REQUESTED,核团版本不变 ✓ + 同户再开 / 已有有效发票户 → 589571;他团订单 → 589572;单位抬头缺税号 → 581514 ✓ + 录入中 → 589568;无核团行 → 589567;已验团态开票 → 200 ✓ +AC-15 导出 BOM(EF BB BF)、全 CRLF、14 列表头顺序、合计行 = 库中整团合计 ✓ +AC-16 POST 056:OK / NOT_APPLICABLE / PENDING 三种 coverage 正确,非 OK 金额全 null; + 他团节点进 unmatchedNodeIds、rows=[];201 个 → 400;空 → 400 ✓ +权限 user-service 种子:audit:view→管理员/财务/团期管理员;edit、allocate→管理员/团期管理员; + invoice→管理员/财务 ✓ + 逐角色实测(种子执行后等过 10 分钟缓存):团期管理员 050/051/052/052b/055/056 通过、054 → 589507; + 财务 050/054/055/056 通过、051/052/052b → 589507;定制师、运营全部 589507; + 团期管理员对团期 C 真实保存成功(version 0→1) ✓ +``` + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#7932](https://git.1814.love:8443/wx/HL/issues/7932) +- 关联 PR: [#7940](https://git.1814.love:8443/wx/HL/pulls/7940)、[#7944](https://git.1814.love:8443/wx/HL/pulls/7944)、[#7943](https://git.1814.love:8443/wx/HL/pulls/7943)、[#7955](https://git.1814.love:8443/wx/HL/pulls/7955) +- 同日修改接口:`changelogs-v2/2026-09/18_7932_团期验团归档前置核团-修改接口-管理后台.md`(验团 `/settle` 行为变更) + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7932](https://git.1814.love:8443/wx/HL/issues/7932) +- **PR**: [#7940](https://git.1814.love:8443/wx/HL/pulls/7940)、[#7944](https://git.1814.love:8443/wx/HL/pulls/7944)、[#7943](https://git.1814.love:8443/wx/HL/pulls/7943)、[#7955](https://git.1814.love:8443/wx/HL/pulls/7955) +- **Merge commit**: [aed07cc3c](https://git.1814.love:8443/wx/HL/commit/aed07cc3c)(最后一个 PR) + +### 联系人 + +- **后端负责人**: @jw +- **前端负责人**: @mmg diff --git a/changelogs-v2/2026-09/18_7932_团期验团归档前置核团-修改接口-管理后台.md b/changelogs-v2/2026-09/18_7932_团期验团归档前置核团-修改接口-管理后台.md new file mode 100644 index 00000000..805b678c --- /dev/null +++ b/changelogs-v2/2026-09/18_7932_团期验团归档前置核团-修改接口-管理后台.md @@ -0,0 +1,328 @@ +--- +schema: "hl-changelog/v2" +ticket: "7932" +title: "验团归档前置核团定稿并同事务推进核团为已验团,验团反确认同步退回核团" +consumer: "admin" +author: "jw(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "pending" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "已上线的验团归档 POST .../settle 行为变更:核团须先提交核算(ALLOCATED)才能验团,验团同事务把核团推到已验团(CHECKED);新增可选请求体 checkNote(验团意见)。验团反确认 POST .../settle/reopen 同事务把核团从已验团退回已核算。⚠️ 处于核单中(REVIEWING)但还没有核团记录的团,验团会返回 589567,须先在核团 Tab 完成定稿。后端 PR #7944 已合并 dev-v3 并部署 TEST。核团 7 个新接口见同日新增接口 changelog。" +updated_at: "2026-09-18" +base: "dev-v3" +--- + +# order-v3: 🔧 验团归档须先完成核团定稿,验团 / 反确认与核团状态联动 + +> **存放目录**: `changelogs-v2/{YYYY-MM}/`(管理后台,二期 order-v3) +> +> **服务**: hl-order-service-v3(order-v3) +> **PR**: #7944(合并提交 db3abc6c7;后续 #7955 未改这两个接口) +> **Issue**: #7932 +> **日期**: 2026-09-18 +> **影响范围**: 团期详情「验团」「验团反确认」按钮 + +--- + +## ⚠️ 关键变化 + +1. **⚠️ 线上已有接口的行为变更**:`POST .../settle`(验团归档)以前只要团期在「核单中」(REVIEWING)就能点;**现在还要求核团已提交核算**。 + - **处于核单中但还没有核团记录的团,验团会返回 `589567`**(「该团期尚未进入核团:尚无核团记录,请先在核团 Tab 完成核算定稿后再验团」)。 + - 核团还在录入中,验团返回 `589568`。 + - 运营须先到「核团验团」Tab:打开面板(自动生成草稿)→ 保存 → 提交核算,然后再验团。 +2. 验团成功时,核团与团期**在同一个事务里**一起变:核团 `ALLOCATED → CHECKED`、团期 `REVIEWING → SETTLED`,不会出现一个变了一个没变。 +3. 验团**新增可选请求体** `{ "checkNote": "..." }`(验团意见,≤512 字);不传 body 与以前一样能调。 +4. `POST .../settle/reopen`(验团反确认)同时把核团从「已验团」退回「已核算」,并清空验团意见;要改分摊数须再点「重新核算」。 +5. 路径、响应结构、判权方式都不变。 + +--- + +## 一、背景 + +以前验团只看团期状态,「成本还没录完就能点验团」是已知缺口。#7932 上线核团后,验团改为以核团定稿为前提,并与核团「已验团」合并成一个动作(不另设 `/audit/check` 接口)。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 验团归档(GB-ADM-053) | POST | `/v3/admin/order/group-batch/{groupBatchId}/settle` | 行为变更 + 新增可选请求体 | 须核团已核算;同事务推进核团为已验团 | +| 2 | 验团反确认 | POST | `/v3/admin/order/group-batch/{groupBatchId}/settle/reopen` | 行为变更 | 同事务把核团从已验团退回已核算 | + +--- + +## 三、接口详情 + +### 1. 验团归档(GB-ADM-053) `POST /v3/admin/order/group-batch/{groupBatchId}/settle` + +**VO**: `GroupBatchSettleReqVO`(可选)→ `Result` + +#### 使用场景 + +团期详情 / 核团 Tab「验团」按钮:核对完成后归档,团期进入已验团终态。按钮建议只在 `batchStatus=REVIEWING` **且**核团面板 `auditStatus=ALLOCATED` 时可点(两个状态都能从核团面板 GB-ADM-050 一次拿到)。 + +**权限(不变)**:按角色——超级管理员 / 管理员 / 财务;不看 `group-batch:audit:*` 权限码。团期管理员能核算但不能验团。 + +#### 入参 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long(传字符串) | ✅ | 团期 ID | 不变 | +| checkNote | Body | String | 否 | ≤ 512 字 | **新增**:验团意见;整个请求体都可以不传 | + +#### 出参 + +不变。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | null | 成功无数据;验团意见 / 验团时间在核团面板 GB-ADM-050 的 `checkNote` / `checkedAt` 里看 | + +#### 请求示例 + +```http +POST /v3/admin/order/group-batch/2100857935637663745/settle +Authorization: Bearer +Content-Type: application/json + +{ "checkNote": "成本已逐项核对" } +``` + +不带意见时可以不传请求体(老前端写法照常可用)。 + +#### 响应示例 + +(TEST 真实响应,2026-09-18,核团已核算的团期;之后核团 `CHECKED`、团期 `SETTLED`) + +```json +{ "code": 200, "message": "成功", "data": null, "success": true } +``` + +#### 空数据 / 降级响应 + +写接口,无空数据形态。被拒时整笔回滚:团期状态、核团状态、验团意见都不变(TEST 已核对)。 + +```json +{ "code": 589568, "message": "核团当前状态不允许该操作:当前「录入中」,需要「已核算」;请先在核团 Tab 完成核算定稿后再验团", "data": null, "success": false } +``` + +#### 错误响应 + +(TEST 真实响应:核单中、但还没有核团记录的团 —— **本次行为变更的主要影响面**) + +```json +{ "code": 589567, "message": "该团期尚未进入核团:尚无核团记录,请先在核团 Tab 完成核算定稿后再验团", "data": null, "success": false } +``` + +| code | 触发条件 | +|------|----------| +| 589507 | 非超级管理员 / 管理员 / 财务(不变) | +| 589500 | 团期不存在(不变) | +| 589555 | 团期已验团(不变;并发时后到的一次也报这个) | +| 589501 | 团期不在核单中(不变) | +| **589567** | **新增**:团期在核单中,但还没有核团记录 | +| **589568** | **新增**:核团不在「已核算」(如还在录入中) | +| **589573** | **新增**:推进核团状态时与他人操作冲突,刷新后重试 | +| 400 | `checkNote` 超过 512 字 | + +#### 业务边界 + +- 判断顺序:先判权限 → 团期存在 → 团期状态(不是核单中就按原来的 589555 / 589501 报)→ 再判核团。 +- 核团已是「已验团」(极少数并发场景)时不改验团意见,交给团期状态判断。 +- 验团后核团四张表只读:保存 / 提交核算 / 重新核算都会被拒(589568),开票仍可。 + +--- + +### 2. 验团反确认 `POST /v3/admin/order/group-batch/{groupBatchId}/settle/reopen` + +**VO**: `Result`(无请求体) + +#### 使用场景 + +已验团的团发现有误时点「验团反确认」:团期 `SETTLED → REVIEWING`,**同时**核团 `CHECKED → ALLOCATED`,清空验团人、验团时间与意见(旧值写进团期时间线)。要改分摊数,再到核团 Tab 点「重新核算」(已开票的团会被拦)。 + +**权限(不变)**:超级管理员 / 管理员 / 财务。 + +#### 入参 + +本次入参**不变**。 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long(传字符串) | ✅ | 团期 ID | 无请求体 | + +#### 出参 + +本次出参**不变**。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | null | 成功无数据 | + +#### 请求示例 + +```http +POST /v3/admin/order/group-batch/2100857935637663745/settle/reopen +Authorization: Bearer +``` + +无请求体。 + +#### 响应示例 + +(TEST 真实响应,2026-09-18;之后团期 `REVIEWING`、核团 `ALLOCATED`,`checkNote` / `checkedAt` 变为 null,逐户定稿保留) + +```json +{ "code": 200, "message": "成功", "data": null, "success": true } +``` + +#### 空数据 / 降级响应 + +本单上线前就已验团、没有核团记录的存量团:反确认照常成功,只退团期状态,不动核团(反确认是纠错退路,不因核团数据缺失而堵死)。 + +```json +{ "code": 200, "message": "成功", "data": null, "success": true } +``` + +#### 错误响应 + +```json +{ "code": 589501, "message": "团期状态不允许当前操作", "data": null, "success": false } +``` + +| code | 触发条件 | +|------|----------| +| 589507 | 非超级管理员 / 管理员 / 财务(不变) | +| 589500 | 团期不存在(不变) | +| 589501 | 团期不是已验团(不变) | +| **589573** | **新增**:退回核团状态时与他人操作冲突 | + +#### 业务边界 + +- 反确认后核团停在「已核算」,逐户定稿不清空;需要改数时再「重新核算」。 +- 旧的验团人、时间、意见写在团期时间线(验团事件的附加信息)里,可追溯。 + +--- + +## 四、契约约束与正确调用方式 + +### 前端需要做的 + +1. **验团按钮可点条件**:`batchStatus === 'REVIEWING' && auditStatus === 'ALLOCATED'`(取自核团面板 GB-ADM-050)。核团未定稿时置灰并提示「请先在核团 Tab 完成核算定稿」。 +2. **验团弹窗加可选「验团意见」输入框**(≤512 字),作为 `checkNote` 提交;不填可不传 body。 +3. **589567 / 589568 直接展示后端 message**(已带引导语),并引导跳到核团 Tab。 +4. 验团 / 反确认成功后刷新核团面板(核团状态会一起变)。 + +### ✅ 正确 / ❌ 错误 用法 + +| 场景 | 做法 | +|------|------| +| ✅ 验团带意见 | `{ "checkNote": "成本已逐项核对" }` | +| ✅ 验团不带意见 | 不传 body,或传 `{}` | +| ❌ 核团还在录入中就点验团 | 589568 | +| ❌ 核单中但从没打开过核团 Tab 就点验团 | 589567(本次新增的拒绝) | +| ❌ 调 `/audit/check` 做验团 | 没有这个接口,验团就是 `/settle` | + +--- + +## 五、数据库行为 + +- 无表结构变更、无 Flyway。 +- 验团成功:团期状态改为已验团;核团状态改为已验团,写验团人、验团时间、验团意见,版本 +1;团期时间线的验团事件附加记录核团迁移与验团意见。 +- 反确认成功:团期状态退回核单中;核团状态退回已核算,清空验团人、时间、意见,版本 +1;旧值写进时间线。 +- 任何一步被拒,团期与核团整笔回滚,零写入(TEST 已核对 589567 / 589568 两种拒绝后团期仍为核单中、核团行数不变)。 + +--- + +## 六、边界行为 + +- 核单中 + 无核团记录 → 验团 589567(**改前可以直接验团**)。 +- 核单中 + 核团录入中 → 验团 589568(**改前可以直接验团**)。 +- 核单中 + 核团已核算 → 验团成功,核团同步变已验团。 +- 已验团 → 再点验团 589555(不变);反确认成功,核团退回已核算。 +- 本单上线前已验团的存量团(无核团记录)→ 反确认照常成功。 + +--- + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|------|------|------| +| settle 请求体 | 无 | 可选 `{ checkNote: String ≤512 }` | +| settle / reopen 路径、响应 | — | 不变 | +| settle 错误码 | 589507 / 589500 / 589555 / 589501 | 另加 589567 / 589568 / 589573 | +| reopen 错误码 | 589507 / 589500 / 589501 | 另加 589573 | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|------|------|------| +| 验团前置 | 团期在核单中即可 | 团期在核单中 **且** 核团已核算 | +| 核单中但没有核团记录的团点验团 | 成功归档 | **589567 拒绝** | +| 验团对核团的影响 | 无(当时还没有核团) | 核团 `ALLOCATED → CHECKED`,同一事务 | +| 验团意见 | 无处填写 | `checkNote`,在核团面板回显 | +| 反确认对核团的影响 | 无 | 核团 `CHECKED → ALLOCATED`,清空验团意见 | + +## 六.7、影响评估 + +- **是否破坏向后兼容**: 结构兼容(老前端不传 body 照常可调);**行为上收紧**——原来能直接验团的「核单中」团期,现在必须先完成核团定稿。 +- **存量在途团影响**: TEST / 线上所有处于核单中、还没做核团的团期,验团都会被 589567 拒绝,需要运营先到核团 Tab 完成「打开面板 → 保存 → 提交核算」。上线前建议通知运营与财务。 +- **前端是否必须同步上线**: 否,不改也不会出错(后端会拦并给出引导语);但建议同步上线第四节的按钮条件与验团意见输入框,避免运营「点了才知道不行」。 +- **前端 workaround 清理点**: 无。 + +--- + +## 七、不影响范围 + +- **仅影响**: 验团归档、验团反确认两个接口的前置与联动。 +- **零影响**: + - 发起核单 `POST .../review/start`(出行完毕 → 核单中) + - 团期共享成本录入 / 列表 / 汇总 + - 订单侧第 1 层核单与财务复核 + - 验团 / 反确认的判权口径(仍是超级管理员 / 管理员 / 财务) + +--- + +## 八、测试环境已验证 + +被测版本:hl-order-service-v3 = dev-v3(#7944 合并提交 `db3abc6c7`,其后 #7955 合并 `aed07cc3c` 未改这两个接口),经网关实测。 + +``` +E(核单中,无核团记录) settle → 589567「尚无核团记录,请先在核团 Tab 完成核算定稿后再验团」;团期仍 REVIEWING、核团 0 行 ✓ +B(核团录入中,无 body) settle → 589568「当前「录入中」,需要「已核算」…」;团期仍 REVIEWING ✓ +A(核团录入中) settle → 589568;零写入 ✓ +A(核团已核算) settle + checkNote → 200;核团 CHECKED、版本 7→8、checkNote 已写;团期 SETTLED; + 时间线 BATCH_SETTLE 附核团 ALLOCATED→CHECKED ✓ +A(已验团) 保存 / 提交核算 / 重新核算 → 均 589568;录共享成本 → 589501 ✓ +A(已验团) reopen → 200;团期 REVIEWING;核团 ALLOCATED、版本 8→9、验团人时与意见清空; + 逐户定稿 3 户保留;时间线附旧验团人时与意见 ✓ +``` + +--- + +## 十、相关文档 + +- 关联 Issue: [wx/HL#7932](https://git.1814.love:8443/wx/HL/issues/7932) +- 关联 PR: [wx/HL#7944](https://git.1814.love:8443/wx/HL/pulls/7944) +- 同日新增接口:`changelogs-v2/2026-09/18_7932_团期核团核算开票导出与节点下钻-新增接口-管理后台.md`(核团 7 个接口、权限码、错误码全表) + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7932](https://git.1814.love:8443/wx/HL/issues/7932) +- **PR**: [#7944](https://git.1814.love:8443/wx/HL/pulls/7944) +- **Merge commit**: [db3abc6c7](https://git.1814.love:8443/wx/HL/commit/db3abc6c7) + +### 联系人 + +- **后端负责人**: @jw +- **前端负责人**: @mmg