# 002 持久化层技术决策 对应 Issue:B-39(`docs/issues/002-p0-persistence-and-immutable-imports.md`)。 ## 数据库:SQLite(Python 标准库 `sqlite3`) - 单文件、事务完整、零新增依赖,与当前标准库服务器和离线内网部署环境匹配。 - 外键约束、CHECK 约束、唯一约束和触发器均可用,足以承载不可变证据模型。 - 金额以 `TEXT` 保存 `Decimal` 的原始字符串,读取时还原为 `Decimal`, 绝不经过二进制浮点。 - 后续若并发写入成为瓶颈,可开启 WAL 或平迁 PostgreSQL;迁移版本表 `schema_migrations` 不绑定具体引擎方言之外的特性。 ## 迁移工具:仓库内置版本化迁移器(`src/bank_importer/db.py`) - 每条迁移包含 `up` / `down` 两段 SQL,按版本号顺序执行并记录在 `schema_migrations` 表中;重复执行无副作用。 - 不引入 Alembic 等外部工具:当前模型规模小,内置迁移器保持零依赖, 且回滚路径明确(`python -m bank_importer.db --rollback-to <版本>`)。 - 服务启动时自动执行 `migrate`,空数据库即可完整建表。 ## 文件存储:内容寻址的本地文件系统(`data/files/`) - 上传文件先计算 SHA-256,再写入 `data/files/<哈希前两位>/<完整哈希>.<扩展名>`。 - 发布采用临时文件 + 硬链接:目标要么完整出现、要么不存在,且永不覆盖。 - 相同内容(即使文件名不同)只保存一份文件、一条 `source_files` 记录。 - `data/` 已加入 `.gitignore`,真实流水不进入版本库。 ## 幂等与状态机 - 文件级幂等:`source_files.sha256` 唯一约束。重复上传插入一条 `status='duplicate'` 的批次审计记录,指向首个批次,不产生第二份事实。 - 行级幂等:`source_rows` 上 `UNIQUE (sheet_batch_id, source_row)`。 - 批次状态:`parsing → parsed | exception | failed`,另有 `duplicate`。 失败/异常批次保留 `import_exceptions` 诊断(阶段、消息、原始文件名), 不产生任何已确认源行。 - 事务边界:文件落盘后,`source_files` + 批次 + 工作表批次 + 源行在单个 SQLite 事务内提交;解析失败时批次状态与异常记录同样在事务内落库。 ## 不可变性 - `source_files`、`sheet_batches`、`source_rows` 三张表由数据库触发器禁止 `UPDATE` 和 `DELETE`,任何修改只能走后续阶段的冲销/审计调整流程。 - 该约束由 `tests/test_persistence.py` 自动化验证。