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:
17
apps/backend/api/finance/accounts.get.ts
Normal file
17
apps/backend/api/finance/accounts.get.ts
Normal 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);
|
||||
});
|
||||
10
apps/backend/api/finance/budgets.get.ts
Normal file
10
apps/backend/api/finance/budgets.get.ts
Normal 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);
|
||||
});
|
||||
33
apps/backend/api/finance/budgets.post.ts
Normal file
33
apps/backend/api/finance/budgets.post.ts
Normal 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);
|
||||
});
|
||||
22
apps/backend/api/finance/budgets/[id].delete.ts
Normal file
22
apps/backend/api/finance/budgets/[id].delete.ts
Normal 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: '删除成功' });
|
||||
});
|
||||
48
apps/backend/api/finance/budgets/[id].put.ts
Normal file
48
apps/backend/api/finance/budgets/[id].put.ts
Normal 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);
|
||||
});
|
||||
13
apps/backend/api/finance/categories.get.ts
Normal file
13
apps/backend/api/finance/categories.get.ts
Normal 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);
|
||||
});
|
||||
23
apps/backend/api/finance/categories.post.ts
Normal file
23
apps/backend/api/finance/categories.post.ts
Normal 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);
|
||||
});
|
||||
18
apps/backend/api/finance/categories/[id].delete.ts
Normal file
18
apps/backend/api/finance/categories/[id].delete.ts
Normal 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: '删除成功' });
|
||||
});
|
||||
27
apps/backend/api/finance/categories/[id].put.ts
Normal file
27
apps/backend/api/finance/categories/[id].put.ts
Normal 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);
|
||||
});
|
||||
6
apps/backend/api/finance/currencies.get.ts
Normal file
6
apps/backend/api/finance/currencies.get.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { listCurrencies } from '~/utils/finance-metadata';
|
||||
import { useResponseSuccess } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(async () => {
|
||||
return useResponseSuccess(listCurrencies());
|
||||
});
|
||||
30
apps/backend/api/finance/exchange-rates.get.ts
Normal file
30
apps/backend/api/finance/exchange-rates.get.ts
Normal 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);
|
||||
});
|
||||
12
apps/backend/api/finance/transactions.get.ts
Normal file
12
apps/backend/api/finance/transactions.get.ts
Normal 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);
|
||||
});
|
||||
33
apps/backend/api/finance/transactions.post.ts
Normal file
33
apps/backend/api/finance/transactions.post.ts
Normal 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);
|
||||
});
|
||||
19
apps/backend/api/finance/transactions/[id].delete.ts
Normal file
19
apps/backend/api/finance/transactions/[id].delete.ts
Normal 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: '删除成功' });
|
||||
});
|
||||
47
apps/backend/api/finance/transactions/[id].put.ts
Normal file
47
apps/backend/api/finance/transactions/[id].put.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user