update
Deploy / deploy (push) Canceled after 0s

This commit is contained in:
xuyue committed 2026-09-27 19:36:28 +08:00
1 parent 23844ae301
commit d25b5dbe6d
268 files changed
+3684 -8861

No files matched your search

+5 -14
View File
@@ -42,12 +42,8 @@ const missing: Array<{ language: string; target: string; node: string }> = []
for (const [language, table] of Object.entries(AST_NODE_TARGETS_BY_LANGUAGE)) {
const wasmPath = WASM_BY_LANGUAGE[language]
if (!wasmPath) {
console.log(
`⚠ ${language} 在 AST_NODE_TARGETS_BY_LANGUAGE 里,但这个脚本没有它的语法 wasm`,
)
console.log(
` 加语言时记得同步 WASM_BY_LANGUAGE 和 judge/ast.ts 的 loadLanguage`,
)
console.log(`⚠ ${language} 在 AST_NODE_TARGETS_BY_LANGUAGE 里,但这个脚本没有它的语法 wasm`)
console.log(` 加语言时记得同步 WASM_BY_LANGUAGE 和 judge/ast.ts 的 loadLanguage`)
process.exit(2)
}
const loaded = await Language.load(wasmPath)
@@ -59,8 +55,7 @@ for (const [language, table] of Object.entries(AST_NODE_TARGETS_BY_LANGUAGE)) {
}
for (const [target, entry] of Object.entries(table)) {
checked++
if (!declared.has(entry.node))
missing.push({ language, target, node: entry.node })
if (!declared.has(entry.node)) missing.push({ language, target, node: entry.node })
}
}
@@ -71,11 +66,7 @@ if (missing.length === 0) {
}
for (const { language, target, node } of missing) {
console.log(`\n⚠ ${language} 的 ${target} → "${node}"`)
console.log(
` 这个节点类型在语法里不存在,规则永远失败(或永远通过),且不报错`,
)
console.log(
` 改法:在 packages/contract/src/problem.ts 把它的 node 改成语法里真实的名字`,
)
console.log(` 这个节点类型在语法里不存在,规则永远失败(或永远通过),且不报错`)
console.log(` 改法:在 packages/contract/src/problem.ts 把它的 node 改成语法里真实的名字`)
}
process.exit(1)
+5 -14
View File
@@ -64,9 +64,7 @@ export function shadows(pattern: string, target: string) {
function collect(): Route[] {
const routerFile = new Map<string, string>()
for (const file of walk(SRC)) {
for (const m of readFileSync(file, "utf8").matchAll(
/export const (\w+) = new Hono/g,
)) {
for (const m of readFileSync(file, "utf8").matchAll(/export const (\w+) = new Hono/g)) {
routerFile.set(m[1]!, file)
}
}
@@ -96,14 +94,10 @@ function collect(): Route[] {
// 挂载顺序就是匹配顺序,所以必须按 index.ts 里出现的先后来摊平
const index = readFileSync(join(SRC, "index.ts"), "utf8")
const adminIndex = readFileSync(join(SRC, "routes/admin/index.ts"), "utf8")
const adminMounts = [
...adminIndex.matchAll(/\.route\(\s*"([^"]*)"\s*,\s*(\w+)\s*\)/g),
]
const adminMounts = [...adminIndex.matchAll(/\.route\(\s*"([^"]*)"\s*,\s*(\w+)\s*\)/g)]
const all: Route[] = []
for (const m of index.matchAll(
/app\.route\(\s*"([^"]+)"\s*,\s*(\w+)\s*\)/g,
)) {
for (const m of index.matchAll(/app\.route\(\s*"([^"]+)"\s*,\s*(\w+)\s*\)/g)) {
const [, prefix, router] = m
if (router === "adminRoutes") {
for (const a of adminMounts) all.push(...routesOf(a[2]!, prefix! + a[1]!))
@@ -119,8 +113,7 @@ const hits: [Route, Route][] = []
for (let i = 0; i < routes.length; i++) {
for (let j = i + 1; j < routes.length; j++) {
if (routes[i]!.method !== routes[j]!.method) continue
if (shadows(routes[i]!.path, routes[j]!.path))
hits.push([routes[i]!, routes[j]!])
if (shadows(routes[i]!.path, routes[j]!.path)) hits.push([routes[i]!, routes[j]!])
}
}
@@ -131,9 +124,7 @@ if (hits.length === 0) {
}
for (const [first, second] of hits) {
console.log(`\n⚠ ${second.method} ${second.path} (${second.file})`)
console.log(
` 进不去:被先注册的 ${first.method} ${first.path} 吃掉(${first.file})`,
)
console.log(` 进不去:被先注册的 ${first.method} ${first.path} 吃掉(${first.file})`)
console.log(` 改法:把它挪到那条之前注册,或换一个不同形的路径`)
}
process.exit(1)
+32 -48
View File
@@ -3,11 +3,7 @@ import { eq, sql } from "drizzle-orm"
import { db, schema } from "../db"
import { JudgeStatus, isAccepted } from "../judge/status"
import { asRecord } from "../routes/helpers"
import {
metaAchievements,
refreshUnlockedCount,
rescanAchievement,
} from "../services/achievements"
import { metaAchievements, refreshUnlockedCount, rescanAchievement } from "../services/achievements"
/**
* 把反范式的计数列重算回与 submission 表一致。
@@ -162,8 +158,8 @@ async function expectedProfiles() {
function stable(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`
if (value && typeof value === "object") {
const entries = Object.entries(value as Record<string, unknown>).sort(
([a], [b]) => (a < b ? -1 : 1),
const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) =>
a < b ? -1 : 1,
)
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stable(v)}`).join(",")}}`
}
@@ -210,13 +206,9 @@ async function unlockedCountPlan(plan: Plan) {
achievementId: schema.userAchievement.achievementId,
})
.from(schema.userAchievement)
.where(
sql`${schema.userAchievement.achievementId} in ${metas.map((meta) => meta.id)}`,
)
.where(sql`${schema.userAchievement.achievementId} in ${metas.map((meta) => meta.id)}`)
: []
const held = new Set(
holders.map((row) => `${row.userId}:${row.achievementId}`),
)
const held = new Set(holders.map((row) => `${row.userId}:${row.achievementId}`))
for (const row of rows) {
const label = `用户 ${row.user_id}`
@@ -231,9 +223,7 @@ async function unlockedCountPlan(plan: Plan) {
}
for (const meta of metas) {
const met =
meta.operator === "gte"
? row.actual >= meta.threshold
: row.actual <= meta.threshold
meta.operator === "gte" ? row.actual >= meta.threshold : row.actual <= meta.threshold
if (!met || held.has(`${row.user_id}:${meta.id}`)) continue
plan.diffs.push({
label,
@@ -248,29 +238,28 @@ async function unlockedCountPlan(plan: Plan) {
/** 只算差异,不写库。预演和落库后的复核共用它 —— 两边口径必须是同一份代码 */
async function computePlan(): Promise<Plan> {
const [problems, profiles, expectedProblem, expectedProfile] =
await Promise.all([
db
.select({
id: schema.problem.id,
displayId: schema.problem.displayId,
submissionNumber: schema.problem.submissionNumber,
acceptedNumber: schema.problem.acceptedNumber,
statisticInfo: schema.problem.statisticInfo,
})
.from(schema.problem),
db
.select({
id: schema.userProfile.id,
userId: schema.userProfile.userId,
submissionNumber: schema.userProfile.submissionNumber,
acceptedNumber: schema.userProfile.acceptedNumber,
acmProblemsStatus: schema.userProfile.acmProblemsStatus,
})
.from(schema.userProfile),
expectedProblems(),
expectedProfiles(),
])
const [problems, profiles, expectedProblem, expectedProfile] = await Promise.all([
db
.select({
id: schema.problem.id,
displayId: schema.problem.displayId,
submissionNumber: schema.problem.submissionNumber,
acceptedNumber: schema.problem.acceptedNumber,
statisticInfo: schema.problem.statisticInfo,
})
.from(schema.problem),
db
.select({
id: schema.userProfile.id,
userId: schema.userProfile.userId,
submissionNumber: schema.userProfile.submissionNumber,
acceptedNumber: schema.userProfile.acceptedNumber,
acmProblemsStatus: schema.userProfile.acmProblemsStatus,
})
.from(schema.userProfile),
expectedProblems(),
expectedProfiles(),
])
const plan: Plan = {
diffs: [],
@@ -304,9 +293,7 @@ async function computePlan(): Promise<Plan> {
after: want.acceptedNumber,
})
}
if (
stable(asRecord(problem.statisticInfo)) !== stable(want.statisticInfo)
) {
if (stable(asRecord(problem.statisticInfo)) !== stable(want.statisticInfo)) {
rows.push({
label,
field: "statistic_info",
@@ -332,8 +319,7 @@ async function computePlan(): Promise<Plan> {
const merged: Record<string, unknown> = { ...existing }
delete merged.problems
delete merged.contest_problems
for (const [bucket, value] of Object.entries(want.status))
merged[bucket] = value
for (const [bucket, value] of Object.entries(want.status)) merged[bucket] = value
const label = `用户 ${profile.userId}`
const rows: Diff[] = []
@@ -383,8 +369,7 @@ function report(plan: Plan) {
` ${diff.label} ${diff.field}: ${JSON.stringify(diff.before)} → ${JSON.stringify(diff.after)}`,
)
}
if (plan.diffs.length > 40)
console.log(` ……另有 ${plan.diffs.length - 40} 处`)
if (plan.diffs.length > 40) console.log(` ……另有 ${plan.diffs.length - 40} 处`)
}
/** 退出码:0 = 一致或预演正常,1 = 落库后复核仍有差异 */
@@ -427,8 +412,7 @@ export async function recount(options: { apply: boolean }) {
// 补发幂等(唯一键 + 冲突忽略),重跑不会重复发
const recounted = await refreshUnlockedCount(plan.unlockedCountFixes)
if (plan.metaGrants.length) {
for (const meta of await metaAchievements())
await rescanAchievement(meta.id)
for (const meta of await metaAchievements()) await rescanAchievement(meta.id)
}
console.log(
`\n已订正题目 ${plan.problemFixes.length} 道、用户 ${plan.profileFixes.length} 人、已解锁数 ${recounted.length} 人,补发元成就 ${plan.metaGrants.length} 条,复核中……`,
+2 -5
View File
@@ -9,8 +9,7 @@ import { db, schema } from "../db"
* 所以这里按 DATABASE_URL 的主机名拦一道,需要绕过时显式设 OJ2_SEED_FORCE=true。
*/
const url =
process.env.DATABASE_URL ??
"postgres://onlinejudge:onlinejudge@localhost:5433/onlinejudge"
process.env.DATABASE_URL ?? "postgres://onlinejudge:onlinejudge@localhost:5433/onlinejudge"
const host = (() => {
try {
return new URL(url).hostname
@@ -95,9 +94,7 @@ async function seed(account: SeedAccount) {
})
}
console.log(
` ${account.adminType.padEnd(13)} ${user.username} / ${account.password}`,
)
console.log(` ${account.adminType.padEnd(13)} ${user.username} / ${account.password}`)
}
console.log("Seeded development logins:")