feat: 完成 P1 全部功能 + 修复 proxy 导出 + 切换 MySQL 端口至 14013
## P1 功能(20 项) - 站内消息系统、家长仪表盘、学生考勤管理 - Excel 导入导出、用户批量导入、成绩导出 - 排课规则+自动排课+课表调整 - 成绩趋势+对比分析、密码安全策略、速率限制 - 数据变更日志、文件预览+存储策略、全文检索 - 依赖审计集成 CI、数据库定时备份、E2E 测试完善 - 通知偏好管理 ## 基础设施修复 - src/proxy.ts: 将 middleware 导出重命名为 proxy(Next.js 16 要求) - .env: MySQL 端口从 13002 切换至 14013 - scripts/create-db.ts: 新增数据库初始化脚本 ## 架构文档同步 - 004_architecture_impact_map.md 和 005_architecture_data.json 完整记录所有新增表、模块、路由、权限、依赖关系
This commit is contained in:
128
src/app/(auth)/privacy/page.tsx
Normal file
128
src/app/(auth)/privacy/page.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/components/ui/card"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "隐私政策 - Next_Edu",
|
||||
description: "Next_Edu 隐私政策与个人信息保护说明",
|
||||
}
|
||||
|
||||
const SECTION_CLASS = "space-y-2"
|
||||
const HEADING_CLASS = "text-base font-semibold text-foreground"
|
||||
const TEXT_CLASS = "text-sm leading-relaxed text-muted-foreground"
|
||||
const LIST_CLASS = "ml-4 list-disc space-y-1 text-sm leading-relaxed text-muted-foreground"
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-3xl space-y-6 py-8">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">隐私政策</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
最近更新日期:2026 年 6 月 16 日
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>引言</CardTitle>
|
||||
<CardDescription>
|
||||
Next_Edu(以下简称“我们”)是一款面向 K12 教育场景的智慧教务管理系统,我们高度重视用户个人信息保护。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>一、信息收集说明</h2>
|
||||
<p className={TEXT_CLASS}>
|
||||
为提供教学服务,我们会收集以下类型的信息:
|
||||
</p>
|
||||
<ul className={LIST_CLASS}>
|
||||
<li>账户信息:姓名、邮箱、密码(加密存储)、手机号码。</li>
|
||||
<li>身份信息:角色(学生/教师/家长等)、年级、班级、出生日期。</li>
|
||||
<li>未成年人保护信息:监护人姓名、联系电话、与未成年人关系。</li>
|
||||
<li>学习数据:作业作答、考试成绩、学习行为记录。</li>
|
||||
<li>设备信息:浏览器类型、访问日志(用于安全与运维)。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>二、信息使用说明</h2>
|
||||
<p className={TEXT_CLASS}>收集的信息将用于:</p>
|
||||
<ul className={LIST_CLASS}>
|
||||
<li>提供注册、登录、班级管理与教学功能。</li>
|
||||
<li>生成学习报告与学情分析,辅助教学决策。</li>
|
||||
<li>在监护人授权下,向家长推送子女学习情况。</li>
|
||||
<li>保障账户安全、防范风险与合规审计。</li>
|
||||
<li>改进产品体验,不会用于与教学无关的商业用途。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>三、信息保护措施</h2>
|
||||
<ul className={LIST_CLASS}>
|
||||
<li>密码使用 bcrypt 算法加密存储,AI 服务密钥使用 AES 加密。</li>
|
||||
<li>采用基于角色的访问控制(RBAC)与数据范围(DataScope)行级过滤。</li>
|
||||
<li>HTTPS 传输加密,数据库访问受网络与权限隔离。</li>
|
||||
<li>仅授权人员在最小必要原则下访问用户数据,并留存审计日志。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>四、用户权利</h2>
|
||||
<ul className={LIST_CLASS}>
|
||||
<li>查询权:您可在个人资料页查看我们持有的您的信息。</li>
|
||||
<li>更正权:您可随时修改姓名、手机、地址等个人资料。</li>
|
||||
<li>删除权:您可申请注销账户,我们将在合理期限内删除相关数据。</li>
|
||||
<li>撤回同意权:监护人可随时撤回对未成年人使用服务的同意。</li>
|
||||
<li>数据可携带权:您可申请导出您的学习数据。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>五、Cookie 政策</h2>
|
||||
<p className={TEXT_CLASS}>
|
||||
我们使用 Cookie 与本地存储维持登录会话、记住偏好设置。会话 Cookie
|
||||
在您登出后失效;持久化 Cookie 仅用于必要的功能体验,不用于跨站追踪广告。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>六、未成年人保护条款</h2>
|
||||
<ul className={LIST_CLASS}>
|
||||
<li>未满 14 周岁的用户须在监护人陪同下注册,并填写监护人信息。</li>
|
||||
<li>14 周岁以上不满 18 周岁的用户,注册时须确认已获得监护人同意。</li>
|
||||
<li>我们对未成年人个人信息采取更严格的访问控制与加密存储。</li>
|
||||
<li>不会向未成年人推送与其学习无关的商业信息。</li>
|
||||
<li>监护人可通过联系方式申请查阅、更正或删除未成年人的信息。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>七、联系方式</h2>
|
||||
<p className={TEXT_CLASS}>
|
||||
如您对本隐私政策有任何疑问、建议或投诉,可通过以下方式联系我们:
|
||||
</p>
|
||||
<ul className={LIST_CLASS}>
|
||||
<li>邮箱:privacy@next-edu.example.com</li>
|
||||
<li>客服电话:400-000-0000(工作日 9:00-18:00)</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div className="border-t pt-4 text-center">
|
||||
<Link
|
||||
href="/register"
|
||||
className="text-sm font-medium text-primary underline underline-offset-4 hover:opacity-80"
|
||||
>
|
||||
返回注册
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -11,12 +11,27 @@ export const metadata: Metadata = {
|
||||
description: "Create an account",
|
||||
}
|
||||
|
||||
const ADULT_AGE = 18
|
||||
|
||||
const normalizeBcryptHash = (value: string) => {
|
||||
if (value.startsWith("$2")) return value
|
||||
if (value.startsWith("$")) return `$2b${value}`
|
||||
return `$2b$${value}`
|
||||
}
|
||||
|
||||
function calcAge(birth: string): number | null {
|
||||
if (!birth) return null
|
||||
const birthDate = new Date(birth)
|
||||
if (Number.isNaN(birthDate.getTime())) return null
|
||||
const now = new Date()
|
||||
let age = now.getFullYear() - birthDate.getFullYear()
|
||||
const monthDiff = now.getMonth() - birthDate.getMonth()
|
||||
if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birthDate.getDate())) {
|
||||
age -= 1
|
||||
}
|
||||
return age >= 0 ? age : null
|
||||
}
|
||||
|
||||
export default function RegisterPage() {
|
||||
async function registerAction(formData: FormData): Promise<ActionState> {
|
||||
"use server"
|
||||
@@ -33,11 +48,23 @@ export default function RegisterPage() {
|
||||
const name = String(formData.get("name") ?? "").trim()
|
||||
const email = String(formData.get("email") ?? "").trim().toLowerCase()
|
||||
const password = String(formData.get("password") ?? "")
|
||||
const birthDateRaw = String(formData.get("birthDate") ?? "").trim()
|
||||
const guardianName = String(formData.get("guardianName") ?? "").trim()
|
||||
const guardianPhone = String(formData.get("guardianPhone") ?? "").trim()
|
||||
const guardianRelation = String(formData.get("guardianRelation") ?? "").trim()
|
||||
|
||||
if (!email) return { success: false, message: "请输入邮箱" }
|
||||
if (!password) return { success: false, message: "请输入密码" }
|
||||
if (password.length < 6) return { success: false, message: "密码至少 6 位" }
|
||||
|
||||
const age = calcAge(birthDateRaw)
|
||||
const isMinor = age !== null && age < ADULT_AGE
|
||||
if (isMinor) {
|
||||
if (!guardianName) return { success: false, message: "未成年人须填写监护人姓名" }
|
||||
if (!guardianPhone) return { success: false, message: "未成年人须填写监护人电话" }
|
||||
if (!guardianRelation) return { success: false, message: "未成年人须选择监护人关系" }
|
||||
}
|
||||
|
||||
const existing = await db.query.users.findFirst({
|
||||
where: eq(users.email, email),
|
||||
columns: { id: true },
|
||||
@@ -51,6 +78,12 @@ export default function RegisterPage() {
|
||||
name: name.length ? name : null,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
birthDate: birthDateRaw ? new Date(birthDateRaw) : null,
|
||||
age: age ?? null,
|
||||
guardianName: guardianName || null,
|
||||
guardianPhone: guardianPhone || null,
|
||||
guardianRelation: guardianRelation || null,
|
||||
consentAcceptedAt: new Date(),
|
||||
})
|
||||
const roleRow = await db.query.roles.findFirst({
|
||||
where: eq(roles.name, "student"),
|
||||
|
||||
116
src/app/(auth)/terms/page.tsx
Normal file
116
src/app/(auth)/terms/page.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import { Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/components/ui/card"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "用户协议 - Next_Edu",
|
||||
description: "Next_Edu 用户服务协议",
|
||||
}
|
||||
|
||||
const SECTION_CLASS = "space-y-2"
|
||||
const HEADING_CLASS = "text-base font-semibold text-foreground"
|
||||
const TEXT_CLASS = "text-sm leading-relaxed text-muted-foreground"
|
||||
const LIST_CLASS = "ml-4 list-disc space-y-1 text-sm leading-relaxed text-muted-foreground"
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-3xl space-y-6 py-8">
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">用户协议</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
最近更新日期:2026 年 6 月 16 日
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>引言</CardTitle>
|
||||
<CardDescription>
|
||||
欢迎使用 Next_Edu 智慧教务管理系统(以下简称“本服务”)。请仔细阅读并同意本协议后,方可注册和使用本服务。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>一、服务说明</h2>
|
||||
<p className={TEXT_CLASS}>
|
||||
本服务面向 K12 学校、教师、学生及家长,提供考试管理、作业批改、题库管理、教材与知识点体系、班级与学校管理、AI 辅助教学等功能。
|
||||
我们保留对服务内容进行更新、调整的权利。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>二、用户注册</h2>
|
||||
<ul className={LIST_CLASS}>
|
||||
<li>用户须使用真实邮箱注册,并对账户密码的安全负责。</li>
|
||||
<li>未成年人注册须在监护人陪同下完成,并填写监护人信息或确认已获得监护人同意。</li>
|
||||
<li>注册时须同意《隐私政策》与本《用户协议》。</li>
|
||||
<li>禁止转让、出借账户,因账户保管不当造成的损失由用户自行承担。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>三、用户行为规范</h2>
|
||||
<ul className={LIST_CLASS}>
|
||||
<li>不得利用本服务从事违法、违规或侵犯他人权益的行为。</li>
|
||||
<li>不得上传或传播涉黄、涉暴、涉政、侵权或有害内容。</li>
|
||||
<li>不得破坏系统安全、尝试未授权访问或干扰其他用户使用。</li>
|
||||
<li>不得批量抓取、爬取平台数据用于商业用途。</li>
|
||||
<li>教师与家长应引导未成年人正确、合理使用本服务。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>四、知识产权</h2>
|
||||
<ul className={LIST_CLASS}>
|
||||
<li>本服务的软件、界面、文案、图标等知识产权归我们或权利人所有。</li>
|
||||
<li>用户上传的题目、教材内容等,知识产权归原作者所有;用户授权我们在服务范围内存储、展示与处理。</li>
|
||||
<li>未经书面许可,不得复制、改编、传播本服务中的受保护内容。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>五、免责声明</h2>
|
||||
<ul className={LIST_CLASS}>
|
||||
<li>本服务按“现状”提供,我们不保证服务持续可用或完全无错误。</li>
|
||||
<li>AI 生成的题目与解析仅供参考,可能存在偏差,使用者应自行审核。</li>
|
||||
<li>因不可抗力、网络故障、第三方服务中断等原因造成的损失,我们不承担责任。</li>
|
||||
<li>用户因违反本协议造成的后果,由用户自行承担。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>六、服务变更、中断与终止</h2>
|
||||
<ul className={LIST_CLASS}>
|
||||
<li>我们可基于运营需要调整、暂停或终止部分或全部服务,并尽量提前公告。</li>
|
||||
<li>用户违反本协议的,我们可限制、暂停或终止其账户。</li>
|
||||
<li>用户可申请注销账户,注销后相关数据将按隐私政策处理。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className={SECTION_CLASS}>
|
||||
<h2 className={HEADING_CLASS}>七、法律适用与争议解决</h2>
|
||||
<ul className={LIST_CLASS}>
|
||||
<li>本协议的订立、执行与解释适用中华人民共和国法律。</li>
|
||||
<li>因本协议或本服务产生的争议,双方应友好协商解决;协商不成的,可向我们所在地有管辖权的人民法院提起诉讼。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<div className="border-t pt-4 text-center">
|
||||
<Link
|
||||
href="/register"
|
||||
className="text-sm font-medium text-primary underline underline-offset-4 hover:opacity-80"
|
||||
>
|
||||
返回注册
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user