liuyun d2c75bd04c chore(openspec): sync foundation specs to main, archive init-shared-foundation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-10 21:17:45 +08:00

99 lines
6.1 KiB
Markdown
Raw Permalink 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.

## 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 重建)。