Files
app_02_bill/docs/数据持久化接口文档.md
karlcarlo c06e74bd01 feat(app): 实现完整的财务管家记账聊天与统计功能
- 新增App主组件,实现聊天、账本、统计三大功能模块切换
- 支持语音及文本输入,自动解析记账信息并生成交易记录
- 交易记录支持编辑、删除、提醒等操作,数据本地持久化
- 管家性格预设和自定义设定,包含头像上传和语气调整
- 会话展示支持关联交易显示,样式区分收入与支出
- 账本页添加日期分组交易列表及饼图统计展示
- 消息列表实现渐变背景和交互动画,提升用户体验
- 支持图片上传并同步发送给AI进行解析记账
- 底部导航实现三标签切换并同步显示对应内容
- 全局事务编辑弹窗支持直接调用和删除操作
- 增加环境变量示例和.gitignore规则,完善工程配置
2026-01-19 20:20:54 +08:00

22 KiB
Raw Permalink Blame History

数据持久化接口文档

概述

本文档详细列出了智能账本应用中所有需要持久化数据的接口,包括存储键名、数据结构、操作方法和使用场景。应用采用浏览器 localStorage 作为持久化存储方案。


1. 交易记录持久化

存储键名

'cui_transactions'

数据类型

Transaction[]

数据结构

interface Transaction {
  id: string;              // 唯一标识符 (UUID)
  item: string;            // 交易项目名称
  amount: number;          // 交易金额
  type: TransactionType;   // 交易类型: 'expense' | 'income'
  category: string;        // 交易分类 (来自 ExpenseCategory 或 IncomeCategory)
  date: string;           // 日期 (ISO格式 YYYY-MM-DD)
  timestamp: number;      // 时间戳 (毫秒)
}

支持的分类

支出分类 (ExpenseCategory)

  • 餐饮、购物、交通、住房、娱乐、医疗
  • 人情、学习、金融、孩子、宠物、汽车
  • 美容、经营、杂项

收入分类 (IncomeCategory)

  • 工资、奖金、理财、兼职、红包、其他收入

操作接口

读取

// 位置: App.tsx L102
const storedTransactions = localStorage.getItem('cui_transactions');
if (storedTransactions) setTransactions(JSON.parse(storedTransactions));

写入

// 位置: App.tsx L129
useEffect(() => { 
  localStorage.setItem('cui_transactions', JSON.stringify(transactions)); 
}, [transactions]);

新增交易

// 位置: App.tsx L192-204
const newTransaction: Transaction = {
  id: uuidv4(),
  item: result.transaction.item,
  amount: result.transaction.amount,
  type: result.transaction.type,
  category: result.transaction.category,
  date: getLocalDateString(),
  timestamp: Date.now()
};
setTransactions(prev => [...prev, newTransaction]);

更新交易

// 位置: App.tsx L144-148
const handleUpdateTransaction = (updated: Transaction) => {
  setTransactions(prev => prev.map(t => t.id === updated.id ? updated : t));
};

删除交易

// 位置: App.tsx L150-156
const handleDeleteTransaction = (id: string) => {
  if (confirm('确定要删除这条记录吗?')) {
    setTransactions(prev => prev.filter(t => t.id !== id));
  }
};

使用场景

  • 聊天界面语音/文字输入记账
  • 图片识别账单自动记账
  • 账本页面手动编辑交易
  • 统计页面数据查询和分析
  • 日历视图显示每日交易

2. 聊天消息持久化

存储键名

'cui_messages'

数据类型

Message[]

数据结构

interface Message {
  id: string;                    // 消息唯一标识符 (UUID)
  role: 'user' | 'assistant';    // 消息角色
  content: string;               // 消息内容
  timestamp: number;             // 时间戳 (毫秒)
  relatedTransactionId?: string; // 关联的交易ID (可选)
}

操作接口

读取

// 位置: App.tsx L103
const storedMessages = localStorage.getItem('cui_messages');
if (storedMessages) setMessages(JSON.parse(storedMessages));

写入

// 位置: App.tsx L135-138
useEffect(() => {
  localStorage.setItem('cui_messages', JSON.stringify(messages));
  if (activeTab === 'chat') scrollToBottom();
}, [messages, activeTab]);

初始化欢迎消息

// 位置: App.tsx L119-126
if (!storedMessages) {
  setMessages([{
    id: 'init',
    role: 'assistant',
    content: `我是${aiSettings.name}。✨\n想记账?快说!🎤`,
    timestamp: Date.now()
  }]);
}

新增用户消息

