--- schema: "hl-changelog/v2" ticket: "7250" title: "团期看板分页芯片透出 chipStats 计数(done/total/error),让「一户打回整格红」可解释" consumer: "admin" author: "jw(GIT)" change_type: "修改接口" backend_status: "deployed" gateway_status: "verified" frontend_status: "pending" frontend_owner: "mmg" frontend_ref: "" target_release: "" verified_at: "2026-09-07" status_note: "PR #7254 已合入 dev-v3(47c9a6d29),AC-1~9 网关实测通过(AC-6d 由 PR #7258 补齐)。本条发布即解除前端汇总工单第 5 项 chipStats 芯片 tooltip 的挂起;前端改动=六芯片悬停 tooltip 透出 done/total/error(颜色仍读 chips.X,chipStats 只解释红成什么程度;文案按 c39fb8a 口径,并经 2026-09-07 复审二次修正约/保异常文案为「{error} 户合同异常」「{error} 户保险异常」——原「合同已作废」「投保失败」以偏概全,后端合同 error 含作废中+已作废、保险 error 含已取消+投保失败,精确原因走明细 GB-ADM-094/095),消费方 src/stores/orderV2Batch.js fetchBatchPage。按「先完成任务清单」排在 #7067 U2-U7 之后,与汇总工单其余 4 项统一汇总审派发,本条保持 pending。" updated_at: "2026-09-07" base: "dev-v3" --- # 团期: 看板芯片透出 chipStats 计数 > **服务**: hl-order-service-v3 > **PR**: #7254 > **Issue**: #7250 > **日期**: 2026-09-07 > **影响范围**: 管理后台团期看板列表的六个配置芯片(房/车/导/摄/约/保) --- ## ⚠️ 关键变化 **只增字段,`chips` 一字未改。** 同一请求的行集、排序、`chips` 六项取值全部不变,老前端不改不报错。 新增 `records[].chipStats.{hotel|vehicle|guide|photo|contract|insurance} = {total, done, error}`。 **颜色一律以 `chips.X` 为准**,`chipStats` 只解释「红成什么程度」——现场 55 户里 1 户被打回, 芯片就整格红,运营看不出是 1 户还是 55 户出问题。 --- ## 一、背景 wx 2026-09-07 在测试服看团期看板,产品「冻干粉发短信给」10-01 期的「房」芯片整格红。 排查结论:该团期 55 户活跃子订单里有 1 户处于「驳回给定制师」,而聚合规则是 「任一户落在失败值集合即整格 ERROR」,且该判定优先于「进行中」。 规则本身是对的——被打回的户需要人工介入,必须显眼——但一个红块表达不出「1/55」还是「55/55」。 wx 拍板:**口径不改,透出计数**。 | | 改前 | 改后 | |---|---|---| | `chips.hotel` | `ERROR` | `ERROR`(**不变**) | | 能看出几户出问题 | 不能 | `chipStats.hotel = {total:55, done:0, error:1}` | --- ## 二、变更接口清单 | # | 接口 | 方法 | 路径 | 变更类型 | 说明 | |---|------|------|------|----------|------| | 1 | 团期看板分页 | GET | `/v3/admin/order/group-batch` | 响应新增字段 | 每行新增 `chipStats` 六芯片计数;`chips` 与其余字段不变 | --- ## 三、接口详情 ### 1. 团期看板分页 `GET /v3/admin/order/group-batch` **VO**: `GroupBatchPageItemRespVO` #### 使用场景 管理后台「团期订单」看板列表(GB-ADM-001)。前端在六个芯片上加悬停提示,用本次新增的三个数 说清「红成什么程度」。调用方 hl-ui `src/stores/orderV2Batch.js` 的 `fetchBatchPage`。 #### 入参 | 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | |------|------|------|------|------|------| | productId | Query | Long | ❌ | — | **入参一个都没变**。传值时走产品全班期合并基底 | | scope | Query | String | ❌ | ONGOING / FINISHED / ALL | 不变(#7189) | | opsStage | Query | String | ❌ | 八桶之一 | 不变 | | batchStatus | Query | String | ❌ | 团期九态 | 不变 | | month | Query | String | ❌ | yyyy-MM | 不变 | | keyword | Query | String | ❌ | — | 不变 | | deadlineFrom / deadlineTo | Query | String(date) | ❌ | — | 不变 | | pageNo / pageSize | Query | Integer | ❌ | 缺省 1 / 20 | 不变 | #### 出参 `Result>` | 字段 | 类型 | 说明 | |------|------|------| | records[].chipStats | Object | **新增**:六芯片计数,键与 `chips` 一一对应,固定返回全部 6 个键 | | records[].chipStats.{芯片}.total | Integer | 计入统计的户数(免闸户不计);零户行为 0 | | records[].chipStats.{芯片}.done | Integer | 已完成户数(房车导摄 DONE / 约 SIGNED / 保 INSURED) | | records[].chipStats.{芯片}.error | Integer | 失败户数(房/车:驳回给定制师、驳回给管理员;约:作废中、已作废;保:已取消、投保失败;导/摄无失败态恒 0) | | records[].chips 及其余全部字段 | — | **完全不变** | #### 请求示例 ```http GET /v3/admin/order/group-batch?productId=2044306857534636034&scope=ALL&pageSize=50 HTTP/1.1 Authorization: Bearer ``` #### 响应示例 ```json { "code": 200, "message": "成功", "success": true, "data": { "records": [ { "groupBatchId": "2096412454643802114", "batchLabel": "7", "batchName": "没,那你", "orderCount": 55, "chips": { "hotel": "ERROR", "vehicle": "TODO", "guide": "TODO", "photo": "TODO", "contract": "TODO", "insurance": "TODO" }, "chipStats": { "hotel": { "total": 55, "done": 0, "error": 1 }, "vehicle": { "total": 55, "done": 0, "error": 0 }, "guide": { "total": 55, "done": 0, "error": 0 }, "photo": { "total": 55, "done": 0, "error": 0 }, "contract": { "total": 55, "done": 0, "error": 0 }, "insurance": { "total": 55, "done": 0, "error": 0 } } } ], "total": 2, "page": 1, "pageSize": 20 } } ``` #### 空数据 / 降级响应 零子订单行(含未建团行 `groupBatchId=null`):三个计数全 0,**`chips` 取值沿用既有规则不变**。 ```json { "groupBatchId": null, "orderCount": 0, "chips": { "hotel": "TODO", "vehicle": "TODO", "guide": "TODO", "photo": "TODO", "contract": "TODO", "insurance": "TODO" }, "chipStats": { "hotel": { "total": 0, "done": 0, "error": 0 }, "vehicle": { "total": 0, "done": 0, "error": 0 }, "guide": { "total": 0, "done": 0, "error": 0 }, "photo": { "total": 0, "done": 0, "error": 0 }, "contract": { "total": 0, "done": 0, "error": 0 }, "insurance": { "total": 0, "done": 0, "error": 0 } } } ``` > 注意:零户行的 `chips` **不是恒 `TODO`**——团期处于 `TRIP_FINISHED` / `REVIEWING` / `SETTLED` 时, > 房车导摄走硬规则 2 得 `DONE`(对空单列表同样生效)。本次只统一零户的三个计数,不动状态。 #### 错误响应 ```json { "code": 589507, "message": "无操作权限(非团期管理员 / 非本定制师名下)", "success": false, "data": null } ``` **无新增错误码**。既有:589507 无权限、589515 传 productId 时产品域不可用。 #### 业务边界 - **只读接口**,无写操作、无新失败分支。 - **性能不退化**:失败计数并入既有 `countByChip` 的同一次遍历,芯片相关查询次数与改前一致; 顺带收敛了改前「`aggregateByChip` 调完 `countByChip` 后 `resolveAggregateStatus` 又算一遍」的重复计算, 总遍历次数不增反减。 - **`chips` 与 `chipStats` 同源**:一次 `aggregateAll` 同时产出,不存在两者打架的窗口。 - **覆盖场景**:见「四、契约约束」的例外矩阵——`error > 0` 不等于芯片一定红。 --- ## 四、契约约束与正确调用方式 ### ✅ 正确 / ❌ 错误消费方式 | 场景 | 做法 | |------|------| | ✅ 芯片颜色 | 一律读 `chips.X`,`chipStats` 只用于 tooltip | | ✅ 完成度文案 | 「已完成 {done}/{total}」 | | ✅ 异常文案 | 房/车「{error} 户已打回」、约「{error} 户**合同异常**」、保「{error} 户**保险异常**」(或统一「{error} 户异常」)。约/保刻意只写「异常」:合同异常含「作废中 / 已作废」两态,保险异常含「已取消 / 投保失败」两态,要精确原因请点进对应芯片明细(约 = GB-ADM-094、保 = GB-ADM-095) | | ❌ 把 done/total 说成「待配」 | `done` 是**已完成**户数,不是待办户数 | | ❌ 把合同异常、保险异常统称「打回」 | 三类失败语义不同,措辞按芯片区分:只有房/车的两个失败态才是「打回」 | | ❌ 把约的 error 写成「合同已作废」、保的 error 写成「投保失败」 | 以偏概全:约含「作废中 / 已作废」,正在作废会被说成已作废;保含「已取消 / 投保失败」,主动退保会被说成投保失败。统一写「合同异常」「保险异常」,精确原因走明细接口 | | ❌ 用 `total − done − error` 当「待配户数」 | 里面混着未开始、进行中、待审核三种;现场 55 户里 48 户 PENDING、1 户 PROCESSING、2 户待审核、3 户未开始、1 户打回,仅凭 `done=0/total=55/error=1` **推不出 48**。要精确分布请点进该芯片对应的逐户明细(GB-ADM-090~095,房 090 / 车 091 / 导 092 / 摄 093 / 约 094 / 保 095) | | ❌ 用 `error > 0` 推断颜色 | 见下方例外矩阵 | ### 例外矩阵:`error` 与 `chips.X` 何时合法背离 后端有三条状态覆盖规则,它们**都保留真实计数、只改状态**: | 场景 | 触发规则 | 该芯片 `error` | `chips.X` | |---|---|---|---| | 团期已流团(CANCELLED),仍有户处于失败态 | 硬规则 1:六芯片恒 `TODO` | > 0 | `TODO` | | 团期已出行完毕 / 核团中 / 已结算,房务仍有打回户 | 硬规则 2:房车导摄恒 DONE | > 0 | `DONE` | | 房车导摄未全部完成,合同或保险有异常户(约:作废中 / 已作废;保:已取消 / 投保失败) | 硬规则 3:约/保恒 `TODO` | > 0 | `TODO` | | 以上都未触发(普通分支) | — | > 0 | `ERROR`(**严格等价**) | 服务端只在普通分支做「`error > 0` 当且仅当 ERROR」的自校验,不一致记 ERROR 日志并以 `chips.X` 为准; 覆盖分支不校验、不记日志。 --- ## 五、数据库行为 **无表变更、无 Flyway、无 H2 schema 变更。** 本次只是把内存里已经算出来的数往外给。 --- ## 六、边界行为 - 未登录 → 401(网关拦截) - 无权限 → 589507 - 传 `productId` 时产品域不可用 → 589515 - 零子订单行 / 未建团行 → `chipStats` 六项 `{0,0,0}`,不为 null,前端不必判空 - 免闸户(`needs_hotel` 等为 false)→ 既不计 `total` 也不计 `error` --- ## 六.5、枚举 / 数据字典 ### 各芯片的失败值集合(决定 `error` 计数) **所属字段**: `chipStats.{芯片}.error` | **类型**: `Integer`(下表是被计入的状态值) | 芯片 | 计入 error 的状态值 | 来源列 | |----|------|------| | hotel / vehicle | `REJECTED_TO_CONSULTANT`、`REJECTED_TO_ADMIN` | `order_main.room_control_status` / `vehicle_control_status` | | contract | `VOIDING`、`VOIDED` | `order_main.contract_status` | | insurance | `CANCELLED`、`FAILED` | `order_main.insurance_status` | | guide / photo | 无失败态,`error` 恒 `0` | `order_main.guide_status` / `photographer_status` | ### 各芯片的完成值(决定 `done` 计数) | 芯片 | 计入 done 的状态值 | |----|------| | hotel / vehicle / guide / photo | `DONE` | | contract | `SIGNED` | | insurance | `INSURED` | ## 六.6、修改前后对比 ### 字段级对比 | 字段 | 改前 | 改后 | |------|------|------| | `records[].chips` | 六个 String | **完全不变** | | `records[].chipStats` | 不存在 | 六芯片 `{total, done, error}` | | 其余全部字段 | — | 不变 | ### 行为级对比 | 行为 | 改前 | 改后 | |------|------|------| | 芯片红色可解释性 | 只有一个 ERROR | 附带 total/done/error 三个数 | | 芯片相关查询次数 | N | **N(不变)** | | 单次聚合的订单遍历次数 | `countByChip` 一趟 + `resolveAggregateStatus` 内部又一趟 | **一趟**(顺带收敛) | ## 六.7、影响评估 - **是否破坏向后兼容**: 否。纯增字段,`chips` 与其余键的值逐项一致。 - **前端是否必须同步上线**: 否。不消费 `chipStats` 则表现与改前完全相同。 - **前端 workaround 清理点**: 无(此前前端没有可清理的兼容逻辑,只是显示不出细节)。 ## 七、不影响范围 - **仅影响**: `GET /v3/admin/order/group-batch` 的响应新增一个键 - **零影响**: - `GET /v3/admin/order/group-batch/board`(board 不含 `chipStats`) - `GET /v3/admin/order/group-batch/{id}` 团期详情 - `GET /v3/admin/order/group-batch/export` 导出(CSV 列未动) - `GET /v3/admin/order/group-batch/{id}/chips/{chip}` 芯片明细 GB-ADM-090~095(本就逐户给状态) - 聚合口径本身(`chips` 的判定规则一字未改) - 小程序、H5 全部接口 --- ## 八、测试环境已验证 ⏳ **尚未部署测试服,本条 changelog 未发布**(`backend_status: pending`)。 部署并过网关实测后,此处补真实请求响应片段与 ✓ 标记,同时把 `backend_status` 改为 `deployed`、 `gateway_status` 改为 `verified`、`verified_at` 填实测日期,再推送。 待测项(对应工单 AC): | AC | 待测项 | |----|--------| | AC-1 | `GET /v3/admin/order/group-batch?productId=2044306857534636034&scope=ALL&pageSize=50`:10-01 期行 `chips.hotel=ERROR` 且 `chipStats.hotel={55,0,1}`,与 `GET .../2096412454643802114/chips/hotel` 的 `totalCount`/`doneCount` 及 items 失败户数逐一相等 | | AC-2 | 同一响应里普通分支行满足严格等价;覆盖分支行按例外矩阵核对;服务端日志无计数不一致告警 | | AC-4 | 改前/改后同一请求逐项 diff,除新增键外完全一致 | | AC-5 | board / 详情 / 导出 / 092 / 093 响应不变 | | AC-8 | 开 SQL 日志请求一页 20 行,芯片相关查询次数与改前一致 | 本地证据(非测试环境):`JAVA_TOOL_OPTIONS=-Xmx3g mvn -o -pl hl-order-service-v3 -am test` 全量 **8734 例 Failures: 0**(7 例 Testcontainers 因本机无 Docker 报错,与基线一致); `GroupBatchChipResolverTest` 47 例(新增 16:AC-6 七例 + AC-6b 四例 + AC-6c 五例)、 `GroupBatchQueryServiceTest` 41 例(新增 2,含「芯片投影只查一次」断言)、 `GroupBatchConverterTest` 50 例(新增 1);ArchTest 六道门禁全绿。 --- ## 九、相关历史 PR | PR | Issue | 说明 | 是否仍有效 | |----|-------|------|------------| | #7207 | #7204 | 导/摄零指派落 `TODO` | ✅ 有效 | | #7192 | #7189 | 看板产品全班期基底与 scope;零户行 chips 口径统一 | ✅ 有效 | | **本 PR #7254** | **#7250** | 芯片透出 chipStats 计数,口径不改 | ✅ 最新 | --- ## 十、相关文档 - 关联 Issue: [wx/HL#7250](https://git.1814.love:8443/wx/HL/issues/7250) - 关联 PR: [wx/HL#7254](https://git.1814.love:8443/wx/HL/pulls/7254) - 前端待办:`changelogs-v2/2026-09/07_frontend_团期看板对齐后端契约待办汇总-前端优化-管理后台.md` 第 5 条(归 mmg,本篇发布后再动手) - 接口文档:`docs/group/团期模块接口文档-v2.0.html` GB-ADM-001、`docs/order-v3/api/API-SPEC.html` 已同步 ## 关联 / 联系人 ### 链接 - **Issue**: [#7250](https://git.1814.love:8443/wx/HL/issues/7250) - **PR**: [#7254](https://git.1814.love:8443/wx/HL/pulls/7254) - **Merge commit**: [47c9a6d29](https://git.1814.love:8443/wx/HL/commit/47c9a6d29) ### 联系人 - **后端负责人**: @jw - **前端负责人**: @mmg(tooltip,见前端汇总 changelog 第 5 条)