文件
hl-api-changelog/changelogs-v2/2026-09/27_7608_团期转订单判权收口批2-修改接口-管理后台.md
T

16 KiB
原始文件 Blame 文件历史

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 且零写入 / 不查库。
  • 角色头缺失: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