文件
hl-api-changelog/changelogs-v2/2026-09/18_7932_团期核团核算开票导出与节点下钻-新增接口-管理后台.md
T

56 KiB
原始文件 Blame 文件历史

schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
schema ticket title consumer author change_type backend_status gateway_status frontend_status frontend_owner frontend_ref target_release verified_at status_note updated_at base
hl-changelog/v2 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)


⚠️ 关键变化

  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

请求示例

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 时必填
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[] 恒为空数组

请求示例

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 行为变更)

关联 / 联系人

链接

联系人

  • 后端负责人: @jw
  • 前端负责人: @mmg