|
| 1 | +//! 周契约 commands(v0.11.4 REQ-200;弹性承诺呈现层系统层)。 |
| 2 | +//! |
| 3 | +//! @ai-context: 弹性承诺纪律(v4 §8.2 P31+N10)——契约是用户自设本周目标, |
| 4 | +//! 不是打卡 KPI:无 streak、无惩罚、欠账不追。本层只做 |
| 5 | +//! 参数校验、调用数据层/纯函数、错误映射(AGENTS.md §6)。 |
| 6 | +//! @ai-context: week_start 由命令层按当前时刻计算(周界口径单一在 |
| 7 | +//! week_contract.rs 纯函数)——前端不传时间,只传目标值。 |
| 8 | +
|
| 9 | +use serde::Serialize; |
| 10 | +use tauri::State; |
| 11 | + |
| 12 | +use crate::commands::AppState; |
| 13 | +use crate::db::unix_seconds; |
| 14 | +use crate::types::WeekContract; |
| 15 | +use crate::week_contract::{aggregate_week, minimal_day_met, week_start_secs}; |
| 16 | + |
| 17 | +/// 承诺天数合法区间(1..=7——一周最多七天,诚实有界)。 |
| 18 | +const TARGET_DAYS_RANGE: std::ops::RangeInclusive<i64> = 1..=7; |
| 19 | +/// 承诺卡数上限(有界防超大 payload;一周 200 次复习已远超合理承诺)。 |
| 20 | +const TARGET_CARDS_MAX: i64 = 200; |
| 21 | + |
| 22 | +/// 周契约状态读数(承诺 vs 实际;UI 周契约卡数据源)。 |
| 23 | +#[derive(Debug, Clone, Serialize)] |
| 24 | +#[serde(rename_all = "camelCase")] |
| 25 | +pub struct WeekContractStatus { |
| 26 | + /// 本周契约(None=未立约——UI 显示设定表单) |
| 27 | + pub contract: Option<WeekContract>, |
| 28 | + /// 周界(周一零点 UTC 秒;UI 转本地日期展示) |
| 29 | + pub week_start: i64, |
| 30 | + /// 本周实际复习天数(按日去重——断签不清零) |
| 31 | + pub actual_days: usize, |
| 32 | + /// 本周实际复习卡次数 |
| 33 | + pub actual_cards: usize, |
| 34 | + /// 最小可行日徽标:本周完成 ≥3 卡即成立(N9/N11 低谷生存最轻形态) |
| 35 | + pub minimal_day_met: bool, |
| 36 | +} |
| 37 | + |
| 38 | +/// 校验组存在性(contracts 外键 + 前端按 id 刷新共用)。 |
| 39 | +fn require_group(state: &AppState, group_id: i64) -> Result<(), String> { |
| 40 | + if group_id <= 0 { |
| 41 | + return Err("无效的组 id".to_string()); |
| 42 | + } |
| 43 | + if state.db.get_group(group_id).map_err(|e| e.to_string())?.is_none() { |
| 44 | + return Err(format!("笔记组不存在: {}", group_id)); |
| 45 | + } |
| 46 | + Ok(()) |
| 47 | +} |
| 48 | + |
| 49 | +/// 设定/覆盖本周契约(幂等——同周同组覆盖目标值;改目标即改承诺)。 |
| 50 | +#[tauri::command] |
| 51 | +pub fn upsert_week_contract( |
| 52 | + state: State<'_, AppState>, |
| 53 | + group_id: i64, |
| 54 | + target_days: i64, |
| 55 | + target_cards: i64, |
| 56 | +) -> Result<WeekContract, String> { |
| 57 | + require_group(&state, group_id)?; |
| 58 | + if !TARGET_DAYS_RANGE.contains(&target_days) { |
| 59 | + return Err(format!("承诺天数需在 1..=7 之间(收到 {})", target_days)); |
| 60 | + } |
| 61 | + if !(1..=TARGET_CARDS_MAX).contains(&target_cards) { |
| 62 | + return Err(format!("承诺卡数需在 1..={} 之间(收到 {})", TARGET_CARDS_MAX, target_cards)); |
| 63 | + } |
| 64 | + let ws = week_start_secs(unix_seconds()); |
| 65 | + state |
| 66 | + .db |
| 67 | + .upsert_week_contract(group_id, ws, target_days, target_cards) |
| 68 | + .map_err(|e| e.to_string()) |
| 69 | +} |
| 70 | + |
| 71 | +/// 本周契约状态(承诺 vs 实际——review_logs 周聚合纯函数)。 |
| 72 | +/// |
| 73 | +/// @ai-context: 取数 → 聚合两步:review_ats_in_week 拿原始时刻列表(数据层), |
| 74 | +/// aggregate_week 纯函数聚合成天数/卡数——口径可单测可追溯。 |
| 75 | +#[tauri::command] |
| 76 | +pub fn week_contract_status(state: State<'_, AppState>, group_id: i64) -> Result<WeekContractStatus, String> { |
| 77 | + require_group(&state, group_id)?; |
| 78 | + let ws = week_start_secs(unix_seconds()); |
| 79 | + let contract = state.db.get_week_contract(group_id, ws).map_err(|e| e.to_string())?; |
| 80 | + let ats = state.db.review_ats_in_week(group_id, ws).map_err(|e| e.to_string())?; |
| 81 | + let agg = aggregate_week(&ats); |
| 82 | + Ok(WeekContractStatus { |
| 83 | + contract, |
| 84 | + week_start: ws, |
| 85 | + actual_days: agg.review_days, |
| 86 | + actual_cards: agg.review_cards, |
| 87 | + minimal_day_met: minimal_day_met(agg.review_cards), |
| 88 | + }) |
| 89 | +} |
0 commit comments