文件
hl-api-changelog/changelogs-v2/2026-09/09_7378_团期行程逐日汇总与逐户下钻-新增接口-管理后台.md
T
2026-09-09 17:57:34 +08:00

22 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 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。

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

  1. nodeKey 只能原样回传,不要自己拼。 它是 (nodeName, resourceType, resourceId) 的 稳定哈希(SHA-256 前 8 字节十六进制),服务端实时算、不落库。同一三元组在任何时候、 任何请求里都得到同一个键;但它是服务端实现细节,前端从汇总响应里取到就行。
  2. 点哪张卡就把那张卡的 dayNumber 带上。 nodeKey 不含天——同一个景区在第 1 天和 第 2 天各排一次是很常见的。不带 dayNumber 时下钻会跨天返回该项的全部出现, 同一户可能出现多行(各行带自己的 dayNumber),与你点的那张卡就对不上了。
  3. 本日合计 dayTotalAmount 只含 consistency=ALL_SAME 的项。 每项有 countedInDayTotal 标明是否计入,天级有 excludedNodeCount 给出被排除的项数—— 请在界面上明确提示,否则运营会把这个数当成「整团实际金额」。整团实际金额是核团的事。
  4. ALL_SAME 与非 ALL_SAME 取值字段不同:ALL_SAME 用 unitPrice / quantity / totalAmount(此时区间字段为 null);VALUE_DIFF / PARTIAL 用 unitPriceMinunitPriceMax / quantityMinquantityMax(此时确定值字段为 null)。 别只读一边。
  5. 团期层没有任何行程写口。 要改行程,用下钻每行带的 orderId 跳到那户的订单行程页去改。
  6. 雪花 ID 一律按字符串处理(groupBatchId / orderId / resourceId 都以字符串下发)。
  7. 权限码 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 另定。

关联 / 联系人