diff --git a/changelogs-v2/2026-09/27_7608_团期转订单判权收口批2-修改接口-管理后台.md b/changelogs-v2/2026-09/27_7608_团期转订单判权收口批2-修改接口-管理后台.md new file mode 100644 index 00000000..bbfdcfd3 --- /dev/null +++ b/changelogs-v2/2026-09/27_7608_团期转订单判权收口批2-修改接口-管理后台.md @@ -0,0 +1,334 @@ +--- +schema: "hl-changelog/v2" +ticket: "7608" +title: "团期转订单收口判权:transfer-in 接 group-batch:manage、transfer-candidates 接 group-batch:view(589507),各带 nacos 灰度开关" +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-27" +status_note: "已部署 dev-v3 到 TEST 并实打验证:五角色矩阵逐格吻合、nacos 两开关三态往返(含还原后复现)、观察日志反证强制态未走旁路。" +updated_at: "2026-09-27" +base: "dev-v3" +--- + +# order-v3: 团期转订单两个端点收口判权(批 2) + +**服务**: hl-order-service-v3 +**PR**: `#8416`(已合入 `dev-v3`,合并后 head `22f9fb83d`) +**Issue**: #7608 + +--- + +## ⚠️ 关键变化 + +🔴 **`transfer-in`(转订单写口)从「任何后台角色都能调」变为「必须持 `group-batch:manage`」,无权限返回 `589507`。** 改前**零判权**,而它**涉钱**:改应收金额、改归属两列、改客户出行日期、动两期名额,且**没有反向端点**。 + +🔴 **`transfer-candidates`(转单弹窗的搜索下拉)从「连权限码都不判」变为「必须持 `group-batch:view`」。** 改前它跨团期返回订单号 / 客户名 / 期号,并支持手机号整串精确检索,而且是上面那个写口的**攻击目标供给器**。 + +🟡 **两个端点必须成对看**:只收口写口会让弹窗「能搜不能转」,只收口读口会「能转搜不到」。本单同批上线。 + +🟢 **响应结构、成功码、业务错误码全部不变。** 有权限的调用方行为与改前逐字节一致。 + +--- + +## 一、背景 + +`#7455` 自称做了「全端点扫描」,但漏掉 `GroupBatchActionController` 里的 4 个端点。根因是审计方法按**类**计数 `GroupBatchPermissionGuard` 命中数——该类因 `advance` / `recheck-material-gate` 两处有 guard 就被整类归入「已判权」。 + +收口分四批:批 0 给裸端点加旁路观察探针;批 1 收口 `cancel-group`(已上线);**本单是批 2**;批 3(`withdraw` 提交)待定案。 + +**为什么批 2 要等**:码的选择依赖批 0 的观察窗。观察窗 2026-09-13~09-27 满 14 天,结论是这两个端点在留存日志里**零调用、无意外调用方**,故按静态推导定案。 +⚠️ 这是「**没有反证**」而非「**有正证**」:TEST 上 order-v3 跑两个实例,8086 实例 09-13~09-25 的日志已不可取(落点随 09-23 端口迁移挪过)。对冲手段就是下面两个 nacos 开关。 + +--- + +## 二、变更接口清单 + +| # | 接口 | 方法 | 路径 | 变更类型 | 说明 | +|---|------|------|------|----------|------| +| 1 | 转订单(子订单跨期转入) | POST | `/v3/admin/order/group-batch/:groupBatchId/sub-order/:orderId/transfer-in` | 修改 | **新增判权**:需 `group-batch:manage`,无权限 `589507`;响应结构与业务错误码不变 | +| 2 | 转订单候选子订单列表 | GET | `/v3/admin/order/group-batch/:groupBatchId/transfer-candidates` | 修改 | **新增判权**:需 `group-batch:view`,无权限 `589507`;响应结构不变 | + +--- + +## 三、接口详情 + +### 1. 转订单 `POST /v3/admin/order/group-batch/:groupBatchId/sub-order/:orderId/transfer-in` + +**VO**: `TransferSubOrderReqVO` → `Result` + +#### 使用场景 + +管理后台团期详情「转订单」弹窗:把另一期的子订单转入本期。**本单起需要 `group-batch:manage` 权限。** + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 转入期(本期)团期主键 | **行为不变** | +| orderId | Path | Long | ✅ | 待转入的子订单 ID | **行为不变** | +| fromGroupBatchId | Body | Long | ✅ | 转出期团期主键 | **行为不变** | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | `TransferSubOrderRespVO` | **结构完全不变** | + +#### 请求示例 + +```http +POST /v3/admin/order/group-batch/2098606570236481538/sub-order/2103877643344691202/transfer-in HTTP/1.1 +X-Admin-Id: 1001 +X-Admin-Role: ADMIN +Content-Type: application/json + +{"fromGroupBatchId": 2098979230774657025} +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": {}, + "success": true +} +``` + +#### 空数据 / 降级响应 + +无分页语义。**降级**:nacos `group-batch.acl.enforce.transfer-in` 置 `false` 时,判权**退化为旁路观察日志**(`GB_ACL_PROBE`)而非静默放行——所有角色恢复放行,行为与改前一致,但每次调用仍在服务端留痕。开关默认 `true`。 + +#### 错误响应 + +| 码 | 符号 | 触发 | 本单 | +|----|------|------|------| +| **589507** | `GROUP_BATCH_PERMISSION_DENIED` | 调用方不持 `group-batch:manage` | 🆕 **本端点新增**(码本身早已存在) | +| 589500 | — | 团期不存在 | 不变 | +| 其它业务码 | — | 转入的各项业务前置 | 不变 | + +`589507` 实打响应体(TEST,2026-09-27): + +```json +{ + "code": 589507, + "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- **拒绝时零写入**:判权在 Controller 入口,排在 Service 任何库读写之前。 +- **`OrderViewGuard.assertNotGroupBatchManagerWrite()` 保留**:#8154 起团期管理员(GBM)「只能看不能改」,那道拦在判权之前、只挡这一个角色,两者并存不互相取代。 +- **取 `manage` 不取 `view`**:本端点涉钱且无反向端点,取写向最高码。 + +--- + +### 2. 转订单候选列表 `GET /v3/admin/order/group-batch/:groupBatchId/transfer-candidates` + +**VO**: `Result>` + +#### 使用场景 + +转订单弹窗的搜索下拉:候选 = 同产品其他可转期下的可转子订单。keyword 匹配订单号 / 客户名 / 期号(模糊)与手机号(**整串精确**,加密列不支持前缀模糊)。**本单起需要 `group-batch:view` 权限。** + +#### 入参字段表 + +| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 | +|------|------|------|------|------|------| +| groupBatchId | Path | Long | ✅ | 转入期(本期)团期主键 | **行为不变** | +| keyword | Query | String | ❌ | 订单号 / 客户名 / 期号 / 手机号(整串) | **行为不变** | + +#### 出参字段表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| data | `List` | **结构完全不变** | + +#### 请求示例 + +```http +GET /v3/admin/order/group-batch/2098606570236481538/transfer-candidates?keyword=GT-26-0085 HTTP/1.1 +X-Admin-Id: 1001 +X-Admin-Role: ADMIN +``` + +#### 响应示例 + +```json +{ + "code": 200, + "message": "成功", + "data": [], + "success": true +} +``` + +#### 空数据 / 降级响应 + +无候选时 `data: []`(**行为不变**)。**降级**:nacos `group-batch.acl.enforce.transfer-candidates` 置 `false` 时退回批 0 形态——只记角色、不发 Feign 判权。 + +#### 错误响应 + +| 码 | 符号 | 触发 | 本单 | +|----|------|------|------| +| **589507** | `GROUP_BATCH_PERMISSION_DENIED` | 调用方不持 `group-batch:view` | 🆕 **本端点新增** | + +`589507` 实打响应体(TEST,2026-09-27): + +```json +{ + "code": 589507, + "message": "无操作权限(当前角色未授予团期权限,或该团期不在您名下)", + "data": null, + "traceId": null, + "success": false +} +``` + +#### 业务边界 + +- **拒绝时不查库**:判权排在查询之前,被拒的请求拿不到任何候选数据。 +- ⚠️ **本单推翻了批 0 自己写的「高频下拉不判权」**:那条理由(逐字触发、每次多一跳 Feign 到 user-service)成立,但挡不住泄露面。真实测出下拉不可接受时,出路是给守卫加短 TTL 本地缓存(**另立单**),不是把开关关掉长期不判。 + +--- + +## 四、契约约束与正确调用方式 + +- 前端应按权限码控制「转订单」入口:写口按 `group-batch:manage`,搜索下拉按 `group-batch:view`。 +- `589507` 建议直接透出 `message`,无需前端自造提示。 +- 权限码与角色绑定**没有运行期写入口**(只能走 Flyway 种子)。 + +--- + +## 五、数据库行为 + +**无 DDL、无数据迁移、无 Flyway 脚本。** + +两个码及其角色授予由既有种子提供:`group-batch:manage`(`V20260906_002`:ADMIN / SUPER_ADMIN)、`group-batch:view`(`V20260831_002` ADMIN/FINANCE/SUPER_ADMIN + `V20260917_001` GROUP_BATCH_MANAGER + `V20260918_005` CUSTOMIZER)。**hl-user-service 零改动** ⇒ 不涉及 #7154 那条「user-service 迁移必须先发」的铁律。 + +--- + +## 六、边界行为 + +- 有权限的调用方:与改前逐字节一致。 +- 无权限的调用方:改前执行成功,改后 `589507` 且零写入 / 不查库。 +- 角色头缺失:`GroupBatchPermissionGuard` **fail-closed**,`roleKey` 为空一律 `589507`(既有口径,行为不变)。 +- user-service 不可达:权限查询 `catch (RuntimeException) → false`,即 **fail-closed**。 + +## 六.6、修改前后对比 + +| 角色 | `transfer-in` 改前 → 改后 | `transfer-candidates` 改前 → 改后 | +|------|---|---| +| `SUPER_ADMIN` | 正常 → 正常 | 正常 → 正常 | +| `ADMIN` | 正常 → 正常 | 正常 → 正常 | +| `FINANCE` | **正常 → 589507** | 正常 → 正常(持 view) | +| `CUSTOMIZER` | **正常 → 589507** | 正常 → 正常(`V20260918_005` 起持 view) | +| `GROUP_BATCH_MANAGER` | 589xxx(#8154 已挡)→ 不变 | 正常 → 正常(持 view) | +| `ROOM_MANAGER` / `VEHICLE_MANAGER` | **正常 → 589507** | **正常 → 589507** | + +## 六.7、影响评估 + +- **前端**:需按两个码分别控制写口与下拉的可见性,否则无码角色会点到必然失败的入口。响应结构无变化,无需改解析逻辑。 +- **后端**:无下游契约变化。`transfer-candidates` 每次调用多一跳 Feign(逐字下拉,可感知)。 +- **回滚**:改对应 nacos key 为 `false`,批 1 实测热刷新 ≈7s,**不需重发服务**。 + +--- + +## 七、不影响范围 + +- `cancel-group`(批 1 已收口)、成团 / 流团 / 名额调整 / 预支 / 确认:判权口径不变。 +- `withdraw` 提交:**本批不动**,仍只有旁路观察。它的收口是批 3,因为「提交集合是否该等于审批集合」需要定案(接 `manage` 会让两个集合相同,毁掉 #7100 建立的两人制)。 +- 小程序端(`consumer: mp`):本单端点均为管理后台端点,无影响。 + +--- + +## 八、测试环境已验证 + +**环境**:TEST(`https://api.test.1814.love`) **验证时间**:2026-09-27 11:31~11:37 +**构建身份**:部署前 `ecc92b95c`(旧字节,批 0 形态)→ 部署后 `22f9fb83d`(本单合并提交),部署前后各打一轮,故下表是**对照**而非单点。 + +### 8.1 五角色矩阵(部署后,强制态) + +| 角色 | `transfer-in`(接 `manage`) | `transfer-candidates`(接 `view`) | +|---|---|---| +| SUPER_ADMIN | `589500` 过守卫 | `589500` 过守卫 | +| ADMIN | `589500` 过守卫 | `589500` 过守卫 | +| CUSTOMIZER | **`589507`** 拦截 | `589500` 过守卫 | +| FINANCE | **`589507`** 拦截 | `589500` 过守卫 | +| ROOM_MANAGER | **`589507`** 拦截 | **`589507`** 拦截 | + +部署前同一组请求:**十格全部非 `589507`**(旧字节只旁路观察不拦截),故新增拦截可归因到本单字节。 + +> **为什么这组读数能证明码选对了**:CUSTOMIZER 在读口过、在写口被拦。若 `transfer-candidates` 误接 `manage`,CUSTOMIZER 读口也会被拦;若误接更宽的口径,ROOM_MANAGER 读口不会被拦。这一格同时验证了**码的选择**和 `V20260918_005` 给 CUSTOMIZER 授 `view` 的**种子实际状态**。 +> **未只用超管自测**:ADMIN 过、CUSTOMIZER/FINANCE/ROOM_MANAGER 被拦,均为非超管角色,绕开了 user-service 的超管短路放行。 + +### 8.2 nacos 灰度开关三态往返 + +配置 `hl-order-service-v3-test.yml`(`tenant=test`)原本**不含** `group-batch.acl.*` 键,即跑代码默认值 `true`。 + +| 态 | 配置 | CUSTOMIZER `transfer-in` | ROOM_MANAGER `transfer-candidates` | ADMIN(对照) | `GB_ACL_PROBE` 观察日志 | +|---|---|---|---|---|---| +| A | 无键(默认 `true`) | `589507` | `589507` | `589500` | **无** | +| B | 两键置 `false` | `589500` | `589500` | `589500` | **有** | +| C | 逐字节还原 | `589507` | `589507` | `589500` | **无** | + +- 热刷新耗时:发布后 12s 内生效(态 A→B 时刻 11:36:38→11:36:52)。 +- **还原已校验到字节**:`md5` 前后同为 `c2206934960057f70b7173159046dc54`,`cmp` 一致;还原动作挂在 `trap EXIT` 上,探针失败也不会把 TEST 留在 `false`。 + +**态 B 的放行是「退回旁路观察」而不是「判权整体失效」**——两条方向相反的证据: + +1. 正面:态 B 期间日志记下 + `endpoint=GB-ADM-071 transfer-in roleKey=CUSTOMIZER intendedPermission=group-batch:manage hasPermission=false enforced=false` + 即「本应判 `manage`、该角色实际不持有、但当前未强制」。守卫算过了权限,只是没拦。 +2. 反面:态 A 与态 C 的时间窗内 `GB_ACL_PROBE` **零命中**。若强制分支底下还在跑观察,这里会有日志。零命中说明走的是 `require()` 而非 `observe()`,`if/else` 是真互斥。 + +> 附带加固 8.1:态 B 六条日志里 `transfer-in` 落 `http-nio-8186`、`transfer-candidates` 落 `http-nio-8086`,**两个 order-v3 实例都是新字节**,8.1 的矩阵不是「只有一个实例部署到」的假象。 +> `transfer-candidates` 的观察日志记 `intendedPermission=(not-evaluated)`,与代码里用 `observeRoleOnly()`(不查权限只记角色)一致。 + +### 8.3 本地全量对照(AC-10) + +| 项 | 读数 | +|---|---| +| 范围 / 结果 | 1000 个测试类,`Tests run: 13614, Failures: 11, Errors: 10, Skipped: 7`,零 OOM,31 分钟 | +| 定责 | 10 个失败类**全部**在基底 `57199b539` 上同样红,逐类比对 tests/F/E 一致 → 无一条由本单引入 | +| 其中 3 类同源 | `fin_reimburse` 列未同步进 `integration/schema.sql`(`#8362` 遗留,finance 域,**不在本单范围**,建议另开单) | + +判据按 `#8370` 立的口径:**「不引入新失败 + 既有失败逐条定责」**,而非「全量全绿」——基底每天在变,小修单担保不了全绿。 + +### 本地证据(已完成) + +| 项 | 读数 | +|---|---| +| `GroupBatchActionControllerTest` | **27/0/0/0**(批 1 的 21 条 + 批 2 新增 6 条) | +| 变异①:`transfer-in` 的 MANAGE→VIEW(接错码) | 红 1 条(判「码对不对」那条) | +| 变异②:`transfer-candidates` 的 `require` 删掉(漏接) | 红 2 条,含 `expected:<589507> but was:<200>` | +| 两次还原 | 均复绿 27/0/0/0,`MUTATION-7608-TEMP` 残留 0 | + +> 批 0 留下的两条「记观察日志但不拦截」用例在本单下**必然会红**,已改写成批 2 形态——这正是它们有分辨力的证明。 + +--- + +## 十、相关文档 + +- Issue `#7608`(含 58 端点判权清点、FINANCE 三步可达链、批 0 观察窗取证) +- 批 0+1:PR `#7616`;AC-9 文档侧:PR `#7661` +- 批 2/批 3 判权口径定案:#7608 评论 #60798(wx,2026-09-23) + +--- + +## 关联 / 联系人 + +- 后端:jw +- 前端:mmg