diff --git a/config/config.example.toml b/config/config.example.toml index de1302b..deb13e9 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -71,10 +71,11 @@ interval_secs = 60 # 归档扫描间隔(秒) enabled = true # 是否启用折叠(默认 true) batch = 2_000 # 每轮最多折叠多少条明细(越小 → 单次持库时间越短) interval_secs = 60 # 折叠扫描间隔(秒) -retain_minutes = 43_200 # 明细保留窗口(**分钟**,默认 43200 = 30 天):早于 - # now-retain_minutes 的已折叠明细每轮删一批。 - # 0 = 不留明细(只留汇总),交易页将只答得出最近一分钟。 - # 旧拼写 retain_days(天)仍认且优先 —— 新配置用本键。 +retain_minutes = 10 # 明细保留窗口(**分钟**,默认 10):早于 now-retain_minutes + # 的已折叠明细每轮删一批。窗口外的明细不丢 —— 交易页读读模型 + # tx_facts,它们以分钟汇总行呈现(一行代表 N 条)。 + # 0 = 不留明细(只留汇总),交易页上每一行都是汇总行。 + # 旧拼写 retain_days(天)仍认且**优先** —— 新配置用本键。 # ============================================================ # 0.5 用量明细保留(rant 2026-10-09T12:28:58 的独立余项):`usage_records` 是同一笔 diff --git a/src/config.rs b/src/config.rs index 3a9380e..78238f2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -252,7 +252,7 @@ fn default_rollup_interval_secs() -> u64 { 60 } fn default_rollup_retain_minutes() -> i64 { - 43_200 + 10 } /// 交易明细汇总([rollup] 段,rant 2026-10-09T12:28:58 验收项 1)。 @@ -264,9 +264,12 @@ fn default_rollup_retain_minutes() -> i64 { /// 删除只删**已折叠且已归档**的 —— 于是删掉的行永远有原件(JSONL)与聚合(汇总表)两个落脚处; /// `[archive]` 关掉时水位 0 ⇒ 不折也不删(fail-closed),这是刻意的。 /// -/// 窗口是**明细**的保留窗口(不是汇总的):窗口内的明细仍逐条留在 `transactions` 里, -/// 交易页照常翻页;窗口外的只剩汇总行(与 JSONL 里的原件)。取 0 即「明细只留汇总」的极值 -/// —— 折叠追平后 `transactions` 会被清空,交易页只答得出最近一分钟,**这是刻意的取舍,不是缺陷**。 +/// 窗口是**明细**的保留窗口(不是汇总的)。默认 **10 分钟**:宿主 2026-10-15 的裁定是「根本没有 +/// 查询真实明细的需求」(rant 2026-10-15T16:25:04.530850+08:00),窗口内的明细仍逐条留下, +/// 窗口外的只剩汇总行(与 JSONL 里的原件)—— 而交易页**看得到**那段历史:它读读模型 +/// `tx_facts`,窗口外的那部分以**分钟级汇总行**呈现(一行代表 N 条,行自己用 `row_count` 交代)。 +/// 取 0 即「明细只留汇总」的极值 —— 折叠追平后 `transactions` 会被清空,交易页上每一行都是 +/// 汇总行,**这是刻意的取舍,不是缺陷**。 /// /// 单位是**分钟**(`retain_minutes`)。此前是**天**(`retain_days`,默认 30)—— 那个单位 /// 表达不出「只留 10 分钟」这样的保留策略,而已落地的删除本来就按 `datetime('now', ?)` 算偏移, @@ -283,8 +286,9 @@ pub struct Rollup { /// 折叠扫描间隔(秒,默认 60;最小 1) #[serde(default = "default_rollup_interval_secs")] pub interval_secs: u64, - /// 明细的保留窗口(**分钟**,默认 `43200` = 30 天):早于 `now - retain_minutes` 的 - /// **已折叠**明细每轮删一批。0 = 不留明细(只留汇总),负数按 0 计。 + /// 明细的保留窗口(**分钟**,默认 `10`):早于 `now - retain_minutes` 的**已折叠**明细每轮 + /// 删一批。0 = 不留明细(只留汇总),负数按 0 计。窗口外的明细在交易页上以分钟汇总行呈现 + /// (读模型 `tx_facts`),不丢信息、只是粒度变粗。 #[serde(default = "default_rollup_retain_minutes")] pub retain_minutes: i64, /// **已弃用**:保留窗口的旧拼写(**天**)。给出时**优先**于 `retain_minutes`(×1440), @@ -621,9 +625,9 @@ mod tests { assert!(d.enabled, "默认启用 —— 折叠是「明细只留汇总」的前置"); assert_eq!(d.batch, 2_000); assert_eq!(d.interval_secs, 60); - assert_eq!(d.retain_minutes, 43_200, "明细默认保留 30 天(43200 分钟)"); + assert_eq!(d.retain_minutes, 10, "明细默认只保留 10 分钟"); assert_eq!(d.retain_days, None, "旧拼写缺省不设 —— 新字段才是真源"); - assert_eq!(d.window_minutes(), 43_200); + assert_eq!(d.window_minutes(), 10); } /// 旧拼写 `retain_days`(**天**)仍然算数(×1440),而且**优先**于新键 —— 老配置原样可用。 diff --git a/src/tx_retention_gate.rs b/src/tx_retention_gate.rs index 561f5c0..2bc0d74 100644 --- a/src/tx_retention_gate.rs +++ b/src/tx_retention_gate.rs @@ -6,19 +6,29 @@ //! 删除只受一条约束 —— `transactions` 的**分页列表**要的是逐行身份(`id`/`counterpart`), //! 汇总行没有它,所以列表**永远**只能从明细表答。于是: //! -//! * **保留窗口必须盖得住列表自己提供的每一个预设区间**。少一天都不行:窗口是 -//! `time < datetime('now', '-N minutes')`,列表最深预设若比它还深,用户点那个预设会 -//! 看到「共 N 条」和实际能翻的行数一起缩水,而**没有任何提示**(`total` 与行同源, -//! 所以也不会自相矛盾 —— 它会安静地少答一段历史)。 +//! * **保留窗口必须能用分钟表达,且它在盖不住列表的预设时必须有「还债」的载体**。 +//! 窗口是 `time < datetime('now', '-N minutes')`,列表最深预设若比它还深、而列表又**只** +//! 答得出窗口内的明细,用户点那个预设就会看到「共 N 条」和实际能翻的行数一起缩水,而 +//! **没有任何提示**(`total` 与行同源,所以也不会自相矛盾 —— 它会安静地少答一段历史)。 +//! 本轴的原判据到此为止;rant `2026-10-15T16:25:04` 的验收项 2 把**后半句**改掉了:列表 +//! 改从读模型 `tx_facts` 取数之后,窗口外那段历史有载体(分钟汇总行),债还清了 —— +//! 于是判据写成「窗口盖得住 **或** 列表不再从明细表取数」,而不是把规则删掉:谁把列表搬回 +//! 明细表,这条立刻重新生效(这正是验收项 5 的变异之一)。单位那一半仍然照旧:**读不到 +//! 分钟键**(又退回按「天」写)即红 —— 天粒度表达不了「只留 10 分钟」,一天以下只剩 `0`, +//! 而 `0` 是「完全不留明细」这条**另一条**规则。 //! 判据两侧都**派生**:预设深度从 `ui/js/app.js::txRangeParams` 读,窗口从 -//! `config/config.example.toml` 的 `[rollup] retain_minutes`(分钟)读。 +//! `config/config.example.toml` 的 `[rollup] retain_minutes`(分钟)读,「列表搬没搬」从 +//! `src/routes/wallet.rs` 的列表端点体里读(不再出现 `FROM transactions` ⇒ 搬了)。 //! * **盖不住的那类区间 ——「无界」的 —— 必须把边界说给页面**。`#tx-range` 里有两个区间 //! 没有深度可言(「全部时间」、自定义起点):它们能整段落在保留窗口之外,静态门禁**表达不了** //! 那种深度。判据因此换成「要么不提供、要么披露」:服务端随响应发布保留边界 //! (`tx_rollup` 的 `DETAIL_SINCE_FIELD`,与删除同一算式),前端在**缺口**上消费它 //! (`summary.entries` 与 `total` 的差)。**披露必须长在缺口上,不许长在「列表空」上**: -//! 明细还在(`total > 0`)时同样会少列,而「只在一条明细都没有时才解释」正好让说明只在 -//! **最不需要它**的场合发射(R104)。两侧同样**全派生**:选项名册从 `ui/index.html` 的 +//! R104 轴的原形状是「只在一条明细都没有时才解释」,而那正好让说明只在**最不需要它**的 +//! 场合发射(零行态屏幕本来就写着「没有匹配记录」)。⚠️ 验收项 2 之后「缺口」的含义换了: +//! `entries > total` 不再说「有一段列不出来」,而说「这一屏里有行代表多条记录」(分钟汇总行)。 +//! 判据的形状(以缺口为准、不带空态闸门、被调用)不变 —— 那句话今天要说的正是「更早的行 +//! 是分钟级汇总」,所以它仍然必须长在**有行**的时候。两侧同样**全派生**:选项名册从 `ui/index.html` 的 //! `#tx-range` 读(哪些无界由「该分支有没有 `MS(…)` 深度」判定),字段名从 `tx_rollup.rs` //! 的常量读。 //! * **删除的边界是「已折叠 ∧ 已归档」**。折叠水位(`transactions_rollup.up_to_id`)与 @@ -83,9 +93,17 @@ const OPTION_VALUE: &str = "