16 KiB
schema, ticket, title, consumer, author, change_type, backend_status, gateway_status, frontend_status, frontend_owner, frontend_ref, target_release, verified_at, status_note, updated_at, base
| schema | ticket | title | consumer | author | change_type | backend_status | gateway_status | frontend_status | frontend_owner | frontend_ref | target_release | verified_at | status_note | updated_at | base |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| hl-changelog/v2 | 7608 | 团期转订单收口判权:transfer-in 接 group-batch:manage、transfer-candidates 接 group-batch:view(589507),各带 nacos 灰度开关 | admin | jw(GIT) | 修改接口 | deployed | verified | not_required | 已部署 dev-v3 到 TEST 并实打验证:五角色矩阵逐格吻合、nacos 两开关三态往返(含还原后复现)、观察日志反证强制态未走旁路。【2026-09-27 mmg】前端判 not_required(零代码,用户拍板守既定架构):getTransferCandidates/transferInSubOrder 均无 silentError,589507 由 request.js 拦截器透 message;转订单入口=canTransferOrder(batchStatus 白名单)&&!isGroupBatchManager,不接 hasPermission;§四/§六.7 建议按码控制可见性与既定架构「可见即可点+589507 兜底、后端不下发 group-batch 按钮码无码可接」冲突,维持现状。 | 2026-09-27 | 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-1309-27 满 14 天,结论是这两个端点在留存日志里零调用、无意外调用方,故按静态推导定案。
⚠️ 这是「没有反证」而非「有正证」:TEST 上 order-v3 跑两个实例,8086 实例 09-1309-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<TransferSubOrderRespVO>
使用场景
管理后台团期详情「转订单」弹窗:把另一期的子订单转入本期。本单起需要 group-batch:manage 权限。
入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | Path | Long | ✅ | 转入期(本期)团期主键 | 行为不变 |
| orderId | Path | Long | ✅ | 待转入的子订单 ID | 行为不变 |
| fromGroupBatchId | Body | Long | ✅ | 转出期团期主键 | 行为不变 |
出参字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| data | TransferSubOrderRespVO |
结构完全不变 |
请求示例
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}
响应示例
{
"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):
{
"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<List<TransferCandidateVO>>
使用场景
转订单弹窗的搜索下拉:候选 = 同产品其他可转期下的可转子订单。keyword 匹配订单号 / 客户名 / 期号(模糊)与手机号(整串精确,加密列不支持前缀模糊)。本单起需要 group-batch:view 权限。
入参字段表
| 字段 | 位置 | 类型 | 必填 | 约束 | 说明 |
|---|---|---|---|---|---|
| groupBatchId | Path | Long | ✅ | 转入期(本期)团期主键 | 行为不变 |
| keyword | Query | String | ❌ | 订单号 / 客户名 / 期号 / 手机号(整串) | 行为不变 |
出参字段表
| 字段 | 类型 | 说明 |
|---|---|---|
| data | List<TransferCandidateVO> |
结构完全不变 |
请求示例
GET /v3/admin/order/group-batch/2098606570236481538/transfer-candidates?keyword=GT-26-0085 HTTP/1.1
X-Admin-Id: 1001
X-Admin-Role: ADMIN
响应示例
{
"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):
{
"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且零写入 / 不查库。 - 角色头缺失:
GroupBatchPermissionGuardfail-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 的放行是「退回旁路观察」而不是「判权整体失效」——两条方向相反的证据:
- 正面:态 B 期间日志记下
endpoint=GB-ADM-071 transfer-in roleKey=CUSTOMIZER intendedPermission=group-batch:manage hasPermission=false enforced=false即「本应判manage、该角色实际不持有、但当前未强制」。守卫算过了权限,只是没拦。 - 反面:态 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