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:
SpecialX
2026-06-22 14:04:55 +08:00
parent a4d096a6fc
commit c90748124d
25 changed files with 2911 additions and 30 deletions

View File

@@ -0,0 +1,85 @@
import { z } from "zod"
/**
* Onboarding 输入校验 schema。
*
* 设计原则(参考 K12 教务铁律):
* - 角色字段不在用户可提交的 schema 中,角色由管理员预分配,服务端从 usersToRoles 读取。
* - 班级代码仅作为"确认/补绑"用途,服务端调用 modules/classes data-access 做强校验。
*
* v3 修复(对标 PowerSchool Access ID + Access Password
* - P0-2 家长绑定验证增强:从"邮箱+生日"(生日仅 365 种可能)升级为"邮箱+生日+手机号后4位"三因子,
* 组合空间提升至 365 × 10000 = 3.65M 种,显著降低枚举攻击风险。
* - P1-4 家长多子女children 数组替代单个 childEmail/childBindingCode支持一次绑定多个子女。
*/
const childSchema = z.object({
childEmail: z
.string()
.trim()
.min(1, "请填写子女邮箱")
.email("子女邮箱格式错误"),
childBirthDate: z
.string()
.trim()
.min(1, "请填写子女生日")
.regex(/^\d{4}-\d{2}-\d{2}$/, "子女生日格式错误YYYY-MM-DD"),
childPhoneSuffix: z
.string()
.trim()
.min(1, "请填写子女手机号后 4 位")
.regex(/^\d{4}$/, "子女手机号后 4 位格式错误"),
childRelation: z
.string()
.trim()
.max(50, "关系字段长度不能超过 50 个字符")
.optional()
.or(z.literal("")),
})
export const OnboardingSchema = z.object({
name: z
.string()
.trim()
.min(1, "请填写姓名")
.max(50, "姓名长度不能超过 50 个字符"),
phone: z
.string()
.trim()
.min(1, "请填写电话")
.regex(/^1\d{10}$/, "请输入有效的手机号11 位,以 1 开头)"),
address: z
.string()
.trim()
.max(200, "住址长度不能超过 200 个字符")
.optional()
.or(z.literal("")),
// 学生/教师补绑班级的邀请码列表(可选,每项为 6 位字母数字v3 新格式)
// v3从 6 位数字升级为 6 位字母数字(剔除歧义字符 0/O/1/I/L兼容旧 6 位数字码
classCodes: z
.array(
z
.string()
.trim()
.regex(
/^[A-Z2-9]{6}$|^\d{6}$/,
"班级邀请码格式错误6 位字母数字或 6 位数字)"
)
)
.max(10, "单次最多绑定 10 个班级")
.optional()
.default([]),
// 教师任课科目可选v3 修复 P0-3服务端循环为每个科目绑定
teacherSubjects: z
.array(z.string().trim().min(1))
.max(10)
.optional()
.default([]),
// 家长绑定子女列表P1-4 多子女支持)
children: z
.array(childSchema)
.max(10, "单次最多绑定 10 个子女")
.optional()
.default([]),
})
export type OnboardingInput = z.infer<typeof OnboardingSchema>