liuyun b81b45d05a
Some checks are pending
CI / backend (push) Waiting to run
CI / frontend (push) Waiting to run
docs: CLAUDE.md 进度表 + guide tenantCode/fetch(true) 修正
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 17:22:16 +08:00

545 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 通用系统管理脚手架 · 开发帮助手册
> 面向 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 自动刷新;注册/忘记密码默认隐藏(`showRegister`/`showForgetPassword` 可开启) |
| **权限管理** | 用户(增删改/重置密码/启停/锁定)、角色(分配资源 + 数据权限)、资源(菜单/按钮/接口统一资源树)、组织(部门树) |
| **系统管理** | 租户、字典(类型 + 项,支持内置保护)、参数配置(键值) |
| **系统监控** | 操作日志、登录日志(只读查询) |
| **布局与主题** | 多布局(左右 / 上下 / 纯上下 + MixBar)、暗黑模式、主题色、多页签、面包屑、全屏 |
| **平台能力** | 多租户隔离(tenantCode)、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_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_DB=0
REDIS_KEY_PREFIX="sysmgr:"
JWT_SECRET="请替换为强密钥(生产至少 32 位)"
JWT_ACCESS_TTL=7200
JWT_REFRESH_TTL=604800
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/
│ # 每个域一个目录:<name>.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<User[]>('/users', { params: { page: 1 } }) // 自动解包,直接拿 data
```
- 自动注入 `Authorization`;响应自动拆 `{code,message,data}`(`code=0` 返回 `data`);`401` 自动刷新 token 并重放;失败抛 `ApiError`。
- 方法:`http.get/post/put/patch/delete/request<T>(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/**` 目录自动生成;页面用 `<route lang="yaml">` 自定义块声明 `meta.layout`(`default` | `blank`)选择布局:
```vue
<route lang="yaml">
meta:
layout: blank
requiresAuth: false
</route>
```
- 菜单从后端 `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<string, any>` | — | 必填,表单数据(就地绑定) |
| `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<string, any>` | — | 搜索条件 |
| `collapseCount` | `number` | `3` | 折叠时显示字段数,超出「展开」 |
**Emits**:`search`、`reset`。
#### 3.6.3 ty-table(表格)
**Props**
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `columns` | `TableColumn[]` | — | 列配置 |
| `data` | `Record<string, any>[]` | — | 行数据 |
| `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<string,string>` | `{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<string, {label,type}>` | `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<PageResult>` | — | 分页查询 |
| `create` | `(data) => Promise<unknown>` | — | 新增 |
| `update` | `(id, data) => Promise<unknown>` | — | 更新 |
| `remove` | `(id) => Promise<unknown>` | — | 删除 |
| `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
<ty-crud
entity-name="用户"
:columns="columns"
:search-schema="searchSchema"
:form-schema="formSchema"
:list="list" :create="create" :update="update" :remove="remove"
id-key="id" name-key="username" :edit-readonly-keys="['username']"
/>
```
#### 3.6.5 showDialog / ty-dialog(弹窗)
`showDialog(options)` 打开弹窗,返回 Promise(确认 resolve 内容组件 `submit()` 数据,取消/关闭 resolve `null`)。`<ty-dialog />` 需挂载一次作为宿主(通常已在根组件)。
```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<string, any>` | 传给内容组件的 props |
| `confirmText` / `cancelText` | `string` | 按钮文案 |
| `showFooter` | `boolean` | 默认显示取消/确定;`false` 时内容组件自带底部 |
| `onConfirm` | `(data) => void\|Promise` | 确认回调 |
#### 3.6.6 useRequest
```ts
useRequest<TData, TParams>(fetcher, options?)
// fetcher: (...args: [...TParams, AbortSignal]) => Promise<TData>
```
**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: '<code>'` 或 `useDict('<code>')` 引用。
> 内置字典(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(true)` 强制刷新 |
| 登录失败 | 确认 seed 已跑、账号 `touyun / Touyun@123`、租户 `touyun` 存在且 enabled |
| 内置项 value 改不动 | 内置项受保护,仅 label/排序/颜色可改(有意为之) |