tcjs/docs/superpowers/specs/2026-06-11-persistence-repo...

153 lines
8.3 KiB
Markdown
Raw 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.

# 历史存储与报告导出设计(三期)
> 关联:一期 `2026-05-15-rust-tauri-mvp-design.md`、二期 `2026-06-11-judgment-logic-design.md`。
> 对应 PDF 第 6 节「输出」样品编号、计算日期、最佳估计值、95% 置信区间、k=1 相对扩展不确定度、不符合概率。
## 1. 目标
一/二期已完成计算与判定。本期补齐"留痕与出报告"两块工程能力:
- **历史存储**:每次计算的输入与结果落库,可检索、回看、复算、删除。
- **报告导出**将一次完整结果导出为可交付文件PDF 检测报告 + Excel 数据表)。
非目标:多用户/权限、云同步、检测报告的官方排版模板(先满足内容完整、可打印)。
## 2. 范围
| 能力 | 内容 |
|---|---|
| 历史存储 | 保存/列表/详情/删除/复算一条记录 |
| Excel 导出 | 单条记录导出为 `.xlsx`(输入表 + 结果表 + 判定) |
| PDF 报告 | 单条记录导出为 A4 检测报告(中文) |
| 列表检索 | 按样品编号 / 日期范围 / 材料类型 / 判定结论过滤 |
## 3. 数据模型与存储
### 3.1 选型
SQLite经由 **`rusqlite`bundled 特性)** 直接在 `src-tauri` 内访问,不引入 `tauri-plugin-sql`。理由:计算结果是结构化 JSON存取简单bundled 模式免去系统 SQLite 依赖Windows 打包干净。
库文件位置:`tauri::api::path::app_data_dir()` 下的 `history.db`(随应用数据目录,卸载可清理)。
### 3.2 表结构
```sql
CREATE TABLE IF NOT EXISTS records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sample_id TEXT, -- 样品编号(可空)
material_type TEXT NOT NULL, -- MaterialType 序列化
calc_date TEXT, -- 计算日期 YYYY-MM-DD
created_at TEXT NOT NULL, -- 入库时间 RFC3339前端传入
ira_value REAL NOT NULL, -- 冗余列,供列表展示/排序/过滤
ir_value REAL NOT NULL,
validity TEXT NOT NULL, -- Validity 序列化
verdict TEXT NOT NULL, -- Verdict 序列化(用于列表标签与过滤)
input_json TEXT NOT NULL, -- 完整 SampleInput
result_json TEXT NOT NULL -- 完整 CalculationResult
);
CREATE INDEX IF NOT EXISTS idx_records_created ON records(created_at);
CREATE INDEX IF NOT EXISTS idx_records_sample ON records(sample_id);
```
设计要点:完整 `input_json` / `result_json` 保证回看与复算的保真;冗余标量列只为列表查询与过滤,避免反序列化全表。
### 3.3 复算保真
复算 = 读 `input_json``calculate_sample` 重新计算。因 MCM 用固定种子,结果可复现,可与存档 `result_json` 比对作为一致性自检(库内核版本升级时用于回归)。
## 4. 后端命令(`src-tauri`
新增 Tauri 命令,全部返回 `Result<_, String>`
```rust
save_record(input: SampleInput, result: CalculationResult, created_at: String) -> i64 // 返回 id
list_records(filter: RecordFilter) -> Vec<RecordSummary>
get_record(id: i64) -> RecordDetail // 含 input + result
delete_record(id: i64) -> ()
export_excel(id: i64, path: String) -> () // 写 .xlsx 到用户选定路径
export_report(id: i64, path: String) -> () // 见第 6 节
```
```rust
struct RecordFilter {
sample_id: Option<String>, // 模糊匹配
material_type: Option<MaterialType>,
date_from: Option<String>,
date_to: Option<String>,
verdict_kind: Option<String>, // "Qualified" / "Unqualified" / ...
}
struct RecordSummary {
id: i64, sample_id: Option<String>, material_type: MaterialType,
calc_date: Option<String>, created_at: String,
ira_value: f64, ir_value: f64, validity: Validity, verdict: Verdict,
}
```
数据库连接用 `tauri::State<Mutex<Connection>>``setup` 中初始化并建表。
新增 crate`src-tauri/Cargo.toml``rusqlite { features = ["bundled"] }`、`rust_xlsxwriter`。PDF 方案见第 6 节(可能 0 依赖)。
## 5. Excel 导出
`rust_xlsxwriter`(纯 Rust、原生 UTF-8无 CJK 字体问题)。生成单工作簿三段:
1. **样品信息**:编号、日期、材料类型、有效性、最终判定。
2. **测量与校准**:各次 Ra/Th/K 测量值;校准系数 a/U/k。
3. **结果**IRa/Ir 值、u、U、相对扩展不确定度(k=1/k=2)、真值区间;各核素均值/校准活度/A 类/B 类/合成MCM 平均值/标准偏差/区间/合格概率/不符合概率。
路径经 `@tauri-apps/plugin-dialog` 的保存对话框选定Rust 侧写文件。
## 6. PDF 报告
中文 PDF 字体内嵌是主要难点,给两条路线,**推荐路线 A**
- **路线 A推荐— Webview 打印**前端用一个独立的、A4 排版的「报告视图」组件渲染完整结果,调用 `window.print()`(或 Tauri 打印 API用户在打印对话框选「另存为 PDF」。零新依赖、零字体问题、排版用 CSS 即可,所见即所得。代价:依赖系统打印到 PDF非完全静默导出。
- **路线 B — Rust 生成**`printpdf` + 内嵌中文子集字体(如 Noto Sans SC。可完全静默、可定制页眉页脚但需打包字体、自行处理换行与分页工作量大。
建议本期落 A把"报告视图"做成可复用组件(同一组件既用于屏幕预览也用于打印),路线 B 留作后续真正"一键静默导出"需求时再做。
报告内容(对应 PDF 6.16.3
- 抬头:样品编号、计算日期、材料类型、限值表。
- 检测结果IRa、Ir 最佳估计值95% 真值区间 [P2.5, P97.5]k=1 / k=2 相对扩展不确定度。
- 有效性判定(总比活度 vs 37、最终判定合格/不合格/A·B·C/建议增加次数)。
- MCM95% 置信概率下不符合概率。
- 各核素中间量明细表。
## 7. 界面变更
- 顶部加「保存到历史」按钮(计算成功后可用);保存成功后提示。
- 新增「历史」入口(抽屉或独立 Tab列表编号/日期/材料/IRa/Ir/判定)+ 过滤栏;行操作「查看 / 复算 / 导出 Excel / 导出 PDF / 删除」。
- 「查看」打开报告视图(即打印用组件)。
- 计算日期从已有的 DatePicker 复用,`created_at` 取前端当前时间戳。
## 8. 测试
- RustDB 用内存库(`:memory:`)测 save→list→get→delete 往返;过滤条件命中;`export_excel` 生成非空且能被重新打开(校验首部魔数/可解析)。
- 复算一致性:存档 `result_json` 与重算结果逐字段相等(固定种子)。
- 前端:报告视图快照(关键字段渲染)、历史列表过滤交互。
## 9. 已确认决策
1. **PDF 路线**:路线 A —— 前端 A4 报告视图 + `window.print()` 另存为 PDF。✅
2. **历史入口形态**:顶部 Tab 切换(计算 / 历史两页)。✅
3. **保存时机**:仅用户点「保存到历史」时入库,避免调参脏数据。✅
4. **导出范围**PDF 报告 + Excel 数据表都做(实现独立,可分批落地)。✅
## 9.5 实现说明(落地后补记)
- **rusqlite 版本**:本地 Rust 为 1.93`libsqlite3-sys 0.38` 使用了未稳定的 `cfg_select!`,编译失败。已将 `rusqlite` 固定为 `0.37`(拉取 `libsqlite3-sys 0.35`)规避。升级工具链到 ≥1.94 后可放开。
- **Excel 落盘**`export_excel` 写入系统下载目录(取不到则回退文档目录),返回完整路径,前端以提示展示,无需对话框插件。
- **PDF**:前端 `ReportView` + `ReportModal`,「打印 / 导出 PDF」走 `window.print()`,打印 CSS 用 visibility 隔离 `.report-root`,仅输出报告本体(隐藏 Modal 外框与遮罩)。
- **命令入参**`save_record` 用 `SaveArgs` 结构体包裹 `input/result/created_at`,避免 JS↔Rust 多词参数名映射歧义。
- **f64 存储**:完整结果以 JSON 落库serde_json 解析可能有 ~1e-14 的末位 ULP 偏差,对指数无实际影响,测试以容差比较。
## 10. 实施顺序
1. `src-tauri`:加 `rusqlite``setup` 建表 + 连接 State。
2. 实现 save/list/get/delete 命令 + Rust 往返测试。
3. 前端历史列表 + 过滤 + 保存按钮。
4. 报告视图组件(屏幕预览)→ 接 `window.print()` 出 PDF。
5. `rust_xlsxwriter` 导出命令 + 保存对话框。
6. 端到端:算→存→列表过滤→查看→导出。