shared 三层 spec:platform / data-model / api-contract + design + tasks Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
99 lines
6.1 KiB
Markdown
99 lines
6.1 KiB
Markdown
## Context
|
||
|
||
脚手架进入 Phase 0 地基。`openspec/specs/shared/` 当前为空,本 change 产出 platform / data-model / api-contract 三个 capability(见 proposal.md)。技术栈已定:MySQL + Prisma + NestJS 12(后端)、Vue 3(前端)。
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- 给出 shared 三层的技术实现方案:DB schema、命名、鉴权、多租户、数据权限、软删。
|
||
- 产出可直接落为 Prisma schema 与 NestJS 骨架的字段字典与契约约定。
|
||
|
||
**Non-Goals:**
|
||
- 不写产品代码(本 change 只出设计;实现走后续 apply)。
|
||
- 扩展域(POST/NOTICE/JOB/FILE/MSG)后置。
|
||
- 前端具体页面/组件设计(属 Phase 1)。
|
||
|
||
## Decisions
|
||
|
||
### D1 数据库与 ORM:MySQL 8 + Prisma
|
||
选 Prisma 而非 TypeORM/Drizzle:`schema.prisma` 天然就是字段字典的单一载体,类型安全最强,迁移工具(prisma migrate)成熟;与「data-model 字段字典为唯一锚点」的约定严丝合缝。
|
||
备选:TypeORM(NestJS 原生但类型安全弱)、Drizzle(轻量但生态新)。
|
||
|
||
### D2 命名:表 `sys_` 前缀,列 snake_case
|
||
表名带 `sys_` 前缀,避免与 fork 后的业务表冲突。Prisma 模型名不带前缀(`User`/`Role`/`Resource`),用 `@@map("sys_user")` 映射表名、`@map("created_at")` 映射列名——代码干净、DB 清晰。
|
||
|
||
### D3 主键:UUID v7(存 CHAR(36))
|
||
选 UUID v7 而非自增/雪花:全局唯一、时间有序(索引友好)、多租户合并/分布式安全、不泄露业务量。v7 的时间有序缓解字符串主键的索引性能问题。
|
||
|
||
### D4 时间:DB 存 UTC DATETIME(3),API 用 Unix 毫秒
|
||
分层:DB 用 DATETIME(3)(可读、可 SQL 函数、Prisma 原生 DateTime);API 层统一 Unix 毫秒 number(无时区歧义、前端 `new Date(ms)` 直显)。**单位统一毫秒**,写死在 api-contract。
|
||
|
||
### D5 删除:业务软删 + 日志/会话硬删
|
||
业务实体带 `deleted_at`,删除即置值;`sys_audit_log`/`sys_login_log`/`sys_refresh_token` 只增或硬删。查询统一 `deleted_at IS NULL` 过滤。
|
||
|
||
### D6 认证:JWT access + refresh 旋转
|
||
- access:JWT,2h 过期,payload `{ sub, tenantId, jti }`。
|
||
- refresh:随机串(jti),存 `sys_refresh_token`(含 ip/ua/login_at),7d 过期,可撤销。
|
||
- 旋转:换发时作废旧 refresh、签发新 refresh,防重放。
|
||
- 登出/踢人 = 置 `revoked_at`。
|
||
|
||
### D7 三级鉴权守卫
|
||
公开接口(login/captcha/health)无 token;未登记接口仅校验 access;登记为 `type=api` 的接口校验 access + `code`(权限标识)。装饰器 `@RequirePermissions('system:user:list')` 比对用户角色集合。
|
||
|
||
### D8 多租户过滤
|
||
所有业务表带 `tenant_code`;通过 Prisma 扩展 client / middleware 在查询入口统一注入 `tenant_code = 当前租户` 条件,避免每处手写。登录时按 `tenantCode` 定位租户。
|
||
|
||
### D9 数据权限过滤
|
||
`role.data_scope ∈ {全部/本部门/本部门及以下/本人/自定义}`;列表查询前按用户角色集计算可见 `org_id` 集合,统一注入 `org_id IN (...)`;「自定义」查 `sys_role_org`。
|
||
|
||
### D10 枚举:VARCHAR + TS 应用层枚举
|
||
Prisma/MySQL 原生 enum 迁移困难,故用 VARCHAR 存值 + TS `const` 枚举校验;枚举值集中定义在 shared/data-model。
|
||
|
||
### D11 关联键:业务键优先,用户例外用 id
|
||
所有跨实体引用优先以业务键而非主键 id:角色/资源/组织/字典类型/租户用 code,树的父引用用 parent_code;对用户的引用用用户 id。理由:业务键稳定、可读、可预置,代码里 `@RequirePermissions('system:user:list')` 直接引用,无需查 id。resource.code 兼作按钮/接口的权限标识。
|
||
|
||
## 字段字典(字段以 shared/data-model 为源)
|
||
|
||
### 公共字段(业务软删实体通用)
|
||
|
||
| 列 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| id | CHAR(36) PK | UUID v7 |
|
||
| tenant_code | VARCHAR(64) NOT NULL | 租户(sys_tenant 自身无) |
|
||
| created_at | DATETIME(3) | UTC |
|
||
| created_by | CHAR(36) NULL | 创建人 user_id |
|
||
| updated_at | DATETIME(3) | UTC |
|
||
| deleted_at | DATETIME(3) NULL | 软删标记 |
|
||
|
||
### 实体特有字段
|
||
|
||
| 表 | 特有字段 |
|
||
|---|---|
|
||
| sys_tenant | code(租户内唯一)、name、status(enabled/disabled)、remark |
|
||
| sys_user | username、password_hash、nickname、email、phone、avatar、status(enabled/disabled/locked)、org_code |
|
||
| sys_role | code(租户内唯一)、name、data_scope(all/dept/dept_and_child/self/custom)、status、remark |
|
||
| sys_resource | parent_code、type(menu/button/api)、code(租户内唯一,兼权限标识)、name、path、component、method(GET/POST/PUT/DELETE)、api_path、icon、sort、status |
|
||
| sys_org | parent_code、code(租户内唯一)、name、leader、sort、status |
|
||
| sys_dict_type | code(租户内唯一)、name、status、remark |
|
||
| sys_dict_item | dict_type_code、label、value、sort、status |
|
||
| sys_config | key、value、remark |
|
||
| sys_audit_log | user_id、username、module、action、method、path、params(TEXT)、result(TEXT)、status、error_message、ip、user_agent、duration_ms |
|
||
| sys_login_log | username、ip、user_agent、status(success/fail)、message |
|
||
| sys_refresh_token | user_id、jti(UNIQUE)、token_hash、ip、user_agent、login_at、expire_at、revoked_at |
|
||
| sys_user_role | user_id、role_code |
|
||
| sys_role_resource | role_code、resource_code |
|
||
| sys_role_org | role_code、org_code |
|
||
|
||
> 日志/会话类(audit_log/login_log/refresh_token)无 deleted_at,只带 created_at。
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- [未登记接口默认放行 → 漏登记敏感接口即裸奔] → 约定:写操作/敏感查询一律登记为 `type=api` 资源,仅真正公开的留白;评审时抽查。
|
||
- [refresh 旋转的并发窗口] → 旧 refresh 在极短窗口内仍可用的容忍策略;必要时按 user 串行化刷新。
|
||
- [UUID 字符串主键索引/空间开销] → 用 v7 时间有序 + 覆盖索引,避免随机 UUID 的页分裂。
|
||
- [多租户+软删过滤需全局注入] → Prisma 扩展 client 统一处理,杜绝散落的手写 WHERE。
|
||
|
||
## Migration Plan
|
||
|
||
纯新增(无既有数据),`prisma migrate` 直接建表;无回滚压力(地基阶段可 drop 重建)。
|