218 lines
6.9 KiB
TypeScript
218 lines
6.9 KiB
TypeScript
"use server";
|
|
|
|
import { z } from "zod";
|
|
import { and, desc, eq, isNull } from "drizzle-orm";
|
|
import { revalidatePath } from "next/cache";
|
|
import { auth } from "@/lib/auth";
|
|
import { db } from "@/lib/db";
|
|
import { accounts, channels, type Account } from "@/lib/db/schema";
|
|
|
|
async function requireUser() {
|
|
const session = await auth();
|
|
if (!session?.user?.id) {
|
|
throw new Error("请先登录");
|
|
}
|
|
return session.user.id;
|
|
}
|
|
|
|
const accountSchema = z.object({
|
|
name: z.string().min(1, "请输入账户名称").max(100),
|
|
accountType: z.enum(["BANK", "E_WALLET", "CASH"] as const),
|
|
balanceType: z.enum(["ASSET", "LIABILITY", "EQUITY"] as const),
|
|
primaryCurrency: z.string().trim().max(10).optional().nullable(),
|
|
supportedCurrencies: z.array(z.string().trim()).optional().nullable(),
|
|
remark: z.string().max(500).optional().nullable(),
|
|
ext: z.string().optional().nullable(),
|
|
|
|
// 银行账户字段
|
|
issuerName: z.string().max(100).optional().nullable(),
|
|
accountNumber: z.string().max(100).optional().nullable(),
|
|
|
|
// 电子钱包字段
|
|
platform: z.string().max(64).optional().nullable(),
|
|
accountId: z.string().max(100).optional().nullable(),
|
|
|
|
// 现金账户字段
|
|
location: z.string().max(150).optional().nullable(),
|
|
});
|
|
|
|
export type AccountInput = z.infer<typeof accountSchema>;
|
|
|
|
export type AccountWithChannels = Account & {
|
|
channelCount: number;
|
|
};
|
|
|
|
export async function getAccountsAction(): Promise<{
|
|
success: boolean;
|
|
data?: AccountWithChannels[];
|
|
error?: string;
|
|
}> {
|
|
try {
|
|
const userId = await requireUser();
|
|
|
|
const userAccounts = await db
|
|
.select()
|
|
.from(accounts)
|
|
.where(and(eq(accounts.userId, userId), isNull(accounts.deletedAt)))
|
|
.orderBy(desc(accounts.createdAt));
|
|
|
|
const userChannels = await db
|
|
.select({ id: channels.id, refAccounts: channels.refAccounts })
|
|
.from(channels)
|
|
.where(and(eq(channels.userId, userId), isNull(channels.deletedAt)));
|
|
|
|
const result: AccountWithChannels[] = userAccounts.map((acc) => {
|
|
const count = userChannels.filter((ch) =>
|
|
Array.isArray(ch.refAccounts) && ch.refAccounts.includes(acc.id)
|
|
).length;
|
|
return {
|
|
...acc,
|
|
channelCount: count,
|
|
};
|
|
});
|
|
|
|
return { success: true, data: result };
|
|
} catch (err) {
|
|
console.error("getAccountsAction error:", err);
|
|
return { success: false, error: err instanceof Error ? err.message : "获取账户失败" };
|
|
}
|
|
}
|
|
|
|
export async function createAccountAction(
|
|
data: AccountInput
|
|
): Promise<{ success: boolean; data?: Account; error?: string }> {
|
|
try {
|
|
const userId = await requireUser();
|
|
const parsed = accountSchema.safeParse(data);
|
|
if (!parsed.success) {
|
|
return { success: false, error: parsed.error.issues[0]?.message || "参数错误" };
|
|
}
|
|
|
|
const val = parsed.data;
|
|
const [newAccount] = await db
|
|
.insert(accounts)
|
|
.values({
|
|
userId,
|
|
name: val.name.trim(),
|
|
accountType: val.accountType,
|
|
balanceType: val.balanceType,
|
|
primaryCurrency: val.primaryCurrency ? val.primaryCurrency.toUpperCase() : null,
|
|
supportedCurrencies: val.supportedCurrencies && val.supportedCurrencies.length > 0 ? val.supportedCurrencies : null,
|
|
remark: val.remark?.trim() || null,
|
|
ext: val.ext?.trim() || null,
|
|
issuerName: val.issuerName?.trim() || null,
|
|
accountNumber: val.accountNumber?.trim() || null,
|
|
platform: val.platform?.trim() || null,
|
|
accountId: val.accountId?.trim() || null,
|
|
location: val.location?.trim() || null,
|
|
isActive: true,
|
|
})
|
|
.returning();
|
|
|
|
revalidatePath("/accounts");
|
|
revalidatePath("/channels");
|
|
revalidatePath("/");
|
|
return { success: true, data: newAccount };
|
|
} catch (err) {
|
|
console.error("createAccountAction error:", err);
|
|
return { success: false, error: err instanceof Error ? err.message : "创建账户失败" };
|
|
}
|
|
}
|
|
|
|
export async function updateAccountAction(
|
|
id: string,
|
|
data: AccountInput
|
|
): Promise<{ success: boolean; data?: Account; error?: string }> {
|
|
try {
|
|
const userId = await requireUser();
|
|
const parsed = accountSchema.safeParse(data);
|
|
if (!parsed.success) {
|
|
return { success: false, error: parsed.error.issues[0]?.message || "参数错误" };
|
|
}
|
|
|
|
const val = parsed.data;
|
|
const [updated] = await db
|
|
.update(accounts)
|
|
.set({
|
|
name: val.name.trim(),
|
|
accountType: val.accountType,
|
|
balanceType: val.balanceType,
|
|
primaryCurrency: val.primaryCurrency ? val.primaryCurrency.toUpperCase() : null,
|
|
supportedCurrencies: val.supportedCurrencies && val.supportedCurrencies.length > 0 ? val.supportedCurrencies : null,
|
|
remark: val.remark?.trim() || null,
|
|
ext: val.ext?.trim() || null,
|
|
issuerName: val.issuerName?.trim() || null,
|
|
accountNumber: val.accountNumber?.trim() || null,
|
|
platform: val.platform?.trim() || null,
|
|
accountId: val.accountId?.trim() || null,
|
|
location: val.location?.trim() || null,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(and(eq(accounts.id, id), eq(accounts.userId, userId), isNull(accounts.deletedAt)))
|
|
.returning();
|
|
|
|
if (!updated) {
|
|
return { success: false, error: "未找到该账户或无权修改" };
|
|
}
|
|
|
|
revalidatePath("/accounts");
|
|
revalidatePath("/channels");
|
|
revalidatePath("/");
|
|
return { success: true, data: updated };
|
|
} catch (err) {
|
|
console.error("updateAccountAction error:", err);
|
|
return { success: false, error: err instanceof Error ? err.message : "更新账户失败" };
|
|
}
|
|
}
|
|
|
|
export async function toggleAccountActiveAction(
|
|
id: string,
|
|
isActive: boolean
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
const userId = await requireUser();
|
|
await db
|
|
.update(accounts)
|
|
.set({
|
|
isActive,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(and(eq(accounts.id, id), eq(accounts.userId, userId), isNull(accounts.deletedAt)));
|
|
|
|
revalidatePath("/accounts");
|
|
return { success: true };
|
|
} catch (err) {
|
|
console.error("toggleAccountActiveAction error:", err);
|
|
return { success: false, error: err instanceof Error ? err.message : "状态切换失败" };
|
|
}
|
|
}
|
|
|
|
export async function deleteAccountAction(
|
|
id: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
const userId = await requireUser();
|
|
// 软删除
|
|
const [deleted] = await db
|
|
.update(accounts)
|
|
.set({
|
|
deletedAt: new Date(),
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(and(eq(accounts.id, id), eq(accounts.userId, userId), isNull(accounts.deletedAt)))
|
|
.returning();
|
|
|
|
if (!deleted) {
|
|
return { success: false, error: "账户不存在或已删除" };
|
|
}
|
|
|
|
revalidatePath("/accounts");
|
|
revalidatePath("/channels");
|
|
revalidatePath("/");
|
|
return { success: true };
|
|
} catch (err) {
|
|
console.error("deleteAccountAction error:", err);
|
|
return { success: false, error: err instanceof Error ? err.message : "删除账户失败" };
|
|
}
|
|
}
|