// 位置: App.tsx L179-185
const userMsg: Message = {
  id: uuidv4(),
  role: 'user',
  content: imageBase64 ? '已上传图片' : text,
  timestamp: Date.now()
};
setMessages(prev => [...prev, userMsg]);

新增助手消息(含交易回执)

// 位置: App.tsx L206-212
setMessages(prev => [...prev, { 
  id: uuidv4(), 
  role: 'assistant', 
  content: 'RECEIPT', 
  timestamp: Date.now(), 
  relatedTransactionId: newTransactionId 
}]);

使用场景

  • 聊天界面显示对话历史
  • 显示交易回执卡片
  • 从聊天界面编辑/删除关联交易
  • 保留用户与AI的完整对话记录

3. 预算数据持久化

存储键名

'cui_budget'

数据类型

Record<string, number>

数据结构

// 键值对形式,支持多预算项
{
  '总预算': 3000,
  '餐饮预算': 1000,
  '购物预算': 500,
  // ... 更多预算项
}

操作接口

读取(兼容旧版)

// 位置: App.tsx L104, L111-114
const storedBudget = localStorage.getItem('cui_budget');
if (storedBudget) {
  const parsed = JSON.parse(storedBudget);
  setBudgets(typeof parsed === 'number' ? { '总预算': parsed } : parsed);
}

写入

// 位置: App.tsx L130
useEffect(() => { 
  localStorage.setItem('cui_budget', JSON.stringify(budgets)); 
}, [budgets]);

更新预算

// 通过 setBudgets 更新
setBudgets({ ...budgets, '总预算': 5000 });

使用场景

  • 账本页面显示月度预算对比
  • 预算超支提醒
  • 多项预算管理(未来扩展)

兼容性说明

  • 支持旧版单一预算值(纯数字)
  • 新版支持多预算项(对象格式)
  • 自动兼容转换

4. 账单提醒持久化

存储键名

'cui_reminders'

数据类型

BillReminder[]

数据结构

interface BillReminder {
  id: string;                    // 唯一标识符 (UUID)
  title: string;                 // 提醒标题
  amount?: number;               // 金额 (可选)
  frequency: 'monthly' | 'once'; // 频率: 每月 | 单次
  targetDate: string;            // 目标日期: 'DD' (每月) 或 'YYYY-MM-DD' (单次)
  isCompleted: boolean;          // 完成状态
}

操作接口

读取

// 位置: App.tsx L105
const storedReminders = localStorage.getItem('cui_reminders');
if (storedReminders) setReminders(JSON.parse(storedReminders));

写入

// 位置: App.tsx L131
useEffect(() => { 
  localStorage.setItem('cui_reminders', JSON.stringify(reminders)); 
}, [reminders]);

新增提醒

// 位置: App.tsx L158-160
const handleAddReminder = (reminder: BillReminder) => {
  setReminders(prev => [...prev, reminder]);
};

更新提醒

// 位置: App.tsx L162-164
const handleUpdateReminder = (updated: BillReminder) => {
  setReminders(prev => prev.map(r => r.id === updated.id ? updated : r));
};

删除提醒

// 位置: App.tsx L166-170
const handleDeleteReminder = (id: string) => {
  if (confirm('确定要删除这个提醒吗?')) {
    setReminders(prev => prev.filter(r => r.id !== id));
  }
};

切换完成状态

// 位置: App.tsx L172-174
const handleToggleReminder = (id: string) => {
  setReminders(prev => prev.map(r => 
    r.id === id ? { ...r, isCompleted: !r.isCompleted } : r
  ));
};

剩余天数计算

// 位置: BillReminders.tsx L14-38
export const getDaysRemaining = (r: BillReminder): number => {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  
  let targetDate = new Date();
  targetDate.setHours(0, 0, 0, 0);

  if (r.frequency === 'monthly') {
    const day = parseInt(r.targetDate);
    targetDate.setDate(day);
    
    // 如果本月已过期,移至下月
    if (targetDate < today) {
      targetDate.setMonth(targetDate.getMonth() + 1);
    }
  } else {
    // 单次提醒
    targetDate = new Date(r.targetDate);
    targetDate.setHours(0, 0, 0, 0);
  }

  const diffTime = targetDate.getTime() - today.getTime();
  const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
  return diffDays;
};

使用场景

  • 月度账单还款提醒(房贷、信用卡等)
  • 一次性待办事项
  • 日历视图显示提醒日期
  • 账本页面待办列表
  • 逾期提醒和倒计时显示

5. AI助手设置持久化

存储键名

'cui_ai_settings'

数据类型

AISettings

数据结构

interface AISettings {
  name: string;      // AI助手名称
  persona: string;   // AI性格描述
  avatar?: string;   // 头像 (Base64编码图片,可选)
}

