文件
hl-api-changelog/changelogs-v2/2026-09/14_7533_团级行程单与团期签单凭证-新增接口-管理后台.md
T
2026-09-14 15:54:21 +08:00

15 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 7533 团级行程单 + 团期签单凭证(只出 ALL_SAME 项,差异项标「按户另见」;签单按供应商聚合、默认脱敏) admin jw(GIT) 新增接口 deployed verified verified mmg f1319851 2026-09-14 两个纯读端点,管理台新增「团级行程单」「团期签单凭证」两处打印/导出入口即可。行程单只排版全团一致(ALL_SAME)项,差异项渲染成一行「按户另见(m/n 户)」并可用 nodeKey 点回 A6 下钻;签单凭证默认脱敏(showAmount=false,金额全 null),带 showAmount=true 才出金额。金额与 orderId 均为字符串形态防精度丢失。零 DDL、零网关路由改动(/v3/admin/** 既有覆盖)。;前端已交付(f1319851):团期详情「更多操作」加团级行程单/团期签单凭证两打印入口,复用 PrintPreviewModal,差异项可点回 A6 下钻,签单默认脱敏勾选才出金额,checkpoint 13 项全绿 2026-09-14 dev-v3

order-v3: 团级行程单 + 团期签单凭证(管理台读端)

服务: hl-order-service-v3 PR: #7684 Issue: #7533


⚠️ 关键变化

🟢 新增两个只读端点(不改任何既有接口,订单级 print-itinerary / sign-voucher 行为零回归):

  1. 团级行程单 GET .../print-itinerary:调既有团级行程汇总(A5),按 consistency == ALL_SAME 拆分——一致项进 days[].nodes[] 正常排版,差异项(VALUE_DIFF / PARTIAL)降级为 days[].differingNodes[] 一行标记 note="按户另见" 并保留 nodeKey 可下钻。dayTotalAmount 直接取汇总值不重算。
  2. 团期签单凭证 GET .../sign-voucher:按供应商聚合酒店 / 景点 / 游玩条目,同一 hotelId 整团只产一条 entry(householdCount == coveredOrderNos.size())。默认 showAmount=false 脱敏(totalAmount / unitPrice / amount 全 null),showAmount=true 才出金额;scope 可限 ALL/HOTEL/SCENIC/ACTIVITY。
  3. 判权与全部业务落在 GroupBatchDocumentService(顶部第一条判 group-batch:view,顶层无 @Transactional);契约新增批量方法 HouseAssignmentReadContract.listHotelAssignmentsByOrderIds(Map<Long,List> 承载订单归属),住宿读取覆盖「新分房 active allocation + 旧户冻结名单」双源,避免双计与 N+1 放大。

无 DDL、无网关路由改动、订单级两端点结构一字未改。

一、背景

团期管理台需要「一次出全团一张行程单 / 一份供应商签单凭证」。既有 A5 团级行程汇总(GroupBatchItineraryService.summary)已把逐户事实压成 consistency ∈ ALL_SAME / VALUE_DIFF / PARTIAL 的展示摘要;wx 2026-09-11 定案「团级行程单只出 ALL_SAME 项、差异项标『按户另见』」。本单只做文档层:调既有汇总按定案筛选排版(一致性判定一行不改,仍在 GroupBatchItineraryService),签单侧按供应商归并逐户住宿 / 资源事实。住宿读取按 wx 定案「本单自行实现双源、不设 #7327 前置依赖」,为强制发现两边口径漂移设 AC-R4 对账。

二、变更接口清单

# 接口 方法 路径 变更类型 说明
1 出具团级行程单 GET /v3/admin/order/group-batch/:groupBatchId/print-itinerary 新增 只排版 ALL_SAME 项,差异项标「按户另见」并留 nodeKey 可下钻;dayTotalAmount 取汇总不重算
2 出具团期签单凭证 GET /v3/admin/order/group-batch/:groupBatchId/sign-voucher 新增 按供应商聚合酒店/景点/游玩;默认脱敏 showAmount=false;scope 可限 ALL/HOTEL/SCENIC/ACTIVITY

三、接口详情

1. 出具团级行程单 GET /v3/admin/order/group-batch/:groupBatchId/print-itinerary

VO: Result<GroupPrintItineraryRespVO>(path: groupBatchId)

使用场景

团期管理台「打印团级行程单」入口。只排版全团一致的行程项,差异项不出明细、改出一行「按户另见(m/n 户)」标记,前端可用 nodeKey 点回 A6 逐户下钻(GET .../itinerary/nodes/:nodeKey?dayNumber=N)。沿用既有鉴权与网关路由(/v3/admin/** 已覆盖)。

入参字段表

字段 位置 类型 必填 约束 说明
groupBatchId path string(Long) 是 雪花 ID 团期主订单 ID

出参字段表

字段 类型 说明
groupBatchId string(Long) 团期 ID
agencyName string 出团旅行社名称
batchNo / batchName / productName string 期号 / 期名 / 产品名
departDate / endDate string(date) 出发 / 结束日期
totalHouseholds int 活跃(非 CANCELLED)子订单户数
totalPax / totalRooms int 全团人数 / 房数
dayCount int 行程天数
roster array 逐户名册(orderId/orderNo/contactName/人数/roomCount/roomType/specialNeeds;不含手机号 / 证件号)
days array 逐日;每日含 dayNumber/dayDate/dayTitle/dayTotalAmount/nodes[](ALL_SAME 项)/differingNodes[](差异项标记)
days[].nodes[] array 全团一致项:nodeKey/nodeName/nodeType/resourceType/resourceName/unitPrice/quantity 等
days[].differingNodes[] array 差异项:nodeKey/nodeName/consistency(VALUE_DIFF/PARTIAL)/householdCount/totalHouseholds/note="按户另见"
hotels / amountNote / footerNote array/string 酒店汇总 / 金额口径说明 / 页脚

请求示例

GET /v3/admin/order/group-batch/2099318712929566721/print-itinerary
Authorization: Bearer <admin token>

响应示例

{
  "code": 200,
  "msg": "成功",
  "data": {
    "groupBatchId": "2099318712929566721",
    "agencyName": "博华(厦门)国际旅行社有限公司",
    "batchNo": "Q202611202099318646026260481",
    "departDate": "2026-11-20",
    "endDate": "2026-11-22",
    "totalHouseholds": 2,
    "days": [
      {
        "dayNumber": 1,
        "dayDate": "2026-11-20",
        "dayTotalAmount": "120.00",
        "nodes": [ { "nodeKey": "e2c860e2...", "nodeName": "早餐", "resourceType": "RESTAURANT" } ],
        "differingNodes": [ { "nodeKey": "5fd13fb8...", "nodeName": "礼仪接机", "consistency": "PARTIAL", "householdCount": 1, "totalHouseholds": 2, "note": "按户另见" } ]
      }
    ]
  }
}

空数据 / 降级响应

单户团(totalHouseholds == 1)所有项恒判 ALL_SAME、全进 nodes[]、differingNodes[] 为空、仍 200。差异项只在 differingNodes[] 出标记不丢失,逐户明细走 A6 下钻端点。

错误响应

{ "code": 589590, "msg": "团期下没有活跃子订单,无法出具团级文档", "data": null }

团期无活跃子订单 → 589590(不返回空白文档);团期不存在 → 589500;无 group-batch:view → 589507。

业务边界

  • 一致性判定沿用 GroupBatchItineraryService(本单一行不改):occurrences.size() < total → PARTIAL;否则比 unitPrice/quantity 全等 → ALL_SAME,否则 VALUE_DIFF(不比执行时间)。
  • dayTotalAmount 直接取汇总的「只累加 ALL_SAME 项」结果,本单不重算(与 A5 .../itinerary 同一天逐值相等)。
  • 同一 nodeKey 可跨天出现,故 nodes[] 与 differingNodes[] 的不相交断言按逐天成立。

2. 出具团期签单凭证 GET /v3/admin/order/group-batch/:groupBatchId/sign-voucher

VO: Result<GroupSignVoucherRespVO>(path: groupBatchId;query: showAmount / scope)

使用场景

团期管理台「打印供应商签单凭证」。按供应商聚合酒店 / 景点 / 游玩条目,供业务对供应商核对与签单。默认脱敏(不出金额),需出金额时显式传 showAmount=true。

入参字段表

字段 位置 类型 必填 约束 说明
groupBatchId path string(Long) 是 雪花 ID 团期主订单 ID
showAmount query boolean 否 默认 false false 时金额全脱敏为 null
scope query string 否 默认 ALL;枚举 ALL/HOTEL/SCENIC/ACTIVITY 非法值抛 589591

出参字段表

字段 类型 说明
amountVisible boolean 是否出金额(= showAmount)
groupBatchId / batchNo / batchName / productName string 团期标识
entries array 供应商条目
entries[].type string HOTEL / SCENIC / ACTIVITY
entries[].title / supplierName string 条目标题 / 供应商名
entries[].householdCount int 覆盖户数(同 hotelId 整团一条)
entries[].coveredOrderNos array 覆盖订单号(size() == householdCount)
entries[].settleType string 结算方式(签单 / 公司付款 等)
entries[].items array name/date/qty/unit/unitPrice/amount/remark(showAmount=false 时 unitPrice/amount 为 null)
entries[].totalAmount string(decimal) 条目合计(showAmount=false 时为 null;否则 == Σ items.amount)

请求示例

GET /v3/admin/order/group-batch/2099318712929566721/sign-voucher?showAmount=true&scope=HOTEL
Authorization: Bearer <admin token>

响应示例

{
  "code": 200,
  "msg": "成功",
  "data": {
    "amountVisible": true,
    "groupBatchId": "2099318712929566721",
    "entries": [
      {
        "type": "HOTEL",
        "supplierName": "远景酒店",
        "householdCount": 1,
        "coveredOrderNos": ["HL20260914100559129"],
        "settleType": "签单",
        "items": [ { "name": "草原蒙古包", "date": "2026-11-20", "qty": 2, "unit": "间夜", "unitPrice": "380.00", "amount": "760.00" } ],
        "totalAmount": "1520.00"
      }
    ]
  }
}

空数据 / 降级响应

房务未配房的团期返 200 且 entries 中无 HOTEL 条目(不报错);showAmount=false(默认)时 amountVisible=false,entries[].totalAmount 与 items[].unitPrice/amount 全 null,其余字段照常。

错误响应

{ "code": 589591, "msg": "团级签单凭证范围非法(须为 ALL / HOTEL / SCENIC / ACTIVITY)", "data": null }

scope 非法 → 589591(在取数之前 fail-fast);团期无活跃户 → 589590;户数超单次出具上限(200 户)→ 589592;团期不存在 → 589500;无权限 → 589507。

业务边界

  • 酒店按 hotelId 聚合,整团同一酒店只产一条 entry,householdCount == coveredOrderNos.size() == 该酒店覆盖的活跃户数。
  • 住宿逐户事实走批量契约 listHotelAssignmentsByOrderIds(Map 承载订单归属),新户按 active allocation 的 roomCount、旧户沿用冻结名单,取数不随户数放大(Service 层对契约只调一次)。CANCELLED 户不计入(避免双计)。
  • 户数上限 200 为保守闸(56 户实测 print≈255ms / sign≈245ms,远 < 2s);超限抛 589592 分批出具。

四、契约约束与正确调用方式

  • 金额字段(totalAmount/items.unitPrice/items.amount/dayTotalAmount)均为 BigDecimal 序列化为字符串,勿按 number 解析;orderId/groupBatchId 亦为字符串形态防大整数精度丢失。
  • 默认脱敏:不传 showAmount 即 false,金额全 null;出金额必须显式 showAmount=true。
  • 差异项要下钻逐户明细,用 differingNodes[].nodeKey 调 A6 GET .../itinerary/nodes/:nodeKey?dayNumber=N。

五、数据库行为

  • 无表变更、无 Flyway、无新增列、无索引调整、无 H2 *-schema.sql 改动。
  • 纯读:行程走 GroupBatchItineraryService.summary(既有);住宿走 HouseAssignmentReadContract 逐户读(新分房 allocation + 旧户冻结)。

六、边界行为

  • 判权 group-batch:view 是 Service 顶层第一条语句,命中即抛 589507、取数之前返回(判权先于取数)。
  • 错误码段位 589500-589599(团期专属)。本单三码因 #7513 已抢占 589589 而顺延:589590 无活跃户 / 589591 scope 非法 / 589592 超上限。
  • 批量住宿读循环复用逐单双源合流(逐单方法含 CONFIRMED 过滤 / 旧户跳过 / 团期分房行合流),Service 层对契约方法只调一次(不随户数放大)。

七、不影响范围

  • 订单级 GET /v3/admin/order/:orderId/print-itinerary、GET /v3/admin/order/:orderId/sign-voucher 结构与行为一字未改(git diff origin/dev-v3...HEAD 不含其 Service/VO)。
  • 一致性判定(A5/A6 语义)本单一行不改;无网关路由改动、无 Feign/MQ 改动、无 DDL。

八、测试环境已验证

2026-09-14 TEST 网关(自签 admin token,https://api.test.1814.love:9443)+ 真 MySQL 造数,工单 #7533 的 18 条 AC 中 17 条逐条通过(AC-R3 联合项本单侧已验、#7536 分页侧待其交付后重跑):

  • AC-1 造 VALUE_DIFF 团逐天 nodes/differingNodes 的 nodeKey 无交集且两类项并存;AC-2 dayTotalAmount 与 A5 逐值相等(Decimal);AC-3 VALUE_DIFF/PARTIAL 均入 differingNodes 且下钻端点返逐户明细。
  • AC-4 roster == totalHouseholds == 活跃子订单(DB) 且无手机 / 证件字段;AC-5 脱敏两态(false 金额全 null / true 出 1520.00·380.00·760.00)。
  • AC-6 多户同酒店团(3 活跃户)恰一条 HOTEL 条目、householdCount=3==coveredOrderNos.size()==活跃户;AC-8 scope=XXX→589591、零活跃团 →589590、耗时 56 户 print=255ms/sign=245ms,超上限 589592 单测覆盖。
  • AC-10 单户团全进 nodes/无房团无 HOTEL 条目不报错;AC-R1 纯新团出酒店条目 + 混合团逐户房数/日期守恒;AC-R2 三团金额守恒 + 四场景(同资源跨天/部分户/多户/签单 vs 公司付款);AC-R4 双源对账:A 户新源 4 间夜与 group_batch_room_allocation 逐日精确相等、CANCELLED 户被正确排除、B 户 legacy 冻结。
  • AC-7/9/11/12 单测取证;AC-13 全量 mvn -o -pl hl-order-service-v3 -am test — Tests run 10791 / 0 failure / 0 error / 7 skipped,含 RedLineArchTest/MapperBoundaryArchTest,BUILD SUCCESS;AC-14 deploy-status 归属 feat/7533-... | 1594d95ed | 0/N | ok。

十、相关文档

  • 工单 #7533
  • 全过程记录:HL 仓 dev-records/records/2026-09-14-local-7533-group-print-sign-voucher.md

关联 / 联系人

  • 后端:jw(已合入 dev-v3 + TEST 实测)
  • 前端:mmg(新增两个打印/导出入口;金额字符串形态、默认脱敏需传 showAmount=true 出金额;差异项 nodeKey 可下钻 A6)