8.3 KiB
历史存储与报告导出设计(三期)
关联:一期
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 表结构
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>:
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 节
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 字体问题)。生成单工作簿三段:
- 样品信息:编号、日期、材料类型、有效性、最终判定。
- 测量与校准:各次 Ra/Th/K 测量值;校准系数 a/U/k。
- 结果: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.1–6.3):
- 抬头:样品编号、计算日期、材料类型、限值表。
- 检测结果:IRa、Ir 最佳估计值;95% 真值区间 [P2.5, P97.5];k=1 / k=2 相对扩展不确定度。
- 有效性判定(总比活度 vs 37)、最终判定(合格/不合格/A·B·C/建议增加次数)。
- MCM:95% 置信概率下不符合概率。
- 各核素中间量明细表。
7. 界面变更
- 顶部加「保存到历史」按钮(计算成功后可用);保存成功后提示。
- 新增「历史」入口(抽屉或独立 Tab):列表(编号/日期/材料/IRa/Ir/判定)+ 过滤栏;行操作「查看 / 复算 / 导出 Excel / 导出 PDF / 删除」。
- 「查看」打开报告视图(即打印用组件)。
- 计算日期从已有的 DatePicker 复用,
created_at取前端当前时间戳。
8. 测试
- Rust:DB 用内存库(
:memory:)测 save→list→get→delete 往返;过滤条件命中;export_excel生成非空且能被重新打开(校验首部魔数/可解析)。 - 复算一致性:存档
result_json与重算结果逐字段相等(固定种子)。 - 前端:报告视图快照(关键字段渲染)、历史列表过滤交互。
9. 已确认决策
- PDF 路线:路线 A —— 前端 A4 报告视图 +
window.print()另存为 PDF。✅ - 历史入口形态:顶部 Tab 切换(计算 / 历史两页)。✅
- 保存时机:仅用户点「保存到历史」时入库,避免调参脏数据。✅
- 导出范围: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. 实施顺序
src-tauri:加rusqlite,setup建表 + 连接 State。- 实现 save/list/get/delete 命令 + Rust 往返测试。
- 前端历史列表 + 过滤 + 保存按钮。
- 报告视图组件(屏幕预览)→ 接
window.print()出 PDF。 rust_xlsxwriter导出命令 + 保存对话框。- 端到端:算→存→列表过滤→查看→导出。