15 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 | 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 行为零回归):
- 团级行程单
GET .../print-itinerary:调既有团级行程汇总(A5),按consistency == ALL_SAME拆分——一致项进days[].nodes[]正常排版,差异项(VALUE_DIFF/PARTIAL)降级为days[].differingNodes[]一行标记note="按户另见"并保留nodeKey可下钻。dayTotalAmount直接取汇总值不重算。 - 团期签单凭证
GET .../sign-voucher:按供应商聚合酒店 / 景点 / 游玩条目,同一hotelId整团只产一条entry(householdCount == coveredOrderNos.size())。默认showAmount=false脱敏(totalAmount/unitPrice/amount全null),showAmount=true才出金额;scope可限ALL/HOTEL/SCENIC/ACTIVITY。 - 判权与全部业务落在
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调 A6GET .../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-2dayTotalAmount与 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-8scope=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)