From df41f7713d9cfd55d6f5e31f3142131135afdf7b Mon Sep 17 00:00:00 2001 From: jacobxu666 Date: Wed, 19 Aug 2026 15:20:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AE=B6=E9=95=BF=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E9=A1=B5=20/register:=E5=87=AD=E5=AE=B6=E5=BA=AD?= =?UTF-8?q?=E9=82=80=E8=AF=B7=E7=A0=81=E6=B3=A8=E5=86=8C=E5=8A=A0=E5=85=A5?= =?UTF-8?q?=E5=B9=B6=E8=87=AA=E5=8A=A8=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/login/page.tsx | 9 ++++ src/app/register/actions.ts | 68 ++++++++++++++++++++++++++++++ src/app/register/page.tsx | 83 +++++++++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 src/app/register/actions.ts create mode 100644 src/app/register/page.tsx diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 729e276..337f616 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -65,6 +65,15 @@ export default function LoginPage() { 去输入激活码

+

+ 家里另一位家长想加入?{" "} + + 用邀请码加入家庭 + +

diff --git a/src/app/register/actions.ts b/src/app/register/actions.ts new file mode 100644 index 0000000..09f9fbd --- /dev/null +++ b/src/app/register/actions.ts @@ -0,0 +1,68 @@ +"use server"; + +import crypto from "node:crypto"; +import bcrypt from "bcryptjs"; +import { and, eq, gt, isNull } from "drizzle-orm"; +import { redirect } from "next/navigation"; +import { z } from "zod"; +import { db } from "@/db"; +import { familyInvites, users } from "@/db/schema"; +import { createSession } from "@/lib/auth"; + +function hashCode(code: string) { + return crypto.createHash("sha256").update(code).digest("hex"); +} + +const registerSchema = z.object({ + displayName: z.string().trim().min(1, "请输入称呼").max(20, "称呼最多 20 字"), + phone: z.string().regex(/^1\d{10}$/, "请输入 11 位手机号"), + password: z.string().min(6, "密码至少 6 位"), + code: z.string().regex(/^\d{6}$/, "邀请码是 6 位数字"), +}); + +export type RegisterState = { error?: string } | null; + +// xuchao: 家长凭邀请码注册:校验码→查重手机号→事务(码标记已用+建家长)→自动登录跳家长中心 +export async function registerWithInvite( + _prev: RegisterState, + formData: FormData, +): Promise { + const parsed = registerSchema.safeParse({ + displayName: formData.get("displayName"), + phone: formData.get("phone"), + password: formData.get("password"), + code: formData.get("code"), + }); + if (!parsed.success) return { error: parsed.error.issues[0].message }; + + const { displayName, phone, password, code } = parsed.data; + + const [invite] = await db + .select({ id: familyInvites.id, familyId: familyInvites.familyId }) + .from(familyInvites) + .where( + and( + eq(familyInvites.codeHash, hashCode(code)), + isNull(familyInvites.usedAt), + gt(familyInvites.expiresAt, new Date()), + ), + ) + .limit(1); + if (!invite) return { error: "邀请码无效或已过期,请让家长重新生成" }; + + const [dup] = await db.select({ id: users.id }).from(users).where(eq(users.phone, phone)).limit(1); + if (dup) return { error: "该手机号已注册" }; + + const passwordHash = await bcrypt.hash(password, 10); + const newUserId = await db.transaction(async (tx) => { + await tx.update(familyInvites).set({ usedAt: new Date() }).where(eq(familyInvites.id, invite.id)); + const [created] = await tx + .insert(users) + .values({ familyId: invite.familyId, role: "parent", displayName, phone, passwordHash }) + .$returningId(); + return created.id; + }); + + await createSession(newUserId, "家长设备", 30); + redirect("/parent"); +} \ No newline at end of file diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx new file mode 100644 index 0000000..8da58ac --- /dev/null +++ b/src/app/register/page.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useActionState } from "react"; +import Link from "next/link"; +import { UserPlus } from "lucide-react"; +import { BackgroundBlobs } from "@/components/background-blobs"; +import { registerWithInvite } from "./actions"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export default function RegisterPage() { + const [state, formAction, pending] = useActionState(registerWithInvite, null); + + return ( +
+ + + +
+ +
+

加入家庭

+

需要家里已有成员的邀请码

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ {state?.error &&

{state.error}

} + +

+ 已有账号?{" "} + + 去登录 + +

+
+
+
+
+ ); +} \ No newline at end of file