refactor(题单): 难度、状态、分数、提示、总分、百分比和 problemset_submission 表不再读写,先补默认值、下一步再删
Deploy / deploy (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
机房和服务器共用一个库、不一定同时升级,所以分两步退役:0027 只给还非空的几列补默认值, 新代码插行不用再填、没升级的那边照旧能跑;两个站点都换上新代码后,再单独一条迁移删列删表 (要手工放行破坏性迁移,语句写在 0027 的注释里)。 进度算法随之去掉分数和百分比,判题记进度不再往 problemset_submission 写一行(从来没人读)。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c17f88cdbb
commit
646c57c748
10 files changed
+3952
-181
No files matched your search
@@ -0,0 +1,18 @@
|
|||||||
|
-- 题单里不再用的几列 / 一张表,分两步退役(机房和服务器共用这个库、不一定同时升级):
|
||||||
|
--
|
||||||
|
-- 这一步(0027):代码已经不读不写它们了,这里只给还非空的几列补上默认值,让新代码
|
||||||
|
-- 插行时不用再填。没升级的那边照旧读写,互不影响。meta/0027_snapshot.json 里它们已经不在了。
|
||||||
|
--
|
||||||
|
-- 下一步(两个站点都换上新代码以后):另起一条 custom 迁移真正删掉,要手工放行破坏性迁移:
|
||||||
|
-- DROP TABLE "problemset_submission" CASCADE;
|
||||||
|
-- ALTER TABLE "problemset" DROP COLUMN "difficulty";
|
||||||
|
-- ALTER TABLE "problemset" DROP COLUMN "status";
|
||||||
|
-- ALTER TABLE "problemset_problem" DROP COLUMN "score";
|
||||||
|
-- ALTER TABLE "problemset_problem" DROP COLUMN "hint";
|
||||||
|
-- ALTER TABLE "problemset_progress" DROP COLUMN "progress_percentage";
|
||||||
|
-- ALTER TABLE "problemset_progress" DROP COLUMN "total_score";
|
||||||
|
ALTER TABLE "problemset" ALTER COLUMN "difficulty" SET DEFAULT 'Easy';--> statement-breakpoint
|
||||||
|
ALTER TABLE "problemset" ALTER COLUMN "status" SET DEFAULT 'active';--> statement-breakpoint
|
||||||
|
ALTER TABLE "problemset_problem" ALTER COLUMN "score" SET DEFAULT 0;--> statement-breakpoint
|
||||||
|
ALTER TABLE "problemset_progress" ALTER COLUMN "progress_percentage" SET DEFAULT 0;--> statement-breakpoint
|
||||||
|
ALTER TABLE "problemset_progress" ALTER COLUMN "total_score" SET DEFAULT 0;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -190,6 +190,13 @@
|
|||||||
"when": 1791553481647,
|
"when": 1791553481647,
|
||||||
"tag": "0026_problemset_assignment",
|
"tag": "0026_problemset_assignment",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 27,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1791554831341,
|
||||||
|
"tag": "0027_problemset_retire_columns",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -48,8 +48,6 @@ import type {
|
|||||||
ProblemDifficulty,
|
ProblemDifficulty,
|
||||||
ProblemLanguage,
|
ProblemLanguage,
|
||||||
ProblemPermission,
|
ProblemPermission,
|
||||||
ProblemSetDifficulty,
|
|
||||||
ProblemSetStatus,
|
|
||||||
ReactionKey,
|
ReactionKey,
|
||||||
SqlConfig,
|
SqlConfig,
|
||||||
SqlDisplay,
|
SqlDisplay,
|
||||||
@@ -414,8 +412,8 @@ export const problemset = pgTable(
|
|||||||
mode: "string",
|
mode: "string",
|
||||||
}).notNull(),
|
}).notNull(),
|
||||||
visible: boolean().notNull(),
|
visible: boolean().notNull(),
|
||||||
difficulty: text().notNull().$type<ProblemSetDifficulty>(),
|
// difficulty / status 两列 0027 起代码不再读写(线上全是 Easy / active,「状态 + 可见」
|
||||||
status: text().notNull().$type<ProblemSetStatus>(),
|
// 合成了「公开」),等两个站点都换上新代码再删列
|
||||||
createdById: integer("created_by_id").notNull(),
|
createdById: integer("created_by_id").notNull(),
|
||||||
/**
|
/**
|
||||||
* 布置期:assignedAt 是这一轮布置开始的时刻,assignedUntil 是布置到哪天(东八区当天结束)。
|
* 布置期:assignedAt 是这一轮布置开始的时刻,assignedUntil 是布置到哪天(东八区当天结束)。
|
||||||
@@ -456,8 +454,7 @@ export const problemsetProblem = pgTable(
|
|||||||
}),
|
}),
|
||||||
order: integer().notNull(),
|
order: integer().notNull(),
|
||||||
isRequired: boolean("is_required").notNull(),
|
isRequired: boolean("is_required").notNull(),
|
||||||
score: integer().notNull(),
|
// score / hint 两列 0027 起不再读写(分数拿掉了,提示一条都没人填过),待删
|
||||||
hint: text(),
|
|
||||||
problemId: integer("problem_id").notNull(),
|
problemId: integer("problem_id").notNull(),
|
||||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||||
problemsetId: bigint("problemset_id", { mode: "number" }).notNull(),
|
problemsetId: bigint("problemset_id", { mode: "number" }).notNull(),
|
||||||
@@ -502,10 +499,9 @@ export const problemsetProgress = pgTable(
|
|||||||
mode: "string",
|
mode: "string",
|
||||||
}),
|
}),
|
||||||
isCompleted: boolean("is_completed").notNull(),
|
isCompleted: boolean("is_completed").notNull(),
|
||||||
progressPercentage: doublePrecision("progress_percentage").notNull(),
|
// progress_percentage / total_score 两列 0027 起不再读写(百分比随用随算,分数拿掉了),待删
|
||||||
completedProblemsCount: integer("completed_problems_count").notNull(),
|
completedProblemsCount: integer("completed_problems_count").notNull(),
|
||||||
totalProblemsCount: integer("total_problems_count").notNull(),
|
totalProblemsCount: integer("total_problems_count").notNull(),
|
||||||
totalScore: integer("total_score").notNull(),
|
|
||||||
progressDetail: jsonb("progress_detail").notNull(),
|
progressDetail: jsonb("progress_detail").notNull(),
|
||||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||||
problemsetId: bigint("problemset_id", { mode: "number" }).notNull(),
|
problemsetId: bigint("problemset_id", { mode: "number" }).notNull(),
|
||||||
@@ -530,69 +526,7 @@ export const problemsetProgress = pgTable(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
export const problemsetSubmission = pgTable(
|
// problemset_submission 表 0027 起不再读写(每次做对都往里写一行,从来没有地方读它),待删
|
||||||
"problemset_submission",
|
|
||||||
{
|
|
||||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
|
||||||
id: bigint({ mode: "number" }).primaryKey().generatedByDefaultAsIdentity({
|
|
||||||
name: "problemset_submission_id_seq",
|
|
||||||
startWith: 1,
|
|
||||||
increment: 1,
|
|
||||||
minValue: 1,
|
|
||||||
maxValue: "9223372036854775807",
|
|
||||||
cache: 1,
|
|
||||||
}),
|
|
||||||
problemId: integer("problem_id").notNull(),
|
|
||||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
|
||||||
problemsetId: bigint("problemset_id", { mode: "number" }).notNull(),
|
|
||||||
submissionId: text("submission_id").notNull(),
|
|
||||||
userId: integer("user_id").notNull(),
|
|
||||||
},
|
|
||||||
(table) => [
|
|
||||||
index("problemset__problem_1f39fa_idx").using(
|
|
||||||
"btree",
|
|
||||||
table.problemsetId.asc().nullsLast().op("int4_ops"),
|
|
||||||
table.userId.asc().nullsLast().op("int4_ops"),
|
|
||||||
),
|
|
||||||
index("problemset__problem_22f053_idx").using(
|
|
||||||
"btree",
|
|
||||||
table.problemsetId.asc().nullsLast().op("int8_ops"),
|
|
||||||
table.problemId.asc().nullsLast().op("int8_ops"),
|
|
||||||
),
|
|
||||||
index("problemset__user_id_2f1501_idx").using(
|
|
||||||
"btree",
|
|
||||||
table.userId.asc().nullsLast().op("int4_ops"),
|
|
||||||
),
|
|
||||||
index("problemset_submission_problem_id_5629b105").using(
|
|
||||||
"btree",
|
|
||||||
table.problemId.asc().nullsLast().op("int4_ops"),
|
|
||||||
),
|
|
||||||
index("problemset_submission_submission_id_78e2b807").using(
|
|
||||||
"btree",
|
|
||||||
table.submissionId.asc().nullsLast().op("text_ops"),
|
|
||||||
),
|
|
||||||
foreignKey({
|
|
||||||
columns: [table.problemId],
|
|
||||||
foreignColumns: [problem.id],
|
|
||||||
name: "problemset_submission_problem_id_5629b105_fk_problem_id",
|
|
||||||
}).onDelete("cascade"),
|
|
||||||
foreignKey({
|
|
||||||
columns: [table.problemsetId],
|
|
||||||
foreignColumns: [problemset.id],
|
|
||||||
name: "problemset_submission_problemset_id_85290e17_fk_problemset_id",
|
|
||||||
}).onDelete("cascade"),
|
|
||||||
foreignKey({
|
|
||||||
columns: [table.submissionId],
|
|
||||||
foreignColumns: [submission.id],
|
|
||||||
name: "problemset_submission_submission_id_78e2b807_fk_submission_id",
|
|
||||||
}).onDelete("cascade"),
|
|
||||||
foreignKey({
|
|
||||||
columns: [table.userId],
|
|
||||||
foreignColumns: [user.id],
|
|
||||||
name: "problemset_submission_user_id_915fc9c6_fk_user_id",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
export const reaction = pgTable(
|
export const reaction = pgTable(
|
||||||
"reaction",
|
"reaction",
|
||||||
@@ -784,7 +718,7 @@ export const submission = pgTable(
|
|||||||
// 来源题单:学生从题单入口(/problemset/:id/problem/:pid)提交时记下来,
|
// 来源题单:学生从题单入口(/problemset/:id/problem/:pid)提交时记下来,
|
||||||
// 提交列表据此标出「这条来自题单」。**只是来源标记**,题单进度、奖章一概不看它,
|
// 提交列表据此标出「这条来自题单」。**只是来源标记**,题单进度、奖章一概不看它,
|
||||||
// 那些由判完之后的 recordSolvedProblem 按「已加入且含这道题的所有题单」记账。
|
// 那些由判完之后的 recordSolvedProblem 按「已加入且含这道题的所有题单」记账。
|
||||||
// 老数据里只有迁移 0007 从 problemset_submission 回填的首次 AC 有值。
|
// 老数据里只有迁移 0007 从(已废弃的)problemset_submission 回填的首次 AC 有值。
|
||||||
problemsetId: bigint("problemset_id", { mode: "number" }),
|
problemsetId: bigint("problemset_id", { mode: "number" }),
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
|
|||||||
@@ -83,12 +83,7 @@ export async function evaluateFlowchart(
|
|||||||
// 同一个口径(不看是从哪个入口交的)。放在推「评完了」之前:前端收到的时候进度已经
|
// 同一个口径(不看是从哪个入口交的)。放在推「评完了」之前:前端收到的时候进度已经
|
||||||
// 落库,1.5 秒后跳回题单页看到的就是新数据。比赛题不进题单
|
// 落库,1.5 秒后跳回题单页看到的就是新数据。比赛题不进题单
|
||||||
if (isFlowchartPass(result.grade) && row.problem.contestId === null) {
|
if (isFlowchartPass(result.grade) && row.problem.contestId === null) {
|
||||||
await recordSolvedAndNotify(
|
await recordSolvedAndNotify(row.flowchart.userId, row.problem.id, new Date().toISOString())
|
||||||
row.flowchart.userId,
|
|
||||||
row.problem.id,
|
|
||||||
null,
|
|
||||||
new Date().toISOString(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
await publishFlowchartUpdate(row.flowchart.userId, {
|
await publishFlowchartUpdate(row.flowchart.userId, {
|
||||||
type: "flowchart_evaluation_completed",
|
type: "flowchart_evaluation_completed",
|
||||||
|
|||||||
@@ -657,12 +657,7 @@ export async function judgeSubmission(job: JudgeJobData) {
|
|||||||
// 进度已经落库,跳回题单页看到的就是新数据。
|
// 进度已经落库,跳回题单页看到的就是新数据。
|
||||||
// 比赛题不进题单(题单加题时卡了 contestId IS NULL),跳过。
|
// 比赛题不进题单(题单加题时卡了 contestId IS NULL),跳过。
|
||||||
if (row.submission.contestId === null && isAccepted(result)) {
|
if (row.submission.contestId === null && isAccepted(result)) {
|
||||||
await recordSolvedAndNotify(
|
await recordSolvedAndNotify(row.submission.userId, row.problem.id, row.submission.createTime)
|
||||||
row.submission.userId,
|
|
||||||
row.problem.id,
|
|
||||||
row.submission.id,
|
|
||||||
row.submission.createTime,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -192,9 +192,6 @@ adminProblemSetRoutes.post("/problem-sets", requireTeacher, async (c) => {
|
|||||||
description: parsed.data.description,
|
description: parsed.data.description,
|
||||||
visible: parsed.data.visible,
|
visible: parsed.data.visible,
|
||||||
...period,
|
...period,
|
||||||
// 难度、状态两列还在表里、非空,但已经不用了(见契约 admin.ts 的说明)
|
|
||||||
difficulty: "Easy",
|
|
||||||
status: "active",
|
|
||||||
createdById: c.get("user")!.id,
|
createdById: c.get("user")!.id,
|
||||||
createTime: now,
|
createTime: now,
|
||||||
lastUpdateTime: now,
|
lastUpdateTime: now,
|
||||||
@@ -322,14 +319,7 @@ adminProblemSetRoutes.post("/problem-sets/:id/problems", requireTeacher, async (
|
|||||||
else {
|
else {
|
||||||
inSet.add(problem.id)
|
inSet.add(problem.id)
|
||||||
order += 1
|
order += 1
|
||||||
// 分数、提示两列还在表里但已经不用了:score 非空给 0
|
values.push({ problemsetId: row.id, problemId: problem.id, order, isRequired: true })
|
||||||
values.push({
|
|
||||||
problemsetId: row.id,
|
|
||||||
problemId: problem.id,
|
|
||||||
order,
|
|
||||||
isRequired: true,
|
|
||||||
score: 0,
|
|
||||||
})
|
|
||||||
result.added.push(problem.displayId)
|
result.added.push(problem.displayId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,21 +390,8 @@ adminProblemSetRoutes.delete("/problem-sets/:id/problems/:itemId", requireTeache
|
|||||||
eq(schema.problemsetProblem.problemsetId, row.id),
|
eq(schema.problemsetProblem.problemsetId, row.id),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.returning({
|
.returning({ id: schema.problemsetProblem.id })
|
||||||
id: schema.problemsetProblem.id,
|
|
||||||
problemId: schema.problemsetProblem.problemId,
|
|
||||||
})
|
|
||||||
if (deleted.length === 0) return failure(c, 404, "problem-not-in-set", "题目不在该题单中")
|
if (deleted.length === 0) return failure(c, 404, "problem-not-in-set", "题目不在该题单中")
|
||||||
// 这道题在本题单里的提交记录也要清掉,对齐旧栈 problemset/signals.py 的 post_delete。
|
|
||||||
// 不清的话 problemset_submission 会一直攒指向已移出题单的孤儿行。
|
|
||||||
await db
|
|
||||||
.delete(schema.problemsetSubmission)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(schema.problemsetSubmission.problemsetId, row.id),
|
|
||||||
eq(schema.problemsetSubmission.problemId, deleted[0]!.problemId),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
await resyncProgress(row.id)
|
await resyncProgress(row.id)
|
||||||
return success(c, null)
|
return success(c, null)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -368,7 +368,6 @@ problemsetRoutes.post("/problem-set-progress", requireAuth, async (c) => {
|
|||||||
const links = await tx
|
const links = await tx
|
||||||
.select({
|
.select({
|
||||||
problemId: schema.problemsetProblem.problemId,
|
problemId: schema.problemsetProblem.problemId,
|
||||||
score: schema.problemsetProblem.score,
|
|
||||||
isRequired: schema.problemsetProblem.isRequired,
|
isRequired: schema.problemsetProblem.isRequired,
|
||||||
})
|
})
|
||||||
.from(schema.problemsetProblem)
|
.from(schema.problemsetProblem)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { updateAchievementsForProblemSet } from "./achievements"
|
|||||||
|
|
||||||
type BadgeRow = typeof schema.problemsetBadge.$inferSelect
|
type BadgeRow = typeof schema.problemsetBadge.$inferSelect
|
||||||
type ProgressRow = typeof schema.problemsetProgress.$inferSelect
|
type ProgressRow = typeof schema.problemsetProgress.$inferSelect
|
||||||
type ProblemLink = { problemId: number; score: number; isRequired: boolean }
|
type ProblemLink = { problemId: number; isRequired: boolean }
|
||||||
type BadgeCheck = Pick<
|
type BadgeCheck = Pick<
|
||||||
ProgressRow,
|
ProgressRow,
|
||||||
"completedProblemsCount" | "totalProblemsCount" | "progressDetail"
|
"completedProblemsCount" | "totalProblemsCount" | "progressDetail"
|
||||||
@@ -16,10 +16,8 @@ type BadgeCheck = Pick<
|
|||||||
/**
|
/**
|
||||||
* 题单进度的唯一算法:学生做出一道题后的增量更新、后台改动题目后的批量重算,都走这一份。
|
* 题单进度的唯一算法:学生做出一道题后的增量更新、后台改动题目后的批量重算,都走这一份。
|
||||||
*
|
*
|
||||||
* 以前两边各写一遍,于是各自漂了一段。后台那份(resyncProgress)只更新分母和百分比:
|
* 以前两边各写一遍,于是各自漂了一段:后台那份不碰 is_completed(加一道题之后分母变大,
|
||||||
* - 改题目分值时调了它,可它根本不碰 total_score,score 类奖章按陈旧分数判定;
|
* 人还标着「已完成」)、不清理 progress_detail(删掉一道题之后没做的题被算成做了)。
|
||||||
* - 不碰 is_completed,加一道题之后分母变大、百分比掉下来,人还标着「已完成」;
|
|
||||||
* - 不清理 progress_detail,删掉一道题之后 least(completed, total) 会把没做的题算成做了。
|
|
||||||
* 两边不再分叉的唯一办法是只留一处算法,所以这里做成纯函数,两边都只是调用者。
|
* 两边不再分叉的唯一办法是只留一处算法,所以这里做成纯函数,两边都只是调用者。
|
||||||
*/
|
*/
|
||||||
export function computeProgress(
|
export function computeProgress(
|
||||||
@@ -28,20 +26,16 @@ export function computeProgress(
|
|||||||
previousCompleteTime: string | null,
|
previousCompleteTime: string | null,
|
||||||
now = new Date().toISOString(),
|
now = new Date().toISOString(),
|
||||||
) {
|
) {
|
||||||
const scoreByProblem = new Map(links.map((link) => [String(link.problemId), link.score]))
|
const inSet = new Set(links.map((link) => String(link.problemId)))
|
||||||
// 已经移出题单的题目要从 detail 里剔掉,留着它 completed 就会比实际做出的题还多
|
// 已经移出题单的题目要从 detail 里剔掉,留着它 completed 就会比实际做出的题还多。
|
||||||
|
// 老数据的每一项还带着 score(分数已经拿掉了),原样留着无害
|
||||||
const kept: Record<string, unknown> = {}
|
const kept: Record<string, unknown> = {}
|
||||||
let totalScore = 0
|
|
||||||
for (const [key, value] of Object.entries(detail)) {
|
for (const [key, value] of Object.entries(detail)) {
|
||||||
const score = scoreByProblem.get(key)
|
if (inSet.has(key)) kept[key] = value
|
||||||
if (score === undefined) continue
|
|
||||||
totalScore += score
|
|
||||||
// 分值以题单当前的设置为准,detail 里存的是做出那一刻的快照
|
|
||||||
kept[key] = { ...asRecord(value), score }
|
|
||||||
}
|
}
|
||||||
// 分母只算必做题。「(选做)」这个标签一直只是卡片上的一行字,进度分母和 all_problems
|
// 分母只算必做题。「(选做)」这个标签一直只是卡片上的一行字,进度分母和 all_problems
|
||||||
// 奖章照样要求做完 —— 快照里 22 个人做完了全部必做题,界面却显示未完成、全通奖章也拿不到
|
// 奖章照样要求做完 —— 快照里 22 个人做完了全部必做题,界面却显示未完成、全通奖章也拿不到
|
||||||
// (题单 5/6/8/11)。选做题做了仍然计分(totalScore 把它算进去),只是不卡完成。
|
// (题单 5/6/8/11)。选做题做对了照样算进「做对 N 道」奖章,只是不卡完成。
|
||||||
//
|
//
|
||||||
// 一道必做都没标的题单退回「全部都算必做」:那种题单多半是没用这个字段,而不是
|
// 一道必做都没标的题单退回「全部都算必做」:那种题单多半是没用这个字段,而不是
|
||||||
// 真的整单选做;不兜住的话它永远完不成。
|
// 真的整单选做;不兜住的话它永远完不成。
|
||||||
@@ -57,9 +51,6 @@ export function computeProgress(
|
|||||||
progressDetail: kept,
|
progressDetail: kept,
|
||||||
totalProblemsCount: total,
|
totalProblemsCount: total,
|
||||||
completedProblemsCount: completed,
|
completedProblemsCount: completed,
|
||||||
totalScore,
|
|
||||||
// 乘 10000 四舍五入再除 100,保留两位小数
|
|
||||||
progressPercentage: total > 0 ? Math.round((completed / total) * 10000) / 100 : 0,
|
|
||||||
isCompleted,
|
isCompleted,
|
||||||
// 只设不清,语义是「曾经完成于」,对齐旧栈 problemset/models.py:218。
|
// 只设不清,语义是「曾经完成于」,对齐旧栈 problemset/models.py:218。
|
||||||
//
|
//
|
||||||
@@ -78,7 +69,7 @@ type ProgressWrite = ReturnType<typeof computeProgress> & { id: number }
|
|||||||
* 而每行要写的值都已经在内存里算好了,没有一个依赖数据库现有的值。
|
* 而每行要写的值都已经在内存里算好了,没有一个依赖数据库现有的值。
|
||||||
*/
|
*/
|
||||||
async function writeProgress(rows: ProgressWrite[]) {
|
async function writeProgress(rows: ProgressWrite[]) {
|
||||||
// 每行 8 个参数,留足余量避开 Postgres 的 65535 个绑定参数上限
|
// 每行 6 个参数,留足余量避开 Postgres 的 65535 个绑定参数上限
|
||||||
for (let start = 0; start < rows.length; start += 1000) {
|
for (let start = 0; start < rows.length; start += 1000) {
|
||||||
const chunk = rows.slice(start, start + 1000)
|
const chunk = rows.slice(start, start + 1000)
|
||||||
const values = sql.join(
|
const values = sql.join(
|
||||||
@@ -88,8 +79,6 @@ async function writeProgress(rows: ProgressWrite[]) {
|
|||||||
${JSON.stringify(row.progressDetail)}::jsonb,
|
${JSON.stringify(row.progressDetail)}::jsonb,
|
||||||
${row.totalProblemsCount}::int,
|
${row.totalProblemsCount}::int,
|
||||||
${row.completedProblemsCount}::int,
|
${row.completedProblemsCount}::int,
|
||||||
${row.totalScore}::int,
|
|
||||||
${row.progressPercentage}::double precision,
|
|
||||||
${row.isCompleted}::boolean,
|
${row.isCompleted}::boolean,
|
||||||
${row.completeTime}::timestamptz
|
${row.completeTime}::timestamptz
|
||||||
)`,
|
)`,
|
||||||
@@ -101,12 +90,10 @@ async function writeProgress(rows: ProgressWrite[]) {
|
|||||||
progress_detail = v.detail,
|
progress_detail = v.detail,
|
||||||
total_problems_count = v.total_count,
|
total_problems_count = v.total_count,
|
||||||
completed_problems_count = v.completed_count,
|
completed_problems_count = v.completed_count,
|
||||||
total_score = v.total_score,
|
|
||||||
progress_percentage = v.percentage,
|
|
||||||
is_completed = v.is_completed,
|
is_completed = v.is_completed,
|
||||||
complete_time = v.complete_time
|
complete_time = v.complete_time
|
||||||
from (values ${values}) as v(
|
from (values ${values}) as v(
|
||||||
id, detail, total_count, completed_count, total_score, percentage, is_completed, complete_time
|
id, detail, total_count, completed_count, is_completed, complete_time
|
||||||
)
|
)
|
||||||
where pg.id = v.id
|
where pg.id = v.id
|
||||||
`)
|
`)
|
||||||
@@ -202,7 +189,6 @@ export async function resyncProgress(problemsetId: number) {
|
|||||||
db
|
db
|
||||||
.select({
|
.select({
|
||||||
problemId: schema.problemsetProblem.problemId,
|
problemId: schema.problemsetProblem.problemId,
|
||||||
score: schema.problemsetProblem.score,
|
|
||||||
isRequired: schema.problemsetProblem.isRequired,
|
isRequired: schema.problemsetProblem.isRequired,
|
||||||
})
|
})
|
||||||
.from(schema.problemsetProblem)
|
.from(schema.problemsetProblem)
|
||||||
@@ -237,17 +223,7 @@ export async function resyncProgress(problemsetId: number) {
|
|||||||
*
|
*
|
||||||
* 不按 visible / status 过滤:进度是学生自己的记录,老师把题单藏起来不该让它停止累积。
|
* 不按 visible / status 过滤:进度是学生自己的记录,老师把题单藏起来不该让它停止累积。
|
||||||
*/
|
*/
|
||||||
export async function recordSolvedProblem(
|
export async function recordSolvedProblem(userId: number, problemId: number, solvedAt: string) {
|
||||||
userId: number,
|
|
||||||
problemId: number,
|
|
||||||
/**
|
|
||||||
* 做出来的那次代码提交。流程图画到 A/S 算完成时传 null:problemset_submission.submission_id
|
|
||||||
* 外键指向代码提交表,流程图提交的 id 进不去。不影响防抄闸门的解锁 ——
|
|
||||||
* 那道闸看的是 progress_detail(本文件下面的 problemSetLockCutoffs)
|
|
||||||
*/
|
|
||||||
submissionId: string | null,
|
|
||||||
solvedAt: string,
|
|
||||||
) {
|
|
||||||
const joined = await db
|
const joined = await db
|
||||||
.select({ problemsetId: schema.problemsetProgress.problemsetId })
|
.select({ problemsetId: schema.problemsetProgress.problemsetId })
|
||||||
.from(schema.problemsetProgress)
|
.from(schema.problemsetProgress)
|
||||||
@@ -276,37 +252,17 @@ export async function recordSolvedProblem(
|
|||||||
.limit(1)
|
.limit(1)
|
||||||
if (!progress) return []
|
if (!progress) return []
|
||||||
|
|
||||||
// 提交记录先补上,即使这道题早就记过 —— 老数据里有记了进度没记提交的行
|
|
||||||
const [existing] = await tx
|
|
||||||
.select({ id: schema.problemsetSubmission.id })
|
|
||||||
.from(schema.problemsetSubmission)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(schema.problemsetSubmission.problemsetId, problemsetId),
|
|
||||||
eq(schema.problemsetSubmission.userId, userId),
|
|
||||||
eq(schema.problemsetSubmission.problemId, problemId),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.limit(1)
|
|
||||||
if (!existing && submissionId !== null) {
|
|
||||||
await tx
|
|
||||||
.insert(schema.problemsetSubmission)
|
|
||||||
.values({ problemsetId, userId, submissionId, problemId })
|
|
||||||
}
|
|
||||||
|
|
||||||
const detail = asRecord(progress.progressDetail)
|
const detail = asRecord(progress.progressDetail)
|
||||||
if (String(problemId) in detail) return []
|
if (String(problemId) in detail) return []
|
||||||
const links = await tx
|
const links = await tx
|
||||||
.select({
|
.select({
|
||||||
problemId: schema.problemsetProblem.problemId,
|
problemId: schema.problemsetProblem.problemId,
|
||||||
score: schema.problemsetProblem.score,
|
|
||||||
isRequired: schema.problemsetProblem.isRequired,
|
isRequired: schema.problemsetProblem.isRequired,
|
||||||
})
|
})
|
||||||
.from(schema.problemsetProblem)
|
.from(schema.problemsetProblem)
|
||||||
.where(eq(schema.problemsetProblem.problemsetId, problemsetId))
|
.where(eq(schema.problemsetProblem.problemsetId, problemsetId))
|
||||||
const link = links.find((item) => item.problemId === problemId)
|
if (!links.some((item) => item.problemId === problemId)) return []
|
||||||
if (!link) return []
|
detail[String(problemId)] = { submit_time: solvedAt }
|
||||||
detail[String(problemId)] = { score: link.score, submit_time: solvedAt }
|
|
||||||
const update = computeProgress(detail, links, progress.completeTime)
|
const update = computeProgress(detail, links, progress.completeTime)
|
||||||
await tx
|
await tx
|
||||||
.update(schema.problemsetProgress)
|
.update(schema.problemsetProgress)
|
||||||
@@ -347,14 +303,9 @@ export async function recordSolvedProblem(
|
|||||||
* 流程图评分(flowchart/run.ts)两条路都在「这道题做完了」的那一刻调它。
|
* 流程图评分(flowchart/run.ts)两条路都在「这道题做完了」的那一刻调它。
|
||||||
* 记账失败不往外抛:判题 / 评分本身已经落库,不能因为进度没记上就把整次判题算失败。
|
* 记账失败不往外抛:判题 / 评分本身已经落库,不能因为进度没记上就把整次判题算失败。
|
||||||
*/
|
*/
|
||||||
export async function recordSolvedAndNotify(
|
export async function recordSolvedAndNotify(userId: number, problemId: number, solvedAt: string) {
|
||||||
userId: number,
|
|
||||||
problemId: number,
|
|
||||||
submissionId: string | null,
|
|
||||||
solvedAt: string,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const { updated, earned } = await recordSolvedProblem(userId, problemId, submissionId, solvedAt)
|
const { updated, earned } = await recordSolvedProblem(userId, problemId, solvedAt)
|
||||||
if (earned.length > 0) {
|
if (earned.length > 0) {
|
||||||
await publishAchievementNotification(
|
await publishAchievementNotification(
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
@@ -359,11 +359,6 @@ export const updateAcmHelperRequestSchema = z.object({
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- 题单管理
|
// ---------------------------------------------------------------- 题单管理
|
||||||
|
|
||||||
// 难度、状态两列还留在表里(0026 没删列,删列要手工放行破坏性迁移),
|
|
||||||
// 但界面和接口都不再用:线上 16 个题单全是 Easy / active,「状态 + 可见」两道闸合成了「公开」
|
|
||||||
export const problemSetDifficultySchema = z.enum(["Easy", "Medium", "Hard"])
|
|
||||||
export const problemSetStatusSchema = z.enum(["draft", "active", "archived"])
|
|
||||||
|
|
||||||
export const adminProblemSetSchema = z.object({
|
export const adminProblemSetSchema = z.object({
|
||||||
id: z.number().int(),
|
id: z.number().int(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
@@ -951,8 +946,6 @@ export type UploadImageResponse = z.infer<typeof uploadImageResponseSchema>
|
|||||||
export type CreateContestRequest = z.infer<typeof createContestRequestSchema>
|
export type CreateContestRequest = z.infer<typeof createContestRequestSchema>
|
||||||
export type UpdateContestRequest = z.infer<typeof updateContestRequestSchema>
|
export type UpdateContestRequest = z.infer<typeof updateContestRequestSchema>
|
||||||
export type UpdateAcmHelperRequest = z.infer<typeof updateAcmHelperRequestSchema>
|
export type UpdateAcmHelperRequest = z.infer<typeof updateAcmHelperRequestSchema>
|
||||||
export type ProblemSetDifficulty = z.infer<typeof problemSetDifficultySchema>
|
|
||||||
export type ProblemSetStatus = z.infer<typeof problemSetStatusSchema>
|
|
||||||
export type AdminProblemSet = z.infer<typeof adminProblemSetSchema>
|
export type AdminProblemSet = z.infer<typeof adminProblemSetSchema>
|
||||||
export type AdminProblemSetList = z.infer<typeof adminProblemSetListSchema>
|
export type AdminProblemSetList = z.infer<typeof adminProblemSetListSchema>
|
||||||
export type CreateProblemSetRequest = z.infer<typeof createProblemSetRequestSchema>
|
export type CreateProblemSetRequest = z.infer<typeof createProblemSetRequestSchema>
|
||||||
|
|||||||
Reference in new issue
Block a user