feat: migrate backend storage to postgres
This commit is contained in:
@@ -6,7 +6,7 @@ export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
const currency = query.currency as string | undefined;
|
||||
|
||||
let accounts = listAccounts();
|
||||
let accounts = await listAccounts();
|
||||
|
||||
if (currency) {
|
||||
accounts = accounts.filter((account) => account.currency === currency);
|
||||
|
||||
@@ -6,7 +6,7 @@ export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
const type = query.type as 'expense' | 'income' | undefined;
|
||||
|
||||
const categories = fetchCategories({ type });
|
||||
const categories = await fetchCategories({ type });
|
||||
|
||||
return useResponseSuccess(categories);
|
||||
});
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import type { TransactionStatus } from '~/utils/finance-repository';
|
||||
|
||||
import { readBody } from 'h3';
|
||||
import {
|
||||
createTransaction,
|
||||
type TransactionStatus,
|
||||
} from '~/utils/finance-repository';
|
||||
import { createTransaction } from '~/utils/finance-repository';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
import { notifyTransactionWebhook } from '~/utils/telegram-webhook';
|
||||
|
||||
const DEFAULT_CURRENCY = 'CNY';
|
||||
const DEFAULT_STATUS: TransactionStatus = 'pending';
|
||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
||||
const ALLOWED_STATUSES = new Set<TransactionStatus>([
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected',
|
||||
'paid',
|
||||
];
|
||||
]);
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
@@ -33,11 +32,11 @@ export default defineEventHandler(async (event) => {
|
||||
const status =
|
||||
(body.status as TransactionStatus | undefined) ?? DEFAULT_STATUS;
|
||||
|
||||
if (!ALLOWED_STATUSES.includes(status)) {
|
||||
if (!ALLOWED_STATUSES.has(status)) {
|
||||
return useResponseError('状态值不合法', -1);
|
||||
}
|
||||
|
||||
const reimbursement = createTransaction({
|
||||
const reimbursement = await createTransaction({
|
||||
type,
|
||||
amount,
|
||||
currency: body.currency ?? DEFAULT_CURRENCY,
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import type { TransactionStatus } from '~/utils/finance-repository';
|
||||
|
||||
import { getRouterParam, readBody } from 'h3';
|
||||
import {
|
||||
restoreTransaction,
|
||||
updateTransaction,
|
||||
type TransactionStatus,
|
||||
} from '~/utils/finance-repository';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
||||
const ALLOWED_STATUSES = new Set<TransactionStatus>([
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected',
|
||||
'paid',
|
||||
];
|
||||
]);
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = Number(getRouterParam(event, 'id'));
|
||||
@@ -23,7 +24,7 @@ export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
if (body?.isDeleted === false) {
|
||||
const restored = restoreTransaction(id);
|
||||
const restored = await restoreTransaction(id);
|
||||
if (!restored) {
|
||||
return useResponseError('报销单不存在', -1);
|
||||
}
|
||||
@@ -52,7 +53,7 @@ export default defineEventHandler(async (event) => {
|
||||
if (body?.isDeleted !== undefined) payload.isDeleted = body.isDeleted;
|
||||
if (body?.status !== undefined) {
|
||||
const status = body.status as TransactionStatus;
|
||||
if (!ALLOWED_STATUSES.includes(status)) {
|
||||
if (!ALLOWED_STATUSES.has(status)) {
|
||||
return useResponseError('状态值不合法', -1);
|
||||
}
|
||||
payload.status = status;
|
||||
@@ -76,7 +77,7 @@ export default defineEventHandler(async (event) => {
|
||||
payload.approvedAt = body.approvedAt ?? null;
|
||||
}
|
||||
|
||||
const updated = updateTransaction(id, payload);
|
||||
const updated = await updateTransaction(id, payload);
|
||||
if (!updated) {
|
||||
return useResponseError('报销单不存在', -1);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => {
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0) as TransactionStatus[])
|
||||
: (['approved', 'paid'] satisfies TransactionStatus[]);
|
||||
const transactions = fetchTransactions({
|
||||
const transactions = await fetchTransactions({
|
||||
type,
|
||||
includeDeleted,
|
||||
statuses,
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import type { TransactionStatus } from '~/utils/finance-repository';
|
||||
|
||||
import { readBody } from 'h3';
|
||||
import {
|
||||
createTransaction,
|
||||
type TransactionStatus,
|
||||
getAccountById,
|
||||
getCategoryById,
|
||||
} from '~/utils/finance-repository';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
import { notifyTransactionWebhook } from '~/utils/telegram-webhook';
|
||||
import { notifyTransaction } from '~/utils/telegram-bot';
|
||||
import db from '~/utils/sqlite';
|
||||
import { notifyTransactionWebhook } from '~/utils/telegram-webhook';
|
||||
|
||||
const DEFAULT_CURRENCY = 'CNY';
|
||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
||||
const ALLOWED_STATUSES = new Set<TransactionStatus>([
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected',
|
||||
'paid',
|
||||
];
|
||||
]);
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
@@ -29,13 +31,12 @@ export default defineEventHandler(async (event) => {
|
||||
return useResponseError('金额格式不正确', -1);
|
||||
}
|
||||
|
||||
const status =
|
||||
(body.status as TransactionStatus | undefined) ?? 'approved';
|
||||
if (!ALLOWED_STATUSES.includes(status)) {
|
||||
const status = (body.status as TransactionStatus | undefined) ?? 'approved';
|
||||
if (!ALLOWED_STATUSES.has(status)) {
|
||||
return useResponseError('状态值不合法', -1);
|
||||
}
|
||||
|
||||
const transaction = createTransaction({
|
||||
const transaction = await createTransaction({
|
||||
type: body.type,
|
||||
amount,
|
||||
currency: body.currency ?? DEFAULT_CURRENCY,
|
||||
@@ -61,23 +62,12 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
// 发送Telegram通知(新功能)
|
||||
try {
|
||||
// 获取分类和账户名称
|
||||
let categoryName: string | undefined;
|
||||
let accountName: string | undefined;
|
||||
|
||||
if (transaction.categoryId) {
|
||||
const category = db
|
||||
.prepare<{ name: string }>('SELECT name FROM finance_categories WHERE id = ?')
|
||||
.get(transaction.categoryId);
|
||||
categoryName = category?.name;
|
||||
}
|
||||
|
||||
if (transaction.accountId) {
|
||||
const account = db
|
||||
.prepare<{ name: string }>('SELECT name FROM finance_accounts WHERE id = ?')
|
||||
.get(transaction.accountId);
|
||||
accountName = account?.name;
|
||||
}
|
||||
const category = transaction.categoryId
|
||||
? await getCategoryById(transaction.categoryId)
|
||||
: null;
|
||||
const account = transaction.accountId
|
||||
? await getAccountById(transaction.accountId)
|
||||
: null;
|
||||
|
||||
await notifyTransaction(
|
||||
{
|
||||
@@ -85,8 +75,8 @@ export default defineEventHandler(async (event) => {
|
||||
type: transaction.type,
|
||||
amount: transaction.amount,
|
||||
currency: transaction.currency,
|
||||
categoryName,
|
||||
accountName,
|
||||
categoryName: category?.name,
|
||||
accountName: account?.name,
|
||||
transactionDate: transaction.transactionDate,
|
||||
description: transaction.description || undefined,
|
||||
status: transaction.status,
|
||||
|
||||
@@ -9,7 +9,7 @@ export default defineEventHandler(async (event) => {
|
||||
return useResponseError('参数错误', -1);
|
||||
}
|
||||
|
||||
const updated = softDeleteTransaction(id);
|
||||
const updated = await softDeleteTransaction(id);
|
||||
if (!updated) {
|
||||
return useResponseError('交易不存在', -1);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import type { TransactionStatus } from '~/utils/finance-repository';
|
||||
|
||||
import { getRouterParam, readBody } from 'h3';
|
||||
import {
|
||||
restoreTransaction,
|
||||
updateTransaction,
|
||||
type TransactionStatus,
|
||||
} from '~/utils/finance-repository';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
||||
const ALLOWED_STATUSES = new Set<TransactionStatus>([
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected',
|
||||
'paid',
|
||||
];
|
||||
]);
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = Number(getRouterParam(event, 'id'));
|
||||
@@ -23,7 +24,7 @@ export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
if (body?.isDeleted === false) {
|
||||
const restored = restoreTransaction(id);
|
||||
const restored = await restoreTransaction(id);
|
||||
if (!restored) {
|
||||
return useResponseError('交易不存在', -1);
|
||||
}
|
||||
@@ -52,7 +53,7 @@ export default defineEventHandler(async (event) => {
|
||||
if (body?.isDeleted !== undefined) payload.isDeleted = body.isDeleted;
|
||||
if (body?.status !== undefined) {
|
||||
const status = body.status as TransactionStatus;
|
||||
if (!ALLOWED_STATUSES.includes(status)) {
|
||||
if (!ALLOWED_STATUSES.has(status)) {
|
||||
return useResponseError('状态值不合法', -1);
|
||||
}
|
||||
payload.status = status;
|
||||
@@ -76,7 +77,7 @@ export default defineEventHandler(async (event) => {
|
||||
payload.approvedAt = body.approvedAt ?? null;
|
||||
}
|
||||
|
||||
const updated = updateTransaction(id, payload);
|
||||
const updated = await updateTransaction(id, payload);
|
||||
if (!updated) {
|
||||
return useResponseError('交易不存在', -1);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import db from '~/utils/sqlite';
|
||||
import { query } from '~/utils/db';
|
||||
import { useResponseSuccess } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(() => {
|
||||
const configs = db
|
||||
.prepare<{ id: number; name: string; bot_token: string; chat_id: string; notification_types: string; is_enabled: number; created_at: string; updated_at: string }>(
|
||||
`
|
||||
SELECT id, name, bot_token, chat_id, notification_types, is_enabled, created_at, updated_at
|
||||
FROM telegram_notification_configs
|
||||
ORDER BY created_at DESC
|
||||
`,
|
||||
)
|
||||
.all();
|
||||
export default defineEventHandler(async () => {
|
||||
const { rows } = await query<{
|
||||
id: number;
|
||||
name: string;
|
||||
bot_token: string;
|
||||
chat_id: string;
|
||||
notification_types: string;
|
||||
is_enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}>(
|
||||
`SELECT id, name, bot_token, chat_id, notification_types, is_enabled, created_at, updated_at
|
||||
FROM telegram_notification_configs
|
||||
ORDER BY created_at DESC`,
|
||||
);
|
||||
|
||||
const result = configs.map((row) => ({
|
||||
const result = rows.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
botToken: row.bot_token,
|
||||
chatId: row.chat_id,
|
||||
notificationTypes: JSON.parse(row.notification_types) as string[],
|
||||
isEnabled: row.is_enabled === 1,
|
||||
isEnabled: row.is_enabled,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readBody } from 'h3';
|
||||
import db from '~/utils/sqlite';
|
||||
import { query } from '~/utils/db';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
import { testTelegramConfig } from '~/utils/telegram-bot';
|
||||
|
||||
@@ -25,31 +25,48 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const result = db
|
||||
.prepare<unknown, [string, string, string, string, number, string, string]>(
|
||||
`
|
||||
INSERT INTO telegram_notification_configs (name, bot_token, chat_id, notification_types, is_enabled, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
)
|
||||
.run(
|
||||
const { rows } = await query<{
|
||||
id: number;
|
||||
name: string;
|
||||
bot_token: string;
|
||||
chat_id: string;
|
||||
notification_types: string;
|
||||
is_enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}>(
|
||||
`INSERT INTO telegram_notification_configs (
|
||||
name,
|
||||
bot_token,
|
||||
chat_id,
|
||||
notification_types,
|
||||
is_enabled,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, name, bot_token, chat_id, notification_types, is_enabled, created_at, updated_at`,
|
||||
[
|
||||
body.name,
|
||||
body.botToken,
|
||||
body.chatId,
|
||||
JSON.stringify(notificationTypes),
|
||||
body.isEnabled !== false ? 1 : 0,
|
||||
body.isEnabled !== false,
|
||||
now,
|
||||
now,
|
||||
);
|
||||
],
|
||||
);
|
||||
|
||||
const row = rows[0];
|
||||
|
||||
return useResponseSuccess({
|
||||
id: result.lastInsertRowid,
|
||||
name: body.name,
|
||||
botToken: body.botToken,
|
||||
chatId: body.chatId,
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
botToken: row.bot_token,
|
||||
chatId: row.chat_id,
|
||||
notificationTypes,
|
||||
isEnabled: body.isEnabled !== false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
isEnabled: row.is_enabled,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import db from '~/utils/sqlite';
|
||||
import { query } from '~/utils/db';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const id = event.context.params?.id;
|
||||
if (!id) {
|
||||
export default defineEventHandler(async (event) => {
|
||||
const idParam = event.context.params?.id;
|
||||
const id = Number(idParam);
|
||||
if (!idParam || Number.isNaN(id)) {
|
||||
return useResponseError('缺少ID参数', -1);
|
||||
}
|
||||
|
||||
const result = db
|
||||
.prepare('DELETE FROM telegram_notification_configs WHERE id = ?')
|
||||
.run(id);
|
||||
const result = await query(
|
||||
'DELETE FROM telegram_notification_configs WHERE id = $1',
|
||||
[id],
|
||||
);
|
||||
|
||||
if (result.changes === 0) {
|
||||
if (result.rowCount === 0) {
|
||||
return useResponseError('配置不存在或删除失败', -1);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
import { readBody } from 'h3';
|
||||
import db from '~/utils/sqlite';
|
||||
import { query } from '~/utils/db';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
import { testTelegramConfig } from '~/utils/telegram-bot';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = event.context.params?.id;
|
||||
if (!id) {
|
||||
const idParam = event.context.params?.id;
|
||||
const id = Number(idParam);
|
||||
if (!idParam || Number.isNaN(id)) {
|
||||
return useResponseError('缺少ID参数', -1);
|
||||
}
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
// 如果更新了botToken或chatId,需要测试配置
|
||||
if (body.botToken || body.chatId) {
|
||||
const existing = db
|
||||
.prepare<{ bot_token: string; chat_id: string }>('SELECT bot_token, chat_id FROM telegram_notification_configs WHERE id = ?')
|
||||
.get(id);
|
||||
if (body.botToken !== undefined || body.chatId !== undefined) {
|
||||
const { rows } = await query<{
|
||||
bot_token: string;
|
||||
chat_id: string;
|
||||
}>(
|
||||
'SELECT bot_token, chat_id FROM telegram_notification_configs WHERE id = $1',
|
||||
[id],
|
||||
);
|
||||
const existing = rows[0];
|
||||
|
||||
if (!existing) {
|
||||
return useResponseError('配置不存在', -1);
|
||||
}
|
||||
|
||||
const tokenToTest = body.botToken || existing.bot_token;
|
||||
const chatIdToTest = body.chatId || existing.chat_id;
|
||||
const tokenToTest = body.botToken ?? existing.bot_token;
|
||||
const chatIdToTest = body.chatId ?? existing.chat_id;
|
||||
|
||||
const testResult = await testTelegramConfig(tokenToTest, chatIdToTest);
|
||||
if (!testResult.success) {
|
||||
@@ -34,51 +40,65 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
|
||||
const updates: string[] = [];
|
||||
const values: (string | number)[] = [];
|
||||
const values: any[] = [];
|
||||
|
||||
if (body.name !== undefined) {
|
||||
updates.push('name = ?');
|
||||
values.push(body.name);
|
||||
updates.push(`name = $${values.length}`);
|
||||
}
|
||||
|
||||
if (body.botToken !== undefined) {
|
||||
updates.push('bot_token = ?');
|
||||
values.push(body.botToken);
|
||||
updates.push(`bot_token = $${values.length}`);
|
||||
}
|
||||
|
||||
if (body.chatId !== undefined) {
|
||||
updates.push('chat_id = ?');
|
||||
values.push(body.chatId);
|
||||
updates.push(`chat_id = $${values.length}`);
|
||||
}
|
||||
|
||||
if (body.notificationTypes !== undefined) {
|
||||
updates.push('notification_types = ?');
|
||||
values.push(JSON.stringify(body.notificationTypes));
|
||||
updates.push(`notification_types = $${values.length}`);
|
||||
}
|
||||
|
||||
if (body.isEnabled !== undefined) {
|
||||
updates.push('is_enabled = ?');
|
||||
values.push(body.isEnabled ? 1 : 0);
|
||||
values.push(body.isEnabled !== false);
|
||||
updates.push(`is_enabled = $${values.length}`);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return useResponseError('没有可更新的字段', -1);
|
||||
}
|
||||
|
||||
updates.push('updated_at = ?');
|
||||
values.push(new Date().toISOString());
|
||||
updates.push(`updated_at = $${values.length}`);
|
||||
values.push(id);
|
||||
const idPosition = values.length;
|
||||
|
||||
db.prepare(`UPDATE telegram_notification_configs SET ${updates.join(', ')} WHERE id = ?`).run(
|
||||
...values,
|
||||
const updateResult = await query(
|
||||
`UPDATE telegram_notification_configs
|
||||
SET ${updates.join(', ')}
|
||||
WHERE id = $${idPosition}`,
|
||||
values,
|
||||
);
|
||||
|
||||
const updated = db
|
||||
.prepare<{ id: number; name: string; bot_token: string; chat_id: string; notification_types: string; is_enabled: number; created_at: string; updated_at: string }>(
|
||||
'SELECT * FROM telegram_notification_configs WHERE id = ?',
|
||||
)
|
||||
.get(id);
|
||||
if (updateResult.rowCount === 0) {
|
||||
return useResponseError('配置不存在', -1);
|
||||
}
|
||||
|
||||
const { rows: updatedRows } = await query<{
|
||||
id: number;
|
||||
name: string;
|
||||
bot_token: string;
|
||||
chat_id: string;
|
||||
notification_types: string;
|
||||
is_enabled: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}>('SELECT * FROM telegram_notification_configs WHERE id = $1', [id]);
|
||||
|
||||
const updated = updatedRows[0];
|
||||
if (!updated) {
|
||||
return useResponseError('更新失败', -1);
|
||||
}
|
||||
@@ -89,7 +109,7 @@ export default defineEventHandler(async (event) => {
|
||||
botToken: updated.bot_token,
|
||||
chatId: updated.chat_id,
|
||||
notificationTypes: JSON.parse(updated.notification_types) as string[],
|
||||
isEnabled: updated.is_enabled === 1,
|
||||
isEnabled: updated.is_enabled,
|
||||
createdAt: updated.created_at,
|
||||
updatedAt: updated.updated_at,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user