feat(api): 集成后端API,实现数据持久化和用户登录

- 新增token管理和用户信息状态,支持基于token的自动登录
- 实现登录或注册接口调用,自动根据URL参数完成身份验证
- 从后端加载交易记录、消息和待办提醒,替代本地缓存读写
- 对交易记录的增删改操作同步到数据库,确保数据一致性
- 对消息和提醒的增删改操作支持服务器同步,提升数据可靠性
- 优化消息发送逻辑,先保存用户消息,再处理AI回复并存库
- 添加待办提醒切换状态API调用,确保状态同步服务器
- 改进界面互动,支持文件与头像上传功能,更新管家形象
- 采用异步处理并增加错误捕获和用户提示,增强稳定性
This commit is contained in:
2026-01-22 18:25:42 +08:00
parent c06e74bd01
commit 194de6ec06
7 changed files with 1595 additions and 499 deletions

1122
App.tsx

File diff suppressed because it is too large Load Diff

View File

@@ -23,8 +23,8 @@ const EditTransactionModal: React.FC<EditTransactionModalProps> = ({ transaction
}; };
return ( return (
<div className="fixed inset-0 z-[60] flex items-end md:items-center justify-center bg-black/50 backdrop-blur-sm p-4"> <div className="fixed inset-0 z-[60] flex items-end md:items-center justify-center bg-black/50 backdrop-blur-sm p-4 pb-28 md:pb-4">
<div className="bg-white w-full max-w-md rounded-2xl p-6 shadow-2xl animate-float md:animate-none"> <div className="bg-white w-full max-w-md rounded-2xl p-6 shadow-2xl md:animate-none max-h-[calc(100vh-120px)] overflow-y-auto no-scrollbar">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold text-gray-900">编辑账单</h3> <h3 className="text-lg font-bold text-gray-900">编辑账单</h3>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600"> <button onClick={onClose} className="text-gray-400 hover:text-gray-600">

View File

@@ -6,8 +6,8 @@ import BillReminders, { getDaysRemaining } from './BillReminders';
interface LedgerPageProps { interface LedgerPageProps {
transactions: Transaction[]; transactions: Transaction[];
onUpdateTransaction: (t: Transaction) => void; onUpdateTransaction: (t: Transaction) => Promise<void>;
onDeleteTransaction: (id: string) => void; onDeleteTransaction: (id: string) => Promise<void>;
budgets: Record<string, number>; budgets: Record<string, number>;
setBudgets: (val: Record<string, number>) => void; setBudgets: (val: Record<string, number>) => void;
reminders: BillReminder[]; reminders: BillReminder[];
@@ -200,7 +200,24 @@ const LedgerPage: React.FC<LedgerPageProps> = ({
{subTab === 'calendar' && <CalendarView transactions={transactions} reminders={reminders} viewDate={selectedMonth} />} {subTab === 'calendar' && <CalendarView transactions={transactions} reminders={reminders} viewDate={selectedMonth} />}
{subTab === 'bills' && <BillReminders reminders={reminders.filter(r => r.frequency === 'monthly' || r.targetDate.startsWith(selectedMonth))} onAdd={onAddReminder} onUpdate={onUpdateReminder} onDelete={onDeleteReminder} onToggle={onToggleReminder} />} {subTab === 'bills' && <BillReminders reminders={reminders.filter(r => r.frequency === 'monthly' || r.targetDate.startsWith(selectedMonth))} onAdd={onAddReminder} onUpdate={onUpdateReminder} onDelete={onDeleteReminder} onToggle={onToggleReminder} />}
</div> </div>
{editingTransaction && <EditTransactionModal transaction={editingTransaction} onSave={(updated) => { onUpdateTransaction(updated); setEditingTransaction(null); }} onDelete={(id) => { onDeleteTransaction(id); setEditingTransaction(null); }} onClose={() => setEditingTransaction(null)} />} {editingTransaction && (
<EditTransactionModal
transaction={editingTransaction}
onSave={async (updated) => {
try {
await onUpdateTransaction(updated);
setEditingTransaction(null);
} catch (e) {
// Error handled in App.tsx
}
}}
onDelete={async (id) => {
await onDeleteTransaction(id);
setEditingTransaction(null);
}}
onClose={() => setEditingTransaction(null)}
/>
)}
</div> </div>
); );
}; };

View File

@@ -11,6 +11,8 @@ const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isF
const recognitionRef = useRef<any>(null); const recognitionRef = useRef<any>(null);
const timerRef = useRef<number | null>(null); const timerRef = useRef<number | null>(null);
const isStartingRef = useRef(false);
useEffect(() => { useEffect(() => {
if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) { if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
@@ -22,30 +24,37 @@ const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isF
recognitionRef.current.onresult = (event: any) => { recognitionRef.current.onresult = (event: any) => {
const transcript = event.results[0][0].transcript; const transcript = event.results[0][0].transcript;
if (transcript) onTranscript(transcript); if (transcript) onTranscript(transcript);
setIsListening(false);
}; };
recognitionRef.current.onerror = (event: any) => { recognitionRef.current.onerror = (event: any) => {
if (event.error === 'network') {
alert("语音识别网络连接失败。请检查网络或尝试切换浏览器(推荐 Chrome/Edge)。");
}
console.error('Speech recognition error', event.error); console.error('Speech recognition error', event.error);
setIsListening(false);
}; };
recognitionRef.current.onend = () => { recognitionRef.current.onend = () => {
setIsListening(false); setIsListening(false);
isStartingRef.current = false;
}; };
} }
}, [onTranscript]); }, [onTranscript]);
const startListening = (e: React.MouseEvent | React.TouchEvent) => { const startListening = (e: React.PointerEvent) => {
e.preventDefault(); if (e.cancelable) e.preventDefault();
if (isProcessing || isListening) return;
if (isProcessing || isListening || isStartingRef.current) return;
if (recognitionRef.current) { if (recognitionRef.current) {
try { try {
isStartingRef.current = true;
recognitionRef.current.start(); recognitionRef.current.start();
setIsListening(true); setIsListening(true);
} catch (e) { } catch (err) {
console.error("Failed to start recognition", e); console.error("Failed to start recognition", err);
// 如果 start 失败,立即重置状态,否则会锁死
isStartingRef.current = false;
setIsListening(false);
} }
} else { } else {
alert("您的浏览器不支持语音识别,请使用 Chrome 或 Edge。"); alert("您的浏览器不支持语音识别,请使用 Chrome 或 Edge。");
@@ -53,20 +62,23 @@ const VoiceInput: React.FC<VoiceInputProps> = ({ onTranscript, isProcessing, isF
}; };
const stopListening = () => { const stopListening = () => {
// 仅在识别引擎正在运行时尝试停止
// 不要在这里重置 isListening 和 isStartingRef,等待 onend 回调
if (isListening) { if (isListening) {
recognitionRef.current?.stop(); try {
setIsListening(false); recognitionRef.current?.stop();
} catch (err) {
console.error("Failed to stop recognition", err);
}
} }
}; };
return ( return (
<div className={`${isFullWidth ? 'w-full' : ''} relative select-none touch-none`}> <div className={`${isFullWidth ? 'w-full' : ''} relative select-none touch-none`}>
<button <button
onMouseDown={startListening} onPointerDown={startListening}
onMouseUp={stopListening} onPointerUp={stopListening}
onMouseLeave={stopListening} onPointerLeave={stopListening}
onTouchStart={startListening}
onTouchEnd={stopListening}
disabled={isProcessing} disabled={isProcessing}
className={`relative flex items-center justify-center transition-all duration-300 active:scale-95 shadow-lg ${ className={`relative flex items-center justify-center transition-all duration-300 active:scale-95 shadow-lg ${
isFullWidth isFullWidth

View File

@@ -1,4 +1,5 @@
# App_02_记账 # App_02_记账
- 应用信息 - 应用信息
- 应用名称 - 应用名称
- 记账 - 记账
@@ -9,83 +10,158 @@
- 应用描述 - 应用描述
- 一个精明、毒舌但实际上很负责任的财务经理。说话喜欢带刺,吐槽用户乱花钱,但关键时刻会给出靠谱建议。 - 一个精明、毒舌但实际上很负责任的财务经理。说话喜欢带刺,吐槽用户乱花钱,但关键时刻会给出靠谱建议。
- 数据结构 - 数据结构
- Project 项目分类 - 枚举数据
- name 名称(name) - TransactionType 交易类型
- desc 背景(context) - EXPENSE = "expense"
- target 目标(goal) - INCOME = "income"
- value 价值(value) - ExpenseCategory 支出分类
- FOOD = "餐饮"
- SHOPPING = "购物"
- TRANSPORT = "交通"
- HOUSING = "住房"
- ENTERTAINMENT = "娱乐"
- MEDICAL = "医疗"
- SOCIAL = "人情"
- EDUCATION = "学习"
- FINANCE = "金融"
- CHILD = "孩子"
- PET = "宠物"
- CAR = "汽车"
- BEAUTY = "美容"
- BUSINESS = "经营"
- MISC = "杂项"
- IncomeCategory 收入分类
- SALARY = "工资"
- BONUS = "奖金"
- INVESTMENT = "理财"
- PARTTIME = "兼职"
- GIFT = "红包"
- OTHER = "其他收入"
- MessageRole 消息角色
- USER = "user"
- ASSISTANT = "assistant"
- ReminderFrequency 提醒频率
- MONTHLY = "monthly"
- ONCE = "once"
- Common 通用属性
- seq 排序
- isEnabled 是否启用
- creator 创建人 - creator 创建人
- Member - created_at 创建时间
- Task 任务项目 - updated_at 更新时间
- title 标题(title) - Transaction 交易记录
- project 分类(projectId) - item 交易项目名称
- Project - amount: float 交易金额
- desc 描述(notes) - type: TransactionType 交易类型
- startDate 开始日期(date) - category: ExpenseCategory | IncomeCategory 交易分类
- startTime 开始时间(startTime) - date 交易日期 YYYY-MM-DD
- isCompleted 是否完成(isCompleted) - timestamp: int 时间戳(毫秒)
- created_at 创建时间(createdAt) - Message 聊天消息
- creator 创建人 - role: MessageRole 消息角色
- Member - content 消息内容
- timestamp: int 时间戳(毫秒)
- transaction_id 关联的交易ID
- Budget 预算
- member_id 会员ID
- budgets: dict 预算字典 {名称: 金额}
- month 预算月份 YYYY-MM
- Reminder 账单提醒
- member_id 会员ID
- title 提醒标题
- amount: float 金额(可选)
- frequency: ReminderFrequency 频率
- target_date 目标日期: DD(每月) 或 YYYY-MM-DD(单次)
- is_completed: bool 完成状态
- AISettings AI助手设置
- member_id 会员ID
- name AI助手名称
- persona AI性格描述
- avatar 头像Base64编码
- UserPreference 用户偏好设置
- member_id 会员ID
- custom_preset_name 自定义预设名称
- custom_preset_description 自定义预设描述
- theme_index: int 主题索引
- API接口 - API接口
- auth 授权 - Auth 授权
- POST /mp/auth/get_token - POST /api/wx/get_token
- 获取token - 获取token
- GET /mp/auth/profile - GET /api/wx/profile
- 获取个人信息 - 获取个人信息
- 项目 - transaction Bill: 交易记录
- GET /mp/todo/projects - GET /api/bill/transactions
- 项目列表 - 获取分页交易记录
- GET /mp/todo/projects/{project_id} - GET /api/bill/transactions/{id}
- 获取项目详细 - 获取交易记录详情
- POST /mp/todo/projects - POST /api/bill/transactions
- 创建项目 - 创建交易记录
- PUT /mp/todo/projects/{project_id} - PUT /api/bill/transactions/{id}
- 更新项目 - 更新交易记录
- DELETE /mp/todo/projects/{project_id} - DELETE /api/bill/transactions/{id}
- 删除项目 - 删除交易记录
- 任务 - Message Bill: 聊天消息
- GET /mp/todo/tasks - GET /api/bill/messages
- 任务列表 - 获取分页聊天消息
- GET /mp/todo/tasks/{task_id} - GET /api/bill/messages/{id}
- 获取任务详细 - 获取聊天消息详情
- POST /mp/todo/tasks - POST /api/bill/messages
- 创建任务 - 创建聊天消息
- PUT /mp/todo/tasks/{task_id} - PUT /api/bill/messages/{id}
- 更新任务 - 更新聊天消息
- DELETE /mp/todo/tasks/{task_id} - DELETE /api/bill/messages/{id}
- 删除项目 - 删除聊天消息
- PUT /mp/todo/tasks/{task_id}/completed - Budget Bill: 预算
- 设置为已完成 - GET /api/bill/budgets
- PUT /mp/todo/tasks/{task_id}/uncompleted - 获取分页预算
- 设置为未完成 - GET /api/bill/budgets/{id}
- 获取预算详情
- POST /api/bill/transactions
- 创建交易记录
- PUT /api/bill/transactions/{id}
- 更新交易记录
- DELETE /api/bill/transactions/{id}
- 删除交易记录
- BillReminder Bill: 账单提醒
- GET /api/bill/bill_reminders
- 获取分页账单提醒
- GET /api/bill/bill_reminders/{id}
- 获取账单提醒详情
- POST /api/bill/transactions
- 创建交易记录
- PUT /api/bill/transactions/{id}
- 更新交易记录
- DELETE /api/bill/transactions/{id}
- 删除交易记录
- AISettings AI助手设置
- GET /api/h5/ai_settings
- 获取AI助手设置
- PUT /api/h5/ai_settings
- 更新AI助手设置
- POST /api/h5/ai_settings/reset
- 重置AI助手设置
- UserPreference 用户偏好设置
- GET /api/h5/user_preference
- 获取用户偏好设置
- PUT /api/h5/user_preference
- 更新用户偏好设置
- 问题/需求 - 问题/需求
- 单用户转换为多用户模式 - 单用户转换为多用户模式
- 增加登录验证 - 增加登录验证
- 增加creator字段 - 增加creator字段
- 登录问题 - 登录问题
- 小程序跳转到H5, 需要检测登录状态
- 如果已经登录, 需要把登录信息传递到H5首页
- 如果未登录跳转至小程序登录页
- URL传递参数openid和mobile - URL传递参数openid和mobile
- H5端首页先检测token状态 - H5端首页先检测token状态
- 如果存在并且未过期, 则携带token获取用户信息 - 如果存在并且未过期, 则携带token获取用户信息
- 如果不存在则通过openid和mobile获取token, 并获取用户信息 - 如果不存在则通过openid和mobile获取token, 并获取用户信息
- 新建项目问题 - 语音接口识别
- 目前是实时更新模式, 保存到本地缓存没问题, 但是要持久化到数据库就会报错 - 接口鲁棒性调试
- 增加保存按钮, 点击保存后提交接口, 并同步本地数据 - 识别消息对接创建交易记录接口
- 分别处理本地新建项目和提交接口新建项目的逻辑, 并处理错误提示, 例如项目名称重复 - 约定和调试大模型返回数据
- 任务过滤查询
- 日期等于今天的任务
- 日期小于今日的任务
- 查询所有任务匹配到日历中
- 今日任务和日历中任务的匹配调节
- 测试任务2没有显示在今日任务中
- 最可能原因是日期转换过程中的时区差异。当API返回的日期时间字符串 "2026-01-14T00:00:00" 在客户端进行时区转换时,可能会因为客户端的时区设置导致日期变成 2026-01-13 或其他日期,从而不满足 t.date === systemDate 的条件。
- 多个在增删改查操作中存在的时区转换问题
- 开发任务 - 开发任务
- 小程序跳转时增加登录判断和URL传参(openid和mobile)
- 1小时/人
- H5首页接收openid和mobile参数 - H5首页接收openid和mobile参数
- 0.5小时/人 - 0.5小时/人
- H5首页token检测并获取, 通过接口获取用户信息 - H5首页token检测并获取, 通过接口获取用户信息
@@ -94,11 +170,19 @@
- 2小时/人 - 2小时/人
- 开发H5端获取用户信息接口 - 开发H5端获取用户信息接口
- 1.5小时/人 - 1.5小时/人
- 开发项目增删改查接口 - 开发交易记录增删改查接口
- 4小时/人 - 4小时/人
- 开发任务增删改查接口和已完成/未完成接口 - 开发聊天增删改查接口
- 5小时/人 - 6小时/人
- 开发任务按时间过滤列表接口 - 开发预算增删改查接口
- 2小时/人 - 4小时/人
- 调试H5页面交互, 调用项目和任务接口 - 开发账单提醒增删改查完成接口
- 6~10小时/人 - 6小时/人
- 开发AI助手设置增删改查接口
- 4小时/人
- 开发用户偏好设置增删改查接口
- 3小时/人
- 调试H5页面交互, 调用交易记录、聊天、预算、账单提醒、助手设置、用户偏好接口
- 8~12小时/人
- 切换线上配置,打包、发布到线上服务器
- 2小时/人

View File

@@ -4,7 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>崔经理记账</title> <title>崔经理记账</title>
<script src="https://cdn.tailwindcss.com"></script> <!-- <script src="https://cdn.tailwindcss.com"></script> -->
<script src="https://mp.yizhezhe.com/static/tailwindcss-3.4.17.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700;900&display=swap" rel="stylesheet">

678
services/api.ts Normal file
View File

@@ -0,0 +1,678 @@
import { Transaction, Message, BillReminder } from '../types';
// API基础配置
const getApiUrl = (): string => {
return process.env.API_URL || 'http://localhost:8000';
};
const getAuthToken = (): string | null => {
return localStorage.getItem('auth_token');
};
// 统一响应格式
interface ApiResponse<T = any> {
code: number;
msg?: string;
message?: string;
result?: T;
}
// 分页响应格式
interface PaginatedResponse<T> {
items: T[];
skip: number;
total: number;
}
// 统计数据响应格式
interface TransactionStats {
month: string;
total_income: number;
total_expense: number;
balance: number;
expense_by_category: Record<string, number>;
income_by_category: Record<string, number>;
daily_expense: Array<{ date: string; amount: number }>;
}
// 预算响应格式
interface Budget {
id: string;
user_id?: string;
member_id?: string;
budgets: Record<string, number>;
month: string;
created_at?: string;
updated_at?: string;
}
// 预算执行情况
interface BudgetExecution {
month: string;
budgets: Record<string, {
budget: number;
spent: number;
remaining: number;
percentage: number;
}>;
total_budget: number;
total_spent: number;
total_remaining: number;
is_over_budget: boolean;
}
// AI设置
interface AISettings {
id?: string;
user_id?: string;
member_id?: string;
name: string;
persona: string;
avatar?: string;
created_at?: string;
updated_at?: string;
}
// 通用请求函数
async function request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const token = getAuthToken();
const apiUrl = getApiUrl();
const url = `${apiUrl}${endpoint}`;
console.log(`请求地址: ${url}`);
const defaultHeaders: HeadersInit = {
'Content-Type': 'application/json',
};
if (token) {
defaultHeaders['Authorization'] = `Bearer ${token}`;
}
const config: RequestInit = {
...options,
headers: {
...defaultHeaders,
...options.headers,
},
};
try {
const response = await fetch(url, config);
const data: ApiResponse<T> = await response.json();
if (!response.ok || (data.code && data.code !== 200)) {
const errorMsg = data.msg || data.message || `HTTP error! status: ${response.status}`;
throw new Error(errorMsg);
}
return data.result as T;
} catch (error) {
console.error('API请求错误:', error);
throw error;
}
}
// ==================== 用户认证接口 ====================
/**
* 登录或注册
*/
export async function loginOrRegister(openid: string, mobile: string): Promise<{
token: string;
user: any;
}> {
const apiUrl = getApiUrl();
console.log(`请求地址: ${apiUrl}/wx/loginOrRegister`);
const response = await fetch(`${apiUrl}/wx/loginOrRegister`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ openid, mobile }),
});
const data = await response.json();
if (data.code === 200 && data.result?.token) {
// 保存token到localStorage
localStorage.setItem('auth_token', data.result.token);
}
return data.result;
}
/**
* 获取用户信息
*/
export async function getUserProfile(): Promise<any> {
return request<any>('/wx/profile', {
method: 'GET',
});
}
/**
* 退出登录
*/
export function logout(): void {
localStorage.removeItem('auth_token');
}
// ==================== 交易记录接口 ====================
/**
* 创建交易记录
*/
export async function createTransaction(data: {
item: string;
amount: number;
type: 'expense' | 'income';
category: string;
date: string;
timestamp: number;
}): Promise<Transaction> {
return request<Transaction>('/bill/transactions/', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* 查询交易记录列表(分页)
*/
export async function getTransactions(params?: {
skip?: number;
limit?: number;
type?: 'expense' | 'income';
category?: string;
start_date?: string;
end_date?: string;
month?: string;
}): Promise<PaginatedResponse<Transaction>> {
const queryString = new URLSearchParams(
Object.entries(params || {}).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = String(value);
}
return acc;
}, {} as Record<string, string>)
).toString();
return request<PaginatedResponse<Transaction>>(
`/bill/transactions/${queryString ? `?${queryString}` : ''}`
);
}
/**
* 获取单个交易记录
*/
export async function getTransaction(id: string): Promise<Transaction> {
return request<Transaction>(`/bill/transactions/${id}`);
}
/**
* 更新交易记录
*/
export async function updateTransaction(
id: string,
data: Partial<{
item: string;
amount: number;
type: 'expense' | 'income';
category: string;
date: string;
timestamp: number;
}>
): Promise<Transaction> {
return request<Transaction>(`/bill/transactions/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
/**
* 删除交易记录
*/
export async function deleteTransaction(id: string): Promise<void> {
return request<void>(`/bill/transactions/${id}`, {
method: 'DELETE',
});
}
/**
* 批量删除交易记录
*/
export async function batchDeleteTransactions(
transactionIds: string[]
): Promise<{ deleted_count: number }> {
return request<{ deleted_count: number }>('/bill/transactions/batch', {
method: 'DELETE',
body: JSON.stringify({ transaction_ids: transactionIds }),
});
}
/**
* 获取交易统计数据
*/
export async function getTransactionStats(
month: string
): Promise<TransactionStats> {
return request<TransactionStats>(`/bill/transactions/stats/summary?month=${month}`);
}
// ==================== 聊天消息接口 ====================
/**
* 创建聊天消息
*/
export async function createMessage(data: {
role: 'user' | 'assistant';
content: string;
timestamp: number;
transaction_id?: string;
}): Promise<Message> {
return request<Message>('/bill/messages/', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* 查询聊天消息列表
*/
export async function getMessages(params?: {
skip?: number;
limit?: number;
role?: 'user' | 'assistant';
start_timestamp?: number;
end_timestamp?: number;
}): Promise<PaginatedResponse<Message>> {
const queryString = new URLSearchParams(
Object.entries(params || {}).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = String(value);
}
return acc;
}, {} as Record<string, string>)
).toString();
return request<PaginatedResponse<Message>>(
`/bill/messages/${queryString ? `?${queryString}` : ''}`
);
}
/**
* 获取单个消息
*/
export async function getMessage(id: string): Promise<Message> {
return request<Message>(`/bill/messages/${id}`);
}
/**
* 删除消息
*/
export async function deleteMessage(id: string): Promise<void> {
return request<void>(`/bill/messages/${id}`, {
method: 'DELETE',
});
}
/**
* 清空聊天历史
*/
export async function clearMessages(): Promise<{ deleted_count: number }> {
return request<{ deleted_count: number }>('/bill/messages/clear', {
method: 'DELETE',
});
}
// ==================== 预算管理接口 ====================
/**
* 获取月度预算
*/
export async function getMonthlyBudget(month: string): Promise<Budget> {
return request<Budget>(`/bill/budgets/${month}`);
}
/**
* 设置/更新月度预算
*/
export async function updateMonthlyBudget(
month: string,
budgets: Record<string, number>
): Promise<Budget> {
return request<Budget>(`/bill/budgets/${month}`, {
method: 'PUT',
body: JSON.stringify({ budgets }),
});
}
/**
* 获取预算执行情况
*/
export async function getBudgetExecution(
month: string
): Promise<BudgetExecution> {
return request<BudgetExecution>(`/bill/budgets/${month}/execution`);
}
// ==================== 账单提醒接口 ====================
/**
* 创建账单提醒
*/
export async function createReminder(data: {
title: string;
amount?: number;
frequency: 'monthly' | 'once';
target_date: string;
}): Promise<BillReminder> {
return request<BillReminder>('/bill/reminders/', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* 查询账单提醒列表
*/
export async function getReminders(params?: {
skip?: number;
limit?: number;
frequency?: 'monthly' | 'once';
is_completed?: boolean;
month?: string;
}): Promise<PaginatedResponse<BillReminder>> {
const queryString = new URLSearchParams(
Object.entries(params || {}).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = String(value);
}
return acc;
}, {} as Record<string, string>)
).toString();
return request<PaginatedResponse<BillReminder>>(
`/bill/reminders/${queryString ? `?${queryString}` : ''}`
);
}
/**
* 获取单个提醒
*/
export async function getReminder(id: string): Promise<BillReminder> {
return request<BillReminder>(`/bill/reminders/${id}`);
}
/**
* 更新账单提醒
*/
export async function updateReminder(
id: string,
data: Partial<{
title: string;
amount?: number;
frequency: 'monthly' | 'once';
target_date: string;
}>
): Promise<BillReminder> {
return request<BillReminder>(`/bill/reminders/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
/**
* 切换提醒完成状态
*/
export async function toggleReminder(
id: string
): Promise<{ id: string; is_completed: boolean }> {
return request<{ id: string; is_completed: boolean }>(
`/bill/reminders/${id}/toggle`,
{
method: 'PATCH',
}
);
}
/**
* 删除账单提醒
*/
export async function deleteReminder(id: string): Promise<void> {
return request<void>(`/bill/reminders/${id}`, {
method: 'DELETE',
});
}
/**
* 获取即将到期的提醒
*/
export async function getUpcomingReminders(
days: number = 7
): Promise<PaginatedResponse<BillReminder>> {
return request<PaginatedResponse<BillReminder>>(
`/bill/reminders/upcoming?days=${days}`
);
}
// ==================== AI设置接口 ====================
/**
* 获取AI设置
*/
export async function getAISettings(): Promise<AISettings> {
return request<AISettings>('/h5/ai-settings');
}
/**
* 更新AI设置
*/
export async function updateAISettings(data: {
name?: string;
persona?: string;
avatar?: string;
}): Promise<AISettings> {
return request<AISettings>('/h5/ai-settings', {
method: 'PUT',
body: JSON.stringify(data),
});
}
/**
* 重置AI设置为默认
*/
export async function resetAISettings(): Promise<AISettings> {
return request<AISettings>('/h5/ai-settings/reset', {
method: 'POST',
});
}
// ==================== 用户偏好接口 ====================
interface UserPreferences {
id?: string;
user_id?: string;
custom_preset_name?: string;
custom_preset_description?: string;
theme_index?: number;
created_at?: string;
updated_at?: string;
}
/**
* 获取用户偏好
*/
export async function getUserPreferences(): Promise<UserPreferences> {
return request<UserPreferences>('/h5/preferences');
}
/**
* 更新用户偏好
*/
export async function updateUserPreferences(
data: Partial<UserPreferences>
): Promise<UserPreferences> {
return request<UserPreferences>('/h5/preferences', {
method: 'PUT',
body: JSON.stringify(data),
});
}
// ==================== 数据同步接口 ====================
interface SyncData {
transactions?: Transaction[];
messages?: Message[];
budgets?: Record<string, number>;
reminders?: BillReminder[];
ai_settings?: AISettings;
preferences?: UserPreferences;
}
/**
* 全量数据同步(上传)
*/
export async function uploadSyncData(data: SyncData): Promise<{
synced_at: string;
summary: {
transactions_count: number;
messages_count: number;
reminders_count: number;
};
}> {
return request('/bill/sync/upload', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* 全量数据同步(下载)
*/
export async function downloadSyncData(): Promise<SyncData> {
return request<SyncData>('/bill/sync/download');
}
/**
* 增量数据同步
*/
export async function incrementalSync(data: {
last_sync_at: string;
changes: SyncData;
}): Promise<{
synced_at: string;
server_changes: SyncData;
}> {
return request('/bill/sync/incremental', {
method: 'POST',
body: JSON.stringify(data),
});
}
// ==================== AI处理接口 ====================
interface AIProcessResult {
transaction: {
item: string;
amount: number;
type: 'expense' | 'income';
category: string;
} | null;
reply: string;
created_transaction_id?: string;
created_message_ids?: string[];
}
/**
* AI处理用户消息
*/
export async function processAIMessage(data: {
message: string;
image_base64?: string;
mime_type?: string;
}): Promise<AIProcessResult> {
return request<AIProcessResult>('/ai/process', {
method: 'POST',
body: JSON.stringify(data),
});
}
/**
* AI识别账单图片
*/
export async function recognizeReceipt(
imageFile: File
): Promise<AIProcessResult> {
const formData = new FormData();
formData.append('image', imageFile);
// 注意: FormData需要特殊处理,不设置Content-Type让浏览器自动设置
return request<AIProcessResult>('/ai/recognize-receipt', {
method: 'POST',
body: formData,
headers: {}, // 清空默认headers,让fetch自动处理multipart/form-data
});
}
// ==================== 导出所有API ====================
export default {
// 用户认证
loginOrRegister,
getUserProfile,
logout,
// 交易记录
createTransaction,
getTransactions,
getTransaction,
updateTransaction,
deleteTransaction,
batchDeleteTransactions,
getTransactionStats,
// 聊天消息
createMessage,
getMessages,
getMessage,
deleteMessage,
clearMessages,
// 预算管理
getMonthlyBudget,
updateMonthlyBudget,
getBudgetExecution,
// 账单提醒
createReminder,
getReminders,
getReminder,
updateReminder,
toggleReminder,
deleteReminder,
getUpcomingReminders,
// AI设置
getAISettings,
updateAISettings,
resetAISettings,
// 用户偏好
getUserPreferences,
updateUserPreferences,
// 数据同步
uploadSyncData,
downloadSyncData,
incrementalSync,
// AI处理
processAIMessage,
recognizeReceipt,
};