fix(学生管理员): 260 班看板 23 人、数据统计只有 22 人,学生管理员还能看同学的作业代码——学生管理员一律按学生算
Deploy / deploy (push) Canceled after 0s

- 统计、题目页各班表、流程图统计的班级名单原来只认普通用户,改成和看板、排行榜一样的
  STUDENT_ROLES,学生管理员进名单、进分母
- 看别人的提交代码和流程图只给老师、超管和出题人;题单防抄对学生管理员也生效。
  原来「比赛进行中学生管理员不能看」那条特例随之去掉
- 题单的「用户进度」原来只给题单创建者,其余老师点开报「题单不存在」;改成所有老师都能看,
  和看板、统计对所有班开放一致。后台能改能删的接口仍只给创建者

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-08 14:00:11 +08:00
1 parent 30f34f0e36
commit af2ca31de5
6 files changed
+52 -47

No files matched your search

+14 -7
View File
@@ -11,6 +11,7 @@ import {
type FlowchartStatistics,
type FlowchartSubmission,
NO_CLASS,
STUDENT_ROLES,
} from "@oj2/contract"
import { and, asc, count, desc, eq, inArray, isNull, sql, type SQL } from "drizzle-orm"
import { Hono } from "hono"
@@ -57,7 +58,7 @@ function hiddenByProblemSet(
row: { userId: number; problemId: number; createTime: string },
joinTimes: Map<number, string>,
) {
if (row.userId !== user.id || isAdminRole(user)) return false
if (row.userId !== user.id || isTeacherOrAbove(user)) return false
const joinTime = joinTimes.get(row.problemId)
return joinTime !== undefined && Date.parse(row.createTime) < Date.parse(joinTime)
}
@@ -69,12 +70,13 @@ function canView(
joinTimes: Map<number, string>,
) {
if (hiddenByProblemSet(user, row, joinTimes)) return false
return row.userId === user.id || isAdminRole(user) || problem.createdById === user.id
// 角色捷径只给老师:学生管理员算学生,不能看同学的流程图(同 submission.ts 的 canViewSubmission)
return row.userId === user.id || isTeacherOrAbove(user) || problem.createdById === user.id
}
/** 只有学生看自己的提交时才需要查题单;其余情况给一张空表,省一次查询 */
function joinTimesFor(user: AuthUser, rows: Array<{ userId: number; problemId: number }>) {
if (isAdminRole(user)) return Promise.resolve(new Map<number, string>())
if (isTeacherOrAbove(user)) return Promise.resolve(new Map<number, string>())
const own = rows.filter((row) => row.userId === user.id).map((row) => row.problemId)
return problemSetJoinTimes(user.id, [...new Set(own)])
}
@@ -287,7 +289,8 @@ flowchartRoutes.get("/flowcharts", requireAuth, async (c) => {
// 「只看自己」盖过用户名;普通学生不填用户名时也只看自己
const className = c.req.query("className")?.trim()
const onlyMyself =
c.req.query("myself") === "1" || (!username && !className && user.adminType === "Regular User")
c.req.query("myself") === "1" ||
(!username && !className && STUDENT_ROLES.includes(user.adminType))
const filters: Array<SQL | undefined> = []
filters.push(
...(await Promise.all([
@@ -430,7 +433,7 @@ flowchartRoutes.get("/flowcharts/statistics", requireTeacher, async (c) => {
// 不相干的人,当没有花名册
const roster =
scoped && c.req.query("className")?.trim() !== NO_CLASS
? matched.filter((row) => !row.isDisabled && row.adminType === "Regular User")
? matched.filter((row) => !row.isDisabled && STUDENT_ROLES.includes(row.adminType))
: []
/**
@@ -532,7 +535,11 @@ flowchartRoutes.get("/flowcharts/statistics", requireTeacher, async (c) => {
.from(schema.flowchartSubmission)
.innerJoin(schema.user, eq(schema.user.id, schema.flowchartSubmission.userId))
.where(
and(where, eq(schema.user.adminType, "Regular User"), eq(schema.user.isDisabled, false)),
and(
where,
inArray(schema.user.adminType, [...STUDENT_ROLES]),
eq(schema.user.isDisabled, false),
),
)
.groupBy(schema.user.id, schema.user.username, schema.user.className)
.orderBy(sql`max(${schema.flowchartSubmission.aiScore}) asc nulls first`)
@@ -664,7 +671,7 @@ flowchartRoutes.post("/flowcharts/:id/retry", requireAuth, async (c) => {
.limit(1)
if (!row || !canView(user, row.flowchart, row.problem, await joinTimesFor(user, [row.flowchart])))
return failure(c, 404, "flowchart-not-found", "Submission does not exist")
// 看得见不等于能重评:canView 放进来的学生管理员、出题人只能看。重评会清掉原来的
// 看得见不等于能重评:canView 放进来的出题人只能看。重评会清掉原来的
// 评分再抽一次,别人的提交只有老师能动
if (row.flowchart.userId !== user.id && !isTeacherOrAbove(user))
return failure(c, 403, "permission-denied", "权限不足")