diff --git a/changelogs-v2/2026-09/08_7316_团期全团需求汇总补权限码与在团口径-结算汇总已核单数纠正-修改接口-管理后台.md b/changelogs-v2/2026-09/08_7316_团期全团需求汇总补权限码与在团口径-结算汇总已核单数纠正-修改接口-管理后台.md new file mode 100644 index 00000000..4199a43d --- /dev/null +++ b/changelogs-v2/2026-09/08_7316_团期全团需求汇总补权限码与在团口径-结算汇总已核单数纠正-修改接口-管理后台.md @@ -0,0 +1,430 @@ +--- +schema: "hl-changelog/v2" +ticket: "7316" +title: "全团需求汇总补权限码 group-batch:view,activeOrderCount 口径改为在团(仅排除 CANCELLED,含 COMPLETED);团期结算 D2 汇总 settledOrderCount 改用 settled 标志判据" +consumer: "admin" +author: "wx(GIT)" +change_type: "修改接口" +backend_status: "deployed" +gateway_status: "verified" +frontend_status: "not_required" +frontend_owner: "" +frontend_ref: "" +target_release: "" +verified_at: "" +status_note: "本篇覆盖两个 PR:#7407(合并提交 d951c0469,全团需求汇总补权限码 + activeOrderCount 在团口径)+ #7408(合并提交 aafc6ae48,团期结算 D2 汇总 settledOrderCount 判据由 summary==null 改为 !Boolean.TRUE.equals(settled))。两次改动均已部署 TEST 环境(hl-order-service-v3 @ aafc6ae48,双实例 UP)并完成网关实测,backend_status=deployed、gateway_status=verified 均为真实态,实测证据见第八节。" +updated_at: "2026-09-10" +base: "dev-v3" +--- + +# 团期模块:全团需求汇总补权限码 group-batch:view 并纠正在团口径 + 结算汇总已核单数纠正 + +> **服务**: `hl-order-service-v3` +> **PR**: #7407、#7408 +> **Issue**: #7316 +> **日期**: 2026-09-10 +> **影响范围**: 管理后台团期看板「全团需求汇总」面板 + 团期结算 D2 汇总面板(两个只读端点) + +--- + +## ⚠️ 关键变化 + +**两个端点的请求参数与响应结构均完全不变**,本次共改四件事: + +1. **新增权限码校验**:`GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary` 现在要求当前角色持平台权限码 `group-batch:view`,校验在 Controller 入口执行,先于任何业务逻辑。不持有 → 业务错误码 **589507**(`GROUP_BATCH_PERMISSION_DENIED`,message:「无操作权限(非团期管理员 / 非本定制师名下)」,HTTP 仍 200)。**此前该端点零判权**,任一能过网关的后台账号都能读到整团逐户特殊需求(`orderSpecialTags`,忌口 / 婚房 / 无障碍等客户隐私)与逐日房型间数。持 `group-batch:view` 的角色为 `ADMIN` / `FINANCE` / `SUPER_ADMIN`(`hl-user-service` 迁移 `V20260831_002__add_group_batch_permissions.sql:16-25`)。同一 Tab 内「看」用 `group-batch:view`、「动」(确认 / 打回需求)用 `group-batch:demand:confirm`,两码不复用。 + +2. **数据口径纠正(PR #7407)**:`requirement-summary` 的 `activeOrderCount` 与 `settlement/summary` 的 `totalActiveOrderCount` 由「活跃子订单数(按 `productBatchId`,额外排除 `COMPLETED`)」改为「**在团子订单数**(按 `groupBatchId` 双通道并集,仅排除 `CANCELLED`,**含 `COMPLETED`**)」。 + - **后果 A**:团期进入核单期后子订单多为 `COMPLETED`,旧口径会把这些整批滤掉,两处汇总常年趋近全 0 / 空;新口径能正常显示。 + - **后果 B(前端会看到数字变化,务必知悉)**:新集合走「按 `group_batch_id` 的双通道并集」——一跳直连 `order_main.group_batch_id` 优先占位,回退通道(按旧 `product_batch_id` 关联)仅在该行 `group_batch_id IS NULL` 时才补位。也就是说**换过团的子订单按 `group_batch_id`(权威归属)计入新团、从原 `product_batch_id` 所指的团移出**。因此同一团期这个数字有的会变大、有的会变小,这是纠正原有错算,不是缺陷。 + +3. **`settlement/summary` 的 `settledOrderCount` 取值口径修正(PR #7408,dev-v3 增补一)**: + - **缺陷**:`SettlementService.getSummary` 查不到 `order_settlement_summary` 行时返回的是 `settled=false` 的**空壳 VO**(仅 `orderId` / `advanceSummary` 有值),**从不返回 `null`**;而 `GroupBatchSettlementService#summary` 侧原判据是 `if (summary == null) continue;`,表达「该户尚未核单则跳过」,该分支**恒不命中** → `settledOrderCount` **恒等于在团户数**(把没人核过单的团直接显示成「全团已核单」)。 + - **为什么现在才暴露**:#7407 合并之前,取数口径额外排除 `COMPLETED`,核单期这个集合本就为空、循环根本不执行,`settledOrderCount` 恰好落在 0,看起来「对」;#7407 把集合补全为「在团」之后,循环开始真正执行,上述恒不命中的判据才第一次产生可观察的错误结果。 + - **修复**:判据改为 `!Boolean.TRUE.equals(summary.getSettled())`。 + - **前端影响**:`settledOrderCount` 从「恒等于在团户数」变成「真实已核单户数」,**多数团期该数字会变小**。若前端有「已核单 / 在团」的进度条或百分比展示,数值会随之变化(变准确)。`subOrderTotalActualCost` / `subOrderTotalProfit` 的累加逻辑本身未变,只是现在只累加 `settled=true` 的户(此前判据虽然写的是「跳过未核单」,但因恒不命中,实际上从未跳过任何户,累加范围与本次修复后一致,金额侧数值不受影响,只有计数字段 `settledOrderCount` 变化)。 + - **契约**:请求与响应结构完全不变,仅 `settledOrderCount` 取值变化。 + +4. **部署与实测状态**:以上改动已合并 `dev-v3`(PR #7407 合并提交 `d951c0469` + PR #7408 合并提交 `aafc6ae48`),已部署 TEST 环境(`hl-order-service-v3` @ `aafc6ae48`,双实例 UP)并完成网关实测,实测记录见「八、测试环境已验证」。 + +前端无需改代码即可正常运行(权限拒绝走通用错误展示);如页面把 `requirement-summary.activeOrderCount` 文案写成「活跃子订单数」,建议改为「在团子订单数」;另建议为该面板补权限态隐藏与 589507 专属提示文案(非强制)。若前端把 `settlement/summary.settledOrderCount` 用作进度条分子,请确认分母仍取 `totalActiveOrderCount`(本身不受本次修复影响)。 + +**⚠️ 请 mmg 确认一件事(#7316 定案 10)**:请确认 hl-ui 调用 `requirement-summary` 的**页面与角色范围**。后端侧已核实:`group-batch:view` 在 `origin/dev-v3` 上**已被同一块团期看板的多个只读路径要求**(看板芯片 / 行程 / 合同 / 流团审批),因此能读这块看板的角色本来就持有该权限,本次是把最后一个漏网端点对齐;**若 mmg 发现某页面被挡,处置办法是给该角色补 `group-batch:view` 种子,不改后端代码**。 + +--- + +## 一、背景 + +`GroupBatchRequirementService#summary` 与 `GroupBatchSettlementService#summary`(D2 汇总)此前都调用 `OrderService#selectActiveOrderIdsByProductBatchId(productBatchId)` 取「活跃子订单」,其口径来自 assignment 域「还需派活」的语义(额外排除 `COMPLETED`)。团期进核单期后子订单大量变为 `COMPLETED`,导致两处汇总被整批滤掉。同时该只读端点透出的 `orderSpecialTags`(客户特殊需求)与逐日房型间数属敏感/运营数据,此前完全零权限校验。PR #7407(合并提交 `d951c0469`)一次性处理两处口径 + 补权限码。 + +PR #7407 合并后测试环境实测发现:`settlement/summary` 的 `settledOrderCount` 在「全团都没人核单」的场景下仍然显示等于在团户数(见「二、变更接口清单」#2 说明),根因是 `GroupBatchSettlementService#summary` 判「已核单」用的是 `summary == null`,而 `SettlementService.getSummary` 对未核单户从不返回 `null`(返回 `settled=false` 空壳)。PR #7408(合并提交 `aafc6ae48`)修正该判据,作为 #7316 的增补一并入同一工单。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|---|---|---|---|---| +| 1 | 全团需求汇总 | GET | `/v3/admin/order/group-batch/{groupBatchId}/requirement-summary` | 行为修改 | 新增权限码 `group-batch:view` 校验 + `activeOrderCount` 口径改为「在团」 | +| 2 | 团期结算 D2 汇总 | GET | `/v3/admin/order/group-batch/{groupBatchId}/settlement/summary` | 行为修改 | `totalActiveOrderCount` 口径改为「在团」(同 #7407)+ `settledOrderCount` 判据改用 `settled` 标志,不再判 `null`(#7408) | + +--- + +## 三、接口详情 + +### 1. 全团需求汇总 `GET /v3/admin/order/group-batch/{groupBatchId}/requirement-summary` + +**VO**: `无请求体 → GroupRequirementSummaryRespVO` + +#### 使用场景 + +团期看板「全团需求汇总」面板,团期管理员 / 定制师查看整团逐日房型需求、大巴座位需求、各户特殊需求汇总。调用前端必须持有权限码 `group-batch:view` 的角色(`ADMIN` / `FINANCE` / `SUPER_ADMIN`),否则拿到 589507。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| `groupBatchId` | Path | Long | 是 | 团期主键(雪花 ID) | 团期 ID | + +无 Body、无 Query 参数。 + +#### 出参字段表 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data.activeOrderCount` | int | **在团子订单数**(仅排除 `CANCELLED`,含 `COMPLETED`)——PR #7407 唯一改动的字段口径 | +| `data.hotelRequirementCount` | int | 已提交用房需求的子订单数 | +| `data.vehicleRequirementCount` | int | 已提交用车需求的子订单数 | +| `data.dailyRoomBreakdown` | Array\ | 逐日房间汇总(按 `dayNumber` 升序;不含客户自订晚 `customerSelfBooked=true`,某晚全团自订则无该晚条目) | +| `data.dailyRoomBreakdown[].dayNumber` | int | 行程天数(从 1 开始) | +| `data.dailyRoomBreakdown[].rooms` | Array\ | 该天各房型合计 | +| `data.dailyRoomBreakdown[].rooms[].roomCategory` | String | 房型名称(如 双床房 / 大床房) | +| `data.dailyRoomBreakdown[].rooms[].totalRoomCount` | int | 合计间数 | +| `data.vehicleSeatSummary` | Array\ | 大巴座位汇总(按车型) | +| `data.vehicleSeatSummary[].vehicleType` | String | 车型名称(如 大巴 / 商务车) | +| `data.vehicleSeatSummary[].totalSeats` | int | 合计座位数(`seats × count` 之和) | +| `data.vehicleSeatSummary[].totalCount` | int | 合计车辆台数 | +| `data.orderSpecialTags` | Array\ | 各子订单 `specialTags`(每户特殊需求) | +| `data.orderSpecialTags[].orderId` | Long | 子订单 ID | +| `data.orderSpecialTags[].requirementType` | String | 需求类型(`HOTEL` / `VEHICLE`) | +| `data.orderSpecialTags[].specialTags` | Array\ | 特殊需求标签列表 | + +#### 请求示例 + +```json +GET /v3/admin/order/group-batch/1001/requirement-summary +Authorization: Bearer <持 group-batch:view 的管理端 token> +``` + +(路径示例中的 `groupBatchId=1001` 取自单测 `GroupBatchRequirementSummaryTest` 的 `GROUP_BATCH_ID` 常量,非真实团期;真实网关实测使用的团期 ID 见「八、测试环境已验证」。) + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "activeOrderCount": 3, + "hotelRequirementCount": 2, + "vehicleRequirementCount": 1, + "dailyRoomBreakdown": [ + { "dayNumber": 1, "rooms": [ { "roomCategory": "双床房", "totalRoomCount": 3 } ] }, + { "dayNumber": 2, "rooms": [ { "roomCategory": "大床房", "totalRoomCount": 3 } ] } + ], + "vehicleSeatSummary": [ + { "vehicleType": "大巴", "totalSeats": 90, "totalCount": 2 }, + { "vehicleType": "商务车", "totalSeats": 14, "totalCount": 2 } + ], + "orderSpecialTags": [ + { "orderId": 100, "requirementType": "HOTEL", "specialTags": ["高楼层", "婴儿床"] } + ] + } +} +``` + +**取值来源说明**:以上字段结构与数值组合自源码仓库既有单测夹具(`GroupBatchRequirementControllerTest#requirementSummary_success_returns200`、`GroupBatchRequirementSummaryTest` 的逐日房间与大巴座位聚合用例),用于展示字段完整结构与嵌套形态,**不是同一次真实网关调用的输出**。本端点已在 TEST 环境完成真实网关实测(`code`/`activeOrderCount` 等具体数值见「八、测试环境已验证」),二者不是同一次调用,请以第八节的实测数值为准。 + +#### 空数据 / 降级响应 + +团期无在团子订单时(`selectInGroupOrderIdsByGroupBatchId` 返回空列表),`activeOrderCount` / `hotelRequirementCount` / `vehicleRequirementCount` 均为 `0`,`dailyRoomBreakdown` / `vehicleSeatSummary` / `orderSpecialTags` 均为空数组,返回 200(`GroupBatchRequirementSummaryTest#summary_noActiveOrders_returnsEmptySummary` 覆盖)。本接口不调用外部 Feign / MQ(权限校验除外),无第三方降级分支。 + +#### 错误响应 + +```json +{ "code": 589507, "message": "无操作权限(非团期管理员 / 非本定制师名下)", "success": false, "data": null } +``` + +```json +{ "code": 589500, "message": "团期不存在", "success": false, "data": null } +``` + +(589507 已在 TEST 环境网关实测复现,见「八、测试环境已验证」。) + +#### 业务边界 + +- 权限校验在 Controller 入口执行(`GroupBatchRequirementController#summary` 先调 `permissionGuard.require(...)` 再调 Service),先于任何业务逻辑,满足「事务内禁同步 Feign」。 +- 持 `group-batch:view` 的角色仅 `ADMIN` / `FINANCE` / `SUPER_ADMIN`;其余角色(含 `CUSTOMIZER`、`VEHICLE_MANAGER` 等)一律 589507。 +- `adminId` 或角色 key 缺失(未经网关鉴权透传)同样 fail-closed 直接拒绝 589507,不发 Feign。 +- 权限服务(user-service)Feign 异常 / 非成功响应 / 空值一律按无权限处理(fail-closed),不放行。 +- 团期不存在返回 589500,判定顺序在权限通过之后(Service 层 `groupBatchService.requireById` 判断)。 +- 「看」与「动」权限码不复用:本端点只读用 `group-batch:view`;需求确认 / 打回等写动作用 `group-batch:demand:confirm`(#7210)。 +- `activeOrderCount` 与「## 2. 团期结算 D2 汇总」的 `totalActiveOrderCount` 同口径(同一 `selectInGroupOrderIdsByGroupBatchId` 通道),两个端点的计数应始终一致。 + +--- + +### 2. 团期结算 D2 汇总 `GET /v3/admin/order/group-batch/{groupBatchId}/settlement/summary` + +**VO**: `无请求体 → GroupBatchSettlementSummaryRespVO` + +#### 使用场景 + +团期看板「结算」Tab,团期管理员 / 财务查看整团核单进度(已核单户数 / 在团户数)与成本、毛利、共享成本、预支汇总。**本端点无权限码要求**(当前登录态能过网关即可,不校验 `group-batch:view`,与「## 1.全团需求汇总」不同,见「业务边界」)。 + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|---|---|---|---|---|---| +| `groupBatchId` | Path | Long | 是 | 团期主键(雪花 ID) | 团期 ID | + +无 Body、无 Query 参数。 + +#### 出参字段表 `Result` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `data.settledOrderCount` | int | 已核单子订单数(`SettlementSummaryRespVO.settled=true` 的数量)——**PR #7408 唯一修复点**:判据改用 `settled` 标志,不再判 `summary==null`(旧判据恒不命中) | +| `data.totalActiveOrderCount` | int | 在团子订单总数(仅排除 `CANCELLED`,含 `COMPLETED`,与「## 1.」的 `activeOrderCount` 同口径)——PR #7407 随口径切换改动 | +| `data.subOrderTotalActualCost` | BigDecimal | 子订单实际成本合计(Σ `settlement_summary.totalActualCost`,仅累加 `settled=true` 的户) | +| `data.subOrderTotalProfit` | BigDecimal | 子订单毛利合计(Σ `settlement_summary.profitAmount`,仅累加 `settled=true` 的户) | +| `data.sharedCostTotal` | BigDecimal | 团期共享成本合计(Σ `order_batch_settlement.amount`) | +| `data.sharedCostByType` | Array\ | 团期共享成本按类型明细 | +| `data.sharedCostByType[].costType` | String | 成本类型(如 `BUS`) | +| `data.sharedCostByType[].costTypeDesc` | String | 成本类型展示标签(如 整团大巴) | +| `data.sharedCostByType[].total` | BigDecimal | 该类型成本合计 | +| `data.groupAdvanceApproved` | BigDecimal | 整团已拨付预支(scope=`GROUP_BATCH` 且 `APPROVED` 合计;不含子订单级;不计入 `grandTotalCost`) | +| `data.groupAdvancePending` | BigDecimal | 整团待审批预支(scope=`GROUP_BATCH` 且 `SUBMITTED` 合计;仅提示,不参与任何成本或毛利公式) | +| `data.grandTotalCost` | BigDecimal | 团期总成本(`subOrderTotalActualCost` + `sharedCostTotal`,不含预支) | +| `data.actualTravelerCount` | int | 实际出行人数(所有活跃子订单 adultCount+childCount+youngChildCount 之和) | +| `data.perPersonSharedCost` | BigDecimal | 人均共享成本(`sharedCostTotal` ÷ `actualTravelerCount`,出行人数为 0 时为 `null`) | + +#### 请求示例 + +```json +GET /v3/admin/order/group-batch/10/settlement/summary +Authorization: Bearer <任意已登录管理端 token,无需 group-batch:view> +``` + +(路径示例中的 `groupBatchId=10` 取自单测 `GroupBatchSettlementControllerTest`,非真实团期;真实网关实测使用的团期 ID 见「八、测试环境已验证」。) + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "success": true, + "data": { + "settledOrderCount": 3, + "totalActiveOrderCount": 4, + "subOrderTotalActualCost": "30000.00", + "subOrderTotalProfit": null, + "sharedCostTotal": "2000.00", + "sharedCostByType": [], + "groupAdvanceApproved": null, + "groupAdvancePending": null, + "grandTotalCost": "32000.00", + "actualTravelerCount": 15, + "perPersonSharedCost": "133.33" + } +} +``` + +**取值来源说明**:以上字段结构与数值取自源码仓库既有单测夹具(`GroupBatchSettlementControllerTest#summary_success_returnsSummary`),该夹具未设置 `subOrderTotalProfit` / `groupAdvanceApproved` / `groupAdvancePending`,故按未赋值字段的真实序列化结果标 `null`;用于展示字段完整结构,不是同一次真实网关调用的输出。本端点已在 TEST 环境完成真实网关实测,`settledOrderCount`(本次修复的字段)的实测数值见「八、测试环境已验证」,与本示例不是同一次调用,请以第八节为准。 + +#### 空数据 / 降级响应 + +团期无在团子订单时(`selectInGroupOrderIdsByGroupBatchId` 返回空列表),`settledOrderCount` / `totalActiveOrderCount` / `subOrderTotalActualCost` / `subOrderTotalProfit` 均为 `0`;团期共享成本合计 `sharedCostTotal` 独立计算,不受在团订单数影响(即使 0 个在团订单,已录入的共享成本仍会汇总进 `grandTotalCost`);实际出行人数为 `0` 时 `perPersonSharedCost` 为 `null`(不除零),`GroupBatchSettlementServiceTest#summary_zeroPeople_perPersonNull` 覆盖该场景。本接口不调用外部 Feign(除 `OrderService`/`SettlementService`/`GroupBatchAdvanceQueryService` 内部契约),无第三方降级分支。 + +#### 错误响应 + +```json +{ "code": 589500, "message": "团期不存在", "success": false, "data": null } +``` + +#### 业务边界 + +- 「已核单」判据只看 `SettlementSummaryRespVO.settled` 标志,不能判 `null`——`SettlementService.getSummary` 对查不到 `order_settlement_summary` 行的子订单返回的是 `settled=false` 的空壳 VO(仅 `orderId`/`advanceSummary` 有值),**从不返回 `null`**,这是 PR #7408 修复的唯一缺陷点。 +- `totalActiveOrderCount` 与「## 1.全团需求汇总」的 `activeOrderCount` 同口径(在团 = 仅排除 `CANCELLED`,含 `COMPLETED`),两者用同一条 `OrderService#selectInGroupOrderIdsByGroupBatchId(groupBatchId)`。 +- 本端点**无权限码校验**,任何能过网关鉴权的后台账号都能调用;与 `requirement-summary` 的判权状态不同步(一个要 `group-batch:view`、一个不要),这是既有设计,本次未新增或移除该端点的权限要求。 +- `subOrderTotalActualCost` / `subOrderTotalProfit` 只累加 `settled=true` 的子订单;未核单户是**跳过**不计入累加,不是计 `0` 累加(数学效果一致,但语义上是跳过)。 +- `groupAdvanceApproved` / `groupAdvancePending` 只计 scope=`GROUP_BATCH` 的团期级预支,不含子订单级(子订单级预支已进各户核单报销单,整团层再算一次会重复扣回);`groupAdvanceApproved`/`groupAdvancePending` 均不计入 `grandTotalCost`(预支是资金拨付不是成本)。 +- 团期不存在 → 589500,判定在 `groupBatchService.requireById` 完成。 + +--- + +## 四、契约约束与正确调用方式 + +### 全团需求汇总(`GET .../requirement-summary`) + +| 场景 | 结果 | +|---|---| +| ✅ 当前角色(token 角色)持 `group-batch:view` | 200,正常返回汇总 | +| ❌ 当前角色不持 `group-batch:view`(含库角色持但 token 角色未持的分叉场景) | 589507 | +| ❌ 未经网关鉴权(`X-Admin-Id`/角色缺失) | 589507(fail-closed,不发 Feign 判权) | +| ❌ `groupBatchId` 对应团期不存在 | 589500 | + +- 本端点无请求体,前端不需要拼装任何 payload,只需确保网关正确透传当前登录角色的鉴权头。 +- 若某角色此前能看到该面板但今后应该被拒绝(或反之),须去 `hl-user-service` 的 `admin_role_permission` 调整该角色是否持有 `group-batch:view`,接口本身不提供角色开关。 + +### 团期结算 D2 汇总(`GET .../settlement/summary`) + +| 场景 | 结果 | +|---|---| +| ✅ 任意已登录管理端角色调用(无需 `group-batch:view`) | 200,正常返回汇总 | +| ❌ `groupBatchId` 对应团期不存在 | 589500 | +| ⚠️ `settledOrderCount` 小于 `totalActiveOrderCount` | **正常现象**(本次修复后为真实已核单户数,不再恒等于在团户数),不是缺陷 | + +- 本端点无权限码限制,不需要为它单独申请或校验 `group-batch:view`。 +- 前端若用 `settledOrderCount / totalActiveOrderCount` 算「核单进度」百分比,修复后该比例会更小、更准确;无需改代码,只是数值会变化。 + +--- + +## 五、数据库行为 + +- 两个端点均只读,无写操作,无 Flyway、无表结构变更。 +- `requirement-summary` 的权限判定读 `hl-user-service` 的角色权限码(Feign `roleHasPermission`),非本服务库表;远端异常按失败关闭处理,不放行。 +- `activeOrderCount` / `totalActiveOrderCount` 统计口径底层改调 `OrderService#selectInGroupOrderIdsByGroupBatchId(groupBatchId)`:一跳直连查 `order_main.group_batch_id`(主通道,结果优先占位),若能解析出对应旧 `product_batch_id` 再查一次回退通道(仅补一跳没有覆盖、且该行 `group_batch_id IS NULL` 的订单),两者按 `orderId` 去重合并,全程只读、无写入。 +- `settledOrderCount` 判据依据 `SettlementSummaryRespVO.settled` 布尔标志,而非「该子订单是否存在 `order_settlement_summary` 行」的等价判断误用——`SettlementService.getSummary` 在查不到行时会构造并返回 `settled=false` 的空壳对象(非 `null`),本身不涉及任何写操作。 + +--- + +## 六、边界行为 + +- 未持 `group-batch:view` 调用 `requirement-summary` → 589507(fail-closed,先于业务逻辑)。 +- 团期不存在 → 两端点均 589500。 +- 团期无在团子订单 → `requirement-summary` 计数全 0、列表全空;`settlement/summary` 的 `settledOrderCount`/`totalActiveOrderCount`/子订单成本合计均为 0,共享成本合计独立计算不受影响,不报错。 +- `requirement-summary` 的逐日房间汇总不含客户自订晚(`customerSelfBooked=true`);某晚全团自订则该晚在 `dailyRoomBreakdown` 里无条目(既有规则,本次未改动)。 +- 权限服务(user-service)不可用时 `requirement-summary` 同样按无权限处理,不因下游故障放宽判权;`settlement/summary` 本身不依赖权限服务。 +- `settlement/summary` 的未核单户不计入 `settledOrderCount`,也不计入 `subOrderTotalActualCost`/`subOrderTotalProfit` 累加(跳过,不是计 0)。 + +--- + +## 六.5、枚举 / 数据字典 + +### 权限码 `group-batch:view` 的角色授权(来源:`hl-user-service` `sys_role.role_key` × `admin_role_permission`,迁移 `V20260831_002__add_group_batch_permissions.sql:16-25`) + +**所属字段**: Controller 入口 `permissionGuard.require(adminId, roleKey, GroupBatchPermissionGuard.PERMISSION_VIEW)` | **类型**: 角色 `role_key` 字符串 + +| role_key | 是否持有 `group-batch:view` | +|---|---| +| `ADMIN` | 是 | +| `FINANCE` | 是 | +| `SUPER_ADMIN` | 是 | +| 其余角色(如 `CUSTOMIZER` / `VEHICLE_MANAGER`) | 否 → 589507 | + +### `requirementType`(`GroupRequirementSummaryRespVO.OrderSpecialTagItem.requirementType`) + +**所属字段**: `data.orderSpecialTags[].requirementType` | **类型**: `String` + +| 值 | 含义 | +|---|---| +| `HOTEL` | 该子订单的用房需求 `specialTags` | +| `VEHICLE` | 该子订单的用车需求 `specialTags` | + +### `settled`(`SettlementSummaryRespVO.settled`,`GroupBatchSettlementService#summary` 判「已核单」唯一依据) + +**所属字段**: `SettlementService.getSummary(orderId)` 返回值内部字段(不直接出现在 `settlement/summary` 响应里,是 `settledOrderCount` 计数逻辑的判据) | **类型**: `Boolean` + +| 值 | 含义 | +|---|---| +| `true` | `order_settlement_summary` 存在该子订单的核单行,VO 其余成本/毛利字段均有值 | +| `false` | 查不到核单行,返回空壳 VO(仅 `orderId`/`advanceSummary` 有值,其余字段为 `null`)——**不是 `null` 本身**,PR #7408 之前误判 `null` 恒不命中 | + +--- + +## 六.6、修改前后对比 + +### 字段级对比 + +| 字段 | 改前 | 改后 | +|---|---|---| +| `requirement-summary.activeOrderCount` / `settlement/summary.totalActiveOrderCount` 语义 | 活跃子订单数(按 `productBatchId`,额外排除 `COMPLETED`,即 assignment 域「还需派活」口径) | 在团子订单数(按 `groupBatchId` 双通道并集,仅排除 `CANCELLED`,含 `COMPLETED`) | +| 取数方法 | `OrderService#selectActiveOrderIdsByProductBatchId(productBatchId)` | `OrderService#selectInGroupOrderIdsByGroupBatchId(groupBatchId)` | +| `requirement-summary` 权限要求 | 无 | 需要 `group-batch:view`,否则 589507 | +| `settlement/summary` 权限要求 | 无 | 无(未变) | +| `settlement/summary.settledOrderCount` 判据 | `summary == null` → continue(`getSummary` 从不返回 `null`,恒不命中) | `!Boolean.TRUE.equals(summary.getSettled())` → continue | +| 两端点响应结构(字段名 / 类型 / 嵌套) | — | **完全不变** | + +### 行为级对比 + +| 行为 | 改前 | 改后 | +|---|---|---| +| 团期核单期调用 `requirement-summary`/`settlement/summary`(子订单多为 `COMPLETED`) | 整批被旧活跃口径滤掉,汇总趋近全 0 / 空 | `COMPLETED` 计入在团,能正常显示 | +| 无 `group-batch:view` 的账号调用 `requirement-summary` | 200,能读到整团逐户 `specialTags`(客户特殊需求)与逐日房型间数 | 589507,拒绝读取 | +| 换过团的子订单(旧 `product_batch_id` 与当前团不一致) | 按 `productBatchId` 计入原(旧)团 | 按 `group_batch_id`(权威归属)计入新团,从原团移出 | +| 全团都没人核单的团调用 `settlement/summary` | `settledOrderCount` 恒等于在团户数(错误显示「全团已核单」) | `settledOrderCount` = 0(真实进度) | +| 部分核单的团调用 `settlement/summary` | `settledOrderCount` 恒等于在团户数(不反映真实核单进度) | `settledOrderCount` = 真实已核单户数(小于或等于在团户数) | + +--- + +## 六.7、影响评估 + +- **是否破坏向后兼容**:**部分**——① `requirement-summary` 新增权限,原来任意能过网关的后台账号都能调,现在非 `ADMIN`/`FINANCE`/`SUPER_ADMIN` 会从 200 变 589507;② `activeOrderCount`/`totalActiveOrderCount` 与逐日 / 逐车汇总的口径变化会让同一团期返回的数字发生变化(核单期由少变多;跨团换团的户可能使某些团期数字变大、另一些变小,双向皆有可能);③ `settlement/summary.settledOrderCount` 从「恒等于在团户数」变成「真实已核单户数」,**多数团期该数字会变小**。 +- **前端是否必须同步上线**:**否**——不同步页面仍可用:`requirement-summary` 权限拒绝会走通用错误提示展示后端 `message`;`settlement/summary` 契约结构不变,数值自动变准确,无需前端配合。只是非授权角色看到的是通用文案而非专属提示,且字段文案「活跃子订单数」与新语义不完全贴合。 +- **前端 workaround 清理点**:若前端曾因「团期进核单期后全团需求汇总常年空白」做过特殊兜底或隐藏逻辑,可在确认新口径部署生效后清理;若前端曾把 `settledOrderCount` 当作「总户数」展示或作为进度条分母,需确认分母取的是 `totalActiveOrderCount` 而不是 `settledOrderCount`(`settledOrderCount` 本身是分子,此前因缺陷恰好等于分母,掩盖了这个潜在误用,本次修复后会暴露出来)。 + +--- + +## 七、不影响范围 + +- 两个端点的请求参数、响应结构(字段名、类型、嵌套层级)本次完全不变,只变语义、取值口径与其中一个端点的鉴权。 +- 团期需求确认 / 打回三个端点(`confirm` / `confirm-check` / `reject`)使用的权限码仍是 `group-batch:demand:confirm`,不受本次影响。 +- `settlement/summary` 本身**没有新增权限码**,与 `requirement-summary` 的判权状态不同步是既有设计,本次未改变。 +- 无 Flyway、无表结构变更、无新端点、网关路由零改动,只涉及 `hl-order-service-v3` 单模块。 + +--- + +## 八、测试环境已验证 + +**环境**:测试服网关 `https://api.test.1814.love:9443`,`hl-order-service-v3` @ `aafc6ae48`(含 PR #7407 + #7408),双实例 UP。 + +| 场景 | 请求 | 结果 | +|---|---|---| +| 无权限 | CUSTOMIZER token 调 `requirement-summary`(团期 `2096412454643802114`) | `code=589507`,`data=null` | +| 有权限 | ADMIN token 调同一端点 | `code=200`,`activeOrderCount=55`、`hotelRequirementCount=53`、`vehicleRequirementCount=1` | +| CANCELLED 不计 | 同一团 72 单中 17 单 `CANCELLED` | `activeOrderCount=55`(72−17),5 个已知 CANCELLED 订单号在响应里 grep 命中数均为 0 | +| 全 COMPLETED 团 | 团期 `2097498512387104770`(3 户全部改成 `COMPLETED` 后) | `activeOrderCount=3`、`dailyRoomBreakdown` 非空、`orderSpecialTags` 非空;同一份数据按旧口径手算为 **0** | +| 已核单数 | 同一团,3 户中仅 1 户有 `order_settlement_summary` 行 | `settledOrderCount=1`(**不是** `totalActiveOrderCount=3`)、`subOrderTotalActualCost=3000.00`、`subOrderTotalProfit=7000.00` | + +**换团归属对照**(说明为什么部分团期数字会变): + +| 团期 | 旧口径手算 | 实际返回 | +|---|---|---| +| `2096495107078328322`(#7158验收班期) | 2 | **1**(该户已换团,从本团移出) | +| `2096510069465088002`(#7178验收班期) | 0 | **1**(该户权威归属是本团,移入) | + +**造数说明**:上述全 COMPLETED 团与核单行均为取证造数,取证后已全部删除并二次核对恢复。 + +--- + +## 十、相关文档 + +- 团期看板权限地基 #6902:`group-batch:list` / `group-batch:view` / `group-batch:export` 三权限码首次注册(`V20260831_002__add_group_batch_permissions.sql`)。 +- 团期需求确认 / 打回改造 #7210:`group-batch:demand:confirm` 权限码,同一 `GroupBatchPermissionGuard` 类。 +- 团期财务总览与预支 #7154:`groupAdvanceApproved` / `groupAdvancePending` 的 scope=`GROUP_BATCH` 预支口径来源。 + +--- + +## 关联 / 联系人 + +### 链接 + +- **Issue**: [#7316](https://git.1814.love:8443/wx/HL/issues/7316) +- **PR**: [#7407](https://git.1814.love:8443/wx/HL/pulls/7407)(合并提交 [d951c0469](https://git.1814.love:8443/wx/HL/commit/d951c046956051c41b2a6a7e92e36ef77019031d)) +- **PR**: [#7408](https://git.1814.love:8443/wx/HL/pulls/7408)(合并提交 [aafc6ae48](https://git.1814.love:8443/wx/HL/commit/aafc6ae48cbd83271f5904dbb3907dc98dd7c348)) + +### 联系人 + +- **后端负责人**: wx +- **待确认对象(前端 hl-ui)**: mmg——请确认 `requirement-summary` 调用方的页面与角色范围(见「⚠️ 关键变化」末尾)