feat(role): rename_guest_role_to_student

This commit is contained in:
cfngc4594 2026-05-15 11:40:40 +08:00
parent c4e7a3b6f5
commit 2cbe91d487
27 changed files with 120 additions and 101 deletions

View File

@ -0,0 +1,19 @@
/*
Warnings:
- The values [GUEST] on the enum `Role` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "Role_new" AS ENUM ('ADMIN', 'STUDENT', 'TEACHER');
ALTER TABLE "User" ALTER COLUMN "role" DROP DEFAULT;
ALTER TABLE "User" ALTER COLUMN "role" TYPE "Role_new" USING ("role"::text::"Role_new");
ALTER TYPE "Role" RENAME TO "Role_old";
ALTER TYPE "Role_new" RENAME TO "Role";
DROP TYPE "Role_old";
ALTER TABLE "User" ALTER COLUMN "role" SET DEFAULT 'STUDENT';
COMMIT;
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "role" SET DEFAULT 'STUDENT';

View File

@ -10,7 +10,7 @@ generator client {
enum Role { enum Role {
ADMIN ADMIN
GUEST STUDENT
TEACHER TEACHER
} }
@ -90,7 +90,7 @@ model User {
password String? password String?
emailVerified DateTime? emailVerified DateTime?
image String? image String?
role Role @default(GUEST) role Role @default(STUDENT)
accounts Account[] accounts Account[]
sessions Session[] sessions Session[]

View File

@ -2999,12 +2999,12 @@ export async function main() {
where: { email }, where: { email },
update: { update: {
name: `学生${index + 1}`, name: `学生${index + 1}`,
role: "GUEST", role: "STUDENT",
}, },
create: { create: {
name: `学生${index + 1}`, name: `学生${index + 1}`,
email, email,
role: "GUEST", role: "STUDENT",
}, },
}) })
) )

View File

@ -41,7 +41,7 @@ export function assertTeacherOrAdmin(actor: AuthenticatedActor) {
} }
export function assertStudent(actor: AuthenticatedActor) { export function assertStudent(actor: AuthenticatedActor) {
if (actor.role !== "GUEST") { if (actor.role !== "STUDENT") {
throw new Error("仅学生可访问"); throw new Error("仅学生可访问");
} }
} }

View File

@ -130,7 +130,7 @@ export async function enrollStudents(courseId: string, studentIds: string[]) {
const students = await prisma.user.findMany({ const students = await prisma.user.findMany({
where: { where: {
id: { in: uniqueStudentIds }, id: { in: uniqueStudentIds },
role: "GUEST", role: "STUDENT",
}, },
select: { id: true }, select: { id: true },
}); });
@ -197,7 +197,7 @@ export async function listAvailableStudents() {
assertTeacherOrAdmin(actor); assertTeacherOrAdmin(actor);
return prisma.user.findMany({ return prisma.user.findMany({
where: { role: "GUEST" }, where: { role: "STUDENT" },
orderBy: { createdAt: "desc" }, orderBy: { createdAt: "desc" },
select: { select: {
id: true, id: true,

View File

@ -47,16 +47,16 @@ export default async function Layout({ children }: LayoutProps) {
return <AdminSidebar user={user} />; return <AdminSidebar user={user} />;
case "TEACHER": case "TEACHER":
return <TeacherSidebar user={user} />; return <TeacherSidebar user={user} />;
case "GUEST": case "STUDENT":
default: default:
// 学生(GUEST需要查询错题数据 // 学生(STUDENT需要查询错题数据
return <AppSidebar user={user} wrongProblems={[]} />; return <AppSidebar user={user} wrongProblems={[]} />;
} }
}; };
// 只有学生才需要查询错题数据 // 只有学生才需要查询错题数据
let wrongProblemsData: WrongProblem[] = []; let wrongProblemsData: WrongProblem[] = [];
if (fullUser.role === "GUEST") { if (fullUser.role === "STUDENT") {
// 查询未完成未AC题目的最新一次提交 // 查询未完成未AC题目的最新一次提交
const wrongProblems = await prisma.problem.findMany({ const wrongProblems = await prisma.problem.findMany({
where: { where: {
@ -98,7 +98,7 @@ export default async function Layout({ children }: LayoutProps) {
return ( return (
<SidebarProvider> <SidebarProvider>
{fullUser.role === "GUEST" ? ( {fullUser.role === "STUDENT" ? (
<AppSidebar user={user} wrongProblems={wrongProblemsData} /> <AppSidebar user={user} wrongProblems={wrongProblemsData} />
) : ( ) : (
renderSidebar() renderSidebar()

View File

@ -12,7 +12,7 @@ interface User {
email: string; email: string;
emailVerified?: Date | null; emailVerified?: Date | null;
image: string | null; image: string | null;
role: "GUEST" | "USER" | "ADMIN" | "TEACHER"; role: "STUDENT" | "ADMIN" | "TEACHER";
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }

View File

@ -94,7 +94,7 @@ export default async function DashboardPage() {
// 教师统计 // 教师统计
const [totalStudents, totalProblems, totalSubmissions, recentSubmissions] = const [totalStudents, totalProblems, totalSubmissions, recentSubmissions] =
await Promise.all([ await Promise.all([
prisma.user.count({ where: { role: "GUEST" } }), prisma.user.count({ where: { role: "STUDENT" } }),
prisma.problem.count({ where: { isPublished: true } }), prisma.problem.count({ where: { isPublished: true } }),
prisma.submission.count(), prisma.submission.count(),
prisma.submission.findMany({ prisma.submission.findMany({
@ -204,8 +204,8 @@ export default async function DashboardPage() {
icon: Target, icon: Target,
}, },
{ {
label: "用户管理", label: "学生管理",
href: "/dashboard/usermanagement/guest", href: "/dashboard/usermanagement/student",
icon: Users, icon: Users,
}, },
{ {
@ -246,8 +246,8 @@ export default async function DashboardPage() {
], ],
actions: [ actions: [
{ {
label: "用户管理", label: "学生管理",
href: "/dashboard/usermanagement/guest", href: "/dashboard/usermanagement/student",
icon: Users, icon: Users,
}, },
{ {
@ -311,7 +311,7 @@ export default async function DashboardPage() {
const config = getRoleConfig(); const config = getRoleConfig();
const completionRate = const completionRate =
fullUser.role === "GUEST" fullUser.role === "STUDENT"
? (stats.totalProblems || 0) > 0 ? (stats.totalProblems || 0) > 0
? ((stats.completedProblems || 0) / (stats.totalProblems || 1)) * 100 ? ((stats.completedProblems || 0) / (stats.totalProblems || 1)) * 100
: 0 : 0
@ -349,7 +349,7 @@ export default async function DashboardPage() {
</div> </div>
{/* 学生进度条 */} {/* 学生进度条 */}
{fullUser.role === "GUEST" && ( {fullUser.role === "STUDENT" && (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">

View File

@ -5,5 +5,5 @@ export default async function StudentCoursesLayout({
}: { }: {
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return <ProtectedLayout roles={["GUEST"]}>{children}</ProtectedLayout>; return <ProtectedLayout roles={["STUDENT"]}>{children}</ProtectedLayout>;
} }

View File

@ -6,10 +6,10 @@ import { Role } from "@/generated/client";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import type { User } from "@/generated/client"; import type { User } from "@/generated/client";
type UserType = "admin" | "teacher" | "guest"; type ResourceType = "admin" | "teacher" | "student";
export async function createUser( export async function createUser(
userType: UserType, resourceType: ResourceType,
data: Omit<User, "id" | "createdAt" | "updatedAt"> & { password?: string } data: Omit<User, "id" | "createdAt" | "updatedAt"> & { password?: string }
) { ) {
let password = data.password; let password = data.password;
@ -17,13 +17,13 @@ export async function createUser(
password = await bcrypt.hash(password, 10); password = await bcrypt.hash(password, 10);
} }
const role = userType.toUpperCase() as Role; const role = resourceType.toUpperCase() as Role;
await prisma.user.create({ data: { ...data, password, role } }); await prisma.user.create({ data: { ...data, password, role } });
revalidatePath(`/usermanagement/${userType}`); revalidatePath(`/usermanagement/${resourceType}`);
} }
export async function updateUser( export async function updateUser(
userType: UserType, resourceType: ResourceType,
id: string, id: string,
data: Partial<Omit<User, "id" | "createdAt" | "updatedAt">> data: Partial<Omit<User, "id" | "createdAt" | "updatedAt">>
) { ) {
@ -38,10 +38,10 @@ export async function updateUser(
} }
await prisma.user.update({ where: { id }, data: updateData }); await prisma.user.update({ where: { id }, data: updateData });
revalidatePath(`/usermanagement/${userType}`); revalidatePath(`/usermanagement/${resourceType}`);
} }
export async function deleteUser(userType: UserType, id: string) { export async function deleteUser(resourceType: ResourceType, id: string) {
await prisma.user.delete({ where: { id } }); await prisma.user.delete({ where: { id } });
revalidatePath(`/usermanagement/${userType}`); revalidatePath(`/usermanagement/${resourceType}`);
} }

View File

@ -2,5 +2,5 @@ import { adminConfig } from "@/features/user-management/config/admin";
import GenericPage from "@/features/user-management/components/generic-page"; import GenericPage from "@/features/user-management/components/generic-page";
export default function AdminPage() { export default function AdminPage() {
return <GenericPage userType="admin" config={adminConfig} />; return <GenericPage resourceType="admin" config={adminConfig} />;
} }

View File

@ -1,6 +0,0 @@
import { guestConfig } from "@/features/user-management/config/guest";
import GenericPage from "@/features/user-management/components/generic-page";
export default function GuestPage() {
return <GenericPage userType="guest" config={guestConfig} />;
}

View File

@ -2,5 +2,5 @@ import { problemConfig } from "@/features/user-management/config/problem";
import GenericPage from "@/features/user-management/components/generic-page"; import GenericPage from "@/features/user-management/components/generic-page";
export default function ProblemPage() { export default function ProblemPage() {
return <GenericPage userType="problem" config={problemConfig} />; return <GenericPage resourceType="problem" config={problemConfig} />;
} }

View File

@ -1,6 +1,6 @@
import GenericLayout from "../components/GenericLayout"; import GenericLayout from "../components/GenericLayout";
export default function GuestLayout({ export default function StudentLayout({
children, children,
}: { }: {
children: React.ReactNode; children: React.ReactNode;

View File

@ -0,0 +1,6 @@
import { studentConfig } from "@/features/user-management/config/student";
import GenericPage from "@/features/user-management/components/generic-page";
export default function StudentPage() {
return <GenericPage resourceType="student" config={studentConfig} />;
}

View File

@ -2,5 +2,5 @@ import { teacherConfig } from "@/features/user-management/config/teacher";
import GenericPage from "@/features/user-management/components/generic-page"; import GenericPage from "@/features/user-management/components/generic-page";
export default function TeacherPage() { export default function TeacherPage() {
return <GenericPage userType="teacher" config={teacherConfig} />; return <GenericPage resourceType="teacher" config={teacherConfig} />;
} }

View File

@ -6,7 +6,7 @@ interface LayoutProps {
const Layout = ({ children }: LayoutProps) => { const Layout = ({ children }: LayoutProps) => {
return ( return (
<ProtectedLayout roles={["ADMIN", "TEACHER", "GUEST"]}> <ProtectedLayout roles={["ADMIN", "TEACHER", "STUDENT"]}>
{children} {children}
</ProtectedLayout> </ProtectedLayout>
); );

View File

@ -84,7 +84,7 @@ export const judge = async (
const canAccessAssignment = const canAccessAssignment =
actor.role === "ADMIN" || actor.role === "ADMIN" ||
(actor.role === "TEACHER" && isTeacherOwner) || (actor.role === "TEACHER" && isTeacherOwner) ||
(actor.role === "GUEST" && isStudentEnrolled); (actor.role === "STUDENT" && isStudentEnrolled);
if (!canAccessAssignment) { if (!canAccessAssignment) {
await createSystemErrorSubmission("No permission for assignment", { await createSystemErrorSubmission("No permission for assignment", {
@ -100,7 +100,7 @@ export const judge = async (
return Status.SE; return Status.SE;
} }
if (!assignment.published && actor.role === "GUEST") { if (!assignment.published && actor.role === "STUDENT") {
await createSystemErrorSubmission("Assignment is not published", { await createSystemErrorSubmission("Assignment is not published", {
assignmentId: assignment.id, assignmentId: assignment.id,
}); });

View File

@ -44,7 +44,7 @@ export function DynamicBreadcrumb() {
admin: "管理后台", admin: "管理后台",
teacher: "教师平台", teacher: "教师平台",
student: "学生平台", student: "学生平台",
usermanagement: "用户管理", usermanagement: "账号管理",
courses: "课程", courses: "课程",
assignments: "作业", assignments: "作业",
userdashboard: "用户仪表板", userdashboard: "用户仪表板",

View File

@ -26,7 +26,7 @@ const adminData = {
isActive: true, isActive: true,
items: [ items: [
{ title: "管理员管理", url: "/dashboard/usermanagement/admin" }, { title: "管理员管理", url: "/dashboard/usermanagement/admin" },
{ title: "用户管理", url: "/dashboard/usermanagement/guest" }, { title: "学生管理", url: "/dashboard/usermanagement/student" },
{ title: "教师管理", url: "/dashboard/usermanagement/teacher" }, { title: "教师管理", url: "/dashboard/usermanagement/teacher" },
{ title: "题目管理", url: "/dashboard/usermanagement/problem" }, { title: "题目管理", url: "/dashboard/usermanagement/problem" },
], ],

View File

@ -26,8 +26,8 @@ const data = {
isActive: true, isActive: true,
items: [ items: [
{ {
title: "用户管理", title: "学生管理",
url: "/dashboard/usermanagement/guest", url: "/dashboard/usermanagement/student",
}, },
{ {
title: "题目管理", title: "题目管理",

View File

@ -5,19 +5,19 @@ import { UserConfig } from "./user-table";
import type { User, Problem } from "@/generated/client"; import type { User, Problem } from "@/generated/client";
interface GenericPageProps { interface GenericPageProps {
userType: "admin" | "teacher" | "guest" | "problem"; resourceType: "admin" | "teacher" | "student" | "problem";
config: UserConfig; config: UserConfig;
} }
export default async function GenericPage({ export default async function GenericPage({
userType, resourceType,
config, config,
}: GenericPageProps) { }: GenericPageProps) {
if (userType === "problem") { if (resourceType === "problem") {
const data: Problem[] = await prisma.problem.findMany({}); const data: Problem[] = await prisma.problem.findMany({});
return <UserTable config={config} data={data} />; return <UserTable config={config} data={data} />;
} else { } else {
const role = userType.toUpperCase() as Role; const role = resourceType.toUpperCase() as Role;
const data: User[] = await prisma.user.findMany({ where: { role } }); const data: User[] = await prisma.user.findMany({ where: { role } });
return <UserTable config={config} data={data} />; return <UserTable config={config} data={data} />;
} }

View File

@ -78,7 +78,7 @@ import {
} from "@/app/(protected)/dashboard/usermanagement/actions/problemActions"; } from "@/app/(protected)/dashboard/usermanagement/actions/problemActions";
export interface UserConfig { export interface UserConfig {
userType: string; resourceType: string;
title: string; title: string;
apiPath: string; apiPath: string;
columns: Array<{ columns: Array<{
@ -154,7 +154,7 @@ const addProblemSchema = z.object({
}); });
export function UserTable(props: UserTableProps) { export function UserTable(props: UserTableProps) {
const isProblem = props.config.userType === "problem"; const isProblem = props.config.resourceType === "problem";
const router = useRouter(); const router = useRouter();
const problemData = isProblem ? (props.data as Problem[]) : undefined; const problemData = isProblem ? (props.data as Problem[]) : undefined;
@ -324,7 +324,7 @@ export function UserTable(props: UserTableProps) {
createdAt: "", createdAt: "",
image: null, image: null,
emailVerified: null, emailVerified: null,
role: Role.GUEST, role: Role.STUDENT,
}, },
}); });
React.useEffect(() => { React.useEffect(() => {
@ -336,7 +336,7 @@ export function UserTable(props: UserTableProps) {
createdAt: "", createdAt: "",
image: null, image: null,
emailVerified: null, emailVerified: null,
role: Role.GUEST, role: Role.STUDENT,
}); });
} }
}, [open, form]); }, [open, form]);
@ -354,19 +354,19 @@ export function UserTable(props: UserTableProps) {
...data, ...data,
image: data.image ?? null, image: data.image ?? null,
emailVerified: data.emailVerified ?? null, emailVerified: data.emailVerified ?? null,
role: data.role ?? Role.GUEST, role: data.role ?? Role.STUDENT,
}; };
if (!submitData.name) submitData.name = ""; if (!submitData.name) submitData.name = "";
if (!submitData.createdAt) if (!submitData.createdAt)
submitData.createdAt = new Date().toISOString(); submitData.createdAt = new Date().toISOString();
else else
submitData.createdAt = new Date(submitData.createdAt).toISOString(); submitData.createdAt = new Date(submitData.createdAt).toISOString();
if (props.config.userType === "admin") if (props.config.resourceType === "admin")
await createUser("admin", submitData); await createUser("admin", submitData);
else if (props.config.userType === "teacher") else if (props.config.resourceType === "teacher")
await createUser("teacher", submitData); await createUser("teacher", submitData);
else if (props.config.userType === "guest") else if (props.config.resourceType === "student")
await createUser("guest", submitData); await createUser("student", submitData);
onOpenChange(false); onOpenChange(false);
toast.success("添加成功", { duration: 1500 }); toast.success("添加成功", { duration: 1500 });
router.refresh(); router.refresh();
@ -610,7 +610,7 @@ export function UserTable(props: UserTableProps) {
name: user.name ?? "", name: user.name ?? "",
email: user.email ?? "", email: user.email ?? "",
password: "", password: "",
role: user.role ?? Role.GUEST, role: user.role ?? Role.STUDENT,
createdAt: user.createdAt createdAt: user.createdAt
? new Date(user.createdAt).toISOString().slice(0, 16) ? new Date(user.createdAt).toISOString().slice(0, 16)
: "", : "",
@ -625,7 +625,7 @@ export function UserTable(props: UserTableProps) {
name: user.name ?? "", name: user.name ?? "",
email: user.email ?? "", email: user.email ?? "",
password: "", password: "",
role: user.role ?? Role.GUEST, role: user.role ?? Role.STUDENT,
createdAt: user.createdAt createdAt: user.createdAt
? new Date(user.createdAt).toISOString().slice(0, 16) ? new Date(user.createdAt).toISOString().slice(0, 16)
: "", : "",
@ -644,15 +644,15 @@ export function UserTable(props: UserTableProps) {
: new Date().toISOString(), : new Date().toISOString(),
image: data.image ?? null, image: data.image ?? null,
emailVerified: data.emailVerified ?? null, emailVerified: data.emailVerified ?? null,
role: data.role ?? Role.GUEST, role: data.role ?? Role.STUDENT,
}; };
const id = typeof submitData.id === "string" ? submitData.id : ""; const id = typeof submitData.id === "string" ? submitData.id : "";
if (props.config.userType === "admin") if (props.config.resourceType === "admin")
await updateUser("admin", id, submitData); await updateUser("admin", id, submitData);
else if (props.config.userType === "teacher") else if (props.config.resourceType === "teacher")
await updateUser("teacher", id, submitData); await updateUser("teacher", id, submitData);
else if (props.config.userType === "guest") else if (props.config.resourceType === "student")
await updateUser("guest", id, submitData); await updateUser("student", id, submitData);
onOpenChange(false); onOpenChange(false);
toast.success("修改成功", { duration: 1500 }); toast.success("修改成功", { duration: 1500 });
} catch { } catch {
@ -710,7 +710,7 @@ export function UserTable(props: UserTableProps) {
</div> </div>
))} ))}
{/* 编辑时显示角色选择 */} {/* 编辑时显示角色选择 */}
{props.config.userType !== "problem" && ( {props.config.resourceType !== "problem" && (
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="role" className="text-right"> <Label htmlFor="role" className="text-right">
@ -725,18 +725,18 @@ export function UserTable(props: UserTableProps) {
<SelectValue placeholder="请选择角色" /> <SelectValue placeholder="请选择角色" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{props.config.userType === "guest" && ( {props.config.resourceType === "student" && (
<> <>
<SelectItem value="GUEST"></SelectItem> <SelectItem value="STUDENT"></SelectItem>
<SelectItem value="TEACHER"></SelectItem> <SelectItem value="TEACHER"></SelectItem>
</> </>
)} )}
{(props.config.userType === "teacher" || {(props.config.resourceType === "teacher" ||
props.config.userType === "admin") && ( props.config.resourceType === "admin") && (
<> <>
<SelectItem value="ADMIN"></SelectItem> <SelectItem value="ADMIN"></SelectItem>
<SelectItem value="TEACHER"></SelectItem> <SelectItem value="TEACHER"></SelectItem>
<SelectItem value="GUEST"></SelectItem> <SelectItem value="STUDENT"></SelectItem>
</> </>
)} )}
</SelectContent> </SelectContent>
@ -1068,10 +1068,10 @@ export function UserTable(props: UserTableProps) {
await deleteProblem((row.original as Problem).id); await deleteProblem((row.original as Problem).id);
} else { } else {
await deleteUser( await deleteUser(
props.config.userType as props.config.resourceType as
| "admin" | "admin"
| "teacher" | "teacher"
| "guest", | "student",
(row.original as User).id (row.original as User).id
); );
} }
@ -1113,7 +1113,7 @@ export function UserTable(props: UserTableProps) {
await deleteProblem((pendingDeleteItem as Problem).id); await deleteProblem((pendingDeleteItem as Problem).id);
} else { } else {
await deleteUser( await deleteUser(
props.config.userType as "admin" | "teacher" | "guest", props.config.resourceType as "admin" | "teacher" | "student",
(pendingDeleteItem as User).id (pendingDeleteItem as User).id
); );
} }

View File

@ -95,14 +95,14 @@ export const basePagination = {
// 创建用户配置的工厂函数 // 创建用户配置的工厂函数
export function createUserConfig( export function createUserConfig(
userType: string, resourceType: string,
title: string, title: string,
addLabel: string, addLabel: string,
namePlaceholder: string, namePlaceholder: string,
emailPlaceholder: string emailPlaceholder: string
) { ) {
return { return {
userType, resourceType,
title, title,
apiPath: "/api/user", apiPath: "/api/user",
columns: baseColumns, columns: baseColumns,

View File

@ -1,24 +0,0 @@
import {
createUserConfig,
baseUserSchema,
baseAddUserSchema,
baseEditUserSchema,
} from "./base-config";
import { z } from "zod";
export const guestSchema = baseUserSchema;
export type Guest = z.infer<typeof guestSchema>;
export const addGuestSchema = baseAddUserSchema;
export type AddGuestFormData = z.infer<typeof addGuestSchema>;
export const editGuestSchema = baseEditUserSchema;
export type EditGuestFormData = z.infer<typeof editGuestSchema>;
export const guestConfig = createUserConfig(
"guest",
"客户列表",
"添加客户",
"请输入客户姓名",
"请输入客户邮箱"
);

View File

@ -19,7 +19,7 @@ export const editProblemSchema = z.object({
}); });
export const problemConfig = { export const problemConfig = {
userType: "problem", resourceType: "problem",
title: "题目列表", title: "题目列表",
apiPath: "/api/problem", apiPath: "/api/problem",
columns: [ columns: [

View File

@ -0,0 +1,24 @@
import {
createUserConfig,
baseUserSchema,
baseAddUserSchema,
baseEditUserSchema,
} from "./base-config";
import { z } from "zod";
export const studentSchema = baseUserSchema;
export type Student = z.infer<typeof studentSchema>;
export const addStudentSchema = baseAddUserSchema;
export type AddStudentFormData = z.infer<typeof addStudentSchema>;
export const editStudentSchema = baseEditUserSchema;
export type EditStudentFormData = z.infer<typeof editStudentSchema>;
export const studentConfig = createUserConfig(
"student",
"学生列表",
"添加学生",
"请输入学生姓名",
"请输入学生邮箱"
);