feat(db): 新增 exchange_rates 汇率表,支持联合主键与基础交叉汇率数据

This commit is contained in:
kennethcheng 2026-04-27 23:36:14 +08:00
parent 8f17573fa4
commit 84b8dc3226
4 changed files with 120 additions and 1 deletions

16
Memory.md Normal file
View File

@ -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的交叉汇率架构。

24
scripts/seed-exchange.ts Normal file
View File

@ -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);

68
src/actions/exchange.ts Normal file
View File

@ -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();
}

View File

@ -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", { export const users = pgTable("users", {
id: uuid("id").primaryKey().$defaultFn(() => crypto.randomUUID()), id: uuid("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
@ -51,3 +51,14 @@ export const transactions = pgTable("transactions", {
createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }) createdAt: timestamp("created_at", { withTimezone: true, mode: "date" })
.defaultNow(), .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),
]);