refactor: 整合财务系统到主应用并重构后端架构

主要变更:
- 将独立的 web-finance 应用整合到 web-antd 主应用中
- 重命名 backend-mock 为 backend,增强后端功能
- 新增财务模块 API 端点(账户、预算、类别、交易)
- 增强财务仪表板和报表功能
- 添加 SQLite 数据存储支持和财务数据导入脚本
- 优化路由结构,删除冗余的 finance-system 模块

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
woshiqp465
2025-10-04 21:14:21 +08:00
parent 9683b940bf
commit 1e42191296
275 changed files with 10221 additions and 22207 deletions

View File

@@ -0,0 +1,17 @@
import { getQuery } from 'h3';
import { listAccounts } from '~/utils/finance-metadata';
import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const currency = query.currency as string | undefined;
let accounts = listAccounts();
if (currency) {
accounts = accounts.filter((account) => account.currency === currency);
}
return useResponseSuccess(accounts);
});

View File

@@ -0,0 +1,10 @@
import { defineEventHandler } from '#nitro';
import { MOCK_BUDGETS } from '../../utils/mock-data';
import { useResponseSuccess } from '../../utils/response';
export default defineEventHandler(() => {
// 返回未删除的预算
const budgets = MOCK_BUDGETS.filter((b) => !b.isDeleted);
return useResponseSuccess(budgets);
});

View File

@@ -0,0 +1,33 @@
import { defineEventHandler, readBody } from '#nitro';
import { MOCK_BUDGETS } from '../../utils/mock-data';
import { useResponseSuccess } from '../../utils/response';
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const newBudget = {
id: Date.now(),
userId: 1,
category: body.category,
categoryId: body.categoryId,
emoji: body.emoji,
limit: body.limit,
spent: body.spent || 0,
remaining: body.remaining || body.limit,
percentage: body.percentage || 0,
currency: body.currency,
period: body.period,
alertThreshold: body.alertThreshold,
description: body.description,
autoRenew: body.autoRenew,
overspendAlert: body.overspendAlert,
dailyReminder: body.dailyReminder,
monthlyTrend: body.monthlyTrend || 0,
createdAt: new Date().toISOString(),
isDeleted: false,
};
MOCK_BUDGETS.push(newBudget);
return useResponseSuccess(newBudget);
});

View File

@@ -0,0 +1,22 @@
import { defineEventHandler, getRouterParam } from '#nitro';
import { MOCK_BUDGETS } from '../../../utils/mock-data';
import { useResponseError, useResponseSuccess } from '../../../utils/response';
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, 'id'));
const index = MOCK_BUDGETS.findIndex((b) => b.id === id);
if (index === -1) {
return useResponseError('预算不存在', -1);
}
// 软删除
MOCK_BUDGETS[index] = {
...MOCK_BUDGETS[index],
isDeleted: true,
deletedAt: new Date().toISOString(),
};
return useResponseSuccess({ message: '删除成功' });
});

View File

@@ -0,0 +1,48 @@
import { defineEventHandler, getRouterParam, readBody } from '#nitro';
import { MOCK_BUDGETS } from '../../../utils/mock-data';
import { useResponseError, useResponseSuccess } from '../../../utils/response';
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, 'id'));
const body = await readBody(event);
const index = MOCK_BUDGETS.findIndex((b) => b.id === id);
if (index === -1) {
return useResponseError('预算不存在', -1);
}
// 如果是恢复操作
if (body.isDeleted === false) {
MOCK_BUDGETS[index] = {
...MOCK_BUDGETS[index],
isDeleted: false,
deletedAt: undefined,
};
return useResponseSuccess(MOCK_BUDGETS[index]);
}
// 普通更新
const updatedBudget = {
...MOCK_BUDGETS[index],
category: body.category ?? MOCK_BUDGETS[index].category,
categoryId: body.categoryId ?? MOCK_BUDGETS[index].categoryId,
emoji: body.emoji ?? MOCK_BUDGETS[index].emoji,
limit: body.limit ?? MOCK_BUDGETS[index].limit,
spent: body.spent ?? MOCK_BUDGETS[index].spent,
remaining: body.remaining ?? MOCK_BUDGETS[index].remaining,
percentage: body.percentage ?? MOCK_BUDGETS[index].percentage,
currency: body.currency ?? MOCK_BUDGETS[index].currency,
period: body.period ?? MOCK_BUDGETS[index].period,
alertThreshold: body.alertThreshold ?? MOCK_BUDGETS[index].alertThreshold,
description: body.description ?? MOCK_BUDGETS[index].description,
autoRenew: body.autoRenew ?? MOCK_BUDGETS[index].autoRenew,
overspendAlert: body.overspendAlert ?? MOCK_BUDGETS[index].overspendAlert,
dailyReminder: body.dailyReminder ?? MOCK_BUDGETS[index].dailyReminder,
monthlyTrend: body.monthlyTrend ?? MOCK_BUDGETS[index].monthlyTrend,
};
MOCK_BUDGETS[index] = updatedBudget;
return useResponseSuccess(updatedBudget);
});

View File

@@ -0,0 +1,13 @@
import { getQuery } from 'h3';
import { fetchCategories } from '~/utils/finance-repository';
import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const type = query.type as 'income' | 'expense' | undefined;
const categories = fetchCategories({ type });
return useResponseSuccess(categories);
});

View File

