22 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 | 7378 | 新增团期行程只读端点 GB-ADM-018/019:逐日汇总 + 某项逐户下钻 | admin | jw(GIT) | 新增接口 | deployed | verified | pending | mmg | 2026-09-09 | 团期详情新增「行程」区块的两个只读端点:一次 GET 拿到全团逐日行程,每项标好「全团一致 / 都有但价量不同 / 只有部分户有」,点某项再 GET 一次下钻到逐户。后端已部署 TEST 并经真实网关逐条实测(含 55 户团压 N+1、差异造数后已全部回滚)。前端三条必看:① 本日合计 dayTotalAmount 只含 consistency=ALL_SAME 的项,每项有 countedInDayTotal、天级有 excludedNodeCount,界面必须提示,否则运营会把它当成整团实际金额(那是核团的事);② ALL_SAME 读 unitPrice/quantity/totalAmount,VALUE_DIFF/PARTIAL 读 unitPriceMin~Max / quantityMin~Max,两边字段互斥;③ nodeKey 不含天,同一景区第 1、2 天各排一次很常见,点哪张卡就把那张卡的 dayNumber 带上下钻,不带会跨天返回、一户多行。团期层无任何行程写口,要改行程用下钻行里的 orderId 跳到该户订单行程页。⚠️ 本需求无原型(2026-08-31 jw 口述新增),界面形态需 jw 与 mmg 另定,后端不阻塞。 | 2026-09-09 | dev-v3 |
团期行程逐日汇总与逐户下钻
一、给前端的一句话
团期详情现在能按天看整团行程了。一次 GET 拿到全团逐日行程,每一项都标好了
「全团一致 / 都有但价量不同 / 只有部分户有」,点某一项再 GET 一次能下钻到逐户明细。
两个都是纯新增只读端点,不动任何既有接口,团期层没有任何行程写口——
要改行程还是去那户的订单行程页(下钻每行都带 orderId 供跳转)。
二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|---|---|---|---|---|
| 1 | 团期行程逐日汇总 | GET | /v3/admin/order/group-batch/:groupBatchId/itinerary |
新增 | 按天返回全团行程,每项带一致性标记与本日合计 |
| 2 | 团期行程某项逐户下钻 | GET | /v3/admin/order/group-batch/:groupBatchId/itinerary/nodes/:nodeKey |
新增 | 某一项的逐户明细,含没有该项的户 |
三、接口详情
1. 团期行程逐日汇总 GET /v3/admin/order/group-batch/:groupBatchId/itinerary
VO: GroupBatchItineraryRespVO
使用场景
团期详情页新增的「行程」区块。运营出团前要一眼看出「整团第 3 天去哪」以及 「哪几户跟大部队不一样」。行程是订单级的:同一期客人产品相同、出发日相同, 创单事务内从产品快照展开固化,骨架本来一样;下单之后每户可以单独加项、改时间、调价。 这个接口把全团各户的行程按天叠在一起,标出哪里已经不一样了。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
groupBatchId |
path | Long | 是 | 雪花 ID,按字符串传 | 团期 ID |
无 query、无 body。不分页——单团期的天数与每天的项数是有限的行程结构,不是无界列表。
出参
Result<GroupBatchItineraryRespVO>。
| 字段 | 类型 | 说明 |
|---|---|---|
groupBatchId |
String | 团期 ID(雪花) |
totalHouseholds |
Integer | 参与统计的活跃户数(分母 M);已取消 / 已退单户不计入 |
dayCountConsistent |
Boolean | 各户行程天数是否一致 |
dayCount |
Integer | 多数户的行程天数;无行程时 0 |
dayCountOutliers |
Array | 天数与多数户不同的户;一致时为 [] |
dayCountOutliers[].orderId |
String | 子订单 ID(雪花) |
dayCountOutliers[].orderNo |
String | 子订单编号 |
dayCountOutliers[].contactName |
String | 联系人 |
dayCountOutliers[].dayCount |
Integer | 该户的行程天数(该户完全没有行程时为 0) |
days |
Array | 逐日汇总,按 dayNumber 升序 |
days[].dayNumber |
Integer | 第几天,从 1 起 |
days[].dayDate |
String | yyyy-MM-dd;各户不一致时取多数户的值 |
days[].dayTitle |
String | 该天标题;取多数户的值 |
days[].dayTotalAmount |
Number | 本日合计——只含 ALL_SAME 项 |
days[].excludedNodeCount |
Integer | 未计入本日合计的项数 |
days[].nodes |
Array | 该天的行程项 |
days[].nodes[].nodeKey |
String | 归组键,下钻接口原样回传 |
days[].nodes[].nodeName |
String | 行程项名称 |
days[].nodes[].nodeType |
String | 行程项类型(SCENIC / RESTAURANT / ACTIVITY / SERVICE / CUSTOM) |
days[].nodes[].resourceType |
String | 资源类型(SCENIC_SPOT / RESTAURANT / ACTIVITY;自定义项可能为 null) |
days[].nodes[].resourceId |
String | 资源 ID(雪花);自定义项为 null |
days[].nodes[].resourceName |
String | 资源名称快照 |
days[].nodes[].startTime |
String | 精确时间 HH:mm;各户不一致时取多数户的值 |
days[].nodes[].timePeriod |
String | 时段说明(上午 / 下午 / 全天);取多数户的值 |
days[].nodes[].consistency |
String | ALL_SAME / VALUE_DIFF / PARTIAL |
days[].nodes[].householdCount |
Integer | 有该项的户数 N |
days[].nodes[].totalHouseholds |
Integer | 参与统计的总户数 M |
days[].nodes[].unitPrice |
Number | 单价;仅 ALL_SAME 有值,否则 null |
days[].nodes[].unitPriceMin / unitPriceMax |
Number | 单价区间;仅非 ALL_SAME 有值 |
days[].nodes[].quantity |
Integer | 数量;仅 ALL_SAME 有值 |
days[].nodes[].quantityMin / quantityMax |
Integer | 数量区间;仅非 ALL_SAME 有值 |
days[].nodes[].totalAmount |
Number | 小计;仅 ALL_SAME 有值 |
days[].nodes[].countedInDayTotal |
Boolean | 是否计入本日合计——只有 ALL_SAME 才是 true |
请求示例
GET /v3/admin/order/group-batch/2097498512387104770/itinerary
Authorization: Bearer <admin token>
响应示例
TEST 实测(3 户团,节选第 1 天的两项 —— 一项全团一致、一项只有一户有):
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2097498512387104770",
"totalHouseholds": 3,
"dayCountConsistent": false,
"dayCount": 3,
"dayCountOutliers": [
{
"orderId": "2097498673159028737",
"orderNo": "HL20260909093347891",
"contactName": "容量验收客3",
"dayCount": 4
}
],
"days": [
{
"dayNumber": 1,
"dayDate": "2026-11-20",
"dayTitle": "抵达与草原初见",
"dayTotalAmount": 545.00,
"excludedNodeCount": 2,
"nodes": [
{
"nodeKey": "e735c79b9a59a955",
"nodeName": "中俄边境公路(卡线)",
"nodeType": "SCENIC",
"resourceType": "SCENIC_SPOT",
"resourceId": "3001000000000000019",
"resourceName": "中俄边境公路(卡线)",
"startTime": null,
"timePeriod": "MORNING",
"consistency": "ALL_SAME",
"householdCount": 3,
"totalHouseholds": 3,
"unitPrice": 80.00,
"unitPriceMin": null,
"unitPriceMax": null,
"quantity": 1,
"quantityMin": null,
"quantityMax": null,
"totalAmount": 80.00,
"countedInDayTotal": true
},
{
"nodeKey": "3f2d83708b6957db",
"nodeName": "驯鹿拉车体验",
"nodeType": "ACTIVITY",
"resourceType": "ACTIVITY",
"resourceId": "2029926122787946497",
"resourceName": "驯鹿拉车体验",
"startTime": null,
"timePeriod": "MORNING",
"consistency": "VALUE_DIFF",
"householdCount": 3,
"totalHouseholds": 3,
"unitPrice": null,
"unitPriceMin": 60.00,
"unitPriceMax": 90.00,
"quantity": null,
"quantityMin": 1,
"quantityMax": 1,
"totalAmount": null,
"countedInDayTotal": false
}
]
}
]
}
}
空数据 / 降级响应
团期存在但没有活跃子订单(或活跃户都没有行程)时回空天数组,不报错:
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2089667627927437313",
"totalHouseholds": 0,
"dayCountConsistent": true,
"dayCount": 0,
"dayCountOutliers": [],
"days": []
}
}
单项降级:total_amount(订单侧的成交小计,签单 / 退款口径下才冻结)为空时,
小计按 unitPrice × quantity 现算;两个都缺(如无价的「早餐」)时 totalAmount 回 null,
不当 0 计入本日合计。startTime / timePeriod / resourceId 为空的项照常返回。
错误响应
团期不存在(与团期详情同码,不是回空数组):
{ "code": 589500, "message": "团期不存在", "data": null, "success": false }
当前角色没有 group-batch:view 权限:
{ "code": 589507, "message": "无操作权限(非团期管理员 / 非本定制师名下)", "data": null, "success": false }
缺少或无效的 token 由网关拦截,返回 401。本端点不产生其它业务错误码。
业务边界
- 只读端点,不改任何数据;团期层没有行程写口。
- 一张表都不建、不给行程表加团期列——实时汇总在团活跃子订单的行程。
- 活跃集与团期其余聚合同口径:已取消 / 已退单 / 已软删户不出现,也不计入分母 M。
- 返回全量,不分页。
dayDate/dayTitle/startTime/timePeriod各户不一致时取多数户的值。dayCount取多数户的天数;与多数不同的户进dayCountOutliers(完全无行程的户以dayCount=0出现)。- 本日合计只含
ALL_SAME项;80与80.00判为同一个价(按数值比,不比标度)。 - 权限码
group-batch:view,与团期看板、六芯片明细、时间线一致。
2. 团期行程某项逐户下钻 GET /v3/admin/order/group-batch/:groupBatchId/itinerary/nodes/:nodeKey
VO: GroupBatchItineraryNodeDetailVO
使用场景
汇总页看到某项标着「2/3 户」或「¥80~150」时点进来,看清楚是哪几户不一样、各自多少。
每一行都带 orderId,据此跳到那户的订单行程页去改——团期层没有任何行程编辑入口。
入参
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
groupBatchId |
path | Long | 是 | 雪花 ID,按字符串传 | 团期 ID |
nodeKey |
path | String | 是 | 原样取自汇总响应 | 归组键 |
dayNumber |
query | Integer | 否 | 从 1 起 | 只看第几天;不传 = 跨天全看 |
无 body。
出参
Result<GroupBatchItineraryNodeDetailVO>。
| 字段 | 类型 | 说明 |
|---|---|---|
groupBatchId |
String | 团期 ID(雪花) |
nodeKey |
String | 原样回显请求里的归组键 |
dayNumber |
Integer | 原样回显请求里的天;未传时 null |
nodeName |
String | 行程项名称;无任何户有该项时 null |
consistency |
String | ALL_SAME / VALUE_DIFF / PARTIAL,与汇总同口径 |
householdCount |
Integer | 有该项的户数 N |
totalHouseholds |
Integer | 参与统计的总户数 M |
items |
Array | 逐户明细,含没有该项的户 |
items[].orderId |
String | 子订单 ID(雪花)——前端据此跳转到该户订单行程 |
items[].orderNo |
String | 子订单编号 |
items[].contactName |
String | 联系人 |
items[].has |
Boolean | 该户有没有这一项 |
items[].dayNumber |
Integer | 第几天;has=false 时 null |
items[].unitPrice |
Number | 单价;has=false 时 null |
items[].quantity |
Integer | 数量;has=false 时 null |
items[].totalAmount |
Number | 小计;has=false 时 null |
items[].startTime |
String | 精确时间 HH:mm |
items[].timePeriod |
String | 时段说明 |
请求示例
GET /v3/admin/order/group-batch/2097498512387104770/itinerary/nodes/3f2d83708b6957db?dayNumber=1
Authorization: Bearer <admin token>
响应示例
TEST 实测(该项三户都有,其中一户把单价从 60 改成了 90):
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2097498512387104770",
"nodeKey": "3f2d83708b6957db",
"dayNumber": 1,
"nodeName": "驯鹿拉车体验",
"consistency": "VALUE_DIFF",
"householdCount": 3,
"totalHouseholds": 3,
"items": [
{
"orderId": "2097498512223526914",
"orderNo": "HL20260909093309393",
"contactName": "容量验收客",
"has": true,
"dayNumber": 1,
"unitPrice": 60.00,
"quantity": 1,
"totalAmount": 60.00,
"startTime": null,
"timePeriod": "MORNING"
},
{
"orderId": "2097498671368003585",
"orderNo": "HL20260909093347283",
"contactName": "容量验收客2",
"has": true,
"dayNumber": 1,
"unitPrice": 90.00,
"quantity": 1,
"totalAmount": 90.00,
"startTime": null,
"timePeriod": "MORNING"
},
{
"orderId": "2097498673159028737",
"orderNo": "HL20260909093347891",
"contactName": "容量验收客3",
"has": true,
"dayNumber": 1,
"unitPrice": 60.00,
"quantity": 1,
"totalAmount": 60.00,
"startTime": null,
"timePeriod": "MORNING"
}
]
}
}
空数据 / 降级响应
nodeKey 在本团期没有任何户命中(包括拼错的键、带了 dayNumber 但那天没有该项)时,
返回零命中的完整户清单而不是错误——前端拿到的仍是「谁有谁没有」这张表:
{
"code": 200,
"message": "成功",
"data": {
"groupBatchId": "2097498512387104770",
"nodeKey": "deadbeefdeadbeef",
"dayNumber": null,
"nodeName": null,
"consistency": "PARTIAL",
"householdCount": 0,
"totalHouseholds": 3,
"items": [
{ "orderId": "2097498512223526914", "orderNo": "HL20260909093309393", "contactName": "容量验收客", "has": false, "dayNumber": null, "unitPrice": null, "quantity": null, "totalAmount": null, "startTime": null, "timePeriod": null }
]
}
}
团期无活跃子订单时 items 为 []、totalHouseholds=0,同样不报错。
has=false 的行除身份字段外全部为 null,前端按「未参加」渲染即可。
错误响应
团期不存在:
{ "code": 589500, "message": "团期不存在", "data": null, "success": false }
当前角色没有 group-batch:view 权限:
{ "code": 589507, "message": "无操作权限(非团期管理员 / 非本定制师名下)", "data": null, "success": false }
缺少或无效的 token 由网关拦截,返回 401。未知 nodeKey 不是错误,见上一节。
业务边界
- 只读端点;
items里每行的orderId就是「去哪改」的跳转依据。 items含没有该项的户(has=false),顺序与汇总接口的户序一致(按orderId升序)。- 不带
dayNumber时跨天返回该项的全部出现,同一户可能多行;带dayNumber则只看那一天。 consistency与汇总同口径,但取值集合随dayNumber收窄而变——只看第 1 天可能ALL_SAME,跨天看可能VALUE_DIFF。- 同一户同一天重复出现同一项时只取第一条,保证
householdCount <= totalHouseholds。 - 已取消 / 已退单 / 已软删户既不出现在
items里,也不计入totalHouseholds。 - 权限码
group-batch:view。
四、契约约束与正确调用方式
nodeKey只能原样回传,不要自己拼。 它是(nodeName, resourceType, resourceId)的 稳定哈希(SHA-256 前 8 字节十六进制),服务端实时算、不落库。同一三元组在任何时候、 任何请求里都得到同一个键;但它是服务端实现细节,前端从汇总响应里取到就行。- 点哪张卡就把那张卡的
dayNumber带上。nodeKey不含天——同一个景区在第 1 天和 第 2 天各排一次是很常见的。不带dayNumber时下钻会跨天返回该项的全部出现, 同一户可能出现多行(各行带自己的dayNumber),与你点的那张卡就对不上了。 - 本日合计
dayTotalAmount只含consistency=ALL_SAME的项。 每项有countedInDayTotal标明是否计入,天级有excludedNodeCount给出被排除的项数—— 请在界面上明确提示,否则运营会把这个数当成「整团实际金额」。整团实际金额是核团的事。 ALL_SAME与非ALL_SAME取值字段不同:ALL_SAME用unitPrice/quantity/totalAmount(此时区间字段为null);VALUE_DIFF/PARTIAL用unitPriceMinunitPriceMax/quantityMinquantityMax(此时确定值字段为null)。 别只读一边。- 团期层没有任何行程写口。 要改行程,用下钻每行带的
orderId跳到那户的订单行程页去改。 - 雪花 ID 一律按字符串处理(
groupBatchId/orderId/resourceId都以字符串下发)。 - 权限码
group-batch:view,与团期看板、六芯片明细、时间线同码。
六、边界行为
| 场景 | 行为 |
|---|---|
| 团期不存在 | 589500(与团期详情同码,非空数组——这两个端点要先确认团期存在) |
| 团期存在但无活跃子订单 | code=200,days=[]、totalHouseholds=0、dayCount=0、dayCountConsistent=true |
| 有活跃户但都没有行程 | 同上,days=[],不报错 |
| 某户没有行程 | 该户不参与逐项统计,但计入分母 M,且在 dayCountOutliers 里以 dayCount=0 出现 |
| 已取消 / 已退单 / 已软删户 | 不出现在汇总与下钻中,也不计入分母 M |
不存在的 nodeKey |
code=200,householdCount=0、nodeName=null、consistency=PARTIAL,items 里每户 has=false,不报错 |
dayNumber 传了但该天没有该项 |
同上:零命中的空清单,不报错 |
| 各户天数不同 | dayCountConsistent=false,dayCount 取多数户的天数,dayCountOutliers 列出与多数不同的户 |
| 同一户同一天重复出现同一项 | 只取第一条;不替订单侧去重,也不因此把该户算成两户(保证 householdCount <= totalHouseholds) |
80 与 80.00 |
判为同一个价(按数值比较,不比标度),不会误报成 VALUE_DIFF |
单价 / 数量为 null |
参与一致性判定(都为 null 算一致);区间取值时 null 不参与 min/max |
无 group-batch:view 权限 |
589507 |
七、不影响范围
- 不改任何既有接口的路径、入参、出参、错误码。
- 不改订单侧行程的任何读写口。
- 无表变更、无 Flyway、无数据迁移、无新增错误码。
- 网关路由不新增(
/v3/admin/**已覆盖)。 - 只滚
hl-order-service-v3一个服务。
八、测试环境已验证
TEST(api.test.1814.love)真实网关逐条取证。主用团期 2097498512387104770(3 户 · 3 天 · 真实产品展开),
另用 2096412454643802114(55 户)压 N+1、2089667627927437313(0 户)压空态。
差异场景由 SQL 造数,已全部回滚(回滚后复测:3 户 / 3 天 / 全一致 / 第 1 天合计 605.00 / excludedNodeCount=0,与造数前逐字段一致)。
| 验收项 | 实测 |
|---|---|
| 按天返回全团行程,逐项含名称 / 时间 / 单价 × 数量 = 小计 / 本日合计 | ✅ 3 天,合计 605.00 / 385.00 / 730.01 |
| 各户完全一致时全为全团项,本日合计 = 单户本日合计 | ✅ 25 项全 ALL_SAME;第 1 天 30+80+60+35+60+160+100+80 = 605.00 |
| 有户改价 → 该项返回区间,下钻看到每户各自的价 | ✅ VALUE_DIFF,unitPriceMin/Max = 60/90;下钻三行 60 / 90 / 60 |
| 有户加项 → 标 N/M,下钻列出是哪几户加的 | ✅ PARTIAL 1/3,下钻 has=false/false/true |
| 本日合计只含全团一致项,且标出未计入的项 | ✅ 合计由 605.00 降为 545.00,excludedNodeCount=2,两项 countedInDayTotal=false |
| 天数不一致 → 预警 + 列出是哪几户 | ✅ dayCountConsistent=false,dayCount=3,dayCountOutliers 指名到 HL20260909093347891(4 天) |
下钻每行带 orderId 供跳转;团期层无写口 |
✅ 每行均带;本次只新增两个 GET |
nodeKey 稳定、不落库 |
✅ 两次请求 25 个 nodeKey 完全一致;库中无该列 |
| 已取消户不出现在汇总与下钻,也不计入分母 | ✅ 置 CANCELLED 后分母 3→2,其加项与多出的第 4 天一并消失,天数预警自动解除 |
空态与未知 nodeKey |
✅ 0 户团回 days: [];未知键回零命中清单;团期不存在回 589500 |
| 权限 | ✅ CUSTOMIZER 角色两个端点均 589507;无 token 由网关拦 401 |
| 查询次数与户数无关(禁 N+1) | ✅ performance_schema 计数:3 户与 55 户均恰好 2 条 order_itinerary_* 查询;55 户耗时 88ms |
跨天项实测(dayNumber 存在的理由):「巴尔虎蒙古部落」在第 1、2 天各排一次——
不带 dayNumber 下钻回 6 行(3 户 × 2 天,各带自己的 dayNumber),带 dayNumber=1 回 3 行,与第 1 天那张卡一致。
小计兜底实测:该团 total_amount 全为空、unit_price 有值。兜底前本日合计恒为 0,
兜底后 605.00;无价的「早餐」小计仍为 null,不当 0 计入。
十、相关文档
- 实施单编号:GB-ADM-018 / GB-ADM-019(新增),实施单
docs/group/实施单/04-团期行程安排.html - 需求:《团期模块详细设计 v3.0》§3.11「只读汇总、不建表」;SRS §0.13;《数据模型》§A.9D「归组键不落库」
- ⚠️ 前端形态无原型可依:《需求逐屏对照》的团期详情 12 屏里没有「行程」屏,本需求是 2026-08-31 jw 口述新增,契约与验收要点齐备但没有画面。界面长什么样需 jw 与 mmg 另定。