bim-issue-platform

数据库设计

表结构的真源是 server/database/schema.ts(SQLite)/ schema.mysql.ts(MySQL),本文是带中文说明的速览。ER 关系图见 docs/数据库ER图.drawio(draw.io 打开)。

总览(14 张表)

# 表名 中文名 一句话用途
1 users 用户表 人 + AI 都在这里,靠 subject_type 区分
2 projects 项目表 名称 / 系统编号 / 封面 / 取号计数器 / 设置 JSON
3 project_members 项目成员表 某人在某项目里担任什么角色(软移出可复入)
4 tags 分类类目表 四维度分类(类型 / 专业 / 专项 / 子项),41 项标准 seed
5 issues 问题表 核心表
6 issue_tags 问题-类目关联表 多维多选,复合外键防跨项目
7 attachments 附件表 截图 + 过程附件同表,靠 category 区分
8 replies 答复表 只增不改不删
9 project_sections 项目说明表 概况 + 各专业设计说明(Markdown)
10 project_metrics 经济技术指标表 一行一指标,可分组排序
11 audit_log 操作留痕表 谁在何时把哪个字段从什么改成什么
12 idempotency_key 幂等键表 批量写入防重复
13 model_files BIM 模型文件表 IFC 文件,重传 = 新增版本(展示暂缓,存储可用)
14 api_token 个人 API 令牌表 一人一枚,只存哈希
projects(根,一切按项目隔离)
  ├─ project_members / tags / project_sections / project_metrics / model_files
  └─ issues ─┬─ issue_tags(↔ tags 多对多)
             ├─ attachments(可再挂到某条答复下)
             └─ replies
users ── 贯穿所有表(创建人 / 执行人 / 上传人 / 操作主体,含 AI)
users ── api_token(一人一枚)
audit_log + idempotency_key ── 兜住全量留痕与 AI 批量写入防重

通用约定(14 张表统一遵守)

约定 内容 为什么
主键 全库统一 CHAR(26) 的 ULID 时间有序、跨库搬家不用重编号、外部无法枚举数据量
表名 一律复数 统一
枚举 VARCHAR + CHECK 约束,不用 MySQL 原生 ENUM 保证 SQLite / PostgreSQL 兼容;加取值只改代码常量、不用发迁移
时间 库里存 UTC(SQLite TEXT ISO8601 / MySQL DATETIME(3)),读写统一过 server/database/time.ts 两库存储形态不同,统一转换只放一处
软删 需要可回溯的表用 deleted_at(NULL = 未删) 与全量留痕一致,兜住误删
跨项目防护 子表冗余存 project_id 并参与复合外键 数据库自己拦跨项目写入,不靠代码记得查
字符集 MySQL 侧 utf8mb4 / utf8mb4_0900_ai_ci,InnoDB 中文与 emoji
变更方式 迁移脚本 + ORM 抽象,禁手改表 多人同库的底线

复合外键的辅助唯一索引:SQLite 要求外键指向的列有唯一索引,因此 issues / tags / replies 各带一个 UNIQUE(id, project_id)(或 UNIQUE(id, issue_id))辅助索引 —— 看似多余,实为让「跨项目防护」在 SQLite 下也生效。

核心表要点

users

projects

project_members

tags

issues

attachments

replies

只增不改不删(说错了再发一条更正)—— 不加 updated_at / deleted_at,数据干净且留痕天然成立。

audit_log(留痕)

三个身份字段是全库最关键的设计:

字段 含义 人直连 AI 代理执行
actor_id 实际操作者(谁的手动了数据),永远有值 人 AI
commander_id 发令人(谁的权限生效),仅代劳关系有值 NULL 人
agent_id 代理执行的 AI,仅模式 A 有值 NULL AI
channel 操作通道:web / api / ai web / api ai

读法:「谁做的」看 actor_id;「谁的权限生效」看 commander_id(为空则看 actor_id 自己的角色)。changes 存字段级 JSON([{field, before, after}]),只记真变化的字段(diff 为空不落行);长正文列只记 {changed, lengthBefore, lengthAfter} 不存明文。target_type 十种:issue / reply / attachment / tag / member / project / metric / section / model / user(平台账号操作)。

idempotency_key

UNIQUE(actor_id, key) —— 按实际操作者隔离幂等作用域(换人即另算)。存请求指纹(SHA256)与首次响应,重放且指纹相同 → 原样返回;指纹不同 → 409 拒绝。24 小时过期清理。dryRun=true 时不落键(否则「先预览再真跑」必然撞 409)。

api_token

token_hash UNIQUE(SHA256,明文只在生成时展示一次);revoked_at 为空 = 有效;重置即吊销(删旧插新),不做独立吊销流程。

不建的表(及理由)

不建 理由
session 会话表 nuxt-auth-utils sealed 加密 Cookie,无需服务端会话
notification 通知表 站内通知未做;操作记录由 audit_log 承担
export_jobs 导出任务表 带图导出为进程内后台任务 + 流式写盘,任务 ID 重启即失效
issue_participants 参与人表 无用(创建人 / 执行人已覆盖)
登录失败锁定相关表 未定案,定了再补

迁移与对拍

pnpm db:generate            # 由 schema.ts 生成 SQLite 迁移 SQL(drizzle-kit)
pnpm db:generate:mysql      # MySQL 侧
pnpm db:migrate             # 执行迁移
pnpm db:check               # 四份表定义对拍(改了表结构必跑)