默认值

// 位置: App.tsx L18-21
const DEFAULT_AI_SETTINGS: AISettings = {
  name: "崔经理",
  persona: "一个精明、毒舌但实际上很负责任的财务经理。说话喜欢带刺,吐槽用户乱花钱,但关键时刻会给出靠谱建议。"
};

操作接口

读取

// 位置: App.tsx L106
const storedAiSettings = localStorage.getItem('cui_ai_settings');
if (storedAiSettings) setAiSettings(JSON.parse(storedAiSettings));

写入

// 位置: App.tsx L132
useEffect(() => { 
  localStorage.setItem('cui_ai_settings', JSON.stringify(aiSettings)); 
}, [aiSettings]);

更新设置

// 修改名称
setAiSettings({ ...aiSettings, name: '新名字' });

// 修改性格
setAiSettings({ ...aiSettings, persona: '新的性格描述' });

// 更新头像
setAiSettings(prev => ({ ...prev, avatar: base64Data }));

预设性格列表

// 位置: App.tsx L23-44
[
  {
    id: 'toxic',
    name: '崔经理 (默认)',
    description: '精明、毒舌、傲娇。喜欢吐槽你乱花钱...'
  },
  {
    id: 'gentle',
    name: '温柔管家',
    description: '温柔、体贴、治愈。无论你怎么花钱都会温柔记录...'
  },
  {
    id: 'strict',
    name: '严厉会计',
    description: '一丝不苟、冷酷无情。只关心数字...'
  },
  {
    id: 'custom',
    name: '自定义',
    description: '可以根据你喜欢的方式设定助手的语气和风格'
  }
]

使用场景

  • 聊天界面显示AI名称和头像
  • AI回复风格控制
  • Gemini API调用时的persona参数
  • 设置模态框显示和编辑

6. 自定义性格预设持久化

存储键名

'cui_custom_preset_info'

数据类型

{ name: string; description: string }

数据结构

{
  name: '自定义',           // 自定义预设名称
  description: '可以根据你喜欢的方式设定助手的语气和风格'  // 描述
}

操作接口

读取

// 位置: App.tsx L107
const storedCustom = localStorage.getItem('cui_custom_preset_info');
if (storedCustom) setCustomPreset(JSON.parse(storedCustom));

写入

// 位置: App.tsx L133
useEffect(() => { 
  localStorage.setItem('cui_custom_preset_info', JSON.stringify(customPreset)); 
}, [customPreset]);

自动保存自定义预设

// 位置: App.tsx L286-292
const handleSaveSettings = () => {
  const currentPreset = INITIAL_PERSONA_PRESETS.find(
    p => p.description === aiSettings.persona && p.id !== 'custom'
  );
  if (!currentPreset) {
    setCustomPreset({ name: aiSettings.name, description: aiSettings.persona });
  }
  setIsSettingsOpen(false);
};

使用场景

  • 保存用户自定义的AI性格设置
  • 性格预设列表中显示自定义预设
  • 区分预设性格和自定义性格

7. 主题设置持久化

存储键名

'cui_card_theme'

数据类型

string (数字索引)

数据值

'0' | '1' | '2' | '3' | '4'  // 对应5个主题的索引

主题列表

// 位置: LedgerPage.tsx L20-26
const CARD_THEMES = [
  { id: 'dark-indigo', class: 'from-gray-900 via-indigo-950 to-black', themeColor: 'bg-indigo-600', name: '暗夜霓虹' },
  { id: 'sunset', class: 'from-orange-500 via-rose-600 to-pink-700', themeColor: 'bg-rose-600', name: '落日熔金' },
  { id: 'emerald', class: 'from-emerald-600 via-teal-700 to-cyan-900', themeColor: 'bg-emerald-600', name: '薄荷森林' },
  { id: 'royal', class: 'from-blue-700 via-indigo-800 to-indigo-950', themeColor: 'bg-blue-700', name: '皇家礼赞' },
  { id: 'rose', class: 'from-rose-500 via-pink-600 to-purple-800', themeColor: 'bg-purple-600', name: '浪漫巴黎' },
];

操作接口

读取

// 位置: LedgerPage.tsx L41
const [themeIndex, setThemeIndex] = useState(() => 
  Number(localStorage.getItem('cui_card_theme')) || 0
);

写入

// 位置: LedgerPage.tsx L43-45
useEffect(() => {
  localStorage.setItem('cui_card_theme', themeIndex.toString());
}, [themeIndex]);

切换主题

