From de8d04489b220e1d0c0c3428aadb504469f9a06d Mon Sep 17 00:00:00 2001 From: zh Date: Wed, 23 Sep 2026 15:43:40 +0800 Subject: [PATCH] =?UTF-8?q?docs(adr):=20=E8=A1=A5=E9=BD=90=20ADR-0007?= =?UTF-8?q?=EF=BC=88=E6=95=B0=E6=8D=AE=E5=AD=98=E5=82=A8=E9=80=89=E5=9E=8B?= =?UTF-8?q?=E4=B8=8E=E5=8F=8C=E5=BD=A2=E6=80=81=E4=B8=80=E8=87=B4=E8=A7=84?= =?UTF-8?q?=E5=88=99=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 单机版结构化数据用本地 SQLite、会话消息用 JSONL、附件用本地目录;服务端版全部 PostgreSQL。定下七条写法约定(整数毫秒时间、应用侧 ULID 主键、JSONB/TEXT、整数最小单位、禁用库特有逻辑、一套 schema 一个迁移工具、会话消息不落结构化库),并记录访问层选型与四项缺口。来源:任务表决策 D11 / A0.12。 --- docs/adr/0007-storage-and-consistency.md | 64 ++++++++++++++++++++++++ docs/adr/README.md | 1 + 2 files changed, 65 insertions(+) create mode 100644 docs/adr/0007-storage-and-consistency.md diff --git a/docs/adr/0007-storage-and-consistency.md b/docs/adr/0007-storage-and-consistency.md new file mode 100644 index 0000000..d36a40a --- /dev/null +++ b/docs/adr/0007-storage-and-consistency.md @@ -0,0 +1,64 @@ +# 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 只要求两侧库可独立启动 diff --git a/docs/adr/README.md b/docs/adr/README.md index 6606ed9..7c4d4d0 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -44,5 +44,6 @@ ADR 写完不再修改。要改结论就**新写一份**,并在两份里互相 | [0004](./0004-desktop-and-local-capabilities.md) | 桌面端与本地能力边界 | 已接受 | 任务表 A0.5(决策 D4) | | [0005](./0005-cutover-strategy.md) | 直接切换,不并行、不迁移 | 已接受 | 任务表 A0.9(决策 D7) | | [0006](./0006-engine-as-subprocess.md) | 引擎以长驻子进程运行 | 已接受 | 任务表 A3(决策 D10) | +| [0007](./0007-storage-and-consistency.md) | 数据存储选型与双形态一致规则 | 已接受 | 任务表 A0(决策 D11) | 编号在写之前先在本表登记,避免多人并行时撞号。