feat(db): 新增 exchange_rates 汇率表,支持联合主键与基础交叉汇率数据
This commit is contained in:
parent
8f17573fa4
commit
84b8dc3226
16
Memory.md
Normal file
16
Memory.md
Normal 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
24
scripts/seed-exchange.ts
Normal 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
68
src/actions/exchange.ts
Normal 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();
|
||||||
|
}
|
||||||
@ -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),
|
||||||
|
]);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user