--- schema: "hl-changelog/v2" ticket: "7536" title: "团期接口返回值整改·破坏批:A3 子订单列表改分页信封 + 删 5 冗余字段 + include* 默认开 + 财务 12 金额转字符串 + 3 逐行 VO 补团号" consumer: "admin" author: "jw(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "verified" frontend_owner: "mmg" frontend_ref: "657f14dd" target_release: "" verified_at: "2026-09-14" status_note: "破坏性变更,前端必须整页面批量切换、提前协调。A3『报名清单』tab 主数据源 `GET .../orders` 从裸数组改标准分页信封 `Result>`(新增 page/pageSize,默认首页 20 条并带 total),且 includeNeeds/includeTravelers 缺省由 false 改 true(房型/房数/出行人默认返回);同时删除 5 个冗余字段(estimatedCost/adultCount/childCount/youngChildCount/babyCount,人数改用 participantCount + tierName)。财务 tab `GET .../finance` 的 12 个金额字段 JSON 形态由 number 改 string(与报名清单 tab 对齐)。A3/财务/预支三个逐行 VO 各补一个 teamNo(本户团号,逐户不同,未付订金为 null)。零 DDL、零网关路由改动。**注:groupChatUnreadCount 本轮保留不删——hl-ui 全文搜索零命中但未取得 mmg 书面回执,按工单 AC-1 兜底口径保留该字段。** 前端已交付(mmg):报名清单真分页 UI(rosterPage 服务端分页+整团名单 pageSize=200 分离)+teamNo 列+下线预计毛利列+退单候选 records 兜底;commit 657f14dd。" updated_at: "2026-09-14" base: "dev-v3" --- # order-v3: 团期接口返回值整改·破坏批(A3 子订单列表 / 财务 / 预支) **服务**: hl-order-service-v3 **PR**: #7688 **Issue**: #7536 --- ## ⚠️ 关键变化 🔴 **破坏性变更,前端必须整页面批量切换(不提供兼容期双形态返回)**: 1. **A3 子订单列表 `GET .../orders` 返回包装从裸数组改分页信封**:`Result>` → `Result>`。`data` 由裸数组变为 `{records, total, page, pageSize}`;新增 `page`/`pageSize` 查询参数(默认首页 20 条、`pageSize` 上限 200)。老前端读 `data.length` 会拿到 `undefined`、列表整个空白——**这是本单最危险的失败形态**。 2. **删除 5 个冗余字段**(`GroupBatchOrderItemRespVO`):`estimatedCost`(无写入点、恒 null)、`adultCount`/`childCount`/`youngChildCount`/`babyCount`(人数四件套)。前端毛利列删除、人数改用 `participantCount`(总数)+ `tierName`/`tierCode`(档位)。 3. **`includeNeeds` / `includeTravelers` 缺省值由 `false` 改为 `true`**:`roomCount`/`roomType`/`specialNeeds` 与 `travelers[]` 默认返回;前端要精简响应须显式传 `false`。 4. **财务 tab `GET .../finance` 的 12 个 `BigDecimal` 金额字段 JSON 形态 number → string**(补 `@JsonSerialize(using = ToStringSerializer.class)`,与报名清单 tab 对齐)。前端勿按 number 解析这些字段。 5. **A3 / 财务 / 预支三个逐行 VO 各补一个 `teamNo`**(纯加):本户团号(`order_main.team_no`,形如 `26-0001`),**逐户各不相同**,该户订金未支付成功时为 `null`(不返空串、不回退 `orderNo`)。 🟢 **纯加(已随零破坏批 PR-1 先行合入,本单一并归档)**:A3 子订单项补 6 个中文配对字段(`roomTypeName` + `payStatusName`/`contractStatusName`/`insuranceStatusName`/`hotelRequirementStatusName`/`vehicleRequirementStatusName`)。 **无 DDL、无 Flyway、无网关路由改动(`/v3/admin/order/**` 既有覆盖)、无新增错误码。** ## 一、背景 团期控制台(hl-ui 管理后台)「团期详情 → 报名清单 / 财务」两个 tab 有三处肉眼可见坏结果:页头「子订单 N 户」恒显示 0(列表接口无 `total`)、报名清单默认缺「房型/房数」「出行人」两列(藏在 opt-in 开关后)、报名清单多给了「预计毛利」「会话未读」两列(原型没有)。口径来源 `docs/group/团期闭环整改方案-v1.1.md` §6,判据是 wx 定的三条「原型有的必须有;可以多给;不能给离谱的」。本单是三批整改中的**破坏批**,独占「团期子订单列表 A3」「团期财务总览」「团期预支记录」三个端点的全部改动。 ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 团期下子订单列表(报名清单 tab) | GET | `/v3/admin/order/group-batch/:groupBatchId/orders` | 修改 | 裸数组→分页信封 `PageResult`;新增 page/pageSize;include* 默认改 true;删 5 冗余字段;补 teamNo | | 2 | 团期财务总览(财务 tab) | GET | `/v3/admin/order/group-batch/:groupBatchId/finance` | 修改 | 12 个金额字段 JSON number→string;逐户行补 teamNo | | 3 | 团期预支记录 | GET | `/v3/admin/order/group-batch/:groupBatchId/advances` | 修改 | 预支逐行补 teamNo(团期级行 scope=GROUP_BATCH 恒 null) | ## 三、接口详情 ### 1. 团期下子订单列表 `GET /v3/admin/order/group-batch/:groupBatchId/orders` **VO**: `Result>`(path: groupBatchId;query: page / pageSize / includeTravelers / includeNeeds / includeCancelled) #### 使用场景 团期详情页「报名清单」tab 主数据源。改造后默认返首页 20 条并带 `total`,房型/房数/出行人默认返回,6 个裸 code 带中文配对,逐户带团号。沿用既有鉴权 `GroupBatchPermissionGuard.PERMISSION_VIEW` 与网关路由。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | groupBatchId | path | string(Long) | 是 | 雪花 ID | 团期主订单 ID(运营侧团期主键,不是 productBatchId) | | page | query | int | 否 | 缺省 1;<1 归一为 1 | ★新增。页码,从 1 起 | | pageSize | query | int | 否 | 缺省 20;<1 归一为 20;>200 截断为 200 | ★新增。每页条数 | | includeTravelers | query | boolean | 否 | ★默认 `false`→`true` | 是否附 `travelers[]`(证件号/手机号任何情况不返回) | | includeNeeds | query | boolean | 否 | ★默认 `false`→`true` | 是否附 `roomCount`/`roomType`/`specialNeeds` | | includeCancelled | query | boolean | 否 | 缺省 `false`(不变) | 是否含已取消子订单(只返活跃集,剔除 CANCELLED) | #### 出参字段表 | 字段 | 类型 | 说明 | |---|---|---| | data | object | ★改前是裸数组,改后是 `PageResult` 信封 | | data.records | array | 本页子订单行(`GroupBatchOrderItemRespVO`) | | data.total | int | ★整团活跃子订单总数(含未在本页的) | | data.page / data.pageSize | int | ★当前页码 / 每页条数 | | records[].orderId | string(Long) | 子订单 ID(字符串防精度丢失) | | records[].orderNo | string | 订单编号 | | records[].teamNo | string | ★本户团号(`order_main.team_no`),逐户不同,未付订金为 null | | records[].customerName | string | 客户姓名 | | records[].participantCount | int | 出行人总数(= adult+child+youngChild+baby) | | records[].tierCode / tierName | string | 档位码 / 档位名 | | records[].payStatus / payStatusName | string | 支付状态 code / 中文(★Name 纯加) | | records[].contractStatus / contractStatusName | string | 合同状态 code / 中文(无合同时均 null) | | records[].insuranceStatus / insuranceStatusName | string | 保险状态 code / 中文(无保险时均 null) | | records[].hotelRequirementStatus / hotelRequirementStatusName | string | 房需求状态 code / 中文 | | records[].vehicleRequirementStatus / vehicleRequirementStatusName | string | 车需求状态 code / 中文 | | records[].paidAmount / balanceAmount / totalPrice | string(decimal) | 已付 / 待收尾款 / 本户应收(字符串两位小数) | | records[].roomCount / roomType / roomTypeName / specialNeeds | int/string | ★默认返回;roomTypeName 按「、」逐段翻译再拼回 | | records[].travelers | array | ★默认返回;`name`/`type`/`age`/`birthdayInTrip`(无证件号/手机号) | | ~~records[].estimatedCost / adultCount / childCount / youngChildCount / babyCount~~ | — | ★**已删除**(毛利改核单页,人数改用 participantCount + tierName) | #### 请求示例 ```http GET /v3/admin/order/group-batch/2096412454643802114/orders?page=1&pageSize=20 Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "message": "success", "success": true, "traceId": "…", "data": { "records": [ { "orderId": "2000000001", "orderNo": "HL20260601001", "teamNo": "26-0001", "customerName": "王先生家庭", "participantCount": 4, "tierCode": "2A1C", "tierName": "2成人1儿童", "payStatus": "DEPOSIT_PAID", "payStatusName": "已付定金", "paidAmount": "3000.00", "balanceAmount": "9000.00", "totalPrice": "12000.00", "roomCount": 2, "roomType": "FAMILY", "roomTypeName": "家庭房", "specialNeeds": "需要婴儿床", "travelers": [ { "name": "王小明", "type": "CHILD", "age": 6, "birthdayInTrip": false } ] } ], "total": 55, "page": 1, "pageSize": 20 } } ``` #### 空数据 / 降级响应 团期无任何活跃子订单 → `data` 为 `{"records":[],"total":0,"page":1,"pageSize":20}`(**不返 `data:null`**)。`page` 超末页 → `records` 为空数组、`total` 仍为真实总数、`code=200` 不抛异常(前端据 `total` 回退首页)。字典不可达时 `roomTypeName` 回落 `roomType` 原值。 #### 错误响应 ```json { "code": 589500, "message": "团期不存在", "data": null } ``` 团期不存在 → **589500**(`GROUP_BATCH_NOT_FOUND`);无 `group-batch:view` → **589507**(`GROUP_BATCH_PERMISSION_DENIED`)。分页参数越界**不产生错误码**(归一/截断)。 #### 业务边界 - 分页在**内存内**做(切片在 `assembleSubOrders` 装配之后、事务外);不下推 SQL——数据源经 OrderService 跨聚合逐单装配,下推会打散成 N+1,且 `@TableLogic` 与 CANCELLED 过滤都在内存侧。团期子订单 N 上界为单团满员名额,本端点页面级低频。 - `page<1` 归一为 1;`pageSize<1` 归一为 20、`>200` 截断为 200(读接口对越界宽容,不抛异常)。 - `includeTravelers`/`includeNeeds` 传 null 按 true;`includeCancelled` 传 null 按 false。 - 保留一个**不分页的内部全量方法**(`listSubOrders(id, boolean, boolean, boolean)`)供 #7533 团级文档出全团名册;HTTP 入口才走分页(复审修正 1)。 ### 2. 团期财务总览 `GET /v3/admin/order/group-batch/:groupBatchId/finance` **VO**: `Result`(path: groupBatchId) #### 使用场景 团期详情页「财务」tab(GB-ADM-040:四张金额卡 + 逐户付款 + 整团合计)。本次只改**金额字段 JSON 形态**(number→string)与逐户行补 `teamNo`,其余字段名与语义全部不变。权限 `GroupBatchPermissionGuard.PERMISSION_FINANCE_VIEW`(比 view 更严)。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | groupBatchId | path | string(Long) | 是 | 雪花 ID | 团期主订单 ID | #### 出参字段表 | 字段 | 类型 | 说明 | |---|---|---| | receivableAmount / receivedAmount / unpaidAmount | string(decimal) | ★形态 number→string。整团应收 / 已收 / 待收 | | advanceApproved / advancePending / advanceAvailable | string(decimal) | ★形态 number→string。已预支 / 待审批 / 可支取余额 | | totals.totalPrice / totals.paidAmount / totals.unpaidAmount | string(decimal) | ★形态 number→string。整团合计三列 | | items[].totalPrice / items[].paidAmount / items[].unpaidAmount | string(decimal) | ★形态 number→string。逐户应收 / 已付 / 待收 | | items[].teamNo | string | ★纯加。本户团号,逐户不同,未付订金为 null | | withdrawnCount | int | 不变(Integer) | | primaryPayeeName / secondaryPayeeName | string | 不变 | #### 请求示例 ```http GET /v3/admin/order/group-batch/2096412454643802114/finance Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "message": "success", "success": true, "traceId": "…", "data": { "receivableAmount": "40200.00", "receivedAmount": "32900.00", "unpaidAmount": "7300.00", "advanceApproved": "0.00", "advancePending": "0.00", "advanceAvailable": "7300.00", "withdrawnCount": 0, "primaryPayeeName": "李雯", "secondaryPayeeName": null, "totals": { "totalPrice": "40200.00", "paidAmount": "32900.00", "unpaidAmount": "7300.00" }, "items": [ { "orderId": "770145", "orderNo": "GT-26-0081", "teamNo": "26-0001", "customerName": "罗敏", "consultantName": "李雯", "totalPrice": "12300.00", "paidAmount": "12300.00", "unpaidAmount": "0.00" } ] } } ``` #### 空数据 / 降级响应 无活跃子订单团期 `items` 为空数组、四张金额卡为 `"0.00"`(字符串);金额字段恒非 null(`BigDecimal.ZERO` 参与计算)。未付订金的逐户行 `teamNo` 为 null。 #### 错误响应 ```json { "code": 589507, "message": "无权限", "data": null } ``` 团期不存在 → 589500;无 `group-batch:finance:view` → 589507。 #### 业务边界 - 12 个金额字段(顶层 6 + `totals` 3 + `items[]` 3)JSON 形态统一为**带双引号的字符串**;`withdrawnCount`(Integer)不动。本次只消除同页两个 tab 金额形态不一致,**不作通用精度保证**(按分折整数 ≤1e12 在 2^53 内不丢量级,但算术会累积误差)。 - `items[].teamNo` 逐户取值(同一次 `orderService.selectBatchByIds`,零新增查询),不是全团共用一个值。 ### 3. 团期预支记录 `GET /v3/admin/order/group-batch/:groupBatchId/advances` **VO**: `Result>`(path: groupBatchId;倒序、不分页) #### 使用场景 团期详情页预支记录列表。本次只在逐行补 `teamNo`(纯加),其余不变。权限 `GroupBatchPermissionGuard.PERMISSION_FINANCE_VIEW`。 #### 入参字段表 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |---|---|---|---|---|---| | groupBatchId | path | string(Long) | 是 | 雪花 ID | 团期主订单 ID | #### 出参字段表 | 字段 | 类型 | 说明 | |---|---|---| | data | array | 预支记录逐行(倒序、不分页;裸数组,非分页信封) | | data[].orderNo | string | 归属订单号(团期级行 scope=GROUP_BATCH 为 null) | | data[].teamNo | string | ★纯加。归属订单团号;scope=GROUP_BATCH 团期级行恒 null(与 orderNo 同规则) | | data[].scope | string | `ORDER` / `GROUP_BATCH` | | data[].amount / status 等 | — | 不变 | #### 请求示例 ```http GET /v3/admin/order/group-batch/2096412454643802114/advances Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "message": "success", "success": true, "traceId": "…", "data": [ { "orderNo": "GT-26-0081", "teamNo": "26-0001", "scope": "ORDER", "amount": "500.00" }, { "orderNo": null, "teamNo": null, "scope": "GROUP_BATCH", "amount": "1000.00" } ] } ``` #### 空数据 / 降级响应 无预支记录返空 `records`。`scope=GROUP_BATCH` 的团期级行没有归属订单,`orderNo` 与 `teamNo` **同时为 null**(前端按 scope 判断渲染,勿把团期级行的空团号当异常)。 #### 错误响应 ```json { "code": 589507, "message": "无权限", "data": null } ``` 团期不存在 → 589500;无 `group-batch:finance:view` → 589507。 #### 业务边界 - `teamNo` 与 `orderNo` 从**同一张** `Map`(同一次 `selectBatchByIds`)取,不新增查询;`activeIds` 为空时一次都不查。 - 团期级行(`orderId == null`)`teamNo` 恒 null,与既有 `orderNo` 处理同规则。 ## 四、契约约束与正确调用方式 - **A3 分页**:`data` 是 `PageResult` 信封,读 `data.records`(不是 `data`);`data.total` 为整团活跃总数、`data.records.length` 只是本页条数。不传 `page`/`pageSize` 返首页 20 条。 - **金额字符串**:A3 的 `paidAmount`/`balanceAmount`/`totalPrice` 与财务 tab 的 12 个金额字段均为字符串,勿按 number 解析;`orderId`/`groupBatchId` 亦为字符串防精度丢失。 - **teamNo 语义**:本户团号、逐户不同、订金支付成功后才生成;未付订金/团期级行为 `null`——**前端不得**回退成 `orderNo`、填空串或拿团期侧编号顶替(否则无法区分「未付订金」与「已生成」)。 - **默认视图**:报名清单默认已带房型/房数/出行人(无需再传 `includeNeeds=true`/`includeTravelers=true`);毛利列与人数四件套字段已删除,人数改用 `participantCount` + `tierName`/`tierCode`。 ## 五、数据库行为 - 无表变更、无 Flyway、无新增列、无索引调整、无 H2 `*-schema.sql` 改动。 - 分页在内存做(`assembleSubOrders` 返回全量后切片),**无 SQL 变更**;`teamNo` 取自已取全列的订单对象,无新增查询与投影。 ## 六、边界行为 - 分页参数越界一律归一/截断、HTTP 与 `code` 均 200,不新增错误码。 - 空团期返 `{records:[],total:0}` 而非 `data:null`。 - `groupChatUnreadCount` 字段**保留**:hl-ui 全文搜索 `groupChatUnreadCount` 零命中,但未取得 mmg 书面回执,按工单 AC-1 兜底口径「拿不到回执则该字段保留、其余五个冗余字段照删」。 - 复用既有错误码 589500 / 589507,本单错误码配额为「无新增」。 ## 六.6、修改前后对比 | 端点 / 字段 | 改前 | 改后 | |---|---|---| | A3 `data` 包装 | 裸数组 `List` | 分页信封 `PageResult`:`{records,total,page,pageSize}` | | A3 查询参数 | 无 page/pageSize | 新增 `page`(默认 1)/`pageSize`(默认 20,上限 200) | | A3 `includeNeeds` / `includeTravelers` 默认 | `false` | **`true`**(房型/房数/出行人默认返回) | | A3 字段 | 含 `estimatedCost`/`adultCount`/`childCount`/`youngChildCount`/`babyCount` | **删 5 个**(人数改 `participantCount` + `tierName`/`tierCode`;毛利改核单页) | | A3 逐行 | 无 `teamNo` | 补 `teamNo`(逐户不同,未付订金 null) | | 财务顶层 6 + `totals` 3 + `items[]` 3 金额 | JSON `number`(如 `40200.00`) | JSON `string`(如 `"40200.00"`) | | 财务逐户行 | 无 `teamNo` | 补 `teamNo` | | 预支逐行 | 无 `teamNo` | 补 `teamNo`(团期级行 null) | (🟢 A3 的 6 个中文配对字段与三处 teamNo **装配**随零破坏批 PR-1 先行合入,此表为本单破坏批与 PR-1 合并后的整体前后对比。) ## 六.7、影响评估 - **最危险失败形态**:老前端读 `data.length`(原裸数组)改后拿到 `undefined`,报名清单整个空白。缓解:本 changelog 先行 + 拿 mmg 「已知悉、将同步改前端」回执;后端上线后做**后端契约回归**(断言 `records`/`total`/分页字段),**页面回归由 `frontend_status` 单独跟踪、不作为后端关单条件**(复审修正 2)。不提供兼容期双形态返回。 - **金额形态 number→string**:前端解析财务 tab 的 12 个金额字段与 A3 三个金额字段的代码需改(不能直接当数字算术)。 - **删字段**:前端「预计毛利」列删除;人数四件套的直接引用改用 `participantCount`(总数)+ `tierName`/`tierCode`(档位)。 - **teamNo null 分支**:前端逐户/逐行按 `null` 渲染,**不得兜底**成 `orderNo`/空串/团期编号。 - **回滚**:后端零 DDL、零网关路由、零新增错误码,回滚只需回退代码分支;`#7533` 团级文档经保留的内部全量方法读取,不受本单分页影响。 ## 七、不影响范围 - `GroupBatchService.java` 零行改动;订单级端点、团期列表/看板/详情端点结构未改;无网关路由、无 Feign/MQ、无 DDL。 - 财务包内其余 26 处未加 `@JsonSerialize` 的 `BigDecimal` 字段(含 3 处 ReqVO)本单一处未碰。 - 6 个中文配对字段与 3 处 teamNo 的**装配逻辑**随零破坏批 PR-1 先行合入;本破坏批只做删字段 + 分页 + include* 默认 + 金额形态。 ## 八、测试环境已验证 2026-09-14 TEST 网关(`https://api.test.1814.love:9443`,自签 admin token)+ 真 MySQL 实测(部署归属 `hl-order-service-v3 | refactor/7536-a3-breaking-pr2 | 513258beb | 0/N | ok`;本单已合入 dev-v3,PR #7688): - **A3 分页**:56 户团 `?page=1&pageSize=20` → `data={records:[20],total:56,page:1,pageSize:20}`;`page=9` → `records=[]`、`total=56`、不抛异常;`pageSize=500` → `pageSize=200`、records=56;空团 → `{records:[],total:0}`(非 `data:null`)。 - **include* 默认**:不传 → `roomCount`/`roomType`/`specialNeeds`/`travelers` 装配;显式 `includeNeeds=false&includeTravelers=false` → 四项值 null。 - **中文配对**:`roomTypeName`(`STANDARD→标间`)+ 5 个 `<字段>Name`;`contractStatus=null → contractStatusName=null`、`payStatus=DEPOSIT_PAID → 已付定金`。 - **删字段**:`records[0]` 无 `estimatedCost`/`adultCount`/`childCount`/`youngChildCount`/`babyCount`;`groupChatUnreadCount` 按 AC-1 兜底保留。 - **财务 12 金额**:顶层 6 + `totals` 3 + `items[]` 3 全为字符串(`receivableAmount="327600.00"` 等);`withdrawnCount=17` 仍为数字。 - **teamNo**:有团号户 `orders`/`finance` 返 `26-7464` 与 SQL 逐字相等;未付订金户两端点返 JSON `null`。 - **AC-R1(与 #7533 共验)**:56 户团 `print-itinerary` `totalHouseholds=56`、`roster` 56 户、A3 首页仍 20 条、旧内部全量调用点编译通过。 - **全量单测**:`mvn -o -pl hl-order-service-v3 -am test`(Testcontainers)Tests run 10811 / 0 fail / 0 error / 7 skip,含 RedLineArchTest/MapperBoundaryArchTest,BUILD SUCCESS。 ## 十、相关文档 - 工单 #7536 - 方案文档:`docs/group/团期闭环整改方案-v1.1.md` §6.1 / §6.2 / §6.3 - 全过程记录:HL 仓 `dev-records/records/2026-09-14-local-7536-a3-paging-breaking.md` ## 关联 / 联系人 - 后端:jw(破坏批:分页 + 删冗余 + 金额形态;中文配对/teamNo 装配随 PR-1) - 前端:mmg(**必须整页面批量切换**:读 `data.records`/`data.total`;删「预计毛利」「人数四件套」列,人数改 participantCount + tierName;财务金额按字符串解析;三处 teamNo 逐户渲染、null 不兜底)