docs(guide): 补齐会话/锁定/首登改密/keep-alive + 修正资源注册机制
Some checks are pending
CI / backend (push) Waiting to run
CI / frontend (push) Waiting to run

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 22:16:28 +08:00
parent eed300474e
commit 06ce64e6f4

View File

@ -11,11 +11,11 @@
| 功能块 | 说明 | | 功能块 | 说明 |
|---|---| |---|---|
| **认证** | 登录 / 登出;JWT(access 2h + refresh 7d),401 自动刷新;注册/忘记密码默认隐藏(`showRegister`/`showForgetPassword` 可开启) | | **认证** | 登录 / 登出;JWT(access 2h + refresh 7d),401 自动刷新;首登强制改密;注册/忘记密码默认隐藏(`showRegister`/`showForgetPassword` 可开启) |
| **权限管理** | 用户(增删改/重置密码/启停/锁定)、角色(分配资源 + 数据权限)、资源(菜单/按钮/接口统一资源树)、组织(部门树) | | **权限管理** | 用户(增删改/重置密码/启停/锁定解锁)、角色(分配资源 + 数据权限)、资源(菜单/按钮/接口统一资源树)、组织(部门树) |
| **系统管理** | 租户、字典(类型 + 项,支持内置保护)、参数配置(键值) | | **系统管理** | 租户、字典(类型 + 项,支持内置保护)、参数配置(键值) |
| **系统监控** | 操作日志、登录日志(只读查询) | | **系统监控** | 操作日志、登录日志(只读查询)、在线会话(强退单会话 / 踢全部) |
| **布局与主题** | 多布局(左右 / 上下 / 纯上下 + MixBar)、暗黑模式、主题色、多页签、面包屑、全屏 | | **布局与主题** | 多布局(左右 / 上下 / 纯上下 + MixBar)、暗黑模式、主题色、多页签(keep-alive 状态保留)、面包屑、全屏 |
| **平台能力** | 多租户隔离(tenantCode)、RBAC(资源表统一授权 + 数据权限 data_scope)、审计日志落库、字典驱动渲染 | | **平台能力** | 多租户隔离(tenantCode)、RBAC(资源表统一授权 + 数据权限 data_scope)、审计日志落库、字典驱动渲染 |
**代码规范**:本工程用 openspec 追踪需求与变更(spec-driven),新增功能走 `propose → apply → archive` 流程,见 [`CLAUDE.md`](../CLAUDE.md)。本文只讲「代码怎么改」,不讲「流程怎么走」。 **代码规范**:本工程用 openspec 追踪需求与变更(spec-driven),新增功能走 `propose → apply → archive` 流程,见 [`CLAUDE.md`](../CLAUDE.md)。本文只讲「代码怎么改」,不讲「流程怎么走」。
@ -93,7 +93,7 @@ cd sources/sysmgr-admin && pnpm dev:dev
``` ```
src/ src/
├── auth/ # 认证:登录/刷新/登出、菜单树、JWT 守卫、权限守卫、装饰器、请求上下文 ├── auth/ # 认证:登录/刷新/登出、菜单树、JWT 守卫、权限守卫、装饰器、请求上下文
├── users/ roles/ resources/ orgs/ dicts/ configs/ tenants/ audit-logs/ login-logs/ ├── users/ roles/ resources/ orgs/ dicts/ configs/ tenants/ audit-logs/ login-logs/ sessions/
│ # 每个域一个目录:<name>.module.ts / controller / service / dto │ # 每个域一个目录:<name>.module.ts / controller / service / dto
├── prisma/ # PrismaService:统一注入多租户过滤 + 软删过滤 ├── prisma/ # PrismaService:统一注入多租户过滤 + 软删过滤
├── common/ # BaseCrudService、enums、permissions、page.dto、拦截器/过滤器、Redis ├── common/ # BaseCrudService、enums、permissions、page.dto、拦截器/过滤器、Redis
@ -160,6 +160,12 @@ list(@Query() q: PageQuery) { return this.service.paginate(q.page, q.pageSize) }
- 角色 `code === 'super_admin'` 自动绕过权限码校验、`data_scope` 看全量。 - 角色 `code === 'super_admin'` 自动绕过权限码校验、`data_scope` 看全量。
- 数据权限 `role.data_scope ∈ { all, dept, dept_and_child, self, custom }`,`custom` 时经 `role_org` 关联可见部门。 - 数据权限 `role.data_scope ∈ { all, dept, dept_and_child, self, custom }`,`custom` 时经 `role_org` 关联可见部门。
**资源注册(新模块接 RBAC)**:菜单/接口/按钮资源的**单一来源**是 [`src/common/tenant-template.ts`](../sources/sysmgr-nest/src/common/tenant-template.ts) 的 `MENUS` / `APIS` / `BUTTONS`,权限码来源是 [`src/common/permissions.ts`](../sources/sysmgr-nest/src/common/permissions.ts)。新增模块三步:
1. `permissions.ts` 加 `domain:action` 权限码;
2. `tenant-template.ts` 的 `MENUS`/`APIS`/`BUTTONS` 加资源(`parent` 挂到上级菜单 code);
3. 存量租户跑 `pnpm sync:resources` 补齐(幂等,不触碰用户/密码);新租户经 `createTenant` 自动带全。
### 2.5 装饰器与拦截器 ### 2.5 装饰器与拦截器
| 项 | 用途 | | 项 | 用途 |
@ -178,9 +184,15 @@ list(@Query() q: PageQuery) { return this.service.paginate(q.page, q.pageSize) }
- `GET /dicts`(仅登录态)一次返回当前租户全部启用类型 + 启用项,供前端缓存。 - `GET /dicts`(仅登录态)一次返回当前租户全部启用类型 + 启用项,供前端缓存。
- 内置字典取值与 [`src/common/enums.ts`](../sources/sysmgr-nest/src/common/enums.ts) 一一对应,由 seed 预置(`common_status` / `user_status` / `resource_type` / `data_scope` / `login_status`)。 - 内置字典取值与 [`src/common/enums.ts`](../sources/sysmgr-nest/src/common/enums.ts) 一一对应,由 seed 预置(`common_status` / `user_status` / `resource_type` / `data_scope` / `login_status`)。
### 2.7 seed 脚本 ### 2.7 seed 与资源同步
`prisma/seed.ts`:幂等 upsert 租户 `touyun`、用户、`super_admin` 角色、全量资源树(菜单+接口+按钮)、内置字典。新增资源/字典/权限码时同步补进 seed。 `prisma/seed.ts` 只初始化平台租户 `touyun`(租户/用户/角色/资源/字典)。资源与字典的真实单一来源在 `tenant-template.ts` 的 `MENUS`/`APIS`/`BUTTONS`/`DICTS`,`seed` 与 `createTenant` 都调用 `initTenant`;新增资源后对**存量租户**跑 `pnpm sync:resources` 补齐。
### 2.8 账号与会话安全
- **首登强制改密**:`user.must_change_password` 标记临时密码(管理员创建默认强制、重置密码、建租户随机密码);登录响应返回 `mustChangePassword`,`JwtAuthGuard` 对未改密用户**只放行** `PATCH /auth/me/password`,其余接口 403。
- **账号锁定**:`user.locked_at` / `lock_reason` 独立于 `status`(停用);管理员锁定即踢全部会话,解锁同时清登录失败计数。
- **在线会话**:会话 = `refresh_token`,`sid` 标识单次登录(刷新轮换沿用同一 `sid`);`GET /sessions` 列活跃会话,`POST /sessions/:sid/kick` 单会话强退(吊销 refresh + sid 黑名单),`POST /users/:id/kick-sessions` 踢全部(复用 `tokenVersion`)。
--- ---
@ -247,6 +259,7 @@ meta:
``` ```
- 菜单从后端 `GET /auth/menus` 拉取(超管=全部启用菜单,普通用户=角色可访问菜单+祖先),存 `menu` store。 - 菜单从后端 `GET /auth/menus` 拉取(超管=全部启用菜单,普通用户=角色可访问菜单+祖先),存 `menu` store。
- 页签存 `tabs` store,登出/401 清空。 - 页签存 `tabs` store,登出/401 清空。
- **多页签 keep-alive**:布局用 `<keep-alive :include>` 缓存打开页签的组件,切换保留状态、关闭销毁;页面默认缓存,在 `<route>` 里声明 `meta.keepAlive: false` 可关闭。**每个菜单页面须 `defineOptions({ name })`**(name = 路径末段,如 `user`/`login-log`/`dashboard`),作为缓存标识。
### 3.5 样式:UnoCSS @apply ### 3.5 样式:UnoCSS @apply
@ -306,6 +319,8 @@ meta:
| `width` | `string` | 控件宽度 | | `width` | `string` | 控件宽度 |
| `treeData`/`treeProps` | — | `format:'tree-select'` 时的树数据与字段映射 | | `treeData`/`treeProps` | — | `format:'tree-select'` 时的树数据与字段映射 |
> `type:'boolean'` 渲染为开关(el-switch),配合 `default:true/false` 设定默认值;布尔字段常用 `showOn: ['create']` 只在新增时出现。
```ts ```ts
const schema: JsonSchema = { const schema: JsonSchema = {
type: 'object', type: 'object',
@ -479,7 +494,7 @@ useDict(code) // → { items, tagMap, options },均响应式
### 5.1 新增一个业务实体(含权限码 + 页面)完整走查 ### 5.1 新增一个业务实体(含权限码 + 页面)完整走查
以新增「岗位 post」为例: 以新增「岗位 post」为例,完整跑通「后端 → 前端 → 资源注册 → 授权验证」:
**后端** **后端**
1. `prisma/schema.prisma` 加 `Post` 模型(`sys_post`,带 `tenant_code` + 公共字段)。 1. `prisma/schema.prisma` 加 `Post` 模型(`sys_post`,带 `tenant_code` + 公共字段)。
@ -488,12 +503,17 @@ useDict(code) // → { items, tagMap, options },均响应式
4. 新建 `src/posts/`(module/controller/service/dto),service `extends BaseCrudService`。 4. 新建 `src/posts/`(module/controller/service/dto),service `extends BaseCrudService`。
5. controller 挂 `@RequirePermissions(Permissions.post.xxx)` + 写操作 `@AuditLog('post', 'xxx')`。 5. controller 挂 `@RequirePermissions(Permissions.post.xxx)` + 写操作 `@AuditLog('post', 'xxx')`。
6. `app.module.ts` 注册 `PostsModule`。 6. `app.module.ts` 注册 `PostsModule`。
7. `prisma/seed.ts` 的 `APIS` 加 4 条接口资源(method/path/code),超管即全量授权。 7. **资源注册**:`src/common/tenant-template.ts` 的 `APIS` 加 4 条接口资源(`code=权限码`、`method`、`path`、`parent='post'`),`MENUS` 加菜单节点(`parent`、`path`、`component`、`icon`)。
8. 存量租户补资源:`pnpm sync:resources`(幂等);新租户 `createTenant` 自动带全。
**前端** **前端**
1. `src/api/post.ts` 加类型 + `listPosts/createPost/...`。 1. `src/api/post.ts` 加类型 + `listPosts/createPost/updatePost/deletePost`。
2. `src/pages/system/post/index.vue` 用 `ty-crud` 组装(列用 `dictCode`,表单用 JSON Schema)。 2. `src/pages/system/post/index.vue`:`<script setup>` 顶部加 `defineOptions({ name: 'post' })`(keep-alive 缓存标识),用 `ty-crud` 组装(列用 `dictCode`,表单用 JSON Schema,路由由文件路径自动生成)。
3. seed 的 `MENUS` 加菜单节点(path/component/icon),路由自动生成。 3. 若该页不要缓存,加 `<route lang="yaml">meta: keepAlive: false</route>`。
**授权验证**
1. 用超管登录「角色管理」给某角色分配 post 菜单 + 接口资源。
2. 切到该角色的用户登录,应看到「岗位管理」菜单、能操作;无权限用户看不到。
### 5.2 新增一个字典类型 ### 5.2 新增一个字典类型
@ -506,16 +526,16 @@ useDict(code) // → { items, tagMap, options },均响应式
### 5.3 新增一个权限码 / 按钮 ### 5.3 新增一个权限码 / 按钮
1. `common/permissions.ts` 加 `domain.action`。 1. `common/permissions.ts` 加 `domain.action`。
2. `seed.ts` 的 `APIS`(接口)或 `BUTTONS`(纯前端按钮)加对应资源,跑 `pnpm seed`。 2. `tenant-template.ts` 的 `APIS`(接口)或 `BUTTONS`(纯前端按钮)加对应资源;存量租户跑 `pnpm sync:resources`。
3. 后端 controller 用 `@RequirePermissions(...)`;前端按钮权限后续按资源 code 下发渲染。 3. 后端 controller 用 `@RequirePermissions(...)`;前端按钮用 `v-permission` 或资源 code 下发渲染。
### 5.4 新增租户 ### 5.4 新增租户
「系统管理 → 租户管理」新增;或 seed 里 upsert。租户之间 `tenant_code` 隔离,登录时按账号+租户识别当前租户。 「系统管理 → 租户管理」新增;或 seed 里 upsert。租户之间 `tenant_code` 隔离,登录时按账号+租户识别当前租户。
### 5.5 扩展 seed ### 5.5 扩展资源 / 字典
改 `prisma/seed.ts` 的 `MENUS` / `APIS` / `BUTTONS` / `DICTS` 常量,跑 `pnpm seed`(幂等,重复运行不重复插入)。 改 `tenant-template.ts` 的 `MENUS` / `APIS` / `BUTTONS` / `DICTS` 常量;新租户自动生效,存量租户跑 `pnpm sync:resources` 补齐(幂等,重复运行不重复插入)。
--- ---
@ -542,3 +562,6 @@ useDict(code) // → { items, tagMap, options },均响应式
| 改字典 label 页面没变 | 字典缓存未刷新:重新登录,或字典页保存后 `dictStore.fetch(true)` 强制刷新 | | 改字典 label 页面没变 | 字典缓存未刷新:重新登录,或字典页保存后 `dictStore.fetch(true)` 强制刷新 |
| 登录失败 | 确认 seed 已跑、账号 `touyun / Touyun@123`、租户 `touyun` 存在且 enabled | | 登录失败 | 确认 seed 已跑、账号 `touyun / Touyun@123`、租户 `touyun` 存在且 enabled |
| 内置项 value 改不动 | 内置项受保护,仅 label/排序/颜色可改(有意为之) | | 内置项 value 改不动 | 内置项受保护,仅 label/排序/颜色可改(有意为之) |
| 首登登录后所有接口 403 | `must_change_password=true`,需先改密:仅 `PATCH /auth/me/password` 放行,前端会强制跳改密页 |
| 强退后 token 仍短暂可用 | access 最长 2h 内失效(sid 黑名单);refresh 已被吊销无法续期 |
| 切页签数据没保留 | 该页声明了 `meta.keepAlive:false`,或页面缺 `defineOptions({ name })` 导致缓存标识失效 |