文件
hl-api-changelog/changelogs-v2/2026-09/13_7608_团期取消成团接manage判权-修改接口-管理后台.md
T
2026-09-13 13:11:26 +08:00

294 行
15 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
---
schema: "hl-changelog/v2"
ticket: "7608"
title: "团期取消成团接入 group-batch:manage 判权(589507),带 nacos 灰度开关"
consumer: "admin"
author: "jw(GIT)"
change_type: "修改接口"
backend_status: "deployed"
gateway_status: "verified"
frontend_status: "not_required"
frontend_owner: "mmg"
frontend_ref: ""
target_release: ""
verified_at: "2026-09-13"
status_note: "AC-2/AC-3/AC-6 与批 0 探针四项均在 TEST 网关实打通过(构建身份逐批探针确认)。对前端有一处硬影响:CUSTOMIZER / FINANCE / ROOM_MANAGER / VEHICLE_MANAGER 改前可正常取消成团,改后一律 589507,需按 group-batch:manage 隐藏「取消成团」按钮(与「成团」按钮同一个权限码),否则低权限角色会点到必然失败的按钮。响应结构与业务错误码不变,无需改解析逻辑。回滚不需重发服务:改 nacos group-batch.acl.enforce.cancel-group=false,实测约 7s 生效。 前端实证 not_required(2026-09-13, mmg):cancel-group 端点前端尚未接入(cancelGroup/取消成团 全仓零命中),无「取消成团」按钮可隐藏;本域既定口径为阶段门写操作按 batchStatus 白名单显隐+可见即可点+589507 兜底(batch/detail/index.vue:40-42,group-batch 按钮权限本就不接线),589507 由 request.js 拦截器统一透后端 message 天然覆盖。将来若接 cancel-group 按同口径即可,响应结构与业务错误码不变零额外改动。另三端点仅旁路观察对外零变化。"
updated_at: "2026-09-13"
base: "dev-v3"
---
# order-v3: 团期取消成团接入 `group-batch:manage` 判权
**服务**: hl-order-service-v3
**PR**: #7616
**Issue**: #7608
---
## ⚠️ 关键变化
🔴 **`cancel-group` 从「任何后台角色都能调」变为「必须持 `group-batch:manage`」,无权限返回 `589507`。** 改前**零判权**:定制师 / 房务 / 车务 / 财务任一持有效后台 token 的角色都能把一个已成团的团期打回招募中。
> **谁会受影响**:目前 `group-batch:manage` 只授予 `ADMIN` 与 `SUPER_ADMIN`(`V20260906_002` 种子)。`CUSTOMIZER` / `FINANCE` / `ROOM_MANAGER` / `VEHICLE_MANAGER` 在团期 9 个权限码上**零授权**,这些角色调用本端点将从改前的正常执行变为 `589507`。**若前端把「取消成团」按钮展示给了这些角色,需要按权限隐藏,否则用户会点到一个必然失败的按钮。**
🟡 **这是权限不对称的修复,不是新增限制。** 同一对按钮的另一半——**成团**(`POST .../group`)自 `#7158` 起就要求 `group-batch:manage`。改前的状态是:**低权限账号可以撤销高权限账号的动作**。
🟢 **响应结构、成功码、业务错误码全部不变。** 有权限的调用方(ADMIN / SUPER_ADMIN)行为与改前逐字节一致。
---
## 一、背景
`#7455`(P0,已完成)自称做了「全端点扫描」并收口了 15 个零判权端点,但**漏掉了 `GroupBatchActionController` 里的 4 个**。
根因是审计方法本身的缺陷:该方法「逐个 Controller 数 `GroupBatchPermissionGuard` 的命中数,命中 0 即无强制」——这是**按类计数**。`GroupBatchActionController` 因为 `advance` 与 `recheck-material-gate` 两处有 guard,**整个类被归入「已判权」**,于是同类里裸着的 4 个端点全部漏检。
> **推论**:任何「按类计数」的判权审计,都会在**混合类**(部分方法有守卫、部分没有)上系统性漏检。
本单是这 4 个端点收口的**第一批**。`cancel-group` 之所以能不等观察窗直接收口:同一个团期**必须先由持 `manage` 的人成团**才可能被取消,调用方集合可被静态证明是成团调用方的子集,不存在「收口后把真实调用方挡在外面」的风险。另外三个端点本批**只加旁路观察日志、不拦截**,对外行为零变化。
---
## 二、变更接口清单
| # | 接口 | 方法 | 路径 | 变更类型 | 说明 |
|---|------|------|------|----------|------|
| 1 | 取消成团 | POST | `/v3/admin/order/group-batch/:groupBatchId/cancel-group` | 修改 | **新增判权**:需 `group-batch:manage`,无权限 `589507`;响应结构与业务错误码不变 |
---
## 三、接口详情
### 1. 取消成团 `POST /v3/admin/order/group-batch/:groupBatchId/cancel-group`
**VO**: `无请求体(Result<Void>)`
#### 使用场景
hl-ui 管理后台团期详情页「取消成团」按钮:把已成团(`RESOURCE_PREPARING`)的团期打回招募中(`RECRUITING`),并重置四项资源就绪位与物资确认位。**本单起需要 `group-batch:manage` 权限。**
#### 入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|------|------|------|------|------|------|
| groupBatchId | Path | Long | ✅ | 运营团期主键 | **不是**产品侧排期 ID `productBatchId`(**行为不变**) |
无请求体。
#### 出参字段表
| 字段 | 类型 | 说明 |
|------|------|------|
| data | null | **结构完全不变**,`Result<Void>` |
#### 请求示例
```http
POST /v3/admin/order/group-batch/2098606570236481538/cancel-group HTTP/1.1
X-Admin-Id: 1001
X-Admin-Role: ADMIN
```
无请求体。
#### 响应示例
```json
{
"code": 200,
"message": "成功",
"data": null,
"success": true
}
```
#### 空数据 / 降级响应
无分页与列表语义,不存在空数据形态。
**降级**:灰度开关 `group-batch.acl.enforce.cancel-group` 置 `false` 时,判权**退化为旁路观察日志**(`GB_ACL_PROBE`)而非静默放行——此时所有角色恢复放行,行为与改前一致,但每次调用仍在服务端留痕。开关默认 `true`。
#### 错误响应
| 码 | 符号 | 触发 | 本单 |
|----|------|------|------|
| **589507** | `GROUP_BATCH_PERMISSION_DENIED` | 调用方角色不持有 `group-batch:manage` | 🆕 **本端点新增**(码本身早已存在) |
| 589500 | — | 团期不存在 | 不变 |
| 589501 | — | 团期状态不允许(非 `RESOURCE_PREPARING`) | 不变 |
| 589509 | — | 已有已确认子订单或已派单资源,禁止取消成团 | 不变 |
```json
{
"code": 589507,
"message": "无操作权限(非团期管理员 / 非本定制师名下)",
"data": null,
"traceId": null,
"success": false
}
```
#### 业务边界
- **拒绝时零写入**:判权在 Controller 入口,排在 Service 的任何库读写之前。被拒的请求不改 `order_group_batch` 任何列(含 `update_time`)、不写状态流水、不登记 Fleet 释放命令。
- **判权落点显式定在 Controller**:本类的 `advance`、`recheck-material-gate` 已是这个形状。统一判在入口,让「读 Controller 就能判有没有守卫」重新成立——这正是 `#7455` 漏检的方法学根因。
- **超管短路放行**:user-service 对 `SUPER_ADMIN` 短路放行,因此**用超管自测无法发现种子缺失**。本单的验收用 `ADMIN` + 低权限角色实打(见第八节)。
- **本批不收口的三个端点**:`withdraw` 提交、`transfer-in`、`transfer-candidates` 本批**只观察不拦截**,对外行为零变化。它们的收口分别在批 2 / 批 3,需先拿到观察窗数据、且 `withdraw` 需要 user-service 新增权限码种子。
---
## 四、契约约束与正确调用方式
- 前端应按 `group-batch:manage` 控制「取消成团」按钮的可见性/可用性,与「成团」按钮**用同一个权限码**(两者本就是同一对按钮)。
- `589507` 建议直接透出 `message` 文案,无需前端自造提示。
- 权限码与角色绑定**没有运行期写入口**(只能走 Flyway 种子),前端不要尝试动态查询角色权限表来做按钮控制,应使用现有的权限码下发通道。
---
## 五、数据库行为
**无 DDL、无数据迁移。** 本单不含任何 Flyway 脚本。
权限码 `group-batch:manage` 与其角色授予(`ADMIN` / `SUPER_ADMIN`)由既有种子 `V20260906_002` 提供,**user-service 零改动**。
---
## 六、边界行为
- 有权限的调用方:与改前逐字节一致。
- 无权限的调用方:改前执行成功,改后 `589507` 且零写入。
- 角色头缺失:`GroupBatchPermissionGuard` 是 **fail-closed**,`roleKey` 为空一律 `589507`(**行为不变**,该守卫既有口径)。
- user-service 不可达:权限查询 `catch (RuntimeException) → false`,即 **fail-closed**,返回 `589507`。
## 六.6、修改前后对比
| 角色 | 改前 | 改后 |
|------|------|------|
| `SUPER_ADMIN` | 正常执行 | 正常执行(不变) |
| `ADMIN` | 正常执行 | 正常执行(不变) |
| `CUSTOMIZER` | **正常执行** | **589507** |
| `FINANCE` | **正常执行** | **589507** |
| `ROOM_MANAGER` / `VEHICLE_MANAGER` | **正常执行** | **589507** |
## 六.7、影响评估
- **前端**:需按权限隐藏按钮,否则低权限角色会点到必然失败的按钮。响应结构无变化,无需改解析逻辑。
- **后端**:无下游契约变化。
- **回滚**:改 nacos `group-batch.acl.enforce.cancel-group: false`,约 30s 生效,**不需要重发服务**。
---
## 七、不影响范围
- 成团 / 流团 / 名额调整 / 预支 / 物料门复判五个端点:判权口径不变。
- `withdraw` 提交 / `transfer-in` / `transfer-candidates`:本批只加观察日志,**对外行为零变化**。
- 小程序端(`consumer: mp`):本单端点均为管理后台端点,小程序无影响。
---
## 八、测试环境已验证
> **构建归属**:TEST order-v3 部署 `fix/7608-groupbatch-action-acl` @ `ac4fbf0f7`(面板 `git-backend` 确认)。每批取证前后各打一次构建身份探针:`CUSTOMIZER` 调 `cancel-group` 返回 `589507` —— **这个码在 dev-v3 上根本产不出来,它本身就是构建指纹**,可排除「部署假成功、跑的是陈旧产物」。
### 判权收口生效(角色矩阵实打)
对**不存在**的 `groupBatchId` 发请求(不产生任何写入),看返回码即可判定:
| 角色 | 改前(dev-v3) | 改后 | 判定 |
|---|---|---|---|
| `SUPER_ADMIN` | 589500 | **589500** | 过守卫,进业务校验 |
| `ADMIN` | 589500 | **589500** | 过守卫,进业务校验 |
| `CUSTOMIZER` | 589500 | **589507** | 被拦 |
| `FINANCE` | 589500 | **589507** | 被拦 |
**没有只用超管自测**:user-service 对 `SUPER_ADMIN` 短路放行,只用超管验会掩盖种子缺失。上表用 `ADMIN` + 两个低权限角色实打。另查 SQL 确认 `group-batch:manage` 状态 `ACTIVE` 且只绑 `ADMIN` / `SUPER_ADMIN`,无种子缺失。
### 零写入(真实团期,改前/改后逐列对拍)
用自建团期 `2098979230774657025`(`RESOURCE_PREPARING`,四个 ready 位全 0,零已确认子订单 —— `cancelGroup` 三条硬前置全满足,**即「守卫若不拦,这次调用本会真的写进去」**):
| 项 | 结果 |
|---|---|
| `CUSTOMIZER` 返回码 | **589507** |
| `order_group_batch` 改前/改后 | **41 列逐列完全一致**,无差异列 |
| `update_time` | **未变**(`11:37:19` → `11:37:19`) |
| `group_batch_status_log` | **新增 0 行**(2 → 2) |
> 这条必须用**真能被取消**的团期。拿一个取消不了的团期做,守卫不拦它也写不进去,零写入的证明是空的。
### 有权限仍可用(非超管 ADMIN)
同一团期、同一端点,`ADMIN` token → **200**,`RESOURCE_PREPARING` → `RECRUITING`,`update_time` `11:37:19` → `11:37:52`,状态流水 +1 行(`BATCH_CANCEL_GROUP` / operator `1001`)。
> 四个 ready 位的「重置」在本路径上**结构性不可观测**:R10 硬前置要求它们进来时就全 0,重置是 0→0,不可能看到 delta。这是接口语义决定的,不是没验。
### 灰度开关开/关两态(回滚演练)
nacos `hl-order-service-v3-test.yml`(`tenant=test`):
| 时刻 | 动作 | `CUSTOMIZER` 调用结果 |
|---|---|---|
| 11:51:40 | 写入 `group-batch.acl.enforce.cancel-group: false` | — |
| 11:51:47 | 写入后 **7s** | **589500**(恢复放行) |
| 11:51:55 | 写回原文 | — |
| 11:52:02 | 还原后 **7s** | **589507**(收口恢复) |
配置已逐字节还原(md5 与原文相同,`group-batch` key 已消失),**TEST 未被留在关闭态**。实测热刷新 **≈7s**,代码注释里的「30s」是保守上界。
### 三个旁路观察端点:行为零变化(前后对照)
`withdraw` / `transfer-in` / `transfer-candidates` 各用 `CUSTOMIZER`、`FINANCE` 打一次,共六发:
| | 改前(dev-v3,无探针) | 改后(有探针) |
|---|---|---|
| 六发结果 | 全部 `589500` | 全部 `589500` |
| 400 / 500 / 589507 | 无 | 无 |
**逐个对上,零差异** —— 证明新增的观察探针没有把正常请求打挂(探针内吞 `RuntimeException`,且 `has()` 的 Feign 跳是 fail-closed 不外抛)。
> `transfer-in` 带齐了必填字段(含 `fromGroupBatchId`),返回 589500 说明**已进到 Controller 之后**,不是被 `@Valid` 的 400 挡在门外 —— 黑盒探测判权时这一步必须做,否则 400 会掩盖守卫、把有守卫的端点误判成裸奔。
### 观察探针端到端(`GB_ACL_PROBE`,合并进 dev-v3 后补采)
三个旁路端点实际产出的观察日志(`enforced=false` 表示只观察不拦截):
| 端点 | roleKey | `intendedPermission` | `hasPermission` |
|---|---|---|---|
| `GB-ADM-070 withdraw-submit` | `CUSTOMIZER` / `FINANCE` | `group-batch:manage` | **false** |
| `GB-ADM-070 withdraw-submit` | `SUPER_ADMIN` | `group-batch:manage` | **true** |
| `GB-ADM-071 transfer-in` | `ADMIN` | `group-batch:manage` | **true** |
| `GB-ADM-071b transfer-candidates` | `CUSTOMIZER` / `FINANCE` / `ADMIN` | **`(not-evaluated)`** | — |
两点被这张表钉死:
1. **探针的判定与真实权限模型一致** —— 低权限角色 `false`、管理角色 `true`,说明观察窗收上来的数据可以直接作为批 2 / 批 3 选码的依据。
2. **`transfer-candidates` 确实不发 Feign** —— `intendedPermission=(not-evaluated)`。它是转单弹窗的搜索下拉,用户每敲一个字就打一次,走 Feign 判权会同时拖慢下拉并放大 user-service 压力;观察阶段只需要知道「哪些角色在调」。
### 本地全量单测(四数对照)
| 检出 | Tests / Failures / Errors / Skipped |
|---|---|
| 基线 `cb293d2ab`(本分支 merge-base) | `10220 / 0 / 13 / 51` |
| 本分支 `ac4fbf0f7` | **`10232 / 0 / 13 / 51`** |
`+12` 逐条可归因:`+6` = 本单新增的 6 条用例;`+6` = 合并 dev-v3 带进来的 `VehicleRequirementKindErrorCodeMessageTest`(实测正好 6 条)。**Failures / Errors / Skipped 三项零变化**,13 条 Errors 的类名集合完全相同,全部是 Testcontainers 无 Docker 环境所致,与本单无关。
### 未采集到的证据(如实列出)
- **AC-1 的 roleKey × endpoint × 次数观察表**:本单只负责把探针种下去,统计窗 ≥2 周,表本身不在本单产出。
> 采集说明:部署面板对 order-v3 配了分支白名单(`service-ports.conf`),**检出切到特性分支时 order-v3 会从 `/api/services` 消失、日志端点 404**,所以日志类证据是在合并进 `dev-v3` 并部署 `dev-v3` 之后补采的。另:面板日志端点**只读 primary 实例**,双实例轮询会漏,需多打几轮才能覆盖全。
---
## 十、相关文档
- Issue `#7608`(含 58 端点判权清点、网关三层排除、FINANCE 三步可达链)
- PR `#7616`
---
## 关联 / 联系人
- 后端:jw
- 前端:mmg