20 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 | 8747 | 团期行程汇总与下钻按出行中终止日截断:终止户之后的天不再计入覆盖户数,天头列出终止户 | admin | jw(GIT) | 修改接口 | deployed | verified | implemented | mmg | 17c98e509a296fc356835808f8741606d2aad8be | v2.1 | 2026-10-03 | 已合并 dev-v3(9d1e87b38)并部署 TEST,自签 token 经网关实测:一团 3 户同一份 3 天行程、1 户 D1 出行中终止(真实终止接口),D1 仍 3/3 全团一致,D2、D3 各项 2/3 部分户、天头列出终止户、本日合计由 385.00 / 730.01 变为 0;下钻终止户 terminated=true、endDayNumber=1;对照团与两个存量团改前改后去掉新字段逐字一致(38 项);团级行程单、签单凭证改前改后逐字一致。前端待做:天头渲染「N 户已于 Dx 终止」并可展开户清单,不在每个节点行上重复标;下钻行按 terminated / endDayNumber 标「已于 Dx 终止」。前端已交付(2026-10-03):batch-itinerary.js 加 4 展示纯函数(天头提示停同日「N 户已于 Dx 终止」/不同天退化、清单行、下钻行按 endDayNumber!=null 且本行 dayNumber>endDayNumber 判标记,terminated 仅户级开关);ItineraryTab 天头渲染+点击展开户清单(节点行不重复标);ItineraryNodeModal 截断行标「已于 Dx 终止」;API spec 新增 4 例+组件 spec 新建 3 例全绿,提交 17c98e50。 | 2026-10-03 | dev-v3 |
order-v3: 团期行程汇总按出行中终止日截断
服务: hl-order-service-v3
PR: #8766(已合入 dev-v3,合并提交 9d1e87b38)
Issue: #8747
⚠️ 关键变化
🟢 两个接口纯加字段:汇总 days[] 新增 terminatedOrderCount、terminatedOrders[];下钻 items[] 新增 terminated、endDayNumber。其余字段、入参、判权、错误码不变。
🔴 有户出行中终止的团,终止日之后的读数会变:该户仍计入分母 totalHouseholds,但不再计入那几天任何项的户数 householdCount。原本全团一致的项会变成部分户(如 2/3),不再计入本日合计,dayTotalAmount 相应变小(可能为 0)。这是需求口径,不是回归。
🟢 团内没有终止户时,响应除新字段(0 / 空列表 / false / null)外逐字不变。
一、背景
出行中终止(POST /v3/admin/order/:id/terminate)只把订单置为 COMPLETED,并在 order_terminate_refund.end_day_number 记下停在第几天;行程表一行不动。团期行程逐日汇总(GB-ADM-018)与逐户下钻(GB-ADM-019)此前只排除已取消户,所以终止日之后那几天,这一户仍按完整行程计入覆盖户数,节点会被判成「全团一致」并计入本日合计——页面显示整团都去了,实际少一户。
实施单 04 §3.11.3 在 2026-09-01 已定「保留、可见、按天截断」,本单按此落地。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 团期行程逐日汇总(GB-ADM-018) | GET | /v3/admin/order/group-batch/:groupBatchId/itinerary |
修改 | 终止户按天截断;days[] 新增 terminatedOrderCount / terminatedOrders[] |
| 2 | 团期行程某项逐户下钻(GB-ADM-019) | GET | /v3/admin/order/group-batch/:groupBatchId/itinerary/nodes/:nodeKey |
修改 | 户数与一致性按截断口径;items[] 新增 terminated / endDayNumber |
三、接口详情
终止户的计数口径(两个接口相同):
| 项 | 口径 |
|---|---|
| 怎么算「已终止」 | order_terminate_refund 有该户未软删的行;截断天取 end_day_number。按全团活跃户一次批量查,与户数无关 |
分母 totalHouseholds |
计入终止户(已取消户照旧排除) |
dayNumber ≤ endDayNumber 的天 |
该户照常计入 |
dayNumber > endDayNumber 的天 |
该户不计入任何项的户数 householdCount,不参与该天的一致性判定与本日合计 |
| 行程记录 | 不隐藏:下钻照常列出该户那一行(has=true、带价量);汇总项的 nodeIds 照常含该户的节点 ID,核单下钻要用 |
| 节点级「已用」 | 不做(实施单 04 §3.11.3:那是核单的职责) |
本日合计为什么会变小:本日合计只算 ALL_SAME(全团一致)的项。终止户退出后,终止日之后的项最多只有 M−1 户有,按「只有部分户有」判为 PARTIAL,不再计入。所以有户中途终止的团,终止日之后各天的 dayTotalAmount 会变小,所有项都变成部分户时为 0。
1. 团期行程逐日汇总(GB-ADM-018) GET /v3/admin/order/group-batch/:groupBatchId/itinerary
VO: Result<GroupBatchItineraryRespVO>(无请求体)
使用场景
团期详情「行程」页签。天头按 terminatedOrderCount 渲染「N 户已于 Dx 终止」,点开看 terminatedOrders;节点卡片的「N/M 户」与本日合计按新口径显示。
入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | Path | Long | ✅ | 团期 ID | 不变 |
出参字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| days[].terminatedOrderCount | Integer | 🆕 本天已处于出行中终止的户数(截断天 < 本天)。这些户仍计入分母,但不计入本天任何项的户数;无终止户为 0,前端为 0 时不渲染天头提示 |
| days[].terminatedOrders[] | Array | 🆕 本天已终止户清单,按 orderId 升序;无终止户为空数组 |
| days[].terminatedOrders[].orderId | String | 🆕 子订单 ID(雪花,按字符串传) |
| days[].terminatedOrders[].teamNo | String | 🆕 团号;无团号为 null |
| days[].terminatedOrders[].orderNo | String | 🆕 子订单编号 |
| days[].terminatedOrders[].customerName | String | 🆕 客户姓名 |
| days[].terminatedOrders[].endDayNumber | Integer | 🆕 截断天:该户行程停在第几天 |
| days[].nodes[].householdCount | Integer | 口径变:终止户在截断天之后不计入;只有终止户排了的项可为 0 |
| days[].nodes[].consistency / countedInDayTotal | String / Boolean | 口径变:按截断后的户数判;终止日之后原全团一致的项变为 PARTIAL、不计入合计 |
| days[].dayTotalAmount / excludedNodeCount | BigDecimal / Integer | 口径变:随上面的一致性结果重算,可能变小 |
| 其余字段 | — | 不变(totalHouseholds 含终止户;nodeIds 含终止户的节点 ID) |
请求示例
GET /v3/admin/order/group-batch/2106297832652881921/itinerary HTTP/1.1
Authorization: Bearer <管理员 token>
响应示例
TEST 实测(截取第 2 天与其首项;乙户「娜仁其其格」第 1 天行程结束后出行中终止):
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2106297832652881921",
"totalHouseholds": 3,
"dayCountConsistent": true,
"dayCount": 3,
"dayCountOutliers": [],
"days": [
{
"dayNumber": 2,
"dayDate": "2026-10-21",
"dayTitle": "是的复古风的水果",
"dayTotalAmount": 0,
"excludedNodeCount": 7,
"terminatedOrderCount": 1,
"terminatedOrders": [
{
"orderId": "2106297833152004097",
"teamNo": "26-5148",
"orderNo": "HL20261003161831138",
"customerName": "娜仁其其格",
"endDayNumber": 1
}
],
"nodes": [
{
"nodeKey": "c44f160ad5e71a8b",
"nodeIds": ["2106297832707407873", "2106297833231695873", "2106297834452238339"],
"nodeName": "巴尔虎蒙古部落",
"nodeType": "SCENIC",
"resourceType": "SCENIC_SPOT",
"resourceId": "3001000000000000016",
"resourceName": "巴尔虎蒙古部落",
"startTime": null,
"timePeriod": "EARLY_MORNING",
"consistency": "PARTIAL",
"householdCount": 2,
"totalHouseholds": 3,
"unitPrice": null,
"unitPriceMin": 80.0,
"unitPriceMax": 80.0,
"quantity": null,
"quantityMin": 1,
"quantityMax": 1,
"totalAmount": null,
"countedInDayTotal": false
}
]
}
]
}
}
同一团改前(旧构建)第 2 天:各项 householdCount=3、ALL_SAME,dayTotalAmount=385.0,没有终止字段。
空数据 / 降级响应
- 团期无活跃子订单:
days=[],不查行程、不查截断天。 - 团内没有终止户:每天
terminatedOrderCount=0、terminatedOrders=[],其余字段与改前逐字一致。 - 终止在最后一天(截断天 ≥ 行程天数):没有被截断的天,各天
terminatedOrderCount=0。
错误响应
| code | 场景 |
|---|---|
| 589507 | 无 group-batch:view,或定制师查看非本人名下的团期(不变) |
| 589500 | 团期不存在(不变) |
{
"code": 589500,
"message": "团期不存在",
"data": null
}
业务边界
- 截断只影响计数,不改行程数据,不隐藏任何项。
- 已取消户照旧排除,不进分母,也不进截断天查询。
dayDate/dayTitle仍按全部活跃户的多数值取,不受终止影响。- 天数一致性预警(
dayCountConsistent/dayCountOutliers)不受终止影响:终止不改行程天数。
2. 团期行程某项逐户下钻(GB-ADM-019) GET /v3/admin/order/group-batch/:groupBatchId/itinerary/nodes/:nodeKey
VO: Result<GroupBatchItineraryNodeDetailVO>(无请求体)
使用场景
汇总卡片点进来看「哪几户有、哪几户不一样」。终止户那一行照常列出,前端按 terminated / endDayNumber 标「已于 Dx 终止」。
入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | Path | Long | ✅ | 团期 ID | 不变 |
| nodeKey | Path | String | ✅ | 取自汇总接口,原样回传 | 不变 |
| dayNumber | Query | Integer | ❌ | 取自汇总卡片所在天;不传 = 跨天全看 | 不变 |
出参字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| items[].terminated | Boolean | 🆕 该户是否已出行中终止(户级标记,与本行是哪天无关) |
| items[].endDayNumber | Integer | 🆕 截断天;未终止为 null。本行 dayNumber 大于它时,前端标「已于 Dx 终止」 |
| householdCount / consistency | Integer / String | 口径变:只认未被截断的出现,与汇总卡片同口径;跨天下钻时,终止户在截断天及之前有出现就计户 |
| items[].has | Boolean | 不变:该户行程里有没有这一项;终止户截断后的出现仍为 true(记录不隐藏),但不计入 householdCount |
| 其余字段 | — | 不变 |
请求示例
GET /v3/admin/order/group-batch/2106297832652881921/itinerary/nodes/c44f160ad5e71a8b?dayNumber=2 HTTP/1.1
Authorization: Bearer <管理员 token>
响应示例
TEST 实测(第 2 天「巴尔虎蒙古部落」,截取前两户):
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2106297832652881921",
"nodeKey": "c44f160ad5e71a8b",
"dayNumber": 2,
"nodeName": "巴尔虎蒙古部落",
"consistency": "PARTIAL",
"householdCount": 2,
"totalHouseholds": 3,
"items": [
{
"orderId": "2106297832598355970",
"teamNo": "26-8025",
"orderNo": "HL20261003161830984",
"contactName": "孟庆和",
"customerName": "孟庆和",
"has": true,
"dayNumber": 2,
"unitPrice": 80.0,
"quantity": 1,
"totalAmount": 80.0,
"startTime": null,
"timePeriod": "EARLY_MORNING",
"terminated": false,
"endDayNumber": null
},
{
"orderId": "2106297833152004097",
"teamNo": "26-5148",
"orderNo": "HL20261003161831138",
"contactName": "娜仁其其格",
"customerName": "娜仁其其格",
"has": true,
"dayNumber": 2,
"unitPrice": 80.0,
"quantity": 1,
"totalAmount": 80.0,
"startTime": null,
"timePeriod": "EARLY_MORNING",
"terminated": true,
"endDayNumber": 1
}
]
}
}
空数据 / 降级响应
nodeKey没有任何户命中:householdCount=0,每户一行has=false,不报错(不变);terminated/endDayNumber照常按户填。- 团内没有终止户:每行
terminated=false、endDayNumber=null,其余字段与改前逐字一致。
错误响应
| code | 场景 |
|---|---|
| 589507 | 无 group-batch:view,或定制师查看非本人名下的团期(不变) |
| 589500 | 团期不存在(不变) |
{
"code": 589507,
"message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)",
"data": null
}
业务边界
- 同一户跨天多行时,
terminated/endDayNumber每行相同(户级);是否被截断看本行dayNumber是否大于endDayNumber。 - 跨天下钻(不带
dayNumber)的一致性只看未被截断的出现:终止户在截断后改过的价不会把该项判成各户不一致。
四、契约约束与正确调用方式
- 天头提示只看
days[].terminatedOrderCount,为 0 时不渲染;不要在每个节点行上重复标终止。 - 「N/M 户」直接用
householdCount/totalHouseholds,不要自己从下钻行数has=true去数——终止户截断后的行has=true但不计户。 - 下钻行的终止标记按
endDayNumber != null && dayNumber > endDayNumber判,terminated只是户级开关。 nodeIds仍含终止户的节点 ID,原样传给核单下钻POST /v3/admin/order/group-batch/:groupBatchId/settlement/node-lines即可看到全团核单行(含终止户已退未用的门票)。
五、数据库行为
- 零 DDL、零数据迁移、零写入。
- 新增一条只读批量查询:按全团活跃户
order_id IN (...)读order_terminate_refund.end_day_number(与核单下钻同一读口)。两个接口的查询次数由 2 次批量变为 3 次批量,均与户数无关。
六、边界行为
- 截断天是
end_day_number(停在第几天,1 起),该天本身照常计入,之后的天才截断。 - 终止户的
order_status是COMPLETED,留在活跃集里;已取消户(CANCELLED)照旧整体排除。 - 只有终止户在截断后的某天排了的项:汇总里照常列出,
householdCount=0、PARTIAL、不计入合计,nodeIds含该户节点 ID。
六.6、修改前后对比
| 场景 | 改前 | 改后 |
|---|---|---|
| 3 户同一行程,1 户 D1 终止,看 D2 某项 | 3/3、ALL_SAME、计入合计 |
2/3、PARTIAL、不计入合计 |
| 同上,D2 本日合计 | 385.00 | 0 |
| 同上,D1 | 3/3、ALL_SAME |
不变 |
| 同上,D2 天头 | 无终止信息 | terminatedOrderCount=1,列出该户与 endDayNumber=1 |
| 下钻 D2 终止户那一行 | 与其他户无区别 | terminated=true、endDayNumber=1,不计入 householdCount |
| 团内无终止户 | — | 除新字段外逐字不变 |
六.7、影响评估
- 是否破坏向后兼容:字段层面否(纯新增);读数层面,有终止户的团在终止日之后的户数、一致性与本日合计会变——这是需求要修的错误读数。
- 前端是否必须同步上线:否。不改前端时页面照常显示,只是终止日之后的卡片显示部分户、本日合计变小,没有「已终止」的说明;改了才能在天头说明原因。
- 团级文档:团级行程单(
GET .../print-itinerary)与签单凭证(GET .../sign-voucher)也复用行程汇总,但本单让它们走不截断的入口,输出保持原样(TEST 上含终止户的团改前改后逐字一致)。文档是否也按实际截断另行定口径。 - 回滚:revert PR #8766 后重新部署 order-v3,无数据需要恢复。
七、不影响范围
- 两个接口的入参、判权(
group-batch:view+ 定制师归属)、错误码:不变。 - 团级行程单、签单凭证:不变。
- 核单下钻(GB-ADM-056):不变(本来就按户带截断天)。
- 订单级行程、终止行程接口本身、核单与退款计算:不变。
- 散客单(非团期):不涉及。小程序端:无影响。
八、测试环境已验证
环境:TEST(https://api.test.1814.love) 验证时间:2026-10-03 16:18~16:57
构建身份:TEST 后端检出 dev-v3 @ d778c9a71(包含本单合并提交 9d1e87b38),order-v3 两个实例 16:51 前后重启。零写入判据:部署后连查 6 次对照团汇总,每次 days[] 都带 terminatedOrderCount(旧字节没有这个键)。
身份:自签 token 直打网关;造数用超管,读口取证用管理员账号(非超管)。
8.1 造数(全部经业务接口,唯一 SQL 见下)
| 团 | 团号 | 内容 |
|---|---|---|
| 主团 | T26-6215(2106297832652881921) |
新班期出发 2026-10-20,3 天行程每天 8 项;3 户各 2 成人线下全款,第 4 户下单后取消;成团 → 声明整团免车 → 乙户「娜仁其其格」出行中终止 endDayNumber=1 |
| 对照团 | T26-2604(2106297838424244226) |
新班期,2 户全款,不终止 |
| 存量团 | 2106039583672299521(5 户)、2104839654727618562(3 户) |
只读,近 6 小时无改动、无终止户 |
终止接口要求订单处于出行中,TEST 上从成团走到出行中要过七道出团门和夜间任务,故只对乙户一张订单用 SQL 把 order_status 由 CUSTOMIZING 置为 TRAVELLING,随后调真实终止接口;终止后订单 COMPLETED、order_terminate_refund.end_day_number=1。
8.2 改前 / 改后(同一批数据,旧构建取一次、新构建取一次)
| 检查 | 结果 |
|---|---|
| 主团 D1 | 8 项均 3/3、ALL_SAME,terminatedOrderCount=0 |
| 主团 D2 / D3 | 改前各项 3/3、ALL_SAME,本日合计 385.00 / 730.01;改后各项 2/3、PARTIAL、不计入合计,本日合计 0 / 0,terminatedOrderCount=1,清单为乙户、endDayNumber=1 |
主团 D2 / D3 nodeIds |
仍含 3 户的节点 ID |
| 主团下钻 D2(两项) | 乙户 terminated=true、endDayNumber=1;其余两户 false / null;householdCount=2、PARTIAL |
主团 totalHouseholds |
3(终止户计入、取消户排除);取消户在汇总与下钻中均不出现 |
| 对照团、两个存量团 | 汇总、逐项下钻(按天与跨天)去掉新字段后改前改后逐字一致;新字段全为 0 / 空 / false / null |
| 团级行程单、签单凭证(四个团) | 改前改后逐字一致,含主团 |
| 数据指纹 | 四个团改前改后订单与行程行的最近改动时间一致,比对期间没有他人写入 |
合计 94 项检查全部通过。
本地证据
| 项 | 读数 |
|---|---|
GroupBatchItineraryServiceTest |
41 例全过,其中本单新增 10 例(截断、截断当天、最后一天终止、只有终止户的项、跨天下钻、无终止户、取消户、截断天一次批量查询、summaryAsPlanned 不查截断天) |
| 截断天查询次数 | 4 户团断言 mapTerminateEndDayByOrderIds 只调用一次且入参为全部活跃户 |
| 范围回归(有 Docker) | groupbatch + settlement + archunit 共 340 类 / 5066 例,源码可执行类与报告逐类对账 340/340;2 例失败均为基底既有(TeamNoResponseFieldGateTest、GroupBatchAdminReadEndpointOwnershipArchTest,引入于 788b9c149,基底对照跑同样红),本单零新增 |
十、相关文档
- Issue
#8747;PR#8766 - 需求:
docs/group/实施单/04-团期行程安排.html§3.11.2 / §3.11.3 - 前置:Issue
#7378(GB-ADM-018 / 019 原实现)、#7873(核单下钻同一读口)
关联 / 联系人
链接
联系人
- 后端负责人: @jw