From 7cd084d4b36e9621c88ede720707f8772aefd70e Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Tue, 28 Apr 2026 12:20:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=20=E5=8D=87=E7=BA=A7=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E8=A1=A8=E6=96=B0=E5=A2=9E=20name=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=20CRUD=20=E6=A0=B8=E5=BF=83=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Memory.md | 3 +- src/actions/asset.ts | 50 +++++++++++++++++++++++++ src/actions/transaction.ts | 77 +++++++++++++++++++++++++++++++++++++- src/db/schema.ts | 1 + 4 files changed, 128 insertions(+), 3 deletions(-) diff --git a/Memory.md b/Memory.md index fd82880..74d81a3 100644 --- a/Memory.md +++ b/Memory.md @@ -13,4 +13,5 @@ - 完成 UI 层的高精度数据格式化,实现不同资产类型的动态展示精度。 - 完成持仓聚合计算逻辑,并构建了 Dashboard 首页持仓卡片矩阵。 - 引入 recharts 图表引擎,完成 Dashboard 资产分布环形图的构建。 -- 汇率表已建立,支持跨币种(如 BTC->USD)的交叉汇率架构。 \ No newline at end of file +- 汇率表已建立,支持跨币种(如 BTC->USD)的交叉汇率架构。 +- 资产表新增 name 字段,并补全了资产与流水的增删改查 Actions(updateAsset、deleteAsset、updateTransaction、deleteTransaction),createTransaction 支持根据 exchange 自动判定 txCurrency。 \ No newline at end of file diff --git a/src/actions/asset.ts b/src/actions/asset.ts index 3ecb030..0aa6738 100644 --- a/src/actions/asset.ts +++ b/src/actions/asset.ts @@ -39,6 +39,56 @@ export async function getAssets() { return db.select().from(assets); } +const updateAssetSchema = z.object({ + id: z.string().uuid(), + symbol: z.string().min(1, 'Symbol is required').optional(), + exchange: z.string().optional(), + type: z.enum(['STOCK', 'CRYPTO', 'CASH']).optional(), + baseCurrency: z.string().min(2).max(10).optional(), +}); + +export async function updateAsset(params: z.infer) { + const validation = updateAssetSchema.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(assets).set(filteredUpdates).where(eq(assets.id, id)); + revalidatePath('/dashboard'); + return { success: true }; + } catch (error: unknown) { + if ( + error && + typeof error === 'object' && + 'code' in error && + (error as { code: string }).code === '23505' + ) { + return { success: false, error: 'Asset with this symbol already exists' }; + } + throw error; + } +} + +export async function deleteAsset(id: string) { + try { + await db.delete(assets).where(eq(assets.id, id)); + revalidatePath('/dashboard'); + return { success: true }; + } catch (error: unknown) { + throw error; + } +} + const updatePriceSchema = z.object({ assetId: z.string().min(1, 'Asset ID is required'), newPrice: z.string().min(1, 'Price is required'), diff --git a/src/actions/transaction.ts b/src/actions/transaction.ts index 6d49a44..2cf8750 100644 --- a/src/actions/transaction.ts +++ b/src/actions/transaction.ts @@ -1,7 +1,7 @@ 'use server'; import { db } from '@/db'; -import { transactions, transactionTypeEnum, exchangeRates } from '@/db/schema'; +import { transactions, transactionTypeEnum, exchangeRates, assets as assetsTable } from '@/db/schema'; import { z } from 'zod'; import { eq, desc, and } from 'drizzle-orm'; @@ -16,6 +16,17 @@ const createTransactionSchema = z.object({ 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) { @@ -23,7 +34,18 @@ export async function createTransaction(params: z.infer 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 }) @@ -52,6 +74,57 @@ export async function createTransaction(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 diff --git a/src/db/schema.ts b/src/db/schema.ts index 2481fe9..6214fe8 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -18,6 +18,7 @@ export const assetTypeEnum = pgEnum("asset_type_enum", [ export const assets = pgTable("assets", { id: uuid("id").primaryKey().$defaultFn(() => crypto.randomUUID()), symbol: varchar("symbol", { length: 20 }).notNull().unique(), + name: varchar("name", { length: 100 }), type: assetTypeEnum("type").notNull(), exchange: varchar("exchange", { length: 10 }).default('US'), baseCurrency: varchar("base_currency", { length: 10 }).notNull(),