feat(pc,platform): 验收走查修复(平台按学校代管 + 菜单/403/日志范围/分页)

客户在验收里点出来的问题,一次改完。两段改动:

一、平台运营与租户范围(后端为主)
- 新增请求头 X-Tenant-Id:平台运营是唯一跨租户身份,租户拦截器对它整体关掉了过滤,
  于是「组织架构 / 账号管理 / 工作台」拿到的是所有租户混在一起的数据 —— 而只有一所
  学校有数据时,它和学校管理员看到的完全一样,看不出错(这是最危险的一点)。
  现在这些页面必须先选定学校,服务端再把本次请求的租户上下文换成目标租户:
  只认平台身份(其它身份带头一律忽略)、目标租户必须存在且启用。
- OrgService.tree / UserService.list 补显式 tenant_id 条件(原来只靠拦截器,而拦截器对
  平台不生效);TaskService.seesWholeTenant 改用新的 LoginUser.isPlatformStaff() ——
  isPlatformAdmin 回答"当前租户是不是平台租户",代管时它为 false,会让平台退化成普通
  用户(任务列表掉进"班主任只看自己带的班"分支,得到空列表);PLATFORM_ADMIN 补
  TASK_VIEW(PRD §3.2「查看填报进度看板:平台运营 ✅」)。
- 操作日志对平台永远是空的:SQL 写死 tenant_id = 当前租户,而平台的 tenantId 是 0。
  按契约 §13.4 补 tenantId 参数与 log:viewAll:平台默认看全量、可指定某所学校;
  学校管理员恒为本租户,传别家返回 40301(不静默忽略,否则会让人以为"筛过了")。
- 开放接口"推送重试"从未生效(F-112):定时扫描缺 @InterceptorIgnore,异常被 log.warn
  吞掉;同一方法里"接入方已停用"那条 update 也没有租户上下文,会每 10 秒重试到永远。
  E2E 补了"真的重试了"的断言(原来只断言"安排了下次重试")。

二、PC 端走查体验
- 去掉 3 条无用菜单(名单管理 / 表单设计器 / 材料规则):功能都在「收集任务」行内入口,
  菜单里那一条点进去只有指路;后端菜单键同步删除,entryAt 那套机制一并删掉。
- 班主任点工作台必弹 403:工作台无条件调了要 org:view / progress:viewAll 的两个接口。
  403 走 HTTP 状态码,try/catch 拦不住它的弹窗 —— 改成按权限决定发不发请求。
- 操作日志:分页器被裁在可视区外(新增 .g-list-card 让表体自己滚、表头钉住、分页器留在
  卡片底部)+ 去掉页面上那段免责声明;收集任务的状态切换改成单独一行 tab;去掉顶栏假数据
  铃铛;配色按客户要求还原。

顺带修两处"检查器自己不严":check-wiring 新增第 ⑦ 项(PC 端引用的权限码必须存在于
PermissionDef —— 本轮我自己把 progress:viewAll 写成 progress:view:all,判断恒为 false,
而编译 / typecheck / lint / E2E 全都不红);verify-docs 的表格列数检查不再把 GFM 转义
竖线当成列分隔符。两个检查器都拿"故意写错的样例"试过,确认真的会红。

验证:typecheck / mvn package / check-wiring(未接线声明 0 条)/ verify-docs(22 项)全过;
e2e-m0 36、e2e-m1-orgs 48、e2e-m0-user 55、e2e-m5-logs 44、e2e-m5-notify-wiring 43、
e2e-m1-tenant-domain 6、e2e-m8-open 61 全绿;浏览器按角色逐个真点复验。
详见 docs/交接摘要.md §一·三十三 与 §一·三十四。
This commit is contained in:
2026-10-09 17:29:20 +08:00
parent db8aecc30a
commit 0bbac7d228
41 changed files with 1387 additions and 372 deletions
+29 -1
View File
@@ -70,6 +70,25 @@
**Token 有效期**:PC 端 8 小时(可续期)/ 小程序端 7 天(静默续期)
#### `X-Tenant-Id`:平台运营的「当前学校」(可选,仅平台运营)
平台运营(`tenantId = 0`)是唯一跨租户的身份,服务端的租户过滤对它整体关闭。于是
「组织架构」「账号管理」这类**本身就是租户内**的接口,平台不指明学校时会拿到
**所有租户混在一起**的数据 —— 而这看不出错(只有一所学校有数据时,它与学校管理员看到的完全一样)。
| 项 | 约定 |
|---|---|
| Header | `X-Tenant-Id: <tenantId>` |
| 谁能用 | **只认平台运营**。其它身份带这个头一律忽略(改了也不生效,不是"报错") |
| 作用 | 本次请求的租户上下文换成该租户:`/orgs/**`、`/users/**`、`/tasks/**` 等按它过滤与写入 |
| 不带 / 传 `0` | 不切换(`0` 视为"平台自己")。平台的租户列表、全量日志等跨租户能力照旧 |
| 非法值 | 非数字、或租户不存在/已停用 → `40001` |
| 与 token 的关系 | 只对平台生效,**不能**用来访问别的租户(学校管理员带它不会越权) |
| 权限怎么判 | 切换之后**不再享受"平台超管"捷径**,一律按 `PLATFORM_ADMIN` 声明的权限码判 —— 所以那些码必须真的够用(例如「查看填报进度看板」要 `task:view` + `progress:viewAll`) |
> 例:`GET /api/v1/orgs/tree` + `X-Tenant-Id: 1` → 返回租户 1 的组织树;
> 不带该头 → 返回平台自己租户的组织(空树),**不再**把各租户的组织混在一起。
### 1.3 统一响应
```json
@@ -2634,6 +2653,13 @@ Excel 列:`层级` `组织名称` `组织编码` `上级编码` `负责人手
**Query**:`tenantId`、`operatorId`、`module`、`action`、`targetId`、`from` / `to`、分页
**范围**(权限二选一,`@RequirePerm({log:viewTenant, log:viewAll})`):
| 调用者 | 权限 | 看到什么 |
|---|---|---|
| 平台运营 | `log:viewAll` | **默认全量**(跨租户);传 `tenantId` 则只看该单位 |
| 学校管理员 / 教务处 | `log:viewTenant` | **恒为本租户**;传了别的 `tenantId` → `40301`(**不静默忽略**:静默会让人以为"筛过了") |
**响应**
```json
@@ -2669,7 +2695,9 @@ Excel 列:`层级` `组织名称` `组织编码` `上级编码` `负责人手
#### `GET /api/v1/logs/logins`
**Query**:`userId`、`userType`、`result`、`from` / `to`、分页
**Query**:`tenantId`、`userId`、`userType`、`result`、`from` / `to`、分页
**范围**:与操作日志同一口径(`log:viewAll` 可跨租户,学校管理员恒为本租户)。
**响应**:`{ list: [{ id, userId, userName, userType, loginType, ip, location, device, result, failReason, loginTime }], total }`