@@ -0,0 +1,23 @@
import { readBody } from 'h3';
import { createCategoryRecord } from '~/utils/finance-metadata';
import { useResponseError, useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const body = await readBody(event);
if (!body?.name || !body?.type) {
return useResponseError('分类名称和类型为必填项', -1);
}
const category = createCategoryRecord({
name: body.name,
type: body.type,
icon: body.icon,
color: body.color,
userId: 1,
isActive: body.isActive ?? true,
});
return useResponseSuccess(category);
});

View File

@@ -0,0 +1,18 @@
import { getRouterParam } from 'h3';
import { deleteCategoryRecord } from '~/utils/finance-metadata';
import { useResponseError, useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, 'id'));
if (Number.isNaN(id)) {
return useResponseError('参数错误', -1);
}
const deleted = deleteCategoryRecord(id);
if (!deleted) {
return useResponseError('分类不存在', -1);
}
return useResponseSuccess({ message: '删除成功' });
});

View File

@@ -0,0 +1,27 @@
import { getRouterParam, readBody } from 'h3';
import { updateCategoryRecord } from '~/utils/finance-metadata';
import { useResponseError, useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, 'id'));
if (Number.isNaN(id)) {
return useResponseError('参数错误', -1);
}
const body = await readBody(event);
const updated = updateCategoryRecord(id, {
name: body?.name,
icon: body?.icon,
color: body?.color,
userId: body?.userId,
isActive: body?.isActive,
});
if (!updated) {
return useResponseError('分类不存在', -1);
}
return useResponseSuccess(updated);
});

View File

@@ -0,0 +1,6 @@
import { listCurrencies } from '~/utils/finance-metadata';
import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async () => {
return useResponseSuccess(listCurrencies());
});

View File

@@ -0,0 +1,30 @@
import { getQuery } from 'h3';
import { listExchangeRates } from '~/utils/finance-metadata';
import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const fromCurrency = query.from as string | undefined;
const toCurrency = query.to as string | undefined;
const date = query.date as string | undefined;
let rates = listExchangeRates();
if (fromCurrency) {
rates = rates.filter((rate) => rate.fromCurrency === fromCurrency);
}
if (toCurrency) {
rates = rates.filter((rate) => rate.toCurrency === toCurrency);
}
if (date) {
rates = rates.filter((rate) => rate.date === date);
} else if (rates.length > 0) {
const latestDate = rates.reduce((max, rate) => (rate.date > max ? rate.date : max), rates[0].date);
rates = rates.filter((rate) => rate.date === latestDate);
}
return useResponseSuccess(rates);
});

View File

@@ -0,0 +1,12 @@
import { getQuery } from 'h3';
import { fetchTransactions } from '~/utils/finance-repository';
import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const query = getQuery(event);
const type = query.type as string | undefined;
const transactions = fetchTransactions({ type });
return useResponseSuccess(transactions);
});

View File

@@ -0,0 +1,33 @@
import { readBody } from 'h3';
import { createTransaction } from '~/utils/finance-repository';
import { useResponseError, useResponseSuccess } from '~/utils/response';
const DEFAULT_CURRENCY = 'CNY';
export default defineEventHandler(async (event) => {
const body = await readBody(event);
if (!body?.type || !body?.amount || !body?.transactionDate) {
return useResponseError('缺少必填字段', -1);
}
const amount = Number(body.amount);
if (Number.isNaN(amount)) {
return useResponseError('金额格式不正确', -1);
}
const transaction = createTransaction({
type: body.type,
amount,
currency: body.currency ?? DEFAULT_CURRENCY,
categoryId: body.categoryId ?? null,
accountId: body.accountId ?? null,
transactionDate: body.transactionDate,
description: body.description ?? '',
project: body.project ?? null,
memo: body.memo ?? null,
});
return useResponseSuccess(transaction);
});

View File

@@ -0,0 +1,19 @@
import { getRouterParam } from 'h3';
import { softDeleteTransaction } from '~/utils/finance-repository';
import { useResponseError, useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, 'id'));
if (Number.isNaN(id)) {
return useResponseError('参数错误', -1);
}
const updated = softDeleteTransaction(id);
if (!updated) {
return useResponseError('交易不存在', -1);
}
return useResponseSuccess({ message: '删除成功' });
});

View File

@@ -0,0 +1,47 @@
import { getRouterParam, readBody } from 'h3';
import { restoreTransaction, updateTransaction } from '~/utils/finance-repository';
import { useResponseError, useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const id = Number(getRouterParam(event, 'id'));
if (Number.isNaN(id)) {
return useResponseError('参数错误', -1);
}
const body = await readBody(event);
if (body?.isDeleted === false) {
const restored = restoreTransaction(id);
if (!restored) {
return useResponseError('交易不存在', -1);
}
return useResponseSuccess(restored);
}
const payload: Record<string, unknown> = {};
if (body?.type) payload.type = body.type;
if (body?.amount !== undefined) {
const amount = Number(body.amount);
if (Number.isNaN(amount)) {
return useResponseError('金额格式不正确', -1);
}
payload.amount = amount;
}
if (body?.currency) payload.currency = body.currency;
if (body?.categoryId !== undefined) payload.categoryId = body.categoryId ?? null;
if (body?.accountId !== undefined) payload.accountId = body.accountId ?? null;
if (body?.transactionDate) payload.transactionDate = body.transactionDate;
if (body?.description !== undefined) payload.description = body.description ?? '';
if (body?.project !== undefined) payload.project = body.project ?? null;
if (body?.memo !== undefined) payload.memo = body.memo ?? null;
if (body?.isDeleted !== undefined) payload.isDeleted = body.isDeleted;
const updated = updateTransaction(id, payload);
if (!updated) {
return useResponseError('交易不存在', -1);
}
return useResponseSuccess(updated);
});