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

908 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数据持久化接口文档
## 概述
本文档详细列出了智能账本应用中所有需要持久化数据的接口,包括存储键名、数据结构、操作方法和使用场景。应用采用浏览器 `localStorage` 作为持久化存储方案。
---
## 1. 交易记录持久化
### 存储键名
```typescript
'cui_transactions'
```
### 数据类型
```typescript
Transaction[]
```
### 数据结构
```typescript
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)
- 工资、奖金、理财、兼职、红包、其他收入
### 操作接口
#### 读取
```typescript
// 位置: App.tsx L102
const storedTransactions = localStorage.getItem('cui_transactions');
if (storedTransactions) setTransactions(JSON.parse(storedTransactions));
```
#### 写入
```typescript
// 位置: App.tsx L129
useEffect(() => {
localStorage.setItem('cui_transactions', JSON.stringify(transactions));
}, [transactions]);
```
#### 新增交易
```typescript
// 位置: 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]);
```
#### 更新交易
```typescript
// 位置: App.tsx L144-148
const handleUpdateTransaction = (updated: Transaction) => {
setTransactions(prev => prev.map(t => t.id === updated.id ? updated : t));
};
```
#### 删除交易
```typescript
// 位置: App.tsx L150-156
const handleDeleteTransaction = (id: string) => {
if (confirm('确定要删除这条记录吗?')) {
setTransactions(prev => prev.filter(t => t.id !== id));
}
};
```
### 使用场景
- 聊天界面语音/文字输入记账
- 图片识别账单自动记账
- 账本页面手动编辑交易
- 统计页面数据查询和分析
- 日历视图显示每日交易
---
## 2. 聊天消息持久化
### 存储键名
```typescript
'cui_messages'
```
### 数据类型
```typescript
Message[]
```
### 数据结构
```typescript
interface Message {
id: string; // 消息唯一标识符 (UUID)
role: 'user' | 'assistant'; // 消息角色
content: string; // 消息内容
timestamp: number; // 时间戳 (毫秒)
relatedTransactionId?: string; // 关联的交易ID (可选)
}
```
### 操作接口
#### 读取
```typescript
// 位置: App.tsx L103
const storedMessages = localStorage.getItem('cui_messages');
if (storedMessages) setMessages(JSON.parse(storedMessages));
```
#### 写入
```typescript
// 位置: App.tsx L135-138
useEffect(() => {
localStorage.setItem('cui_messages', JSON.stringify(messages));
if (activeTab === 'chat') scrollToBottom();
}, [messages, activeTab]);
```
#### 初始化欢迎消息
```typescript
// 位置: App.tsx L119-126
if (!storedMessages) {
setMessages([{
id: 'init',
role: 'assistant',
content: `我是${aiSettings.name}。✨\n想记账?快说!🎤`,
timestamp: Date.now()
}]);
}
```
#### 新增用户消息
```typescript
// 位置: App.tsx L179-185
const userMsg: Message = {
id: uuidv4(),
role: 'user',
content: imageBase64 ? '已上传图片' : text,
timestamp: Date.now()
};
setMessages(prev => [...prev, userMsg]);
```
#### 新增助手消息(含交易回执)
```typescript
// 位置: App.tsx L206-212
setMessages(prev => [...prev, {
id: uuidv4(),
role: 'assistant',
content: 'RECEIPT',
timestamp: Date.now(),
relatedTransactionId: newTransactionId
}]);
```
### 使用场景
- 聊天界面显示对话历史
- 显示交易回执卡片
- 从聊天界面编辑/删除关联交易
- 保留用户与AI的完整对话记录
---
## 3. 预算数据持久化
### 存储键名
```typescript
'cui_budget'
```
### 数据类型
```typescript
Record<string, number>
```
### 数据结构
```typescript
// 键值对形式,支持多预算项
{
'总预算': 3000,
'餐饮预算': 1000,
'购物预算': 500,
// ... 更多预算项
}
```
### 操作接口
#### 读取(兼容旧版)
```typescript
// 位置: App.tsx L104, L111-114
const storedBudget = localStorage.getItem('cui_budget');
if (storedBudget) {
const parsed = JSON.parse(storedBudget);
setBudgets(typeof parsed === 'number' ? { '总预算': parsed } : parsed);
}
```
#### 写入
```typescript
// 位置: App.tsx L130
useEffect(() => {
localStorage.setItem('cui_budget', JSON.stringify(budgets));
}, [budgets]);
```
#### 更新预算
```typescript
// 通过 setBudgets 更新
setBudgets({ ...budgets, '总预算': 5000 });
```
### 使用场景
- 账本页面显示月度预算对比
- 预算超支提醒
- 多项预算管理(未来扩展)
### 兼容性说明
- 支持旧版单一预算值(纯数字)
- 新版支持多预算项(对象格式)
- 自动兼容转换
---
## 4. 账单提醒持久化
### 存储键名
```typescript
'cui_reminders'
```
### 数据类型
```typescript
BillReminder[]
```
### 数据结构
```typescript
interface BillReminder {
id: string; // 唯一标识符 (UUID)
title: string; // 提醒标题
amount?: number; // 金额 (可选)
frequency: 'monthly' | 'once'; // 频率: 每月 | 单次
targetDate: string; // 目标日期: 'DD' (每月) 或 'YYYY-MM-DD' (单次)
isCompleted: boolean; // 完成状态
}
```
### 操作接口
#### 读取
```typescript
// 位置: App.tsx L105
const storedReminders = localStorage.getItem('cui_reminders');
if (storedReminders) setReminders(JSON.parse(storedReminders));
```
#### 写入
```typescript
// 位置: App.tsx L131
useEffect(() => {
localStorage.setItem('cui_reminders', JSON.stringify(reminders));
}, [reminders]);
```
#### 新增提醒
```typescript
// 位置: App.tsx L158-160
const handleAddReminder = (reminder: BillReminder) => {
setReminders(prev => [...prev, reminder]);
};
```
#### 更新提醒
```typescript
// 位置: App.tsx L162-164
const handleUpdateReminder = (updated: BillReminder) => {
setReminders(prev => prev.map(r => r.id === updated.id ? updated : r));
};
```
#### 删除提醒
```typescript
// 位置: App.tsx L166-170
const handleDeleteReminder = (id: string) => {
if (confirm('确定要删除这个提醒吗?')) {
setReminders(prev => prev.filter(r => r.id !== id));
}
};
```
#### 切换完成状态
```typescript
// 位置: App.tsx L172-174
const handleToggleReminder = (id: string) => {
setReminders(prev => prev.map(r =>
r.id === id ? { ...r, isCompleted: !r.isCompleted } : r
));
};
```
### 剩余天数计算
```typescript
// 位置: 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助手设置持久化
### 存储键名
```typescript
'cui_ai_settings'
```
### 数据类型
```typescript
AISettings
```
### 数据结构
```typescript
interface AISettings {
name: string; // AI助手名称
persona: string; // AI性格描述
avatar?: string; // 头像 (Base64编码图片,可选)
}
```
### 默认值
```typescript
// 位置: App.tsx L18-21
const DEFAULT_AI_SETTINGS: AISettings = {
name: "崔经理",
persona: "一个精明、毒舌但实际上很负责任的财务经理。说话喜欢带刺,吐槽用户乱花钱,但关键时刻会给出靠谱建议。"
};
```
### 操作接口
#### 读取
```typescript
// 位置: App.tsx L106
const storedAiSettings = localStorage.getItem('cui_ai_settings');
if (storedAiSettings) setAiSettings(JSON.parse(storedAiSettings));
```
#### 写入
```typescript
// 位置: App.tsx L132
useEffect(() => {
localStorage.setItem('cui_ai_settings', JSON.stringify(aiSettings));
}, [aiSettings]);
```
#### 更新设置
```typescript
// 修改名称
setAiSettings({ ...aiSettings, name: '新名字' });
// 修改性格
setAiSettings({ ...aiSettings, persona: '新的性格描述' });
// 更新头像
setAiSettings(prev => ({ ...prev, avatar: base64Data }));
```
### 预设性格列表
```typescript
// 位置: 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. 自定义性格预设持久化
### 存储键名
```typescript
'cui_custom_preset_info'
```
### 数据类型
```typescript
{ name: string; description: string }
```
### 数据结构
```typescript
{
name: '自定义', // 自定义预设名称
description: '可以根据你喜欢的方式设定助手的语气和风格' // 描述
}
```
### 操作接口
#### 读取
```typescript
// 位置: App.tsx L107
const storedCustom = localStorage.getItem('cui_custom_preset_info');
if (storedCustom) setCustomPreset(JSON.parse(storedCustom));
```
#### 写入
```typescript
// 位置: App.tsx L133
useEffect(() => {
localStorage.setItem('cui_custom_preset_info', JSON.stringify(customPreset));
}, [customPreset]);
```
#### 自动保存自定义预设
```typescript
// 位置: 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. 主题设置持久化
### 存储键名
```typescript
'cui_card_theme'
```
### 数据类型
```typescript
string (数字索引)
```
### 数据值
```typescript
'0' | '1' | '2' | '3' | '4' // 对应5个主题的索引
```
### 主题列表
```typescript
// 位置: 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: '浪漫巴黎' },
];
```
### 操作接口
#### 读取
```typescript
// 位置: LedgerPage.tsx L41
const [themeIndex, setThemeIndex] = useState(() =>
Number(localStorage.getItem('cui_card_theme')) || 0
);
```
#### 写入
```typescript
// 位置: LedgerPage.tsx L43-45
useEffect(() => {
localStorage.setItem('cui_card_theme', themeIndex.toString());
}, [themeIndex]);
```
#### 切换主题
```typescript
// 位置: LedgerPage.tsx L56
const cycleTheme = () => setThemeIndex((prev) => (prev + 1) % CARD_THEMES.length);
```
### 使用场景
- 账本页面顶部卡片主题
- 标签页按钮颜色主题
- 用户个性化界面体验
---
## 8. AI服务接口(非持久化,但涉及数据处理)
### Gemini API 调用接口
#### 服务文件
```typescript
services/geminiService.ts
```
#### 处理用户消息
```typescript
// 位置: geminiService.ts L67-115
export const processUserMessage = async (
message: string,
aiName: string,
aiPersona: string,
imageBase64?: string,
mimeType?: string
): Promise<ParseResult>
```
#### 返回结果
```typescript
// 位置: geminiService.ts L57-65
interface ParseResult {
transaction: {
item: string;
amount: number;
type: TransactionType;
category: string;
} | null;
reply: string;
}
```
#### API模型配置
```typescript
// 位置: geminiService.ts L4-7
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
const MODEL_NAME = "gemini-3-flash-preview";
```
#### 系统指令模板
```typescript
// 位置: geminiService.ts L9-29
const getSystemInstruction = (aiName: string, aiPersona: string) => `
你现在的身份是"${aiName}"。
你的性格设定是:${aiPersona}
你的任务是:
1. 从用户的输入中提取交易信息(内容、金额、类别、交易类型)。
2. 判断是【支出】(expense) 还是【收入】(income)。
3. **严格回复规范**:
- 回复必须**极度简练**。
- 每一段话的字数必须控制在 **30字左右**(越短越好)。
- 全部的回复**最多只能分成 2 段**(中间用一个 \n 换行符隔开)。
- 必须在每一段中都加入**生动的表情包 (Emoji)** 🌈。
- 说话风格要完全符合"${aiName}"的设定。
`;
```
### 使用场景
- 语音/文字输入转交易记录
- 图片识别账单信息
- AI对话回复生成
- 自动分类和金额提取
---
## 数据流架构图
```mermaid
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解析容错
```typescript
try {
const data = JSON.parse(localStorage.getItem('cui_transactions'));
setTransactions(data);
} catch (error) {
console.error('数据解析失败', error);
setTransactions([]); // 回退到默认值
}
```
### 版本兼容
- **预算数据**: 自动兼容旧版纯数字格式
- **新增字段**: 使用可选字段(如 BillReminder.amount?)
- **默认值**: 所有状态都有合理的初始值
### 数据校验建议
```typescript
// 类型守卫示例
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'
);
}
```
---
## 数据备份与恢复
### 导出数据(建议实现)
```typescript
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();
}
```
### 导入数据(建议实现)
```typescript
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. **隐私模式**: 提示用户不要在公共设备上使用
### 清除数据示例
```typescript
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. 批量写入优化
```typescript
// 避免频繁写入,使用防抖
import { debounce } from 'lodash';
const debouncedSave = debounce((key: string, data: any) => {
localStorage.setItem(key, JSON.stringify(data));
}, 500);
```
### 2. 数据分页加载
```typescript
// 仅加载当前月份数据到UI
const currentMonthTransactions = transactions.filter(
t => t.date.startsWith(selectedMonth)
);
```
### 3. 索引优化
```typescript
// 创建日期索引,加速查询
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
**维护者**: 开发团队