feat(db): 升级资产表新增 name,并补全 CRUD 核心逻辑
This commit is contained in:
parent
085659dfef
commit
7cd084d4b3
@ -14,3 +14,4 @@
|
||||
- 完成持仓聚合计算逻辑,并构建了 Dashboard 首页持仓卡片矩阵。
|
||||
- 引入 recharts 图表引擎,完成 Dashboard 资产分布环形图的构建。
|
||||
- 汇率表已建立,支持跨币种(如 BTC->USD)的交叉汇率架构。
|
||||
- 资产表新增 name 字段,并补全了资产与流水的增删改查 Actions(updateAsset、deleteAsset、updateTransaction、deleteTransaction),createTransaction 支持根据 exchange 自动判定 txCurrency。
|
||||
@ -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<typeof updateAssetSchema>) {
|
||||
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'),
|
||||
|
||||
@ -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<string, string> = {
|
||||
'US': 'USD',
|
||||
'HKEX': 'HKD',
|
||||
'SSE': 'CNY',
|
||||
'SZSE': 'CNY',
|
||||
};
|
||||
|
||||
function getCurrencyFromExchange(exchange: string): string {
|
||||
return exchangeToCurrencyMap[exchange] || 'USD';
|
||||
}
|
||||
|
||||
export async function createTransaction(params: z.infer<typeof createTransactionSchema>) {
|
||||
const validation = createTransactionSchema.safeParse(params);
|
||||
if (!validation.success) {
|
||||
@ -23,7 +34,18 @@ export async function createTransaction(params: z.infer<typeof createTransaction
|
||||
}
|
||||
|
||||
try {
|
||||
const data = { ...validation.data };
|
||||
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 })
|
||||
@ -52,6 +74,57 @@ export async function createTransaction(params: z.infer<typeof createTransaction
|
||||
}
|
||||
}
|
||||
|
||||
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<typeof updateTransactionSchema>) {
|
||||
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
|
||||
|
||||
@ -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(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user