# ADR-0007:数据存储选型与双形态一致规则 - **状态**:已接受 - **日期**:2026-09-23 - **决策人**:技术负责人 - **相关**:任务表 A0(决策 D11);设计文档《实施设计》1.2、1.4 ## 背景 平台有两种落地形态:单机版(Electron 桌面端 + 本地平台服务)与远端服务器版。库选什么,决定三件事:单机能不能零运维、远端能不能扛并发写入与防篡改、上层代码会不会因为库不同而分叉。 《实施设计》1.4 已经按「桌面用 SQLite 与 JSONL、服务端用 PostgreSQL」写过一版存储形态表,但没有决策记录,也没定两种库的写法一致性规则。 旧实现的教训很具体:它同时支持 SQLite 与 MySQL,方言差异靠 `Float().with_variant(DOUBLE, "mysql")` 这类逐处变体修补;时间字段既有 `String(32)` 存字符串的,也有 epoch 浮点的。差异不写成明文约定,就会在代码里散成几十处特判。 ## 选项 | 选项 | 做法 | 代价 | 不选的理由 | | --- | --- | --- | --- | | A 单机 SQLite + 服务端 PostgreSQL | 一套方言无关 schema,两种引擎各一套驱动实现 | 要守住一组写法约定 | 选定 | | B 两端都用 PostgreSQL | 单机版也装 PostgreSQL | 单机从「装一个应用」变成「运维一个数据库」 | 工厂现场的单机部署不接受 | | C 两端都用 SQLite | 服务端也用 SQLite | 并发写、在线备份、复制、细粒度权限都受限 | 多用户共享场景撑不住 | | D 单机 SQLite + 服务端 MySQL | 与旧实现一致,沿用经验 | 旧实现的 MySQL 是为对接 `jms_mes`,属外部约束 | 新平台没有这个包袱;PostgreSQL 在 JSONB、并发写与在线备份上更省事 | ## 结论 选 A。同时定下七条写法约定——它们是「一套 schema 跑两种库」的前提,不是风格偏好: | # | 约定 | 原因 | | --- | --- | --- | | 1 | 一套方言无关 schema + 一个迁移工具 | 两边各写一套必然漂移,等联调才发现对不上 | | 2 | 时间统一存整数毫秒 | 避开各库原生时间类型的时区语义差异;旧实现已有两种写法混用 | | 3 | 主键由应用侧生成(ULID),不用自增 | SQLite 与 PostgreSQL 的自增与序列语义不同 | | 4 | JSON 列统一按 JSON 处理:PostgreSQL 落 JSONB,SQLite 落 TEXT | 上层只认 JSON,不写库特有的查询与字面量 | | 5 | 金额与工时用整数最小单位 | 避免小数类型在不同引擎上的精度与舍入差异 | | 6 | 不用触发器、存储过程、数据库专属函数 | 逻辑留在代码里,两种引擎行为才一致 | | 7 | 会话消息不落结构化库 | 单机走 JSONL、服务端走 PostgreSQL,两种实现同一个 `SessionStore` 接口,上层代码共用 | 访问层用 Drizzle ORM:纯 TypeScript,schema 与迁移一体,方言由 schema 定义区分。不选 Prisma——它的查询与迁移依赖额外二进制,对 Electron 打包与离线升级是负担。 存储落点: | 数据 | 单机版 | 远端服务器版 | | --- | --- | --- | | 会话消息 | JSONL(`~/.aps/sessions/<项目>/…`) | PostgreSQL | | 会话索引(列表、搜索、未读) | 本地 SQLite | PostgreSQL | | 执行、审批、审计 | 本地 SQLite | PostgreSQL | | 附件与产物 | 本地目录,只存引用 | 对象存储 + 元数据表 | | 导出与交换 | JSONL | JSONL | 单机版的 SQLite 文件放在应用数据目录,与会话 JSONL 同级,备份即拷贝目录。 ## 影响 - **对代码**:`server/core` 的持久层按七条约定写;schema 与迁移集中在 `server/core/storage`,本地与远端两套驱动实现同一个 `SessionStore` 接口 - **对任务**:A1.1 脚手架按此建存储目录与迁移配置;A3 数据层两套实现一起写,不先写一套再补;B2 拆分后 APS 侧的库由 APS 自己选,平台不干预 - **对文档**:《实施设计》1.4 的存储形态表以此为准,补上 SQLite 文件路径与主键、时间约定;改动随 A0.10 文档入库一起做,避免覆盖飞书文档正文 - **不这么做的后果**:两端 schema 各自演进,差异在联调时集中爆发;时间与主键的差异会散进业务代码,改一次要全量回归 ## 遗留 - SQLite 驱动的最终确认:A1.1 脚手架时实测 `better-sqlite3` 与 Node 内置 `node:sqlite`,选不需原生重编译的那个;若 Drizzle 与 Node ≥ 22.19 或引擎子进程模型冲突,退到 Kysely + 手写迁移,代价是改一份 schema 定义 - 单机版的备份与导出入口:A6 界面设计时定,ADR-0003 的「备份与排查直接用文件」规则要求不依赖数据库工具 - APS 侧数据库选型:归 APS,B2 只要求两侧库可独立启动