- guide.md 补「升级同步(git upstream+merge)+ 可定制面三条铁律」 - shared/platform 主 spec 新增「脚手架可定制面」需求 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
28 KiB
通用系统管理脚手架 · 开发帮助手册
面向 fork 本工程进行二次开发的工程师。读完能跑起来、能看懂结构、能照着新增功能。 更底层的架构与 spec 约定见
docs/architecture.md、docs/spec-conventions.md,工作流见根目录CLAUDE.md。
0. 功能介绍与功能清单
一套可 fork 的「通用系统管理脚手架」:开箱即用后台基础能力,fork 后只改业务、不重复造轮子。后端 NestJS + Prisma + MySQL,前端 Vue 3 + Vite + Pinia + UnoCSS。
| 功能块 | 说明 |
|---|---|
| 认证 | 登录 / 登出;JWT(access 2h + refresh 7d),401 自动刷新;首登强制改密;注册/忘记密码默认隐藏(showRegister/showForgetPassword 可开启) |
| 权限管理 | 用户(增删改/重置密码/启停/锁定解锁)、角色(分配资源 + 数据权限)、资源(菜单/按钮/接口统一资源树)、组织(部门树) |
| 系统管理 | 租户、字典(类型 + 项,支持内置保护)、参数配置(键值) |
| 系统监控 | 操作日志、登录日志(只读查询)、在线会话(强退单会话 / 踢全部) |
| 布局与主题 | 多布局(左右 / 上下 / 纯上下 + MixBar)、暗黑模式、主题色、多页签(keep-alive 状态保留)、面包屑、全屏 |
| 平台能力 | 多租户隔离(tenantCode)、RBAC(资源表统一授权 + 数据权限 data_scope)、审计日志落库、字典驱动渲染 |
代码规范:本工程用 openspec 追踪需求与变更(spec-driven),新增功能走 propose → apply → archive 流程,见 CLAUDE.md。本文只讲「代码怎么改」,不讲「流程怎么走」。
1. 快速开始
1.1 环境要求
| 依赖 | 版本 |
|---|---|
| Node | 22.23.1(以 .nvmrc / engines 为准) |
| pnpm | 12.10.1(以 packageManager 为准) |
| MySQL | 8.x |
| Redis | 6+ |
1.2 拉取与安装
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(后端)
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 启动
# 后端(: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/ sessions/
│ # 每个域一个目录:<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,控制器与 seed 共用。
三级鉴权(由全局 JwtAuthGuard + PermissionsGuard 实现):
| 层级 | 写法 | 效果 |
|---|---|---|
| 公开 | @Public() |
无需 token |
| 仅登录 | 不加权限装饰器 | 只要登录态 |
| 权限码 | @RequirePermissions(Permissions.user.list) |
登录 + 拥有该权限码 |
@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关联可见部门。
资源注册(新模块接 RBAC):菜单/接口/按钮资源的单一来源是 src/common/tenant-template.ts 的 MENUS / APIS / BUTTONS,权限码来源是 src/common/permissions.ts。新增模块三步:
permissions.ts加domain:action权限码;tenant-template.ts的MENUS/APIS/BUTTONS加资源(parent挂到上级菜单 code);- 存量租户跑
pnpm sync:resources补齐(幂等,不触碰用户/密码);新租户经createTenant自动带全。
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一一对应,由 seed 预置(common_status/user_status/resource_type/data_scope/login_status)。
2.7 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)。
2.9 定时任务
用 @nestjs/schedule 的 @Cron 声明定时任务;ScheduleModule.forRoot() 已在 app.module.ts 注册。现有任务:src/common/cleanup.service.ts 每天 03:07 清理过期/吊销的 refresh token 与超过 90 天的审计/登录日志。
新增定时任务:
- 新建 service,方法挂
@Cron('秒 分 时 日 月 周')(cron 表达式,如@Cron('0 3 * * *')每天 03:00); - 在
app.module.ts的providers注册该 service; - 任务体内
try/catch并记录日志(失败不抛出,避免 Cron 中断)。
本期定时任务仅清理一个,暂不做「任务注册表 + 界面手动触发」;有需要再扩展。
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):
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)管理请求状态:
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 读取,不要硬编码中文/颜色映射。
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)选择布局:
<route lang="yaml">
meta:
layout: blank
requiresAuth: false
</route>
- 菜单从后端
GET /auth/menus拉取(超管=全部启用菜单,普通用户=角色可访问菜单+祖先),存menustore。 - 页签存
tabsstore,登出/401 清空。 - 多页签 keep-alive:布局用
<keep-alive :include>缓存打开页签的组件,切换保留状态、关闭销毁;页面默认缓存,在<route>里声明meta.keepAlive: false可关闭。每个菜单页面须defineOptions({ name })(name = 路径末段,如user/login-log/dashboard),作为缓存标识;name 须与路径末段一致且全局唯一(避免未来同末段路径互相影响 keep-alive)。
3.5 样式:UnoCSS @apply
优先用 @apply 工具类,少写裸 CSS。注意主题色工具类写法:
.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' 时的树数据与字段映射 |
type:'boolean'渲染为开关(el-switch),配合default:true/false设定默认值;布尔字段常用showOn: ['create']只在新增时出现。
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 |
自定义格式化 |
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()。
<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 /> 需挂载一次作为宿主(通常已在根组件)。
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
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
useDict(code) // → { items, tagMap, options },均响应式
4. 联调契约
4.1 响应封装
{ "code": 0, "message": "ok", "data": { } }
code=0成功;失败code为业务码(5 位[域码 2 位][序 3 位],单一注册表见src/common/error-codes.ts,如10001账号或密码错误、11002用户名已存在);未登记业务码的异常回退HTTP状态码 × 100(40000/40100/40300/40400/40900/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」为例,完整跑通「后端 → 前端 → 资源注册 → 授权验证」:
后端
prisma/schema.prisma加Post模型(sys_post,带tenant_code+ 公共字段)。npx prisma migrate dev --name add_post+prisma generate。src/common/permissions.ts加post: { list, create, update, delete }。- 新建
src/posts/(module/controller/service/dto),serviceextends BaseCrudService。 - controller 挂
@RequirePermissions(Permissions.post.xxx)+ 写操作@AuditLog('post', 'xxx')。 app.module.ts注册PostsModule。- 资源注册:
src/common/tenant-template.ts的APIS加 4 条接口资源(code=权限码、method、path、parent='post'),MENUS加菜单节点(parent、path、component、icon)。 - 存量租户补资源:
pnpm sync:resources(幂等);新租户createTenant自动带全。
前端
src/api/post.ts加类型 +listPosts/createPost/updatePost/deletePost。src/pages/system/post/index.vue:<script setup>顶部加defineOptions({ name: 'post' })(keep-alive 缓存标识),用ty-crud组装(列用dictCode,表单用 JSON Schema,路由由文件路径自动生成)。- 若该页不要缓存,加
<route lang="yaml">meta: keepAlive: false</route>。
授权验证
- 用超管登录「角色管理」给某角色分配 post 菜单 + 接口资源。
- 切到该角色的用户登录,应看到「岗位管理」菜单、能操作;无权限用户看不到。
5.2 新增一个字典类型
- 登录进入「系统管理 → 字典管理」→ 新增字典类型(填 code/name)。
- 选中类型 → 新增字典项(label/value/标签色/排序)。
- 页面/组件里用
dictCode: '<code>'或useDict('<code>')引用。
内置字典(seed 预置)的
value与内置项受保护,只能改 label/排序/颜色;普通字典完全自由。
5.3 新增一个权限码 / 按钮
common/permissions.ts加domain.action。tenant-template.ts的APIS(接口)或BUTTONS(纯前端按钮)加对应资源;存量租户跑pnpm sync:resources。- 后端 controller 用
@RequirePermissions(...);前端按钮用v-permission或资源 code 下发渲染。
5.4 新增租户
「系统管理 → 租户管理」新增;或 seed 里 upsert。租户之间 tenant_code 隔离,登录时按账号+租户识别当前租户。
5.5 扩展资源 / 字典
改 tenant-template.ts 的 MENUS / APIS / BUTTONS / DICTS 常量;新租户自动生效,存量租户跑 pnpm sync:resources 补齐(幂等,重复运行不重复插入)。
6. 规范与约定
- 提交信息:Conventional Commits(
feat/fix/chore/docs/refactor)。 - 命名:后端模型 camelCase +
@mapsnake_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/排序/颜色可改(有意为之) |
| 首登登录后所有接口 403 | must_change_password=true,需先改密:仅 PATCH /auth/me/password 放行,前端会强制跳改密页 |
| 强退后 token 仍短暂可用 | access 最长 2h 内失效(sid 黑名单);refresh 已被吊销无法续期 |
| 切页签数据没保留 | 该页声明了 meta.keepAlive:false,或页面缺 defineOptions({ name }) 导致缓存标识失效 |
8. 升级与定制(fork 后如何同步上游)
脚手架是「通用底座」,fork 成具体项目后,业务代码和脚手架代码必须物理分离,才能在上游升级时干净合并。
8.1 升级同步(git upstream + merge)
git clone <脚手架地址> my-project
cd my-project
git remote rename origin upstream # 脚手架 = upstream
git remote add origin <我的业务仓库地址> # 业务 = origin
# 日常在 main 上开发业务,升级时:
git fetch upstream
git merge upstream/main # 冲突只在「你改过脚手架文件」的地方出现
- 业务代码纯新增(新模块/新页面/新资源)→ 升级几乎零冲突。
- 业务代码改了脚手架文件 → 上游升级该文件即冲突,改得越深越痛。
- 将来项目变多、需要版本语义时,再评估把脚手架抽成 npm 包 / monorepo 依赖(彻底隔离、按版本升级)。
8.2 可定制面(三条铁律)
| 铁律 | 说明 |
|---|---|
| 框架代码只读 | 脚手架自带模块/页面/服务(users/roles/system/*/common/*/ty-*…)只读不改 |
| 业务代码落点 | 新增放独立业务目录:后端新域 src/<domain>/,前端新页面 src/pages/<biz>/,不侵入框架目录 |
| 定制走扩展点 | 外观/权限优先走数据驱动扩展点:租户品牌(tenant.brand_*)、字典、主题色、资源注册(tenant-template.ts + pnpm sync:resources),而非改源码 |