diff --git a/docs/guide.md b/docs/guide.md new file mode 100644 index 0000000..e794b85 --- /dev/null +++ b/docs/guide.md @@ -0,0 +1,540 @@ +# 通用系统管理脚手架 · 开发帮助手册 + +> 面向 fork 本工程进行二次开发的工程师。读完能跑起来、能看懂结构、能照着新增功能。 +> 更底层的架构与 spec 约定见 [`docs/architecture.md`](./architecture.md)、[`docs/spec-conventions.md`](./spec-conventions.md),工作流见根目录 [`CLAUDE.md`](../CLAUDE.md)。 + +--- + +## 0. 功能介绍与功能清单 + +一套可 fork 的「通用系统管理脚手架」:开箱即用后台基础能力,fork 后只改业务、不重复造轮子。后端 NestJS + Prisma + MySQL,前端 Vue 3 + Vite + Pinia + UnoCSS。 + +| 功能块 | 说明 | +|---|---| +| **认证** | 登录 / 注册 / 忘记密码 / 登出;JWT(access 2h + refresh 7d),401 自动刷新 | +| **权限管理** | 用户(增删改/重置密码/启停/锁定)、角色(分配资源 + 数据权限)、资源(菜单/按钮/接口统一资源树)、组织(部门树) | +| **系统管理** | 租户、字典(类型 + 项,支持内置保护)、参数配置(键值) | +| **系统监控** | 操作日志、登录日志(只读查询) | +| **布局与主题** | 多布局(左右 / 上下 / 纯上下 + MixBar)、暗黑模式、主题色、多页签、面包屑、全屏 | +| **平台能力** | 多租户隔离(tenant_id)、RBAC(资源表统一授权 + 数据权限 data_scope)、审计日志落库、字典驱动渲染 | + +**代码规范**:本工程用 openspec 追踪需求与变更(spec-driven),新增功能走 `propose → apply → archive` 流程,见 [`CLAUDE.md`](../CLAUDE.md)。本文只讲「代码怎么改」,不讲「流程怎么走」。 + +--- + +## 1. 快速开始 + +### 1.1 环境要求 + +| 依赖 | 版本 | +|---|---| +| Node | 22.23.1(以 `.nvmrc` / `engines` 为准) | +| pnpm | 12.10.1(以 `packageManager` 为准) | +| MySQL | 8.x | +| Redis | 6+ | + +### 1.2 拉取与安装 + +```bash +git clone <仓库地址> && cd <目录> +git submodule update --init --recursive # 拉取 sysmgr-nest / sysmgr-admin + +# 后端 +cd sources/sysmgr-nest +cp .env.example .env # 若无 .env.example 则手建,见下 +pnpm install +npx prisma migrate deploy # 应用已有迁移(开发期可用 migrate dev) +pnpm seed # 初始化租户/用户/角色/资源/内置字典 + +# 前端 +cd ../sysmgr-admin +pnpm install +``` + +### 1.3 `.env`(后端) + +```env +DATABASE_URL="mysql://root:密码@127.0.0.1:3306/sysmgr" +REDIS_URL="redis://127.0.0.1:6379" +JWT_ACCESS_SECRET="..." +JWT_REFRESH_SECRET="..." +PORT=3000 +``` + +### 1.4 启动 + +```bash +# 后端(:3000,全局前缀 /api/v1) +cd sources/sysmgr-nest && pnpm start:dev + +# 前端(Vite,直连 VITE_API_BASE_URL,默认 http://localhost:3000/api/v1,见 .env) +cd sources/sysmgr-admin && pnpm dev:dev +``` + +### 1.5 默认账号 + +| 项 | 值 | +|---|---| +| 租户 | `touyun` | +| 用户名 | `touyun` | +| 密码 | `Touyun@123` | +| 角色 | `super_admin`(超管,绕过权限校验、数据权限看全量) | + +--- + +## 2. 后端开发指南(sysmgr-nest) + +### 2.1 目录与分层 + +``` +src/ +├── auth/ # 认证:登录/刷新/登出、菜单树、JWT 守卫、权限守卫、装饰器、请求上下文 +├── users/ roles/ resources/ orgs/ dicts/ configs/ tenants/ audit-logs/ login-logs/ +│ # 每个域一个目录:.module.ts / controller / service / dto +├── prisma/ # PrismaService:统一注入多租户过滤 + 软删过滤 +├── common/ # BaseCrudService、enums、permissions、page.dto、拦截器/过滤器、Redis +└── logging/ # pino 日志、请求体日志拦截器 +``` + +每个域遵循同构结构(以 `tenants` 为例): + +``` +tenants/ +├── tenants.module.ts # 声明 controller + service +├── tenants.controller.ts # 路由 + @RequirePermissions + @AuditLog +├── tenants.service.ts # extends BaseCrudService,写业务 +└── dto.ts # class-validator DTO(创建/更新/查询) +``` + +### 2.2 Prisma 约定 + +- 表名 `sys_` 前缀,列名 snake_case(模型 camelCase + `@map`)。 +- 主键 UUID v7:`id String @id @default(uuid(7)) @db.VarChar(36)`。 +- 业务键关联用 `code`(角色/资源/组织/字典/租户),引用用户用 `id`,树引用用 `parent_code`。 +- 业务软删实体带 `created_at / created_by / updated_at / deleted_at`;日志/会话实体无 `deleted_at`。 +- 多租户:业务实体一律带 `tenant_code`。 + +### 2.3 BaseCrudService + PrismaService + +所有域 service 继承 `BaseCrudService(prisma, '模型名')`,直接获得: + +| 方法 | 说明 | +|---|---| +| `paginate(page, pageSize, where?, orderBy?)` | 分页,返回 `{ list, total, page, pageSize }` | +| `list(where?, orderBy?)` | 列表 | +| `findById(id)` | 按 id 查 | +| `create(data)` | 创建,自动注入 `tenantCode`(Tenant 除外) | +| `update(id, data)` | 更新 | +| `softRemove(id)` | 软删(置 `deletedAt`) | + +`PrismaService` 通过 `$extends` 全局拦截,**无需手写**: +- 读操作自动追加 `tenantCode` 过滤(`Tenant` 模型除外); +- 软删模型(User/Role/Resource/Org/DictType/DictItem/Config)读操作自动追加 `deletedAt: null`。 + +> 因此业务 service 里写 `this.list({ status: 'enabled' })` 即可,租户与软删已由框架兜底。 + +### 2.4 RBAC:资源表统一授权 + +- 菜单、按钮、接口统一在 `resource` 表,`type ∈ { menu, button, api }`。 +- `api`/`button` 型资源的 `code` 即**权限码**,形如 `domain:action`(`user:list`、`user:create`)。 +- 权限码清单在 [`src/common/permissions.ts`](../sources/sysmgr-nest/src/common/permissions.ts),控制器与 seed 共用。 + +**三级鉴权**(由全局 `JwtAuthGuard` + `PermissionsGuard` 实现): + +| 层级 | 写法 | 效果 | +|---|---|---| +| 公开 | `@Public()` | 无需 token | +| 仅登录 | 不加权限装饰器 | 只要登录态 | +| 权限码 | `@RequirePermissions(Permissions.user.list)` | 登录 + 拥有该权限码 | + +```ts +@Get() +@RequirePermissions(Permissions.user.list) // 登录 + user:list +list(@Query() q: PageQuery) { return this.service.paginate(q.page, q.pageSize) } +``` + +- 角色 `code === 'super_admin'` 自动绕过权限码校验、`data_scope` 看全量。 +- 数据权限 `role.data_scope ∈ { all, dept, dept_and_child, self, custom }`,`custom` 时经 `role_org` 关联可见部门。 + +### 2.5 装饰器与拦截器 + +| 项 | 用途 | +|---|---| +| `@Public()` | 跳过登录校验 | +| `@RequirePermissions(...codes)` | 要求权限码 | +| `@AuditLog('模块', '动作')` | 写操作自动落 `sys_audit_log`(异步,失败不阻塞业务) | +| `@CurrentUser` | 取当前登录用户 | + +全局拦截器/过滤器(`app.module.ts` 注册):`TransformInterceptor`(响应封装 `{code,message,data}`)、`AuditLogInterceptor`(审计)、`AllExceptionsFilter`(异常→`code = HTTP*100`)。 + +### 2.6 字典域(内置字典 + 保护) + +- 字典类型/字典项在 `dicts/`;内置字典 `built_in=true`。 +- **保护规则**:内置类型不可删、内置项不可删、内置项 `value` 不可改;`label/sort/color_type` 可改;内置类型下可新增非内置项。 +- `GET /dicts`(仅登录态)一次返回当前租户全部启用类型 + 启用项,供前端缓存。 +- 内置字典取值与 [`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 脚本 + +`prisma/seed.ts`:幂等 upsert 租户 `touyun`、用户、`super_admin` 角色、全量资源树(菜单+接口+按钮)、内置字典。新增资源/字典/权限码时同步补进 seed。 + +--- + +## 3. 前端开发指南(sysmgr-admin) + +### 3.1 目录与自动导入 + +``` +src/ +├── api/ # http.ts(axios 封装)+ 每域 api(user/role/resource/org/dict/...) +├── components/ # ty-* 组件库(全局自动导入,无需 import) +├── composables/ # useRequest、useDict +├── config/ # layout.ts(布局设置)、login.ts(登录页配置)、menu.ts(兜底菜单) +├── layouts/ # default.vue(后台壳)、blank.vue(登录页等) +├── pages/ # 页面(unplugin-vue-router 按目录自动生成路由) +├── stores/modules/ # Pinia:app/auth/menu/tabs/dict +├── types/ # crud.ts(JsonSchema/TableColumn 等)、resource.ts +└── utils/ # dialog.ts(showDialog)、tree.ts、token.ts、theme.ts +``` + +已配置 `unplugin-auto-import` + `unplugin-vue-components`:Vue API、Element Plus、`@/components` 下的组件**均无需手动 import**。 + +### 3.2 网络请求:axios 封装 + useRequest + +统一用全局 `http`(`@/api/http`): + +```ts +const data = await http.get('/users', { params: { page: 1 } }) // 自动解包,直接拿 data +``` + +- 自动注入 `Authorization`;响应自动拆 `{code,message,data}`(`code=0` 返回 `data`);`401` 自动刷新 token 并重放;失败抛 `ApiError`。 +- 方法:`http.get/post/put/patch/delete/request(url, data?, config?)`。 + +`useRequest`(`@/composables/useRequest`)管理请求状态: + +```ts +const { data, loading, run, refresh } = useRequest((signal: AbortSignal) => listUsers(q, signal)) +// 或手动触发:useRequest(fetcher, { manual: true }) → run(...args) +``` + +### 3.3 字典:dict store + useDict + dictCode + +登录后自动缓存全部启用字典,页面/组件通过 `dictCode` 或 `useDict` 读取,**不要硬编码中文/颜色映射**。 + +```ts +const { items, tagMap, options } = useDict('common_status') +// items : [{ label:'启用', value:'enabled', colorType:'success', sort:1 }, ...] +// tagMap : { enabled: { label:'启用', type:'success' }, ... } +// options: [{ label:'启用', value:'enabled' }, ...] +``` + +在 JSON Schema / 表格列里直接写 `dictCode` 即可(见 §3.7 组件 API)。 + +### 3.4 菜单 / 路由 / 布局 + +- 路由由 `unplugin-vue-router` 按 `src/pages/**` 目录自动生成;页面用 `` 自定义块声明 `meta.layout`(`default` | `blank`)选择布局: + +```vue + +meta: + layout: blank + requiresAuth: false + +``` +- 菜单从后端 `GET /auth/menus` 拉取(超管=全部启用菜单,普通用户=角色可访问菜单+祖先),存 `menu` store。 +- 页签存 `tabs` store,登出/401 清空。 + +### 3.5 样式:UnoCSS @apply + +优先用 `@apply` 工具类,少写裸 CSS。注意主题色工具类写法: + +```scss +.foo { + @apply bg-bg-base text-text-primary; // 背景/文字主题色:bg-bg-* / text-text-* + @apply border-solid rounded-base; // 语义色:primary/success/warning/danger/info +} +``` + +### 3.6 组件 API 文档 + +下面 5 个业务组件 + 2 个 composable 是日常开发主力。布局类组件(ty-header/ty-sidebar/ty-tabs 等)由 `layouts/default.vue` 组装,一般业务页无需直接使用。 + +#### 3.6.1 ty-form(JSON Schema 表单) + +**Props** + +| 属性 | 类型 | 默认 | 说明 | +|---|---|---|---| +| `schema` | `JsonSchema` | — | 必填,表单结构描述 | +| `modelValue` | `Record` | — | 必填,表单数据(就地绑定) | +| `inline` | `boolean` | `false` | 行内布局(搜索用) | +| `mode` | `'create' \| 'edit'` | `undefined` | 配合字段 `showOn` 过滤 | +| `enterToSubmit` | `boolean` | `false` | 输入框回车触发 `submit` | +| `readonlyKeys` | `string[]` | `[]` | 强制只读的字段名 | + +**Expose(通过 `ref` 调用)** + +| 方法 | 说明 | +|---|---| +| `submit()` | 校验并返回表单数据(校验失败抛错) | +| `validate()` | 仅校验 | +| `resetFields()` | 重置 | +| `clearValidate()` | 清空校验状态 | + +**Emits**:`submit`。 + +**JsonSchemaProperty 字段速查** + +| 字段 | 类型 | 说明 | +|---|---|---| +| `type` | `'string'\|'integer'\|'number'\|'boolean'` | 控件类型 | +| `title` | `string` | 标签 | +| `placeholder` | `string` | 占位 | +| `enum` / `enumNames` | `(string\|number\|boolean)[]` / `string[]` | 下拉选项(一一对应) | +| `dictCode` | `string` | 字典 code,声明后自动生成下拉(优先于 enum) | +| `format` | `'password'\|'email'\|'textarea'\|'tree-select'\|'date'\|'datetime'` | 特殊控件;`date/datetime` 目前按普通输入框渲染 | +| `default` | `unknown` | 默认值 | +| `readOnly` | `boolean` | 只读 | +| `showOn` | `('create'\|'edit')[]` | 仅指定模式显示 | +| `when` | `{ field; equals?; in? }` | 依赖字段值联动显隐 | +| `minLength`/`maxLength`/`minimum`/`maximum` | `number` | 校验 | +| `prefixIcon` | `string` | 输入框前缀 iconify 图标 | +| `width` | `string` | 控件宽度 | +| `treeData`/`treeProps` | — | `format:'tree-select'` 时的树数据与字段映射 | + +```ts +const schema: JsonSchema = { + type: 'object', + properties: { + status: { type: 'string', title: '状态', dictCode: 'common_status', default: 'enabled' }, + path: { type: 'string', title: '路由', when: { field: 'type', equals: 'menu' } }, + }, + required: ['status'], +} +``` + +#### 3.6.2 ty-search-form(搜索 + 折叠) + +**Props** + +| 属性 | 类型 | 默认 | 说明 | +|---|---|---|---| +| `schema` | `JsonSchema` | — | 搜索字段 | +| `modelValue` | `Record` | — | 搜索条件 | +| `collapseCount` | `number` | `3` | 折叠时显示字段数,超出「展开」 | + +**Emits**:`search`、`reset`。 + +#### 3.6.3 ty-table(表格) + +**Props** + +| 属性 | 类型 | 默认 | 说明 | +|---|---|---|---| +| `columns` | `TableColumn[]` | — | 列配置 | +| `data` | `Record[]` | — | 行数据 | +| `loading` | `boolean` | `false` | 加载态 | +| `selection` | `boolean` | `false` | 多选列 | +| `total` / `page` / `pageSize` / `pageSizes` | `number`/`number[]` | `0`/`1`/`10`/`[10,20,50]` | 分页 | +| `actionsWidth` | `number\|string` | `200` | 操作列宽 | +| `rowKey` | `string` | `'id'` | 行唯一键(树形用 code) | +| `treeProps` | `Record` | `{children:'children'}` | 树形字段映射 | +| `defaultExpandAll` | `boolean` | `false` | 树形默认展开 | + +**Emits**:`update:page`、`update:pageSize`、`selection-change`。 +**Slots**:`actions`(作用域 `{ row }`)。 + +**TableColumn 字段** + +| 字段 | 类型 | 说明 | +|---|---|---| +| `prop` | `string` | 字段名 | +| `label` | `string` | 列标题 | +| `width` / `minWidth` | `number\|string` | 列宽 | +| `align` | `'left'\|'center'\|'right'` | 对齐 | +| `type` | `'text'\|'tag'\|'datetime'` | 渲染类型(默认 text) | +| `tagMap` | `Record` | `type:'tag'` 的值→标签映射 | +| `dictCode` | `string` | `type:'tag'` 时按字典渲染(优先于 tagMap) | +| `formatter` | `(value, row) => string` | 自定义格式化 | + +```ts +const columns: TableColumn[] = [ + { prop: 'status', label: '状态', width: 90, type: 'tag', dictCode: 'common_status' }, + { prop: 'createdAt', label: '创建时间', width: 160, type: 'datetime' }, +] +``` + +#### 3.6.4 ty-crud(整页 CRUD) + +组合「搜索 + 工具栏 + 表格 + 弹窗表单 + 删除」,多数单表页面用它几行搞定。 + +**Props** + +| 属性 | 类型 | 默认 | 说明 | +|---|---|---|---| +| `entityName` | `string` | — | 实体名(标题/确认文案) | +| `columns` | `TableColumn[]` | — | 列 | +| `formSchema` | `JsonSchema` | — | 新增/编辑表单 | +| `searchSchema` | `JsonSchema` | — | 搜索(可选) | +| `list` | `(query, signal?) => Promise` | — | 分页查询 | +| `create` | `(data) => Promise` | — | 新增 | +| `update` | `(id, data) => Promise` | — | 更新 | +| `remove` | `(id) => Promise` | — | 删除 | +| `idKey` / `nameKey` | `string` | `'id'`/`'name'` | 主键/删除确认显示字段 | +| `editReadonlyKeys` | `string[]` | `[]` | 编辑只读字段 | +| `getEditReadonlyKeys` | `(row) => string[]` | — | 按行计算只读(优先) | +| `actions` | `CrudAction[]` | `['edit','delete']` | 操作列(内置或自定义 `{label,type,onClick,show}`) | +| `collapseCount` | `number` | `2` | 操作按钮折叠数 | +| `selection` | `boolean` | `false` | 多选(配合 `#toolbar-right`) | +| `actionsWidth` | `number\|string` | `140` | 操作列宽 | + +**Slots**:`toolbar`、`toolbar-right`(作用域 `{ selectedRows }`)。 +**Expose**:`reload()`。 + +```vue + +``` + +#### 3.6.5 showDialog / ty-dialog(弹窗) + +`showDialog(options)` 打开弹窗,返回 Promise(确认 resolve 内容组件 `submit()` 数据,取消/关闭 resolve `null`)。`` 需挂载一次作为宿主(通常已在根组件)。 + +```ts +const data = await showDialog({ + component: TyForm, // 内容组件(通常传 TyForm) + title: '新增用户', + width: '520px', + props: { schema, modelValue: model, mode: 'create' }, +}) +if (data == null) return // 取消 +await createUser(data) +``` + +**DialogOptions** + +| 字段 | 类型 | 说明 | +|---|---|---| +| `component` | `Component` | 内容组件(须 expose `submit()`) | +| `title` / `width` | `string` | 标题/宽度(默认 480px) | +| `props` | `Record` | 传给内容组件的 props | +| `confirmText` / `cancelText` | `string` | 按钮文案 | +| `showFooter` | `boolean` | 默认显示取消/确定;`false` 时内容组件自带底部 | +| `onConfirm` | `(data) => void\|Promise` | 确认回调 | + +#### 3.6.6 useRequest + +```ts +useRequest(fetcher, options?) +// fetcher: (...args: [...TParams, AbortSignal]) => Promise +``` + +**Options**:`manual`(默认 false 自动请求)、`defaultParams`、`refreshDeps`(依赖变化重请求)、`onBefore/onSuccess/onError/onFinally`。 +**返回**:`data`、`loading`、`error`、`run(...args)`、`refresh()`、`cancel()`、`mutate(data|fn)`。 + +#### 3.6.7 useDict + +```ts +useDict(code) // → { items, tagMap, options },均响应式 +``` + +--- + +## 4. 联调契约 + +### 4.1 响应封装 + +```json +{ "code": 0, "message": "ok", "data": { } } +``` + +- `code=0` 成功;失败 `code = HTTP状态码 * 100`(401→40100,403→40300,400→40000,500→50000)。 +- 前端 `http` 已自动拆包:成功拿 `data`,失败抛 `ApiError(code, message)`。 + +### 4.2 分页 + +列表接口 `page/pageSize`,返回 `{ list, total, page, pageSize }`。 + +### 4.3 三级鉴权 + +公开(`@Public`)→ 仅登录(无装饰器)→ 权限码(`@RequirePermissions`),见 §2.4。 + +### 4.4 契约锚点 + +`shared/api-contract`(响应/错误码/分页/鉴权/接口目录)与 `shared/data-model`(字段字典)是前后端唯一锚点,字段/接口变更先改 `shared/`。 + +--- + +## 5. 常用操作手册(Cookbook) + +### 5.1 新增一个业务实体(含权限码 + 页面)完整走查 + +以新增「岗位 post」为例: + +**后端** +1. `prisma/schema.prisma` 加 `Post` 模型(`sys_post`,带 `tenant_code` + 公共字段)。 +2. `npx prisma migrate dev --name add_post` + `prisma generate`。 +3. `src/common/permissions.ts` 加 `post: { list, create, update, delete }`。 +4. 新建 `src/posts/`(module/controller/service/dto),service `extends BaseCrudService`。 +5. controller 挂 `@RequirePermissions(Permissions.post.xxx)` + 写操作 `@AuditLog('post', 'xxx')`。 +6. `app.module.ts` 注册 `PostsModule`。 +7. `prisma/seed.ts` 的 `APIS` 加 4 条接口资源(method/path/code),超管即全量授权。 + +**前端** +1. `src/api/post.ts` 加类型 + `listPosts/createPost/...`。 +2. `src/pages/system/post/index.vue` 用 `ty-crud` 组装(列用 `dictCode`,表单用 JSON Schema)。 +3. seed 的 `MENUS` 加菜单节点(path/component/icon),路由自动生成。 + +### 5.2 新增一个字典类型 + +1. 登录进入「系统管理 → 字典管理」→ 新增字典类型(填 code/name)。 +2. 选中类型 → 新增字典项(label/value/标签色/排序)。 +3. 页面/组件里用 `dictCode: ''` 或 `useDict('')` 引用。 + +> 内置字典(seed 预置)的 `value` 与内置项受保护,只能改 label/排序/颜色;普通字典完全自由。 + +### 5.3 新增一个权限码 / 按钮 + +1. `common/permissions.ts` 加 `domain.action`。 +2. `seed.ts` 的 `APIS`(接口)或 `BUTTONS`(纯前端按钮)加对应资源,跑 `pnpm seed`。 +3. 后端 controller 用 `@RequirePermissions(...)`;前端按钮权限后续按资源 code 下发渲染。 + +### 5.4 新增租户 + +「系统管理 → 租户管理」新增;或 seed 里 upsert。租户之间 `tenant_code` 隔离,登录时按账号+租户识别当前租户。 + +### 5.5 扩展 seed + +改 `prisma/seed.ts` 的 `MENUS` / `APIS` / `BUTTONS` / `DICTS` 常量,跑 `pnpm seed`(幂等,重复运行不重复插入)。 + +--- + +## 6. 规范与约定 + +- **提交信息**:Conventional Commits(`feat`/`fix`/`chore`/`docs`/`refactor`)。 +- **命名**:后端模型 camelCase + `@map` snake_case;业务键统一 `code`;权限码 `domain:action`。 +- **样式**:UnoCSS `@apply`,少写裸 CSS;主题色用 `bg-bg-*` / `text-text-*`。 +- **自测后停服务**:冒烟测试临时起的服务,测完立即停掉。 +- **单一事实源**:字段/接口以 `shared/data-model`、`shared/api-contract` 为准,不各自命名。 + +--- + +## 7. 排障 FAQ + +| 现象 | 原因 / 处理 | +|---|---| +| 启动报 `EADDRINUSE :3000` | 端口被占,换 `PORT=3100` 或停掉旧进程 | +| `prisma migrate dev` 非交互报错 | 用 `prisma migrate diff` 生成 SQL + `prisma migrate deploy`,或开发期用 `prisma db push` | +| seed 重复插入报唯一约束 | 检查 upsert 的 `where` 是否用了唯一键(如 `tenantCode_code`) | +| 接口返回 401 | token 缺失/过期;前端会自动刷新,刷新失败跳登录 | +| 接口返回 403 | 权限码未授权;确认 resource 表登记了该接口、角色已分配 | +| 字典下拉/标签不显示 | 确认登录后 `dictStore.fetch()` 成功;字典类型/项 `status=enabled`;`dictCode` 拼写一致 | +| 改字典 label 页面没变 | 字典缓存未刷新:重新登录,或字典页保存后 `dictStore.fetch()` | +| 登录失败 | 确认 seed 已跑、账号 `touyun / Touyun@123`、租户 `touyun` 存在且 enabled | +| 内置项 value 改不动 | 内置项受保护,仅 label/排序/颜色可改(有意为之) |