docs: 同步契约 §6.5~6.9 / §13.4、问题清单 F-112~F-124、交接摘要 §一·三十九
1) docs/03-接口设计.md:
- §6.5 补 expireBefore **含当天**的语义与"格式不对返回 40001,不静默忽略筛选";补
"列表 contactPhone 恒为 null"与"packageName 与内嵌 usage{} 不实现(200 行内嵌用量会变成
N+1 统计)"。
- §6.6 的 tenantType 由 3 值改回真实的 **4 值**(1 学校 / 2 认定机构 / 3 报名点 / 4 企业)、
packageId 由 ✅ 必填改为可选(DTO 里本来就是可选),并补副作用(发放 6 个内置角色 + 建管理员
+ 写 CREATE_TENANT 日志)。
- §6.7 的响应示例原来写着 ocrCount=3105 / smsCount=420,**与实现恒 0 矛盾** —— 改成 0 并补齐
契约没写的 notes 与 alerts.used/limit,再加三条口径:实时值 vs 周期、恒 0 的原因(编数字会让
运营拿假数据去谈续费)、**只算这一个租户**(含"库里只有一所学校时看不出"这一点)。
- **新增 §6.8 / §6.9**(PUT /tenants/{id} 与 GET /tenants/{id}):PUT 此前只在 §4.2 的总览表里
出现过一次、全文**没有详情节**;GET 更是只登记在 §17.1 的"清单漏列"组里。§6.8 含字段表、
"不传的字段不修改"、"tenantCode 不可修改"的理由,以及留痕对照表(status→0 是 DISABLE_TENANT,
不是 UPDATE_TENANT)+ "这些日志行的 tenant_id 是 0,要用 target_type/target_id 查"的警告。
- §13.4 的 Query 补 targetType(并说明为什么 targetType+targetId 才是最重要的查询方式:
关键字全文匹配会把"改了它"和"顺带提到它"混在一起,而合规结论不能建立在这种匹配上);
响应补 request_method / request_uri / extraJson;动作表由 9 行扩到**全集 27 个**并按来源分组
(任务 / 名册 / 导出 / 角色 / 租户);写明 request_param / duration_ms 刻意不采集;
并记下"一期只做查询、不做日志导出"这条**此前只写在 LogController 类注释里**的口径
(导出等于把"谁在什么时候看了谁的身份证"这份名单落到本地磁盘,而导出行为本身也要留痕)。
- 登录日志响应补 tenantId,并写明 userType 的权威枚举与 location 恒为 null。
2) docs/08-完成度盘点与问题清单.md:新增 **F-112~F-124** —— 用量越界(含"为什么本地完全看不出来")、
PUT 明文写入加密列、两个枚举能建不能改、租户零留痕、expireBefore 差一天、AUTO_CLOSE 字面量、
模块下拉里有死项、LogController 描述与事实相反、request_param/duration_ms 有列无写入点、
schema-postgresql.sql 缺三张表、**"平台专属权限码 + 查带 tenant_id 的表"这个风险面**、
登录日志缺租户列、userType 注释错位。F-112 标注为本轮最严重的一条。
3) docs/10-用户手册与客服话术.md:「三种角色,三个入口」→ **四种角色**(补平台运营,并写明它是
唯一跨单位的角色、用"组织与账号/收集管理"这类单位内页面之前要先选"当前学校");
§4 速查表的菜单名与路径同步(平台运维 → 日志,含两个页签;新增租户管理一行);
附录的排查话术补 traceId("日志详情里能拿到,那是后端排查时最有用的一个线索")。
4) docs/交接摘要.md:新增 **§一·三十九**,九节 —— 一、口径变更(旧注释原文 + 推翻理由);
二、用量越界那个 bug 的完整来龙去脉(权限码 → ignoreTable → 实测数据分布 → 修法与夹具);
三、另外四条后端缺陷表;四、日志详情为什么不新增接口(轻字段 vs 全库 0 行有值的重字段);
五、AUTO_CLOSE 与检查 ⑩ 的关系(**"这一族检查的价值取决于被检查的那一侧有没有被规范化"**);
六、顺带修掉的五处;七、前端刻意用分段按钮而不是 NTabs 的两条原因;八、验证表 +
"改了后端必须重新打包重启、判据是看返回体里有没有新字段"的提醒;九、约定同步。
另把 E2E 断言总数 1595 → **1617**(四处)。
5) 根 AGENTS.md:接线检查说明补第 ⑩ 项,并写明做这一项时才发现 AUTO_CLOSE 是字符串字面量写的
—— "提成常量"是让这个检查真正生效的前提。
验证:python scripts/verify-docs.py **22/22**;python scripts/check-wiring.py 未接线 **0 条**
(⑦ 11 个权限码 / ⑧ 0 处漏 import / ⑨ 3 个 SVG 全合法 / ⑩ 27 个动作码两端逐字一致);
python scripts/check-api-parity.py 契约 **119** / 已实现 118 / 缺口 1 —— **没变**,符合本轮
"不新增后端接口"的决定。
This commit is contained in:
+145
-2
@@ -2847,7 +2847,7 @@ CI/CD 与 test/prod 环境 → 压测 → 用户手册与客服话术 → 缩略
|
||||
| 手段 | 结果 |
|
||||
|---|---|
|
||||
| `scripts/e2e-m10-role.py`(新增) | **77/77**:改授权后**同一个 token** 立刻生效 / 停用立刻失效 / 内置角色只读 / 越权与自锁 / 删除守卫 `40045` / 内置角色权限以代码为准 + 同步回填幂等 / 跨租户 |
|
||||
| `python scripts/verify-all.py --with-e2e` | **9 项全过 / 0 失败 / 0 跳过**;**37 个脚本 / 1595 条断言 / 0 失败**(2026-10 收敛删掉 2 个脚本后已重跑填数) |
|
||||
| `python scripts/verify-all.py --with-e2e` | **9 项全过 / 0 失败 / 0 跳过**;**37 个脚本 / 1617 条断言 / 0 失败**(2026-10 收敛删掉 2 个脚本后已重跑填数) |
|
||||
| 浏览器实测(真点) | 新建自定义角色 → 账号页角色候选出现它(且平台角色已不在候选里)→ 改权限落库(`sys_role_permission` 逐字核对)→ 删除(软删 + 授权/菜单行一并软删);截图留证 |
|
||||
| 机械校验 | `check-api-parity`(契约 **119** / 已实现 118 / 缺口 1)、`verify-docs` 22/22(原型 **29** 屏)、`check-wiring` 未接线 0、`check-entity-columns` 28 实体一致、`check-manual` 0 错 |
|
||||
| 前端 | `pnpm typecheck` 0 错、`eslint` 0 错(`views/role/**`、`apis/xt/role.ts` 与改动过的账号页) |
|
||||
@@ -2892,7 +2892,7 @@ CI/CD 与 test/prod 环境 → 压测 → 用户手册与客服话术 → 缩略
|
||||
| PC 业务菜单 | 12 | **10** |
|
||||
| 小程序页面 | 10 | **9** |
|
||||
| 原型屏 | 32 | **29**(学生 10 / 班主任 9 / 管理端 10) |
|
||||
| E2E 脚本 | 39 | **37**(删 `e2e-m2-join.py`、`e2e-m7-cleanup.py`);断言 1698 → **1595 条** |
|
||||
| E2E 脚本 | 39 | **37**(删 `e2e-m2-join.py`、`e2e-m7-cleanup.py`);断言 1698 → **1617 条** |
|
||||
|
||||
> ⚠️ **两个容易算错的数**(都踩过):
|
||||
> ① **契约是 119,不是 116**。116 是"删 13 个**代码端点**"(cleanup 8 + join 5),
|
||||
@@ -3183,4 +3183,147 @@ CI/CD 与 test/prod 环境 → 压测 → 用户手册与客服话术 → 缩略
|
||||
根 `AGENTS.md` 的接线检查说明补上第 ⑨ 项。
|
||||
|
||||
|
||||
## §一·三十九 平台运营的租户管理 + 日志详情(2026-10)
|
||||
|
||||
**起因**:用户要求「设计开发平台运营人员对租户的管理功能,还有日志的详情查看功能」。
|
||||
|
||||
### 一、口径变更(**这一节最先要看的东西**)
|
||||
|
||||
`frontend-pc/src/views/tenant/index.vue` 原先头部写着:
|
||||
|
||||
> ⚠️ 列表**没有操作列**:不提供编辑 / 停用入口(客户 2026-10 走查确认的口径)
|
||||
|
||||
**这条口径本轮被用户推翻**(选的是「全量:详情 + 用量 + 编辑 + 停用/启用」)。
|
||||
所以列表现在有操作列(详情 / 编辑 / 停用·启用),并且新增了详情抽屉与编辑弹窗。
|
||||
|
||||
⚠️ **变更理由要一起记下来,否则下一个人读到旧注释会以为是 rebase 丢的**:
|
||||
这条旧口径的动机是"少一个误操作入口",而运营真正需要的是**改到期日**(续费)
|
||||
与**停用学校**(欠费 / 退租)—— 这两个动作没有入口时,只能改数据库。
|
||||
|
||||
另外三条已确认的口径(都是"不做什么",比做什么更容易被顺手破坏):
|
||||
**套餐只读**(全库没有 `sys_package` 表,`package_id` 是无外键裸 bigint)、
|
||||
**不补原型屏**(`docs/prototype/admin-web.html` 保持 10 屏,`合计屏数 : 29` 不动)、
|
||||
**不新增后端接口**(契约 119 不变)。
|
||||
|
||||
### 二、本轮最关键的一件事:用量接口算的是「全平台」的数(F-112)
|
||||
|
||||
**这不是顺手发现的,是审查计划时逐条对照代码挖出来的**,而且它比原计划里所有 UI 改动都严重:
|
||||
|
||||
- 调用方一定是平台运营(`/tenants/{id}/usage` 挂 `tenant:view`,而它在
|
||||
`PermissionDef.platformOnlyCodes()` 里);平台运营的 `tenantId` 是 **0**;
|
||||
- 而 `MybatisPlusConfig.FillTenantLineHandler.ignoreTable()` 对平台身份**直接返回 true**
|
||||
—— **整张表的租户过滤被关掉**;
|
||||
- 于是 `TenantUsageService.distinctStudents()`(查 `collect_roster`)与存储量统计
|
||||
(查 `collect_file`)都**不带租户条件** → 返回全平台的学生数与存储占用,
|
||||
却标着某一所学校的名字。
|
||||
|
||||
**⚠️ 它在本地完全看不出来**:实测库里 `collect_roster` 34 行、`collect_file` 22 行
|
||||
**全在 `tenant_id=1`** —— 只有一所学校有数据时,错误值与正确值**完全相等**。
|
||||
`ActingTenantResolver` 的类注释描述的正是这个陷阱("当只有一所学校有数据时,
|
||||
它和学校管理员看到的一模一样")。
|
||||
|
||||
**修法与验证**:
|
||||
- 统计段包进 `TenantScope.callAs(tenantId, 0L, Set.of(), ...)`(本仓既有机制,
|
||||
`OpenDataService` / `OpenPushService` 在用)。**没有**手写 `tenant_id` 条件 ——
|
||||
那等于放弃拦截器的保护;切走上下文之后 `isPlatformAdmin()` 变 false,过滤自动恢复。
|
||||
- E2E **必须有夹具**才证伪得了:`e2e-m8-orgfile.py` 往新建的 E2E 租户**直插一行
|
||||
`collect_roster`**(该表 NOT NULL 且无默认值的列只有 5 个,不需要走导入夹具),
|
||||
再断言"接口值 == 库里 `where tenant_id=1` 的去重数",并**额外断言
|
||||
`全平台去重数 > 一号学校去重数`** —— 否则这条断言是空转的(这次实测两值确实不等)。
|
||||
结果 **74/74**。
|
||||
|
||||
### 三、另外四条后端缺陷(都在同一次改动里修掉)
|
||||
|
||||
| # | 缺陷 | 后果 |
|
||||
|---|---|---|
|
||||
| F-113 | `PUT /tenants/{id}` 把手机号**明文**写进加密列(`create` 是加密的) | `AesGcmCipher.decrypt` 解不开 Base64 直接抛 → **存过一次电话,这所学校详情页永远打不开**。实测 `id=1` 存的就是明文 `13800138000` |
|
||||
| F-114 | `tenantType` 只放行 1~3(实体/DDL/契约/前端都是 **4 值**);`status` 只放行 0/1(实体有 **2 试用**,前端筛选按钮里就有) | 这两类值**能建不能改**,选了「试用」永远是空列表 |
|
||||
| F-115 | 开通 / 修改 / 停用租户**一条日志都不写**(只有给运维看的 `log.warn`) | 停用一所学校是这套系统里影响面最大的单个动作,却查不到是谁干的 |
|
||||
| F-116 | `expireBefore` 用 `atStartOfDay()` 比 → **差一天**,「今天到期」查不出来 | 续费提醒那类用法正好踩中 |
|
||||
|
||||
对 F-113 的读路径要特别说一句:**必须容错**(解密失败按明文处理 + `log.warn`),
|
||||
否则故障形态是"详情页永久 500",看起来像系统坏了而不是"库里有一行旧数据"。
|
||||
**历史明文行不做数据迁移** —— 迁移里拿不到 AES 密钥。
|
||||
|
||||
留痕口径(**容易记错**):新增 `MODULE_TENANT` 与 4 个动作码,
|
||||
`DISABLE_TENANT` / `ENABLE_TENANT` **不并进** `UPDATE_TENANT`(出争议时第一个要看的就是这个区别)。
|
||||
⚠️ 这些日志行的 `tenant_id` 是 **0**(写日志用的是**操作者**的上下文),
|
||||
查"这所学校被谁停过"要用 `target_type=TENANT` + `target_id`,
|
||||
**不能用 `tenantId` 筛** —— 这条已经写进常量注释与契约 §6.8,因为反直觉。
|
||||
|
||||
### 四、日志详情:为什么不新增接口
|
||||
|
||||
详情要的 `request_method` / `request_uri` / `extra_json` 都是**轻字段**
|
||||
(实测 `max(length(extra_json))=45`、`max(length(request_uri))=31`、`max(length(user_agent))=125`,
|
||||
一页 20 行约 2 KB),所以直接把三列补进 `listOperations` 的 SELECT 就行。
|
||||
|
||||
⚠️ **"别让重字段驮在列表上"这个理由在这里不成立**:真正重的 `request_param` /
|
||||
`duration_ms` **全库 1758 行里 0 行有值**(从来没有 `setXxx` 过)。
|
||||
新增 `GET /logs/operations/{id}` 反而要改契约 119 与 4 份文档。**所以没加。**
|
||||
|
||||
### 五、`AUTO_CLOSE` 与新的检查 ⑩(**这一族检查的价值取决于被检查的那一侧**)
|
||||
|
||||
日志页的 `actionText()` **查不到就原样回显那个码**。而 `AUTO_CLOSE`(定时任务自动截止)
|
||||
**根本不在常量表里** —— 它在 `TaskService.autoClose()` 里是**字符串字面量**写的。
|
||||
也就是说"后端常量 ↔ 前端清单"这种检查**本来就扫不到它**,而它正是量最大的那类任务日志。
|
||||
|
||||
所以这件事要做两步:① 把字面量提成常量(连带 `STATUS_CHANGE` 兜底名);
|
||||
② 新增 `check-wiring.py` 第 ⑩ 项(27 个动作码两端逐字核对)。
|
||||
**已反向验证**:删掉前端一条文案 → 报红 + exit 1。
|
||||
|
||||
⚠️ 写这个检查时踩到的坑:同文件里 `MODULE_OPTIONS` / `TARGET_TYPE_OPTIONS` 也是
|
||||
`{ value: 'TASK' }` 的形状,**整文件扫会把模块名当成动作码收进来** —— 所以只从
|
||||
`OPERATION_ACTIONS` 这个数组里取。
|
||||
|
||||
### 六、顺带修掉的五处
|
||||
|
||||
- **F-117**:上面的 `AUTO_CLOSE` 字面量。
|
||||
- **F-118**:日志页「模块」下拉里 `AUDIT` / `EXPORT` **后端一个写入点都没有**,
|
||||
而真会写的 `FILE` / `ROLE` **不在下拉里** —— 换成了导出的 `MODULE_OPTIONS`
|
||||
(口径:**只列后端真有写入点的**,否则选中死项只会得到空表,让人以为"这段时间没有审核动作")。
|
||||
- **F-119**:`LogController` 的 `/logins` 描述与事实相反(还写着"目前不写这张表(见 F-31)",
|
||||
而 F-31 已闭环)。
|
||||
- **F-123**:**登录日志没有租户列** —— 平台运营看到的日志是跨单位的(`log:viewAll`),
|
||||
而 `LoginLogVO` 里没有 `tenantId`,于是筛得了单位却看不出每行属于哪个单位。
|
||||
`sys_login_log.tenant_id` 从 V2 建表起就有,只是 VO 没带出来。
|
||||
已修(VO + mapper SELECT + 契约 + 界面加「单位」列,**仅平台身份显示**)。
|
||||
- **F-124**:`LoginLogVO.userType` 的 Javadoc 写着「1 超管 / 2 管理员 / 3 教师 / 4 学生」,
|
||||
与权威口径(`1` 平台运营 / `2` 学校管理员 / `3` 班主任 / `4` 学生)错位 ——
|
||||
**这是同一个坑的第三次出现**(该错位枚举曾导致"把班主任当成社会人员拒掉")。
|
||||
已修,并在注释里写明权威来源。⚠️ 前端当时**没有**按评审意见里那句错的口径写、
|
||||
而是复用了 `USER_TYPE_NAMES` —— 这处只影响读代码的人。
|
||||
|
||||
### 七、前端一处刻意的选择:页面内 Tab 用**分段按钮**而不是 `NTabs`
|
||||
|
||||
日志页要做「操作日志 / 登录日志」两个页签,选了工具条上的分段按钮组,原因有两条:
|
||||
① `NTabPane` 的内容要额外 flex 化才能维持 `.page-card` 的高度链,
|
||||
否则表格高度算成 0、**一行都看不见**(本仓踩过);
|
||||
② naive 2.45.3 的 `NTabs` 用 `tab="..."` prop 只渲染注释节点、必须用 `#tab` 插槽。
|
||||
按钮组在视觉上就是两个页签,且不动高度链。**这条已写进 `frontend-pc/AGENTS.md`。**
|
||||
|
||||
### 八、验证
|
||||
|
||||
| 手段 | 结果 |
|
||||
|---|---|
|
||||
| `e2e-m8-orgfile.py` | **74/74**(租户口径断言现共 74 条:脱敏 / 密文 / 留痕 / 状态流转 / 4 值类型 / 到期日边界 / 用量越界) |
|
||||
| `e2e-m5-logs.py` | **44/44**(回归:日志留痕、脱敏、`targetType`+`targetId`、`from`/`to` 当天边界、越权 403) |
|
||||
| `check-wiring.py` | ⑦⑧⑨⑩ 全绿;**⑩ 反向验证过**(删一条文案 → 红 + exit 1) |
|
||||
| `check-api-parity.py` | 契约 119 / 已实现 118 / 缺口 1(**没变**,符合"不加接口"的决定) |
|
||||
| `verify-docs.py` | 22/22 |
|
||||
| `pnpm typecheck` / `pnpm lint` | 0 错 |
|
||||
|
||||
⚠️ **后端改了必须重新打包重启再验**:本轮有两次"改完忘了重打包"的空转风险
|
||||
(`LoginLogVO` 加字段那次、以及 `AUTO_CLOSE` 提常量那次)。
|
||||
判据是**看返回体里有没有新字段**,而不是"接口通了就算"——
|
||||
`/logs/logins` 返回 `tenantId`、`/tenants/1` 返回 `138****8000`(而不是 500),
|
||||
这两条同时成立才说明跑的是新 jar。
|
||||
|
||||
### 九、约定同步
|
||||
|
||||
`frontend-pc/AGENTS.md` 与 `.agents/skills/code-vue/SKILL.md` 新增两条:
|
||||
「详情用 `NDrawer` + `NDescriptions`,**不要为详情新开路由**」、
|
||||
「页面内页签用工具条分段按钮,写明高度链的原因」;
|
||||
根 `AGENTS.md` 的接线检查说明补第 ⑩ 项。
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user