'use server'; import { db } from '@/db'; import { transactions, transactionTypeEnum, exchangeRates, assets as assetsTable } from '@/db/schema'; import { z } from 'zod'; import { eq, desc, and } from 'drizzle-orm'; const createTransactionSchema = z.object({ assetId: z.string().uuid(), txType: z.enum(['BUY', 'SELL', 'DIVIDEND', 'AIRDROP', 'FEE']), quantity: z.string().regex(/^-?\d+(\.\d+)?$/, '数量必须是数字字符串'), price: z.string().regex(/^-?\d+(\.\d+)?$/, '价格必须是数字字符串'), fee: z.string().regex(/^-?\d+(\.\d+)?$/, '手续费必须是数字字符串').default('0'), txCurrency: z.string().min(1), exchangeRate: z.string().regex(/^-?\d+(\.\d+)?$/, '汇率必须是数字字符串').default('1'), executedAt: z.coerce.date(), }); const exchangeToCurrencyMap: Record = { 'US': 'USD', 'HKEX': 'HKD', 'SSE': 'CNY', 'SZSE': 'CNY', }; function getCurrencyFromExchange(exchange: string): string { return exchangeToCurrencyMap[exchange] || 'USD'; } export async function createTransaction(params: z.infer) { const validation = createTransactionSchema.safeParse(params); if (!validation.success) { return { success: false, error: validation.error.issues[0].message }; } try { const asset = await db .select({ exchange: assetsTable.exchange }) .from(assetsTable) .where(eq(assetsTable.id, validation.data.assetId)) .limit(1); let data = { ...validation.data }; if (asset.length > 0 && (!data.txCurrency || data.txCurrency.length === 0)) { data.txCurrency = getCurrencyFromExchange(asset[0].exchange); } if (data.txCurrency !== 'CNY' && (!data.exchangeRate || data.exchangeRate === '1' || data.exchangeRate === '1.00000000')) { const [latestRate] = await db .select({ rate: exchangeRates.rate }) .from(exchangeRates) .where(and( eq(exchangeRates.fromCurrency, data.txCurrency), eq(exchangeRates.toCurrency, 'CNY') )) .limit(1); if (latestRate) { data.exchangeRate = latestRate.rate; } } const [transaction] = await db.insert(transactions).values(data).returning(); return { success: true, data: transaction }; } catch (error: unknown) { if ( error && typeof error === 'object' && 'code' in error && (error as { code: string }).code === '23503' ) { return { success: false, error: '资产不存在' }; } throw error; } } const updateTransactionSchema = z.object({ id: z.string().uuid(), txType: z.enum(['BUY', 'SELL', 'DIVIDEND', 'AIRDROP', 'FEE']).optional(), quantity: z.string().regex(/^-?\d+(\.\d+)?$/, '数量必须是数字字符串').optional(), price: z.string().regex(/^-?\d+(\.\d+)?$/, '价格必须是数字字符串').optional(), fee: z.string().regex(/^-?\d+(\.\d+)?$/, '手续费必须是数字字符串').optional(), txCurrency: z.string().min(1).optional(), exchangeRate: z.string().regex(/^-?\d+(\.\d+)?$/, '汇率必须是数字字符串').optional(), executedAt: z.coerce.date().optional(), }); export async function updateTransaction(params: z.infer) { const validation = updateTransactionSchema.safeParse(params); if (!validation.success) { return { success: false, error: validation.error.issues[0].message }; } const { id, ...updates } = validation.data; const filteredUpdates = Object.fromEntries( Object.entries(updates).filter(([, v]) => v !== undefined) ); if (Object.keys(filteredUpdates).length === 0) { return { success: false, error: 'No fields to update' }; } try { await db.update(transactions).set(filteredUpdates).where(eq(transactions.id, id)); return { success: true }; } catch (error: unknown) { if ( error && typeof error === 'object' && 'code' in error && (error as { code: string }).code === '23503' ) { return { success: false, error: '资产不存在' }; } throw error; } } export async function deleteTransaction(id: string) { try { await db.delete(transactions).where(eq(transactions.id, id)); return { success: true }; } catch (error: unknown) { throw error; } } export async function getTransactions(assetId?: string) { if (assetId) { return db .select() .from(transactions) .where(eq(transactions.assetId, assetId)) .orderBy(desc(transactions.executedAt)); } return db.select().from(transactions).orderBy(desc(transactions.executedAt)); }