diff --git a/apps/backend/api/finance/accounts.get.ts b/apps/backend/api/finance/accounts.get.ts index 7ab1c47a..603ad851 100644 --- a/apps/backend/api/finance/accounts.get.ts +++ b/apps/backend/api/finance/accounts.get.ts @@ -1,5 +1,4 @@ import { getQuery } from 'h3'; - import { listAccounts } from '~/utils/finance-metadata'; import { useResponseSuccess } from '~/utils/response'; diff --git a/apps/backend/api/finance/categories.get.ts b/apps/backend/api/finance/categories.get.ts index 40d335c5..a0af081e 100644 --- a/apps/backend/api/finance/categories.get.ts +++ b/apps/backend/api/finance/categories.get.ts @@ -1,11 +1,10 @@ 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 type = query.type as 'expense' | 'income' | undefined; const categories = fetchCategories({ type }); diff --git a/apps/backend/api/finance/categories.post.ts b/apps/backend/api/finance/categories.post.ts index d6a8f22d..73056169 100644 --- a/apps/backend/api/finance/categories.post.ts +++ b/apps/backend/api/finance/categories.post.ts @@ -1,5 +1,4 @@ import { readBody } from 'h3'; - import { createCategoryRecord } from '~/utils/finance-metadata'; import { useResponseError, useResponseSuccess } from '~/utils/response'; diff --git a/apps/backend/api/finance/categories/[id].delete.ts b/apps/backend/api/finance/categories/[id].delete.ts index 7e3b82a9..80e845d1 100644 --- a/apps/backend/api/finance/categories/[id].delete.ts +++ b/apps/backend/api/finance/categories/[id].delete.ts @@ -1,5 +1,4 @@ import { getRouterParam } from 'h3'; - import { deleteCategoryRecord } from '~/utils/finance-metadata'; import { useResponseError, useResponseSuccess } from '~/utils/response'; diff --git a/apps/backend/api/finance/categories/[id].put.ts b/apps/backend/api/finance/categories/[id].put.ts index 37f5c6c0..5c8bbc22 100644 --- a/apps/backend/api/finance/categories/[id].put.ts +++ b/apps/backend/api/finance/categories/[id].put.ts @@ -1,5 +1,4 @@ import { getRouterParam, readBody } from 'h3'; - import { updateCategoryRecord } from '~/utils/finance-metadata'; import { useResponseError, useResponseSuccess } from '~/utils/response'; diff --git a/apps/backend/api/finance/exchange-rates.get.ts b/apps/backend/api/finance/exchange-rates.get.ts index 98840c4e..ccf26437 100644 --- a/apps/backend/api/finance/exchange-rates.get.ts +++ b/apps/backend/api/finance/exchange-rates.get.ts @@ -1,5 +1,4 @@ import { getQuery } from 'h3'; - import { listExchangeRates } from '~/utils/finance-metadata'; import { useResponseSuccess } from '~/utils/response'; @@ -22,7 +21,10 @@ export default defineEventHandler(async (event) => { 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); + const latestDate = rates.reduce( + (max, rate) => Math.max(rate.date, max), + rates[0].date, + ); rates = rates.filter((rate) => rate.date === latestDate); } diff --git a/apps/backend/api/finance/transactions.get.ts b/apps/backend/api/finance/transactions.get.ts index 269fc2f5..8323ba6d 100644 --- a/apps/backend/api/finance/transactions.get.ts +++ b/apps/backend/api/finance/transactions.get.ts @@ -1,5 +1,4 @@ import { getQuery } from 'h3'; - import { fetchTransactions } from '~/utils/finance-repository'; import { useResponseSuccess } from '~/utils/response'; diff --git a/apps/backend/api/finance/transactions.post.ts b/apps/backend/api/finance/transactions.post.ts index 4aad3532..c0087e7e 100644 --- a/apps/backend/api/finance/transactions.post.ts +++ b/apps/backend/api/finance/transactions.post.ts @@ -1,5 +1,4 @@ import { readBody } from 'h3'; - import { createTransaction } from '~/utils/finance-repository'; import { useResponseError, useResponseSuccess } from '~/utils/response'; diff --git a/apps/backend/api/finance/transactions/[id].delete.ts b/apps/backend/api/finance/transactions/[id].delete.ts index d0121787..0abee8ba 100644 --- a/apps/backend/api/finance/transactions/[id].delete.ts +++ b/apps/backend/api/finance/transactions/[id].delete.ts @@ -1,5 +1,4 @@ import { getRouterParam } from 'h3'; - import { softDeleteTransaction } from '~/utils/finance-repository'; import { useResponseError, useResponseSuccess } from '~/utils/response'; diff --git a/apps/backend/api/finance/transactions/[id].put.ts b/apps/backend/api/finance/transactions/[id].put.ts index c01f1747..d2488497 100644 --- a/apps/backend/api/finance/transactions/[id].put.ts +++ b/apps/backend/api/finance/transactions/[id].put.ts @@ -1,6 +1,8 @@ import { getRouterParam, readBody } from 'h3'; - -import { restoreTransaction, updateTransaction } from '~/utils/finance-repository'; +import { + restoreTransaction, + updateTransaction, +} from '~/utils/finance-repository'; import { useResponseError, useResponseSuccess } from '~/utils/response'; export default defineEventHandler(async (event) => { @@ -30,10 +32,12 @@ export default defineEventHandler(async (event) => { payload.amount = amount; } if (body?.currency) payload.currency = body.currency; - if (body?.categoryId !== undefined) payload.categoryId = body.categoryId ?? null; + 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?.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; diff --git a/apps/backend/scripts/import-finance-data.js b/apps/backend/scripts/import-finance-data.js index 1f784e70..2bac3f86 100644 --- a/apps/backend/scripts/import-finance-data.js +++ b/apps/backend/scripts/import-finance-data.js @@ -1,6 +1,7 @@ #!/usr/bin/env node -const fs = require('fs'); -const path = require('path'); +const fs = require('node:fs'); +const path = require('node:path'); + const Database = require('better-sqlite3'); const args = process.argv.slice(2); @@ -110,7 +111,7 @@ db.exec(` ); `); -const RAW_TEXT = fs.readFileSync(inputPath, 'utf-8').replace(/^\ufeff/, ''); +const RAW_TEXT = fs.readFileSync(inputPath, 'utf8').replace(/^\uFEFF/, ''); const lines = RAW_TEXT.split(/\r?\n/).filter((line) => line.trim().length > 0); if (lines.length <= 1) { console.error('CSV 文件内容为空'); @@ -126,7 +127,14 @@ const ACCOUNT_IDX = header.indexOf('支出人'); const CATEGORY_IDX = header.indexOf('计入'); const SHARE_IDX = header.indexOf('阿德应得分红'); -if (DATE_IDX === -1 || PROJECT_IDX === -1 || TYPE_IDX === -1 || AMOUNT_IDX === -1 || ACCOUNT_IDX === -1 || CATEGORY_IDX === -1) { +if ( + DATE_IDX === -1 || + PROJECT_IDX === -1 || + TYPE_IDX === -1 || + AMOUNT_IDX === -1 || + ACCOUNT_IDX === -1 || + CATEGORY_IDX === -1 +) { console.error('CSV 表头缺少必需字段'); process.exit(1); } @@ -138,9 +146,27 @@ const CURRENCIES = [ ]; const EXCHANGE_RATES = [ - { fromCurrency: 'CNY', toCurrency: 'CNY', rate: 1, date: `${baseYear}-01-01`, source: 'system' }, - { fromCurrency: 'USD', toCurrency: 'CNY', rate: 7.14, date: `${baseYear}-01-01`, source: 'manual' }, - { fromCurrency: 'THB', toCurrency: 'CNY', rate: 0.2, date: `${baseYear}-01-01`, source: 'manual' }, + { + fromCurrency: 'CNY', + toCurrency: 'CNY', + rate: 1, + date: `${baseYear}-01-01`, + source: 'system', + }, + { + fromCurrency: 'USD', + toCurrency: 'CNY', + rate: 7.14, + date: `${baseYear}-01-01`, + source: 'manual', + }, + { + fromCurrency: 'THB', + toCurrency: 'CNY', + rate: 0.2, + date: `${baseYear}-01-01`, + source: 'manual', + }, ]; const DEFAULT_EXPENSE_CATEGORY = '未分类支出'; @@ -178,7 +204,12 @@ function inferCurrency(accountName, amountText) { const name = accountName ?? ''; const text = `${name}${amountText ?? ''}`; const lower = text.toLowerCase(); - if (lower.includes('美金') || lower.includes('usd') || lower.includes('u$') || lower.includes('u ')) { + if ( + lower.includes('美金') || + lower.includes('usd') || + lower.includes('u$') || + lower.includes('u ') + ) { return 'USD'; } if (lower.includes('泰铢') || lower.includes('thb')) { @@ -190,7 +221,9 @@ function inferCurrency(accountName, amountText) { function parseAmount(raw) { if (!raw) return 0; const matches = String(raw) - .replace(/[^0-9.+-]/g, (char) => (char === '+' || char === '-' ? char : ' ')) + .replaceAll(/[^0-9.+-]/g, (char) => + char === '+' || char === '-' ? char : ' ', + ) .match(/[-+]?\d+(?:\.\d+)?/g); if (!matches) return 0; return matches.map(Number).reduce((sum, value) => sum + value, 0); @@ -205,10 +238,18 @@ function normalizeDate(value, monthTracker) { const month = Number(match[1]); const day = Number(match[2]); let year = baseYear; - if (monthTracker.lastMonth !== null && month > monthTracker.lastMonth && monthTracker.wrapped) { + if ( + monthTracker.lastMonth !== null && + month > monthTracker.lastMonth && + monthTracker.wrapped + ) { year -= 1; } - if (monthTracker.lastMonth !== null && month < monthTracker.lastMonth && !monthTracker.wrapped) { + if ( + monthTracker.lastMonth !== null && + month < monthTracker.lastMonth && + !monthTracker.wrapped + ) { monthTracker.wrapped = true; } monthTracker.lastMonth = month; @@ -231,12 +272,25 @@ const insertCategory = db.prepare(` db.transaction(() => { if (!categoryMap.has(`${DEFAULT_INCOME_CATEGORY}-income`)) { - const info = insertCategory.run({ name: DEFAULT_INCOME_CATEGORY, type: 'income', icon: '💰', color: '#10b981' }); + const info = insertCategory.run({ + name: DEFAULT_INCOME_CATEGORY, + type: 'income', + icon: '💰', + color: '#10b981', + }); categoryMap.set(`${DEFAULT_INCOME_CATEGORY}-income`, info.lastInsertRowid); } if (!categoryMap.has(`${DEFAULT_EXPENSE_CATEGORY}-expense`)) { - const info = insertCategory.run({ name: DEFAULT_EXPENSE_CATEGORY, type: 'expense', icon: '🏷️', color: '#6366f1' }); - categoryMap.set(`${DEFAULT_EXPENSE_CATEGORY}-expense`, info.lastInsertRowid); + const info = insertCategory.run({ + name: DEFAULT_EXPENSE_CATEGORY, + type: 'expense', + icon: '🏷️', + color: '#6366f1', + }); + categoryMap.set( + `${DEFAULT_EXPENSE_CATEGORY}-expense`, + info.lastInsertRowid, + ); } })(); @@ -261,20 +315,26 @@ for (let i = 1; i < lines.length; i += 1) { const amountRaw = row[AMOUNT_IDX].trim(); const accountNameRaw = row[ACCOUNT_IDX].trim(); const categoryRaw = row[CATEGORY_IDX].trim(); - const shareRaw = SHARE_IDX >= 0 ? row[SHARE_IDX].trim() : ''; + const shareRaw = SHARE_IDX === -1 ? '' : row[SHARE_IDX].trim(); const amount = parseAmount(amountRaw); if (!amount) { continue; } - const normalizedType = typeText.includes('收') && !typeText.includes('支') ? 'income' : 'expense'; + const normalizedType = + typeText.includes('收') && !typeText.includes('支') ? 'income' : 'expense'; const accountName = accountNameRaw || '美金现金'; const currency = inferCurrency(accountNameRaw, amountRaw); if (!accountMap.has(accountName)) { const icon = currency === 'USD' ? '💵' : currency === 'THB' ? '💱' : '💰'; - const color = currency === 'USD' ? '#1677ff' : currency === 'THB' ? '#22c55e' : '#6366f1'; + const color = + currency === 'USD' + ? '#1677ff' + : currency === 'THB' + ? '#22c55e' + : '#6366f1'; const info = insertAccount.run({ name: accountName, currency, @@ -285,7 +345,11 @@ for (let i = 1; i < lines.length; i += 1) { accountMap.set(accountName, Number(info.lastInsertRowid)); } - const categoryName = categoryRaw || (normalizedType === 'income' ? DEFAULT_INCOME_CATEGORY : DEFAULT_EXPENSE_CATEGORY); + const categoryName = + categoryRaw || + (normalizedType === 'income' + ? DEFAULT_INCOME_CATEGORY + : DEFAULT_EXPENSE_CATEGORY); const categoryKey = `${categoryName}-${normalizedType}`; if (!categoryMap.has(categoryKey)) { const icon = normalizedType === 'income' ? '💰' : '🏷️'; @@ -360,4 +424,6 @@ const insertMany = db.transaction((items) => { insertMany(transactions); -console.log(`已导入 ${transactions.length} 条交易,账户 ${accountMap.size} 个,分类 ${categoryMap.size} 个。`); +console.log( + `已导入 ${transactions.length} 条交易,账户 ${accountMap.size} 个,分类 ${categoryMap.size} 个。`, +); diff --git a/apps/backend/utils/finance-metadata.ts b/apps/backend/utils/finance-metadata.ts index 07ea54c4..5fbb752e 100644 --- a/apps/backend/utils/finance-metadata.ts +++ b/apps/backend/utils/finance-metadata.ts @@ -1,4 +1,10 @@ -import { MOCK_ACCOUNTS, MOCK_BUDGETS, MOCK_CATEGORIES, MOCK_CURRENCIES, MOCK_EXCHANGE_RATES } from './mock-data'; +import { + MOCK_ACCOUNTS, + MOCK_BUDGETS, + MOCK_CATEGORIES, + MOCK_CURRENCIES, + MOCK_EXCHANGE_RATES, +} from './mock-data'; export function listAccounts() { return MOCK_ACCOUNTS; @@ -31,7 +37,7 @@ export function createCategoryRecord(category: any) { } export function updateCategoryRecord(id: number, category: any) { - const index = MOCK_CATEGORIES.findIndex(c => c.id === id); + const index = MOCK_CATEGORIES.findIndex((c) => c.id === id); if (index !== -1) { MOCK_CATEGORIES[index] = { ...MOCK_CATEGORIES[index], ...category }; return MOCK_CATEGORIES[index]; @@ -40,7 +46,7 @@ export function updateCategoryRecord(id: number, category: any) { } export function deleteCategoryRecord(id: number) { - const index = MOCK_CATEGORIES.findIndex(c => c.id === id); + const index = MOCK_CATEGORIES.findIndex((c) => c.id === id); if (index !== -1) { MOCK_CATEGORIES.splice(index, 1); return true; diff --git a/apps/backend/utils/finance-repository.ts b/apps/backend/utils/finance-repository.ts index 67141654..d4427399 100644 --- a/apps/backend/utils/finance-repository.ts +++ b/apps/backend/utils/finance-repository.ts @@ -9,27 +9,27 @@ interface TransactionRow { currency: string; exchange_rate_to_base: number; amount_in_base: number; - category_id: number | null; - account_id: number | null; + category_id: null | number; + account_id: null | number; transaction_date: string; - description: string | null; - project: string | null; - memo: string | null; + description: null | string; + project: null | string; + memo: null | string; created_at: string; is_deleted: number; - deleted_at: string | null; + deleted_at: null | string; } interface TransactionPayload { type: string; amount: number; currency: string; - categoryId?: number | null; - accountId?: number | null; + categoryId?: null | number; + accountId?: null | number; transactionDate: string; description?: string; - project?: string | null; - memo?: string | null; + project?: null | string; + memo?: null | string; createdAt?: string; isDeleted?: boolean; } @@ -41,7 +41,7 @@ function getExchangeRateToBase(currency: string) { const stmt = db.prepare( `SELECT rate FROM finance_exchange_rates WHERE from_currency = ? AND to_currency = ? ORDER BY date DESC LIMIT 1`, ); - const row = stmt.get(currency, BASE_CURRENCY) as { rate: number } | undefined; + const row = stmt.get(currency, BASE_CURRENCY) as undefined | { rate: number }; return row?.rate ?? 1; } @@ -49,7 +49,7 @@ function mapTransaction(row: TransactionRow) { return { id: row.id, userId: 1, - type: row.type as 'income' | 'expense' | 'transfer', + type: row.type as 'expense' | 'income' | 'transfer', amount: row.amount, currency: row.currency, exchangeRateToBase: row.exchange_rate_to_base, @@ -66,7 +66,9 @@ function mapTransaction(row: TransactionRow) { }; } -export function fetchTransactions(options: { type?: string; includeDeleted?: boolean } = {}) { +export function fetchTransactions( + options: { includeDeleted?: boolean; type?: string } = {}, +) { const clauses: string[] = []; const params: Record = {}; @@ -78,7 +80,7 @@ export function fetchTransactions(options: { type?: string; includeDeleted?: boo params.type = options.type; } - const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : ''; + const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : ''; const stmt = db.prepare( `SELECT id, type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, is_deleted, deleted_at FROM finance_transactions ${where} ORDER BY transaction_date DESC, id DESC`, @@ -98,7 +100,10 @@ export function getTransactionById(id: number) { export function createTransaction(payload: TransactionPayload) { const exchangeRate = getExchangeRateToBase(payload.currency); const amountInBase = +(payload.amount * exchangeRate).toFixed(2); - const createdAt = payload.createdAt && payload.createdAt.length ? payload.createdAt : new Date().toISOString(); + const createdAt = + payload.createdAt && payload.createdAt.length > 0 + ? payload.createdAt + : new Date().toISOString(); const stmt = db.prepare( `INSERT INTO finance_transactions (type, amount, currency, exchange_rate_to_base, amount_in_base, category_id, account_id, transaction_date, description, project, memo, created_at, is_deleted) VALUES (@type, @amount, @currency, @exchangeRateToBase, @amountInBase, @categoryId, @accountId, @transactionDate, @description, @project, @memo, @createdAt, 0)`, @@ -171,29 +176,35 @@ export function updateTransaction(id: number, payload: TransactionPayload) { } export function softDeleteTransaction(id: number) { - const stmt = db.prepare(`UPDATE finance_transactions SET is_deleted = 1, deleted_at = @deletedAt WHERE id = @id`); + const stmt = db.prepare( + `UPDATE finance_transactions SET is_deleted = 1, deleted_at = @deletedAt WHERE id = @id`, + ); stmt.run({ id, deletedAt: new Date().toISOString() }); return getTransactionById(id); } export function restoreTransaction(id: number) { - const stmt = db.prepare(`UPDATE finance_transactions SET is_deleted = 0, deleted_at = NULL WHERE id = @id`); + const stmt = db.prepare( + `UPDATE finance_transactions SET is_deleted = 0, deleted_at = NULL WHERE id = @id`, + ); stmt.run({ id }); return getTransactionById(id); } -export function replaceAllTransactions(rows: Array<{ - type: string; - amount: number; - currency: string; - categoryId: number | null; - accountId: number | null; - transactionDate: string; - description: string; - project?: string | null; - memo?: string | null; - createdAt?: string; -}>) { +export function replaceAllTransactions( + rows: Array<{ + accountId: null | number; + amount: number; + categoryId: null | number; + createdAt?: string; + currency: string; + description: string; + memo?: null | string; + project?: null | string; + transactionDate: string; + type: string; + }>, +) { db.prepare('DELETE FROM finance_transactions').run(); const insert = db.prepare( @@ -206,7 +217,7 @@ export function replaceAllTransactions(rows: Array<{ const insertMany = db.transaction((items: Array) => { for (const item of items) { - const row = getRate.get(item.currency) as { rate: number } | undefined; + const row = getRate.get(item.currency) as undefined | { rate: number }; const rate = row?.rate ?? 1; const amountInBase = +(item.amount * rate).toFixed(2); insert.run({ @@ -215,7 +226,9 @@ export function replaceAllTransactions(rows: Array<{ amountInBase, project: item.project ?? null, memo: item.memo ?? null, - createdAt: item.createdAt ?? new Date(`${item.transactionDate}T00:00:00Z`).toISOString(), + createdAt: + item.createdAt ?? + new Date(`${item.transactionDate}T00:00:00Z`).toISOString(), }); } }); @@ -228,9 +241,9 @@ interface CategoryRow { id: number; name: string; type: string; - icon: string | null; - color: string | null; - user_id: number | null; + icon: null | string; + color: null | string; + user_id: null | number; is_active: number; } @@ -239,7 +252,7 @@ function mapCategory(row: CategoryRow) { id: row.id, userId: row.user_id ?? null, name: row.name, - type: row.type as 'income' | 'expense', + type: row.type as 'expense' | 'income', icon: row.icon ?? '📝', color: row.color ?? '#dfe4ea', sortOrder: row.id, @@ -248,8 +261,10 @@ function mapCategory(row: CategoryRow) { }; } -export function fetchCategories(options: { type?: 'income' | 'expense' } = {}) { - const where = options.type ? `WHERE type = @type AND is_active = 1` : 'WHERE is_active = 1'; +export function fetchCategories(options: { type?: 'expense' | 'income' } = {}) { + const where = options.type + ? `WHERE type = @type AND is_active = 1` + : 'WHERE is_active = 1'; const params = options.type ? { type: options.type } : {}; const stmt = db.prepare( diff --git a/apps/backend/utils/sqlite.ts b/apps/backend/utils/sqlite.ts index b9cc9236..39191230 100644 --- a/apps/backend/utils/sqlite.ts +++ b/apps/backend/utils/sqlite.ts @@ -1,5 +1,6 @@ -import Database from 'better-sqlite3'; import { mkdirSync } from 'node:fs'; + +import Database from 'better-sqlite3'; import { dirname, join } from 'pathe'; const dbFile = join(process.cwd(), 'storage', 'finance.db'); diff --git a/apps/web-antd/index.html b/apps/web-antd/index.html index 9b94619f..28127eda 100644 --- a/apps/web-antd/index.html +++ b/apps/web-antd/index.html @@ -32,7 +32,7 @@