fix(verify): e2e 班级码用后还原,避免把演示实例改成随机码
问题: verify:e2e 的班级账号段会临时把班级码改成 E2E####,结束时只还原了密码,
导致演示实例(以及随后跑的打包产物冒烟)拿着文档默认码 G101 登录被 401。
修复:
- e2e 开始前记录该班原本的班级码,结束时连码带密码一起还原
- 新增断言'清理:班级码恢复为验证前的原值(不污染演示数据)',并断言用原码仍可登录
- README 权限矩阵段落补充需求 6/7 的成绩导入取舍说明与放开范围(6 处改动清单)
验收: verify:e2e 126/126(结束后实例仍为 G101/123456)
打包产物(portable.exe) 47/47, packaged=true, login=高一(1)班 classSession=true
This commit is contained in:
1 parent
b07ee4f8d7
commit
910e520d09
2 files changed
+39
-9
No files matched your search
@@ -75,7 +75,7 @@ class-helper/
|
|||||||
│ ├── prisma/migrations/ # 迁移历史(已生成并应用)
|
│ ├── prisma/migrations/ # 迁移历史(已生成并应用)
|
||||||
│ ├── prisma/seed.ts # 种子数据
|
│ ├── prisma/seed.ts # 种子数据
|
||||||
│ ├── prisma.config.ts # Prisma 7 配置(迁移/种子/连接串)
|
│ ├── prisma.config.ts # Prisma 7 配置(迁移/种子/连接串)
|
||||||
│ ├── scripts/verify-e2e.mjs # 后端端到端验收脚本(125 项)
|
│ ├── scripts/verify-e2e.mjs # 后端端到端验收脚本(126 项)
|
||||||
│ └── src/
|
│ └── src/
|
||||||
│ ├── app.ts # Express 装配(静态托管 + 探针 + 限流 + 模块挂载)
|
│ ├── app.ts # Express 装配(静态托管 + 探针 + 限流 + 模块挂载)
|
||||||
│ ├── index.ts # 启动入口(自检/初始化 + HTTP + Socket.IO + 优雅退出)
|
│ ├── index.ts # 启动入口(自检/初始化 + HTTP + Socket.IO + 优雅退出)
|
||||||
@@ -171,12 +171,12 @@ pnpm verify:desktop
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 另开一个终端先启动后端:pnpm dev:server
|
# 另开一个终端先启动后端:pnpm dev:server
|
||||||
pnpm verify:e2e # 后端 + REST + Socket.IO + RBAC + 上课时段拦截 + 叫人 + 导入 + 班级账号:125 项
|
pnpm verify:e2e # 后端 + REST + Socket.IO + RBAC + 上课时段拦截 + 叫人 + 导入 + 班级账号:126 项
|
||||||
pnpm verify:web # Web 管理端真实点击(含权限入口隐藏、手机适配、叫人入口、紧急通知 + 导入弹窗):18 项
|
pnpm verify:web # Web 管理端真实点击(含权限入口隐藏、手机适配、叫人入口、紧急通知 + 导入弹窗):18 项
|
||||||
pnpm verify:desktop # Electron 客户端(含灵动岛动画/收回/已读/作业/叫人、真实链路、课表时间轴、个性化 10 项参数与托盘/退出无残留):47 项
|
pnpm verify:desktop # Electron 客户端(含灵动岛动画/收回/已读/作业/叫人、真实链路、课表时间轴、个性化 10 项参数与托盘/退出无残留):47 项
|
||||||
```
|
```
|
||||||
|
|
||||||
后端脚本验证**实时推送时延、作业完成、成绩下发、权限隔离、上课时段紧急通知拦截、导入与失败回滚、班级账号代全班操作**等 125 项,
|
后端脚本验证**实时推送时延、作业完成、成绩下发、权限隔离、上课时段紧急通知拦截、导入与失败回滚、班级账号代全班操作**等 126 项,
|
||||||
实测通知 37ms、作业 26ms、成绩 25ms 到达(要求 < 5 秒),紧急通知 409 拦截与二次确认后发布均通过;
|
实测通知 37ms、作业 26ms、成绩 25ms 到达(要求 < 5 秒),紧急通知 409 拦截与二次确认后发布均通过;
|
||||||
客户端脚本验证 preload 桥接、渲染进程、IndexedDB 读写、断网回退、**灵动岛四种状态切换**与联网集成
|
客户端脚本验证 preload 桥接、渲染进程、IndexedDB 读写、断网回退、**灵动岛四种状态切换**与联网集成
|
||||||
(**班级账号登录** + 四类数据 + Socket.IO),实测 47/47 通过;退出后再启动一次校验单实例锁与文件锁已释放。
|
(**班级账号登录** + 四类数据 + Socket.IO),实测 47/47 通过;退出后再启动一次校验单实例锁与文件锁已释放。
|
||||||
@@ -220,7 +220,7 @@ pnpm verify:desktop # Electron 客户端(含灵动岛动画/收回/已读
|
|||||||
| `pnpm dist:dir` | 打包客户端免安装目录 `release/win-unpacked`(含可执行文件,最快) |
|
| `pnpm dist:dir` | 打包客户端免安装目录 `release/win-unpacked`(含可执行文件,最快) |
|
||||||
| `pnpm dist:win` | 打包客户端 nsis 安装包 + portable 单文件 EXE |
|
| `pnpm dist:win` | 打包客户端 nsis 安装包 + portable 单文件 EXE |
|
||||||
| `pnpm dist:all` | 服务端安装程序 + 客户端安装程序一起打 |
|
| `pnpm dist:all` | 服务端安装程序 + 客户端安装程序一起打 |
|
||||||
| `pnpm verify:e2e` | 后端端到端验收(125 项,含角色权限矩阵、导入与班级账号,需服务端已启动) |
|
| `pnpm verify:e2e` | 后端端到端验收(126 项,含角色权限矩阵、导入与班级账号,需服务端已启动) |
|
||||||
| `pnpm verify:web` | Web 管理端 UI 真实点击测试(Electron 驱动,18 项,含权限入口隐藏与导入弹窗) |
|
| `pnpm verify:web` | Web 管理端 UI 真实点击测试(Electron 驱动,18 项,含权限入口隐藏与导入弹窗) |
|
||||||
| `pnpm verify:desktop` | EXE 客户端冒烟验证(47 项,含灵动岛动画/收回/已读/作业/叫人、课表时间轴、个性化全参数) |
|
| `pnpm verify:desktop` | EXE 客户端冒烟验证(47 项,含灵动岛动画/收回/已读/作业/叫人、课表时间轴、个性化全参数) |
|
||||||
| `pnpm typecheck` | 全仓库类型检查(含 `vue-tsc`) |
|
| `pnpm typecheck` | 全仓库类型检查(含 `vue-tsc`) |
|
||||||
@@ -754,6 +754,16 @@ POST /api/notifications { …, priority: "NORMAL" }(上课时段)
|
|||||||
> 行为变更提示:按需求"科任老师只有布置作业、叫人、通知权限""班主任另有本班课表管理权限",
|
> 行为变更提示:按需求"科任老师只有布置作业、叫人、通知权限""班主任另有本班课表管理权限",
|
||||||
> 成绩录入与班级/名单管理已收紧为管理员专属;如需把成绩下放给班主任或科任老师,
|
> 成绩录入与班级/名单管理已收紧为管理员专属;如需把成绩下放给班主任或科任老师,
|
||||||
> 只需调整 `packages/shared/src/permissions.ts` 里的 `canManageGrades()` 并同步断言。
|
> 只需调整 `packages/shared/src/permissions.ts` 里的 `canManageGrades()` 并同步断言。
|
||||||
|
>
|
||||||
|
> 需求 6 写的是"老师端与学生端班级账号均可用且不越权",而本仓库按需求 7 的权限清单把
|
||||||
|
> **成绩导入/录入实现为管理员专属**(`verify:e2e` 有两项断言"班主任/科任老师录入成绩 403")。
|
||||||
|
> 这是**有意保留的取舍**,等你确认后再按下面范围放开(约 6 处、一行核心判定):
|
||||||
|
>
|
||||||
|
> 1. `packages/shared/src/permissions.ts`:`canManageGrades()` 改为按班级角色判定(`ADMIN | HEAD`);
|
||||||
|
> 2. `packages/server/src/lib/access.ts`:`assertCanManageGrades(user, classId)` 改为异步并用 `resolveUserClassRole`;
|
||||||
|
> 3. `packages/server/src/modules/grades/*`、`modules/imports/imports.module.ts`:路由从 `requireRole('ADMIN')` 放宽为 `ADMIN | TEACHER`,服务层按班级断言(科任仍 403);
|
||||||
|
> 4. `packages/web-admin/src/views/{GradesView,StudentsView}.vue`:`isAdmin` 改为"管理员或本班班主任";
|
||||||
|
> 5. `packages/server/scripts/verify-e2e.mjs`:班主任成绩断言由 403 改为 201(科任仍 403),本文件的权限矩阵同步更新。
|
||||||
|
|
||||||
## 模块化设计(可随时增删模块)
|
## 模块化设计(可随时增删模块)
|
||||||
|
|
||||||
@@ -829,7 +839,7 @@ pnpm db:migrate && pnpm db:seed
|
|||||||
| 成绩 / 名单表格导入(xlsx·xls·csv) | ✅ | 模板下载 + 预览映射 + 重复处理 + 行号级错误:`verify:e2e` 导入 22 项、`verify:web` 弹窗实测 |
|
| 成绩 / 名单表格导入(xlsx·xls·csv) | ✅ | 模板下载 + 预览映射 + 重复处理 + 行号级错误:`verify:e2e` 导入 22 项、`verify:web` 弹窗实测 |
|
||||||
| ClassIsland 时间配置导入(覆盖 / 合并 / 回滚) | ✅ | 合法 200、非法 400 `IMPORT_INVALID` 且原配置仍为 3 节、merge 覆盖 1 新增 1 共 4 节 |
|
| ClassIsland 时间配置导入(覆盖 / 合并 / 回滚) | ✅ | 合法 200、非法 400 `IMPORT_INVALID` 且原配置仍为 3 节、merge 覆盖 1 新增 1 共 4 节 |
|
||||||
| 安装版覆盖升级自动补迁移 | ✅ | 真实旧库升级日志:`升级安装:已应用 2 个迁移文件,跳过 34 个已存在对象`,`TimeLayout` 自动建表 |
|
| 安装版覆盖升级自动补迁移 | ✅ | 真实旧库升级日志:`升级安装:已应用 2 个迁移文件,跳过 34 个已存在对象`,`TimeLayout` 自动建表 |
|
||||||
| 学生端主体 = 班级(班级码 + 班级密码登录) | ✅ | `verify:e2e` 班级账号 18 项:登录 / 错误密码 401 / 跨班 403 / 发布 403 / 班级码重复 400 / 密码重置 |
|
| 学生端主体 = 班级(班级码 + 班级密码登录) | ✅ | `verify:e2e` 班级账号 19 项:登录 / 错误密码 401 / 跨班 403 / 发布 403 / 班级码重复 400 / 密码重置 / 班级码用后还原 |
|
||||||
| 班级设备代全班操作(已读 · 完成 · 成绩总览) | ✅ | 标记已读写入 6 条(全班 6 人)、教师端 `readCount=6`、`completedCount=6`、`/grades/my` 全班成绩 |
|
| 班级设备代全班操作(已读 · 完成 · 成绩总览) | ✅ | 标记已读写入 6 条(全班 6 人)、教师端 `readCount=6`、`completedCount=6`、`/grades/my` 全班成绩 |
|
||||||
| 客户端只保留班级登录入口 | ✅ | `verify:desktop`:`login=高一(1)班 classSession=true`,导航「成绩」标题变为「本班成绩」 |
|
| 客户端只保留班级登录入口 | ✅ | `verify:desktop`:`login=高一(1)班 classSession=true`,导航「成绩」标题变为「本班成绩」 |
|
||||||
| 能成功打包 Windows EXE | ✅ | `班级小助手-0.1.0-x64-setup.exe` / `-portable.exe` / `win-unpacked/*.exe`(见下表) |
|
| 能成功打包 Windows EXE | ✅ | `班级小助手-0.1.0-x64-setup.exe` / `-portable.exe` / `win-unpacked/*.exe`(见下表) |
|
||||||
@@ -848,7 +858,7 @@ pnpm db:migrate && pnpm db:seed
|
|||||||
|
|
||||||
| 验证 | 结果 |
|
| 验证 | 结果 |
|
||||||
| ----------------------------------------------------------------------------------------- | ----------------------------------- |
|
| ----------------------------------------------------------------------------------------- | ----------------------------------- |
|
||||||
| `pnpm verify:e2e`(开发环境与**安装后的生产实例**各跑一次) | **125/125** ✅ |
|
| `pnpm verify:e2e`(开发环境与**安装后的生产实例**各跑一次) | **126/126** ✅ |
|
||||||
| `pnpm verify:web`(Web 管理端真实点击 + 权限入口隐藏 + 手机适配 + 叫人 + 导入弹窗 + PWA) | **18/18** ✅ |
|
| `pnpm verify:web`(Web 管理端真实点击 + 权限入口隐藏 + 手机适配 + 叫人 + 导入弹窗 + PWA) | **18/18** ✅ |
|
||||||
| `pnpm verify:desktop`(客户端冒烟 + 灵动岛动画/收回 + 个性化全参数 + 托盘与退出无残留) | **47/47** ✅ |
|
| `pnpm verify:desktop`(客户端冒烟 + 灵动岛动画/收回 + 个性化全参数 + 托盘与退出无残留) | **47/47** ✅ |
|
||||||
| `pnpm typecheck` / `pnpm lint` / `pnpm format:check` | 全部通过 ✅ |
|
| `pnpm typecheck` / `pnpm lint` / `pnpm format:check` | 全部通过 ✅ |
|
||||||
|
|||||||
@@ -1237,6 +1237,9 @@ async function main() {
|
|||||||
// ---------------------------------------------------------------- 9. 班级账号(学生端主体 = 班级)
|
// ---------------------------------------------------------------- 9. 班级账号(学生端主体 = 班级)
|
||||||
// 设计:班级码 + 班级密码 → classSession 的 JWT;个人数据(作业完成 / 通知已读 / 成绩)
|
// 设计:班级码 + 班级密码 → classSession 的 JWT;个人数据(作业完成 / 通知已读 / 成绩)
|
||||||
// 由服务端按"全班"范围读写,个人学生不再是登录主体(个人账号接口仍向后兼容)。
|
// 由服务端按"全班"范围读写,个人学生不再是登录主体(个人账号接口仍向后兼容)。
|
||||||
|
// 注意:本段会临时改动班级码,结束时必须恢复(否则演示实例会留下 E2E#### 这种随机码)。
|
||||||
|
const adminClassList = await api('/classes', { token: adminToken });
|
||||||
|
const originalClassAccount = (adminClassList.payload?.data ?? []).find((item) => item.id === classId) ?? {};
|
||||||
const classCode = `E2E${Date.now().toString(36).slice(-4).toUpperCase()}`;
|
const classCode = `E2E${Date.now().toString(36).slice(-4).toUpperCase()}`;
|
||||||
const setAccount = await api(`/classes/${classId}/class-account`, {
|
const setAccount = await api(`/classes/${classId}/class-account`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -1458,7 +1461,7 @@ async function main() {
|
|||||||
`status=${occupiedCode.status} message=${occupiedCode.payload?.message ?? ''}`,
|
`status=${occupiedCode.status} message=${occupiedCode.payload?.message ?? ''}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 清理:删除回归用的通知 / 作业 / 成绩,并把班级密码恢复为默认
|
// 清理:删除回归用的通知 / 作业 / 成绩,把班级码恢复为验证前的原值,密码恢复为默认
|
||||||
if (classNameNoticeId) {
|
if (classNameNoticeId) {
|
||||||
await api(`/notifications/${classNameNoticeId}`, { method: 'DELETE', token: teacherToken });
|
await api(`/notifications/${classNameNoticeId}`, { method: 'DELETE', token: teacherToken });
|
||||||
}
|
}
|
||||||
@@ -1468,11 +1471,28 @@ async function main() {
|
|||||||
if (classNameGrade.payload?.data?.id) {
|
if (classNameGrade.payload?.data?.id) {
|
||||||
await api(`/grades/${classNameGrade.payload.data.id}`, { method: 'DELETE', token: adminToken });
|
await api(`/grades/${classNameGrade.payload.data.id}`, { method: 'DELETE', token: adminToken });
|
||||||
}
|
}
|
||||||
await api(`/classes/${classId}/class-account`, {
|
|
||||||
|
const restoreAccount = await api(`/classes/${classId}/class-account`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
token: adminToken,
|
token: adminToken,
|
||||||
body: { password: '123456' },
|
body: {
|
||||||
|
...(originalClassAccount.code ? { code: originalClassAccount.code } : {}),
|
||||||
|
password: '123456',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
const restoredLogin = originalClassAccount.code
|
||||||
|
? await api('/auth/class-login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { code: originalClassAccount.code, password: '123456' },
|
||||||
|
})
|
||||||
|
: { status: 200 };
|
||||||
|
record(
|
||||||
|
'清理:班级码恢复为验证前的原值(不污染演示数据)',
|
||||||
|
restoreAccount.status === 200 &&
|
||||||
|
(!originalClassAccount.code || restoreAccount.payload?.data?.code === originalClassAccount.code) &&
|
||||||
|
restoredLogin.status === 200,
|
||||||
|
`原值=${originalClassAccount.code || '(未设置)'} 还原后=${restoreAccount.payload?.data?.code ?? '-'} 登录=${restoredLogin.status}`,
|
||||||
|
);
|
||||||
|
|
||||||
socket.close();
|
socket.close();
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user