// 位置: LedgerPage.tsx L56
const cycleTheme = () => setThemeIndex((prev) => (prev + 1) % CARD_THEMES.length);

使用场景

  • 账本页面顶部卡片主题
  • 标签页按钮颜色主题
  • 用户个性化界面体验

8. AI服务接口(非持久化,但涉及数据处理)

Gemini API 调用接口

服务文件

services/geminiService.ts

处理用户消息

// 位置: geminiService.ts L67-115
export const processUserMessage = async (
  message: string, 
  aiName: string, 
  aiPersona: string, 
  imageBase64?: string, 
  mimeType?: string
): Promise<ParseResult>

返回结果

// 位置: geminiService.ts L57-65
interface ParseResult {
  transaction: {
    item: string;
    amount: number;
    type: TransactionType;
    category: string;
  } | null;
  reply: string;
}

API模型配置

// 位置: geminiService.ts L4-7
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
const MODEL_NAME = "gemini-3-flash-preview";

系统指令模板

// 位置: geminiService.ts L9-29
const getSystemInstruction = (aiName: string, aiPersona: string) => `
你现在的身份是"${aiName}"。
你的性格设定是:${aiPersona}

你的任务是:
1. 从用户的输入中提取交易信息(内容、金额、类别、交易类型)。
2. 判断是【支出】(expense) 还是【收入】(income)。
3. **严格回复规范**:
   - 回复必须**极度简练**。
   - 每一段话的字数必须控制在 **30字左右**(越短越好)。
   - 全部的回复**最多只能分成 2 段**(中间用一个 \n 换行符隔开)。
   - 必须在每一段中都加入**生动的表情包 (Emoji)** 🌈。
   - 说话风格要完全符合"${aiName}"的设定。
`;

使用场景

  • 语音/文字输入转交易记录
  • 图片识别账单信息
  • AI对话回复生成
  • 自动分类和金额提取

数据流架构图

graph TB
    subgraph 浏览器存储层
        LS[localStorage]
        LS --> T[cui_transactions]
        LS --> M[cui_messages]
        LS --> B[cui_budget]
        LS --> R[cui_reminders]
        LS --> AS[cui_ai_settings]
        LS --> CP[cui_custom_preset_info]
        LS --> TH[cui_card_theme]
    end
    
    subgraph React状态层
        ST[State Management]
        ST --> transactions[transactions]
        ST --> messages[messages]
        ST --> budgets[budgets]
        ST --> reminders[reminders]
        ST --> aiSettings[aiSettings]
        ST --> customPreset[customPreset]
        ST --> themeIndex[themeIndex]
    end
    
    subgraph 组件层
        APP[App.tsx]
        LEDGER[LedgerPage.tsx]
        EDIT[EditTransactionModal.tsx]
        REMIND[BillReminders.tsx]
        CAL[CalendarView.tsx]
        STATS[StatisticsPage.tsx]
    end
    
    subgraph 外部服务
        GEMINI[Gemini API]
    end
    
    T <--> transactions
    M <--> messages
    B <--> budgets
    R <--> reminders
    AS <--> aiSettings
    CP <--> customPreset
    TH <--> themeIndex
    
    transactions --> APP
    transactions --> LEDGER
    transactions --> STATS
    transactions --> CAL
    
    messages --> APP
    reminders --> LEDGER
    reminders --> REMIND
    reminders --> CAL
    
    APP --> GEMINI
    GEMINI --> APP
    
    APP --> EDIT
    LEDGER --> EDIT

数据大小估算

单条记录大小

  • Transaction: ~150 bytes
  • Message: ~100 bytes
  • BillReminder: ~120 bytes
  • AISettings: ~200 bytes (不含头像) / ~5KB (含头像)

容量预估(localStorage限制 5-10MB)

  • 1000条交易: ~150KB
  • 500条消息: ~50KB
  • 50个提醒: ~6KB
  • AI设置: ~5KB (含头像)

总计: 约 211KB (不含聊天历史图片)

容量优化建议

  1. 定期归档历史数据(超过1年)
  2. 限制聊天历史记录数量(如保留最近500条)
  3. 图片压缩后再转Base64
  4. 实现数据导出/导入功能

错误处理与兼容性

JSON解析容错

try {
  const data = JSON.parse(localStorage.getItem('cui_transactions'));
  setTransactions(data);
} catch (error) {
  console.error('数据解析失败', error);
  setTransactions([]);  // 回退到默认值
}

版本兼容

  • 预算数据: 自动兼容旧版纯数字格式
  • 新增字段: 使用可选字段(如 BillReminder.amount?)
  • 默认值: 所有状态都有合理的初始值

数据校验建议

