refactor(后端): 权限判断收到一处,路由文件之间不再互相 import
- services/permissions.ts:「超管或创建者」原来在比赛、题单、比赛题目(4 处)、 services/contest.ts 各抄一遍,「题目全部权限」在题目和标签两个路由各写一份 - services/submission-filters.ts:提交列表的用户名 / 班级 / 题号筛选,原来 submission.ts、flowchart.ts 从 submission-statistics.ts 这个路由文件里拿 - services/class-activity.ts:课堂做题记录的口径和「正在上课的班」,原来后台 conf.ts 从 classroom.ts 路由里拿 - services/problem-tags.ts:标签名去重、按名查标签,原来 tag.ts 从 admin/problem.ts 拿 纯搬家。三种身份(超管 / 老师 / 学生)打了 26 个受影响的接口,改前改后状态码和响应体 逐字节一致,包括「不是自己的比赛 / 题单 / 题目报不存在」那几条。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5787efe023
commit
465dbfd42f
14 files changed
+388
-361
No files matched your search
@@ -11,7 +11,6 @@ import {
|
||||
type LastVisit,
|
||||
type LessonLanguage,
|
||||
type StatisticInfo,
|
||||
FLOWCHART_PASS_GRADES,
|
||||
} from "@oj2/contract"
|
||||
import { and, asc, desc, eq, gte, inArray, isNotNull, isNull, lt, sql } from "drizzle-orm"
|
||||
import { Hono } from "hono"
|
||||
@@ -22,7 +21,14 @@ import { getLoginWindow } from "../auth/session"
|
||||
import { presenceOf } from "../collab/state"
|
||||
import { db, schema } from "../db"
|
||||
import { failure, parseBody, success } from "../http"
|
||||
import { JudgeStatus } from "../judge/status"
|
||||
import {
|
||||
activityRows,
|
||||
classMemberIds,
|
||||
FLOWCHART_PASSED,
|
||||
lessonLanguageFilter,
|
||||
SOLVED_RESULTS,
|
||||
suggestActiveClass,
|
||||
} from "../services/class-activity"
|
||||
import { accepted } from "../services/learning-stats"
|
||||
import { calendarDay, dayStart, localTime } from "../time"
|
||||
|
||||
@@ -37,9 +43,6 @@ const CLASS_ACTIVITY_MIN_USERS = 5
|
||||
/** 往回找几天:上完课第二天在家补作业、请假回来的学生,都还能看到上一次课做了什么 */
|
||||
const CLASS_ACTIVITY_LOOKBACK_DAYS = 7
|
||||
const CLASS_ACTIVITY_LIMIT = 8
|
||||
/** AST_CHECK_FAILED 也是答案对了,与周榜同口径 */
|
||||
const SOLVED_RESULTS = [JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]
|
||||
|
||||
interface ClassDayGroup {
|
||||
day: string
|
||||
problemId: number
|
||||
@@ -48,93 +51,6 @@ interface ClassDayGroup {
|
||||
acceptedCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 流程图画到 A / S 算这道题做完(设计文档 2026-09-28-problem-page-redesign 第 3 节决定 6)。
|
||||
* 等级由分数推出(flowchart/grade.ts),重新评分会先把 grade 清空,所以 status = 2 其实
|
||||
* 是冗余的,留着是把「评完了」写明。
|
||||
*/
|
||||
const FLOWCHART_PASSED = and(
|
||||
eq(schema.flowchartSubmission.status, 2),
|
||||
inArray(schema.flowchartSubmission.aiGrade, [...FLOWCHART_PASS_GRADES]),
|
||||
)!
|
||||
|
||||
/** 这个班正常状态的学生,给下面的查询当 `user_id in (...)` 用 */
|
||||
function classMemberIds(className: string) {
|
||||
return db
|
||||
.select({ id: schema.user.id })
|
||||
.from(schema.user)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.user.className, className),
|
||||
eq(schema.user.isDisabled, false),
|
||||
inArray(schema.user.adminType, [...STUDENT_ROLES]),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 做题记录:代码提交(不含比赛)和流程图提交并成一张,一次提交一行。课堂上的统计
|
||||
* (这节课是哪几道题、班上几人做完、最近几节课来了几次、哪个班在上课)都从这里数,
|
||||
* 口径和课堂条 / 看板的「做完」一致:
|
||||
*
|
||||
* - 交过就算「动过手」:代码提交不论结果,流程图不论评没评完、评没评失败;
|
||||
* - `solved`:代码 AC / AST_CHECK_FAILED,流程图评到 A / S。
|
||||
*
|
||||
* 有的题一节课全班都在画流程图、代码提交个位数,只数代码的话这节课在统计里等于没上。
|
||||
* 两边都按 `user_id in (...)` + `create_time >=` 走各自的 (user_id, create_time) 索引。
|
||||
*/
|
||||
/**
|
||||
* 老师布置时选了语言:代码只数这个语言交的。SQL 题例外 —— 它只收 SQL,按 C 去数的话,
|
||||
* C 的作业里夹一道 SQL 题,学生怎么交都算不了做完。非 SQL 题交不进 SQL(提交接口按题目的
|
||||
* 语言拦),所以一律放行 SQL 不会把别的题算宽
|
||||
*/
|
||||
function lessonLanguageFilter(language: LessonLanguage | null | undefined) {
|
||||
return language ? inArray(schema.submission.language, [language, "SQL"]) : undefined
|
||||
}
|
||||
|
||||
function activityRows(options: {
|
||||
since: string
|
||||
users?: number[] | ReturnType<typeof classMemberIds>
|
||||
problemIds?: number[]
|
||||
/** 老师布置时选了语言:代码只数这个语言交的(见 lessonLanguageFilter),流程图不受影响 */
|
||||
language?: LessonLanguage | null
|
||||
}) {
|
||||
const { since, users, problemIds, language } = options
|
||||
const code = db
|
||||
.select({
|
||||
userId: schema.submission.userId,
|
||||
problemId: schema.submission.problemId,
|
||||
createTime: schema.submission.createTime,
|
||||
solved: sql<boolean>`${inArray(schema.submission.result, SOLVED_RESULTS)}`.as("solved"),
|
||||
})
|
||||
.from(schema.submission)
|
||||
.where(
|
||||
and(
|
||||
isNull(schema.submission.contestId),
|
||||
gte(schema.submission.createTime, since),
|
||||
users ? inArray(schema.submission.userId, users) : undefined,
|
||||
problemIds ? inArray(schema.submission.problemId, problemIds) : undefined,
|
||||
lessonLanguageFilter(language),
|
||||
),
|
||||
)
|
||||
const flowchart = db
|
||||
.select({
|
||||
userId: schema.flowchartSubmission.userId,
|
||||
problemId: schema.flowchartSubmission.problemId,
|
||||
createTime: schema.flowchartSubmission.createTime,
|
||||
solved: sql<boolean>`${FLOWCHART_PASSED}`.as("solved"),
|
||||
})
|
||||
.from(schema.flowchartSubmission)
|
||||
.where(
|
||||
and(
|
||||
gte(schema.flowchartSubmission.createTime, since),
|
||||
users ? inArray(schema.flowchartSubmission.userId, users) : undefined,
|
||||
problemIds ? inArray(schema.flowchartSubmission.problemId, problemIds) : undefined,
|
||||
),
|
||||
)
|
||||
return code.unionAll(flowchart).as("activity")
|
||||
}
|
||||
|
||||
/**
|
||||
* 某个班从 `since` 起、按(东八区日, 题)分组的做题人数和通过人数。只算正常状态的学生,
|
||||
* 不含比赛提交,代码和流程图合起来数(见 activityRows)。`minUsers` 是「班里在做」的门槛,
|
||||
@@ -468,36 +384,6 @@ const RECENT_LESSONS = 5
|
||||
/** 往回找最近几节课时最多看多远:寒暑假之后开学第一节课,前面几节是上学期的,没意义 */
|
||||
const RECENT_LESSONS_WINDOW_DAYS = 45
|
||||
|
||||
/** 看板没指定班级时,猜「最近这么久里提交人数最多的班」—— 老师多半正在上这个班的课 */
|
||||
const ACTIVE_CLASS_WINDOW_MS = 2 * 60 * 60 * 1000
|
||||
|
||||
/**
|
||||
* 从实际提交猜正在上课的班,而**不是记住老师上次选的**:统计面板吃过那个亏(上一节课
|
||||
* 的班悄悄留在框里,老师看的整个是别人的班)。
|
||||
*/
|
||||
export async function suggestActiveClass() {
|
||||
// 流程图也算:一节课全班都在画流程图时,只数代码提交就猜成了别的班
|
||||
const activity = activityRows({
|
||||
since: new Date(Date.now() - ACTIVE_CLASS_WINDOW_MS).toISOString(),
|
||||
})
|
||||
const users = sql<number>`count(distinct ${activity.userId})::int`
|
||||
const [row] = await db
|
||||
.select({ className: schema.user.className, users })
|
||||
.from(activity)
|
||||
.innerJoin(schema.user, eq(schema.user.id, activity.userId))
|
||||
.where(
|
||||
and(
|
||||
isNotNull(schema.user.className),
|
||||
eq(schema.user.isDisabled, false),
|
||||
inArray(schema.user.adminType, [...STUDENT_ROLES]),
|
||||
),
|
||||
)
|
||||
.groupBy(schema.user.className)
|
||||
.orderBy(desc(users))
|
||||
.limit(1)
|
||||
return row?.className ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* 课堂看板:这个班、今天、这节课的几道题 × 全班学生。老师在课上用它看谁还没开始、
|
||||
* 谁卡住了 —— 2025 秋平均每节课有 14 个平时在用的学生一道都没交(占三分之一),
|
||||
|
||||
Reference in new issue
Block a user