feat: introduce i18n system and class invitation codes
Add complete i18n infrastructure using next-intl (cookie-driven, without i18n routing) with zh-CN/en dictionary files, locale switcher, and NextIntlClientProvider in root layout. Add class invitation code system with new class_invitation_codes table, data-access layer (generate/validate/consume/revoke), server actions with permission checks, rate limiting, and audit logging. Add class-invitation-manager UI component. Refactor onboarding stepper to use i18n translations and accept new invitation code format (6-char alphanumeric) with backward compatibility for legacy 6-digit codes.
This commit is contained in:
@@ -613,22 +613,26 @@ export async function enrollStudentByInvitationCode(studentId: string, invitatio
|
||||
const sid = studentId.trim()
|
||||
const code = invitationCode.trim()
|
||||
if (!sid) throw new Error("Missing student id")
|
||||
if (!/^\d{6}$/.test(code)) throw new Error("Invalid invitation code")
|
||||
if (!code) throw new Error("Invalid invitation code")
|
||||
|
||||
const [cls] = await db
|
||||
.select({ id: classes.id })
|
||||
.from(classes)
|
||||
.where(eq(classes.invitationCode, code))
|
||||
.limit(1)
|
||||
|
||||
if (!cls) throw new Error("Invalid invitation code")
|
||||
// v3:优先走新邀请码体系(validateInvitationCode 内部含 fallback 到旧 classes.invitationCode)
|
||||
const { validateInvitationCode, consumeInvitationCode } = await import("./data-access-invitations")
|
||||
const result = await validateInvitationCode(code)
|
||||
if (!result.valid || !result.classId) {
|
||||
throw new Error("Invalid invitation code")
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(classEnrollments)
|
||||
.values({ classId: cls.id, studentId: sid, status: "active" })
|
||||
.values({ classId: result.classId, studentId: sid, status: "active" })
|
||||
.onDuplicateKeyUpdate({ set: { status: "active" } })
|
||||
|
||||
return cls.id
|
||||
// 消耗新表邀请码(旧表无计数,跳过)
|
||||
if (result.codeId) {
|
||||
await consumeInvitationCode(code)
|
||||
}
|
||||
|
||||
return result.classId
|
||||
}
|
||||
|
||||
export async function enrollTeacherByInvitationCode(
|
||||
@@ -639,7 +643,7 @@ export async function enrollTeacherByInvitationCode(
|
||||
const tid = teacherId.trim()
|
||||
const code = invitationCode.trim()
|
||||
if (!tid) throw new Error("Missing teacher id")
|
||||
if (!/^\d{6}$/.test(code)) throw new Error("Invalid invitation code")
|
||||
if (!code) throw new Error("Invalid invitation code")
|
||||
|
||||
const [teacher] = await db
|
||||
.select({ id: users.id })
|
||||
@@ -651,10 +655,17 @@ export async function enrollTeacherByInvitationCode(
|
||||
|
||||
if (!teacher) throw new Error("Teacher not found")
|
||||
|
||||
// v3:优先走新邀请码体系(validateInvitationCode 内部含 fallback 到旧 classes.invitationCode)
|
||||
const { validateInvitationCode, consumeInvitationCode } = await import("./data-access-invitations")
|
||||
const result = await validateInvitationCode(code)
|
||||
if (!result.valid || !result.classId) {
|
||||
throw new Error("Invalid invitation code")
|
||||
}
|
||||
|
||||
const [cls] = await db
|
||||
.select({ id: classes.id, teacherId: classes.teacherId })
|
||||
.from(classes)
|
||||
.where(eq(classes.invitationCode, code))
|
||||
.where(eq(classes.id, result.classId))
|
||||
.limit(1)
|
||||
|
||||
if (!cls) throw new Error("Invalid invitation code")
|
||||
@@ -747,6 +758,11 @@ export async function enrollTeacherByInvitationCode(
|
||||
if (!assigned) throw new Error("Class already has assigned teachers")
|
||||
}
|
||||
|
||||
// 消耗新表邀请码(旧表无计数,跳过)
|
||||
if (result.codeId) {
|
||||
await consumeInvitationCode(code)
|
||||
}
|
||||
|
||||
return cls.id
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user