feat(ledger): add bookkeeping workbench and transaction views

- Add transactions_dirty table and zero-tolerance cleansing action

- Implement bookkeeping workbench (/bookkeeping) with searchable Combobox channel selector and card suffix / account identifier

- Implement transaction ledger (/transactions) with date groupings, multi-currency metrics, and filters

- Update AGENTS.md guidelines for Base UI Select and Combobox bindings
This commit is contained in:
2026-09-06 23:23:29 +08:00
parent 1ccee1414c
commit c33857ffac
42 changed files with 3329 additions and 598 deletions
+30 -26
View File
@@ -1,10 +1,10 @@
"use server";
"use server"
import { z } from "zod";
import { eq } from "drizzle-orm";
import { db } from "@/lib/db";
import { users } from "@/lib/db/schema";
import { hashPassword } from "@/lib/auth/password";
import { z } from "zod"
import { eq } from "drizzle-orm"
import { db } from "@/lib/db"
import { users } from "@/lib/db/schema"
import { hashPassword } from "@/lib/auth/password"
const registerSchema = z
.object({
@@ -13,16 +13,20 @@ const registerSchema = z
password: z.string().min(8, "密码长度至少需要 8 个字符"),
confirmPassword: z.string().min(1, "请确认密码"),
})
.refine((data: { password: string; confirmPassword: string }) => data.password === data.confirmPassword, {
message: "两次输入的密码不一致",
path: ["confirmPassword"],
});
.refine(
(data: { password: string; confirmPassword: string }) =>
data.password === data.confirmPassword,
{
message: "两次输入的密码不一致",
path: ["confirmPassword"],
}
)
export type RegisterState = {
success?: boolean;
error?: string;
fieldErrors?: Record<string, string[]>;
};
success?: boolean
error?: string
fieldErrors?: Record<string, string[]>
}
export async function registerAction(
prevState: RegisterState | null,
@@ -33,18 +37,18 @@ export async function registerAction(
email: formData.get("email"),
password: formData.get("password"),
confirmPassword: formData.get("confirmPassword"),
};
}
const parsed = registerSchema.safeParse(rawData);
const parsed = registerSchema.safeParse(rawData)
if (!parsed.success) {
return {
success: false,
fieldErrors: parsed.error.flatten().fieldErrors,
};
}
}
const { name, email, password } = parsed.data;
const normalizedEmail = email.toLowerCase().trim();
const { name, email, password } = parsed.data
const normalizedEmail = email.toLowerCase().trim()
try {
// 检查邮箱是否已被注册
@@ -52,17 +56,17 @@ export async function registerAction(
.select()
.from(users)
.where(eq(users.email, normalizedEmail))
.limit(1);
.limit(1)
if (existing) {
return {
success: false,
error: "该邮箱已被注册,请直接登录",
};
}
}
// 使用 Argon2 哈希密码
const passwordHash = await hashPassword(password);
const passwordHash = await hashPassword(password)
// 插入新用户
await db.insert(users).values({
@@ -70,16 +74,16 @@ export async function registerAction(
email: normalizedEmail,
passwordHash,
isActive: true,
});
})
return {
success: true,
};
}
} catch (err) {
console.error("Registration error:", err);
console.error("Registration error:", err)
return {
success: false,
error: "注册失败,请稍后重试",
};
}
}
}