Refs wx/HL#7932 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1277 行
56 KiB
Markdown
1277 行
56 KiB
Markdown
---
|
||
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<GroupBatchAuditRespVO>`
|
||
|
||
#### 使用场景
|
||
|
||
打开团期详情「核团验团」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 <token>
|
||
```
|
||
|
||
无请求体。
|
||
|
||
#### 响应示例
|
||
|
||
(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<GroupBatchAuditWriteRespVO>`
|
||
|
||
#### 使用场景
|
||
|
||
「核团验团」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 <token>
|
||
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<GroupBatchAuditAllocateRespVO>`
|
||
|
||
#### 使用场景
|
||
|
||
录入无误后点「确认核算 / 提交核算」:按库里当前的录入逐户分摊并定稿,状态 `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 <token>
|
||
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<GroupBatchAuditWriteRespVO>`
|
||
|
||
#### 使用场景
|
||
|
||
已核算后发现要改:点「重新核算」,状态 `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 <token>
|
||
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<GroupBatchAuditWriteRespVO>`
|
||
|
||
#### 使用场景
|
||
|
||
核团「开票」弹窗:已核算或已验团时,为本团某一户申请发票。复用系统既有订单发票,发票类型由服务端固定为**增值税普通发票**,开票内容固定「旅游服务费」,金额在后续开票(出票)环节按本户应收计算。
|
||
|
||
**权限**:`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 <token>
|
||
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="<URL 编码>"; filename*=UTF-8''<URL 编码>`;正文 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 <token>
|
||
```
|
||
|
||
无请求体。
|
||
|
||
#### 响应示例
|
||
|
||
(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<GroupBatchSettlementNodeLinesVO>`
|
||
|
||
#### 使用场景
|
||
|
||
团期行程汇总(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 <token>
|
||
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
|