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 {
ADMIN
GUEST
STUDENT
TEACHER
}
@ -90,7 +90,7 @@ model User {
password String?
emailVerified DateTime?
image String?
role Role @default(GUEST)
role Role @default(STUDENT)
accounts Account[]
sessions Session[]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,5 +5,5 @@ export default async function StudentCoursesLayout({
}: {
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 type { User } from "@/generated/client";
type UserType = "admin" | "teacher" | "guest";
type ResourceType = "admin" | "teacher" | "student";
export async function createUser(
userType: UserType,
resourceType: ResourceType,
data: Omit<User, "id" | "createdAt" | "updatedAt"> & { password?: string }
) {
let password = data.password;
@ -17,13 +17,13 @@ export async function createUser(
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 } });
revalidatePath(`/usermanagement/${userType}`);
revalidatePath(`/usermanagement/${resourceType}`);
}
export async function updateUser(
userType: UserType,
resourceType: ResourceType,
id: string,
data: Partial<Omit<User, "id" | "createdAt" | "updatedAt">>
) {
@ -38,10 +38,10 @@ export async function updateUser(
}
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 } });
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";
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";
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";
export default function GuestLayout({
export default function StudentLayout({
children,
}: {
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";
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) => {
return (
<ProtectedLayout roles={["ADMIN", "TEACHER", "GUEST"]}>
<ProtectedLayout roles={["ADMIN", "TEACHER", "STUDENT"]}>
{children}
</ProtectedLayout>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -95,14 +95,14 @@ export const basePagination = {
// 创建用户配置的工厂函数
export function createUserConfig(
userType: string,
resourceType: string,
title: string,
addLabel: string,
namePlaceholder: string,
emailPlaceholder: string
) {
return {
userType,
resourceType,
title,
apiPath: "/api/user",
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 = {
userType: "problem",
resourceType: "problem",
title: "题目列表",
apiPath: "/api/problem",
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",
"学生列表",
"添加学生",
"请输入学生姓名",
"请输入学生邮箱"
);