From 84b8dc32264338771924af1d135569ee611a956d Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Mon, 27 Apr 2026 23:36:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=20=E6=96=B0=E5=A2=9E=20exchange=5Frat?= =?UTF-8?q?es=20=E6=B1=87=E7=8E=87=E8=A1=A8=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=81=94=E5=90=88=E4=B8=BB=E9=94=AE=E4=B8=8E=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E4=BA=A4=E5=8F=89=E6=B1=87=E7=8E=87=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Memory.md | 16 ++++++++++ scripts/seed-exchange.ts | 24 ++++++++++++++ src/actions/exchange.ts | 68 ++++++++++++++++++++++++++++++++++++++++ src/db/schema.ts | 13 +++++++- 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 Memory.md create mode 100644 scripts/seed-exchange.ts create mode 100644 src/actions/exchange.ts diff --git a/Memory.md b/Memory.md new file mode 100644 index 0000000..fd82880 --- /dev/null +++ b/Memory.md @@ -0,0 +1,16 @@ +完成根目录的 Next.js 初始化、基础依赖安装与环境变量配置。 + +- 完成基于单例模式的数据库连接配置,并设定 Drizzle 迁移工具。 +- 修复网络连接,成功将 users 表推送至数据库。 +- 成功定義資產枚舉與 assets 表,支持跨資產標識。 +- 完成核心 transactions 表的建立,並嚴格運用了 numeric(36,18) 的高精度配置。 +- 完成高精度交易流水 (transactions) 的 Server Actions 开发,成功实现了字符串级别的高精度防腐层拦截。 +- 完成 shadcn/ui 初始化,集成 next-themes,并拉取核心组件库。 +- 完成 /dashboard 基础布局架构,接管根路由。 +- 引入 Zod 和 Big.js,完成资产表 (assets) 的 Server Actions 读写接口开发。 +- 完成 /dashboard/assets 页面,成功打通前后端资产录入数据流转。 +- 修复 /dashboard/transactions 404,完成高精度流水录入与展示功能。 +- 完成 UI 层的高精度数据格式化,实现不同资产类型的动态展示精度。 +- 完成持仓聚合计算逻辑,并构建了 Dashboard 首页持仓卡片矩阵。 +- 引入 recharts 图表引擎,完成 Dashboard 资产分布环形图的构建。 +- 汇率表已建立,支持跨币种(如 BTC->USD)的交叉汇率架构。 \ No newline at end of file diff --git a/scripts/seed-exchange.ts b/scripts/seed-exchange.ts new file mode 100644 index 0000000..61a11be --- /dev/null +++ b/scripts/seed-exchange.ts @@ -0,0 +1,24 @@ +import { db } from '@/db'; +import { exchangeRates } from '@/db/schema'; + +const seeds = [ + { fromCurrency: 'USD', toCurrency: 'CNY', rate: '7.23' }, + { fromCurrency: 'HKD', toCurrency: 'CNY', rate: '0.92' }, + { fromCurrency: 'BTC', toCurrency: 'USD', rate: '65000' }, +]; + +async function seed() { + for (const s of seeds) { + await db + .insert(exchangeRates) + .values(s) + .onConflictDoUpdate({ + target: [exchangeRates.fromCurrency, exchangeRates.toCurrency], + set: { rate: s.rate, updatedAt: new Date() }, + }); + console.log(`Seeded: ${s.fromCurrency} -> ${s.toCurrency} = ${s.rate}`); + } + console.log('Exchange rate seed complete.'); +} + +seed().catch(console.error); diff --git a/src/actions/exchange.ts b/src/actions/exchange.ts new file mode 100644 index 0000000..b4060d9 --- /dev/null +++ b/src/actions/exchange.ts @@ -0,0 +1,68 @@ +'use server'; + +import { db } from '@/db'; +import { exchangeRates } from '@/db/schema'; +import { eq } from 'drizzle-orm'; +import { z } from 'zod'; + +const updateExchangeRateSchema = z.object({ + from: z.string().min(1).max(10), + to: z.string().min(1).max(10), + rate: z.string().min(1), +}); + +export async function updateExchangeRate( + from: string, + to: string, + rate: string, +) { + const validation = updateExchangeRateSchema.safeParse({ from, to, rate }); + if (!validation.success) { + return { success: false, error: 'Invalid input' }; + } + + try { + await db + .insert(exchangeRates) + .values({ + fromCurrency: validation.data.from.toUpperCase(), + toCurrency: validation.data.to.toUpperCase(), + rate: validation.data.rate, + }) + .onConflictDoUpdate({ + target: [exchangeRates.fromCurrency, exchangeRates.toCurrency], + set: { + rate: validation.data.rate, + updatedAt: new Date(), + }, + }); + + return { success: true }; + } catch (error: unknown) { + if ( + error && + typeof error === 'object' && + 'code' in error && + (error as { code: string }).code === '23505' + ) { + return { success: false, error: 'Failed to update exchange rate' }; + } + throw error; + } +} + +export async function getExchangeRate(from: string, to: string) { + const result = await db + .select() + .from(exchangeRates) + .where( + eq(exchangeRates.fromCurrency, from.toUpperCase()), + ) + .execute(); + + return result[0] || null; +} + +export async function getAllExchangeRates() { + return db.select().from(exchangeRates).execute(); +} diff --git a/src/db/schema.ts b/src/db/schema.ts index 3fec67e..a7e4245 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,4 +1,4 @@ -import { pgTable, uuid, varchar, timestamp, pgEnum, numeric } from "drizzle-orm/pg-core"; +import { pgTable, uuid, varchar, timestamp, pgEnum, numeric, uniqueIndex } from "drizzle-orm/pg-core"; export const users = pgTable("users", { id: uuid("id").primaryKey().$defaultFn(() => crypto.randomUUID()), @@ -51,3 +51,14 @@ export const transactions = pgTable("transactions", { createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }) .defaultNow(), }); + +export const exchangeRates = pgTable("exchange_rates", { + id: uuid("id").primaryKey().$defaultFn(() => crypto.randomUUID()), + fromCurrency: varchar("from_currency", { length: 10 }).notNull(), + toCurrency: varchar("to_currency", { length: 10 }).notNull(), + rate: numeric("rate", { precision: 20, scale: 8 }).notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }) + .defaultNow(), +}, (table) => [ + uniqueIndex("currency_pair_idx").on(table.fromCurrency, table.toCurrency), +]);