// 类型守卫示例
function isValidTransaction(obj: any): obj is Transaction {
  return (
    typeof obj.id === 'string' &&
    typeof obj.item === 'string' &&
    typeof obj.amount === 'number' &&
    ['expense', 'income'].includes(obj.type) &&
    typeof obj.date === 'string'
  );
}

数据备份与恢复

导出数据(建议实现)

function exportAllData() {
  const data = {
    transactions: JSON.parse(localStorage.getItem('cui_transactions') || '[]'),
    messages: JSON.parse(localStorage.getItem('cui_messages') || '[]'),
    budgets: JSON.parse(localStorage.getItem('cui_budget') || '{}'),
    reminders: JSON.parse(localStorage.getItem('cui_reminders') || '[]'),
    aiSettings: JSON.parse(localStorage.getItem('cui_ai_settings') || '{}'),
    customPreset: JSON.parse(localStorage.getItem('cui_custom_preset_info') || '{}'),
    themeIndex: localStorage.getItem('cui_card_theme') || '0',
    exportDate: new Date().toISOString()
  };
  
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `账本数据备份_${new Date().toISOString().slice(0,10)}.json`;
  a.click();
}

导入数据(建议实现)

function importAllData(file: File) {
  const reader = new FileReader();
  reader.onload = (e) => {
    try {
      const data = JSON.parse(e.target?.result as string);
      localStorage.setItem('cui_transactions', JSON.stringify(data.transactions));
      localStorage.setItem('cui_messages', JSON.stringify(data.messages));
      localStorage.setItem('cui_budget', JSON.stringify(data.budgets));
      localStorage.setItem('cui_reminders', JSON.stringify(data.reminders));
      localStorage.setItem('cui_ai_settings', JSON.stringify(data.aiSettings));
      localStorage.setItem('cui_custom_preset_info', JSON.stringify(data.customPreset));
      localStorage.setItem('cui_card_theme', data.themeIndex);
      
      alert('数据导入成功!请刷新页面');
      window.location.reload();
    } catch (error) {
      alert('数据导入失败:' + error.message);
    }
  };
  reader.readAsText(file);
}

安全性考虑

敏感数据

  • 交易记录: 包含个人财务信息
  • AI头像: Base64图片数据
  • 聊天记录: 可能包含敏感对话

安全建议

  1. 不存储: Gemini API Key(使用环境变量)
  2. 加密存储: 考虑对敏感数据加密(如使用Web Crypto API)
  3. 定期清理: 提供清除所有数据的功能
  4. 隐私模式: 提示用户不要在公共设备上使用

清除数据示例

function clearAllData() {
  if (confirm('确定要清除所有数据吗?此操作不可恢复!')) {
    localStorage.removeItem('cui_transactions');
    localStorage.removeItem('cui_messages');
    localStorage.removeItem('cui_budget');
    localStorage.removeItem('cui_reminders');
    localStorage.removeItem('cui_ai_settings');
    localStorage.removeItem('cui_custom_preset_info');
    localStorage.removeItem('cui_card_theme');
    window.location.reload();
  }
}

性能优化建议

1. 批量写入优化

// 避免频繁写入,使用防抖
import { debounce } from 'lodash';

const debouncedSave = debounce((key: string, data: any) => {
  localStorage.setItem(key, JSON.stringify(data));
}, 500);

2. 数据分页加载

// 仅加载当前月份数据到UI
const currentMonthTransactions = transactions.filter(
  t => t.date.startsWith(selectedMonth)
);

3. 索引优化

// 创建日期索引,加速查询
const transactionsByDate = useMemo(() => {
  return transactions.reduce((acc, t) => {
    if (!acc[t.date]) acc[t.date] = [];
    acc[t.date].push(t);
    return acc;
  }, {} as Record<string, Transaction[]>);
}, [transactions]);

总结

本文档详细列出了应用中所有7个持久化数据接口:

  1. ✅ 交易记录 (cui_transactions) - 核心财务数据
  2. ✅ 聊天消息 (cui_messages) - AI对话历史
  3. ✅ 预算数据 (cui_budget) - 预算管理
  4. ✅ 账单提醒 (cui_reminders) - 待办事项
  5. ✅ AI设置 (cui_ai_settings) - AI助手配置
  6. ✅ 自定义预设 (cui_custom_preset_info) - 性格预设
  7. ✅ 主题设置 (cui_card_theme) - 界面主题

所有接口均采用 localStorage 存储,使用 JSON 序列化,并通过 React useEffect 实现自动持久化。


文档版本: v1.0
生成时间: 2026-01-19
最后更新: 2026-01-19
维护者: 开发团队