56 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 7932 | 团期核团:核团面板、保存、提交核算、重新核算、开票、导出核单与核单节点下钻 | admin | jw(GIT) | 新增接口 | deployed | verified | verified | mmg | 5833f46f20ee1f3f48f5eef053f5c3ec043a9131 | 2026-09-20 | 新增团期详情「核团验团」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。前端已交付(hl-ui v2.1 @ 5833f46f):详情页新增「核团验团」Tab(050 激活才拉不空建草稿、NOT_STARTED 空态;DRAFT 科目行/逐户用量全集保存+改价偏离带出值必填原因+提交核算 readyToAllocate 置灰;ALLOCATED 重新核算 589571 透 message+开票+验团 REVIEWING 双条件;CHECKED 开票+验团反确认;expectedVersion 乐观锁 589573 自动刷新;055 导出 blob 走拦截器 JSON 错误解析;056 行程节点带 nodeIds 才出入口、金额 Number、coverage 四态非 OK 不显 ¥0、合计旁摆已核单户数);权限 group-batch:audit:* 可见即可点+589507 兜底;4 新 spec 24/24,scoped checkpoint 全绿。 | 2026-09-18 | 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)
⚠️ 关键变化
- 新增 7 个接口,支撑「核团验团」Tab:看面板 → 改用量 / 改价保存 → 提交核算(定稿)→ 需要时重新核算 → 开票 → 导出核单;另有一个核单按行程节点逐户下钻。
- 路径、错误码、并发字段都和原型 / 旧文档
12-核算与验团.html不一样,以本文为准(见第四节对照表):路径是/v3/admin/order/group-batch/{groupBatchId}/...,错误码是 589xxx,并发用expectedVersion。 - 权限用新的四个码
group-batch:audit:*,不要再用group-batch:finance:*控制核团按钮显隐(见第四节权限表)。 - 验团按钮不在本文:验团仍调既有
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 |
请求示例
GET /v3/admin/order/group-batch/2100857935637663745/audit
Authorization: Bearer <token>
无请求体。
响应示例
(TEST 真实响应,2026-09-18,已返团团期首次打开;items 11 行 / details 33 行只保留前几行)
{
"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 为示意值)。
{
"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 提示。
错误响应
{ "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 实测请求的结构,只列一行科目与一行用量;实际须提交全集)
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,把一户的用餐改为不参加)
{
"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 已核对零写入)。
{ "code": 589573, "message": "核团数据已被他人修改(当前版本 3,提交版本 2),请刷新后重试", "data": null, "success": false }
错误响应
(TEST 真实响应:改了总额、偏离带出值但没填原因)
{ "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) |
请求示例
POST /v3/admin/order/group-batch/2100857935637663745/audit/allocate
Authorization: Bearer <token>
Content-Type: application/json
{ "expectedVersion": 3 }
响应示例
(TEST 真实响应,2026-09-18;三户中只列第一户)
{
"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
}
空数据 / 降级响应
写接口,无空数据形态。被拒时零写入,状态仍为录入中。
{ "code": 100502, "message": "提交核算处理中,请勿重复提交", "data": null, "success": false }
错误响应
(TEST 真实响应:有一户第 1 层核单未定稿)
{ "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[] | 恒为空数组 |
请求示例
POST /v3/admin/order/group-batch/2100857935637663745/audit/reallocate
Authorization: Bearer <token>
Content-Type: application/json
{ "expectedVersion": 4 }
响应示例
(TEST 真实响应,2026-09-18)
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2100857935637663745",
"auditStatus": "DRAFT",
"auditStatusText": "录入中",
"batchStatus": "REVIEWING",
"readyToAllocate": true,
"blockingOrderIds": [],
"version": 5,
"warnings": []
},
"success": true
}
空数据 / 降级响应
写接口,无空数据形态。被拒时零写入,定稿保持不变。
{ "code": 100502, "message": "重新核算处理中,请勿重复提交", "data": null, "success": false }
错误响应
(TEST 真实响应:本团有一户已开票)
{ "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 时必填 |
| 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[] | 恒为空数组 |
请求示例
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)
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2100857935637663745",
"auditStatus": "ALLOCATED",
"auditStatusText": "已核算",
"batchStatus": "REVIEWING",
"readyToAllocate": true,
"blockingOrderIds": [],
"version": 9,
"warnings": []
},
"success": true
}
空数据 / 降级响应
写接口,无空数据形态。被拒时不产生发票申请。
{ "code": 589568, "message": "核团当前状态不允许该操作:当前「录入中」,需要「已核算」或「已验团」;请先提交核算后再开票", "data": null, "success": false }
错误响应
(TEST 真实响应:选了别的团的订单)
{ "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 |
请求示例
GET /v3/admin/order/group-batch/2100857935637663745/audit/export
Authorization: Bearer <token>
无请求体。
响应示例
(TEST 真实响应,2026-09-18,已核算团期;响应头如下,正文是 CSV 而不是 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 正文:
说明:收入为应收口径(已扣实退);金额单位:元
子订单号,联系人,人数,用房数,住宿,车辆,景区娱乐,用餐,导游,摄影,其他支出,成本合计,收入,毛利
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,第一行为「未定稿:以下为录入中的试算值,提交核算后以定稿为准」,其余同上。
{
"httpStatus": 200,
"Content-Type": "text/csv;charset=utf-8",
"decodedFileName": "核单-Q202610242100859128720101378-未定稿.csv"
}
错误响应
失败时不下载文件,而是返回普通 JSON 错误(HTTP 200 + Result 信封)。前端用 blob 接收时,要先判断 Content-Type 是否为 application/json,是则解析出 message 提示。
(TEST 真实响应:团期还没有核团记录)
{ "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)。
请求示例
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 属于别的团)
{
"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,不报错。
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2100857935637663745",
"rows": [],
"unmatchedNodeIds": ["2100859129223335939"]
},
"success": true
}
错误响应
(TEST 真实响应:传了 201 个节点)
{ "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
- 关联 PR: #7940、#7944、#7943、#7955
- 同日修改接口:
changelogs-v2/2026-09/18_7932_团期验团归档前置核团-修改接口-管理后台.md(验团/settle行为变更)