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

8.3 KiB
Raw Blame History

历史存储与报告导出设计(三期)

关联:一期 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经由 rusqlitebundled 特性) 直接在 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_jsoncalculate_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 中初始化并建表。

新增 cratesrc-tauri/Cargo.tomlrusqlite { 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.93libsqlite3-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_recordSaveArgs 结构体包裹 input/result/created_at,避免 JS↔Rust 多词参数名映射歧义。
  • f64 存储:完整结果以 JSON 落库serde_json 解析可能有 ~1e-14 的末位 ULP 偏差,对指数无实际影响,测试以容差比较。

10. 实施顺序

  1. src-tauri:加 rusqlitesetup 建表 + 连接 State。
  2. 实现 save/list/get/delete 命令 + Rust 往返测试。
  3. 前端历史列表 + 过滤 + 保存按钮。
  4. 报告视图组件(屏幕预览)→ 接 window.print() 出 PDF。
  5. rust_xlsxwriter 导出命令 + 保存对话框。
  6. 端到端:算→存→列表过滤→查看→导出。