- 部署:JUDGE_STATE_DIR 不再强制设;删掉搬库脚本和只为搬库存在的自检、提醒 - 语言:Python3 / Python2 别名表删掉,normalizeLanguage 改名 parseLanguage - 旧链接:后台题单详情的跳转、用户列表 -last_login 的转换 - 旧页面:布置作业必须带 language(null = 没选);不再清旧版出题页草稿 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e074f3ab8
commit
8427475337
23 files changed
+47
-386
No files matched your search
@@ -121,8 +121,8 @@ QingdaoU/JudgeServer 停更在 2024-04(官方镜像的 `latest` 和 `1.6.1`
|
||||
契约 `judgeLanguageSchema` 里的键留着是为了渲染那 62 条历史提交。
|
||||
**`Python3` / `Python2` 这两个旧值已经没有了** —— 0019 迁移把 104530 条提交、937 道题、
|
||||
1235 个用户的成就指标并成了一个 `Python`,0020 顺手把那三种语言从题目的可选语言里摘掉
|
||||
(不摘的话 84 道题的语言下拉还能选 Java,提交必 SYSTEM_ERROR)。查判题配置走
|
||||
`judgeConfigFor()`,它带旧值别名;**回滚要连数据一起回**,只滚代码会让 Python 提交全炸。
|
||||
(不摘的话 84 道题的语言下拉还能选 Java,提交必 SYSTEM_ERROR)。代码里也不再认旧值(别名表 2026-10 删了),
|
||||
**回滚到 0019 之前要连数据一起回**,只滚代码会让 Python 提交全炸。
|
||||
- 换完镜像跑 `bun docker/judge/smoke.ts`:三种语言、六种状态码、gcc 宽松度一起核。
|
||||
|
||||
### AST 代码规则:一张表,外加一个机器检查
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
* 所以内存态够用,不需要 Redis 同步。进程重启丢掉全部状态,两端重连后回到干净状态。
|
||||
*/
|
||||
|
||||
import { normalizeLanguage } from '@oj2/contract'
|
||||
|
||||
export type CollabSocket = Bun.ServerWebSocket<import('../websocket').SubmissionSocketData>
|
||||
|
||||
/**
|
||||
@@ -19,14 +17,10 @@ export const COLLAB_LANGUAGES = ['C', 'C++', 'Python', 'Java', 'JavaScript', 'Go
|
||||
export type CollabLanguage = (typeof COLLAB_LANGUAGES)[number]
|
||||
|
||||
/**
|
||||
* 认不出来的一律当 C:老客户端不带这个字段,而它以前就是写死 C 的。
|
||||
*
|
||||
* 先过契约的别名表 —— 上线那一刻学生页面里还揣着 `Python3`,不翻译的话会**静默**
|
||||
* 落到 C,求助窗口里的代码高亮和同步编辑都按 C 走,没人会报错。
|
||||
* 认不出来的一律当 C。
|
||||
*/
|
||||
export function normalizeCollabLanguage(value: unknown): CollabLanguage {
|
||||
const normalized = normalizeLanguage(value) ?? value
|
||||
return (COLLAB_LANGUAGES as readonly string[]).includes(normalized as string) ? (normalized as CollabLanguage) : 'C'
|
||||
return (COLLAB_LANGUAGES as readonly string[]).includes(value as string) ? (value as CollabLanguage) : 'C'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1327,8 +1327,7 @@ export const classLesson = pgTable(
|
||||
createdBy: integer('created_by').notNull(),
|
||||
/**
|
||||
* 这天第一次布置的时刻,之后「改」只动 updatedAt。清掉(删行)再布置算重新开始。
|
||||
* 加这一列之前布置的用 updated_at 回填(0034)。带默认值是给共库的另一个站点:
|
||||
* 机房那套还没升级时,它的旧代码插入不带这一列
|
||||
* 加这一列之前布置的用 updated_at 回填(0034)
|
||||
*/
|
||||
createdAt: timestamp('created_at', {
|
||||
withTimezone: true,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { normalizeLanguage } from '@oj2/contract'
|
||||
|
||||
/**
|
||||
* 判题沙箱认得的语言,**只有 C / C++ / Python 这三种**。
|
||||
*
|
||||
@@ -11,8 +9,7 @@ import { normalizeLanguage } from '@oj2/contract'
|
||||
* 想恢复某种语言,得同时改这里和 `docker/judge/Dockerfile` 的工具链,再重建镜像。
|
||||
*
|
||||
* `Python` 这个键 2026-09 之前叫 `Python3`(库里还有 3 条更老的 `Python2`),
|
||||
* 0019 迁移把数据并成了一个值。查配置一律走 `judgeConfigFor()`,别直接下标 ——
|
||||
* 那里带着旧值的别名,迁移之前排进队列的任务、旧客户端传上来的值都还能判。
|
||||
* 0019 迁移把数据并成了一个值。
|
||||
*
|
||||
* SQL 题不走这里,走 `judge/sql/`;流程图题走 AI 评分。
|
||||
*/
|
||||
@@ -84,10 +81,7 @@ export const languageConfigs: Record<string, Record<string, unknown>> = {
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* 按语言取判题配置。**判题侧一律走这个函数**,不要直接 `languageConfigs[x]`:
|
||||
* 它先过 `normalizeLanguage()`,所以 `Python3` / `Python2` 这类旧值也能命中。
|
||||
*/
|
||||
/** 按语言取判题配置,判题机不认的语言(Java 等历史值、SQL、流程图)返回 null */
|
||||
export function judgeConfigFor(language: string) {
|
||||
return languageConfigs[language] ?? languageConfigs[normalizeLanguage(language) ?? ''] ?? null
|
||||
return languageConfigs[language] ?? null
|
||||
}
|
||||
@@ -733,8 +733,7 @@ classroomRoutes.put('/classroom/lesson', requireTeacher, async (c) => {
|
||||
const parsed = await parseBody(c, classLessonRequestSchema, '题号或语言不对')
|
||||
if (!parsed.success) return parsed.response
|
||||
const user = c.get('user')!
|
||||
const { className, problemDisplayIds } = parsed.data
|
||||
const language = parsed.data.language ?? null
|
||||
const { className, problemDisplayIds, language } = parsed.data
|
||||
const day = calendarDay()
|
||||
|
||||
const [exists] = await db.select({ id: schema.user.id }).from(schema.user).where(eq(schema.user.className, className)).limit(1)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
AST_SUPPORTED_LANGUAGES,
|
||||
isSqlProblem,
|
||||
normalizeLanguage,
|
||||
parseLanguage,
|
||||
problemListSortSchema,
|
||||
problemTypeFilterSchema,
|
||||
type ChallengeList,
|
||||
@@ -142,9 +142,8 @@ problemRoutes.get('/problems', optionalAuth, async (c) => {
|
||||
if (undone && states && states.solved.size > 0) filters.push(notInArray(schema.problem.id, [...states.solved]))
|
||||
|
||||
const where = and(...filters)
|
||||
// 排序值沿用原来的(地址栏里存着的旧链接照样能用),但「最多 / 最少」现在按**人数**排,
|
||||
// 和列表最后一列「做对 / 做过」对得上:submission_number 是做过的人,accepted_number 是做对的人。
|
||||
// 旧的「画流程图 / 语法检查」排序已经拆成类型筛选,传进来落到默认(最新创建)
|
||||
// 「最多 / 最少」按**人数**排,和列表最后一列「做对 / 做过」对得上:
|
||||
// submission_number 是做过的人,accepted_number 是做对的人。认不出的排序值落到默认(最新创建)
|
||||
const sort = problemListSortSchema.safeParse(c.req.query('sort') ?? '').data ?? ''
|
||||
const byPeople = sort.endsWith('submission_number') || sort.endsWith('accepted_number')
|
||||
const allCounts = byPeople ? await allProblemUserCounts() : null
|
||||
@@ -231,7 +230,7 @@ problemRoutes.get('/problems/random', optionalAuth, async (c) => {
|
||||
const type = problemTypeFilterSchema.safeParse(c.req.query('type')).data
|
||||
// 上次用的是 SQL(刚做完一道 SQL 题)不算偏好:SQL 题一共 9 道,照它挑就成了「随便来一道
|
||||
// SQL」。要 SQL 题的人会选「SQL」知识点或类型,那时按上面说的照样抽得到
|
||||
const saved = normalizeLanguage(c.req.query('language'))
|
||||
const saved = parseLanguage(c.req.query('language'))
|
||||
const language = saved && saved !== 'SQL' && saved !== 'Flowchart' ? saved : 'Python'
|
||||
const states = user ? await problemStates(user.id) : null
|
||||
const solved = states ? [...states.solved] : []
|
||||
|
||||
@@ -233,14 +233,12 @@ export function getUserList(
|
||||
className = '',
|
||||
) {
|
||||
return api.get<AdminUserList>('admin/users', {
|
||||
// "-last_login" 是旧接口传下来的取值(路由 query 里可能还存着),改叫 "-lastLogin";
|
||||
// "-online" 是新增的,原样透传
|
||||
params: {
|
||||
offset,
|
||||
limit,
|
||||
keyword,
|
||||
type,
|
||||
orderBy: orderBy === '-last_login' ? '-lastLogin' : orderBy,
|
||||
orderBy,
|
||||
className: className || undefined,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -521,8 +521,6 @@ function clearDraft() {
|
||||
}
|
||||
|
||||
function loadDraft() {
|
||||
// 旧版出题页的草稿(另一个 key、另一种形状)不认了
|
||||
localStorage.removeItem(STORAGE_KEY.ADMIN_PROBLEM)
|
||||
const saved = draft.value
|
||||
// 草稿是本机存的,形状不对(旧版本、被别的页面写坏)就当没有,别让整页起不来
|
||||
if (saved && typeof saved.form === 'object' && Array.isArray(saved.rows)) {
|
||||
|
||||
@@ -67,7 +67,7 @@ const adminOptions = [
|
||||
const sortOptions = [
|
||||
{ label: '最近建号', value: '' },
|
||||
{ label: '按用户名', value: 'username' },
|
||||
{ label: '最近登录', value: '-last_login' },
|
||||
{ label: '最近登录', value: '-lastLogin' },
|
||||
{ label: '在线优先', value: '-online' },
|
||||
]
|
||||
const [create, toggleCreate] = useToggle(false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { normalizeLanguage } from '@oj2/contract'
|
||||
import { parseLanguage } from '@oj2/contract'
|
||||
import { defineStore } from 'pinia'
|
||||
import { STORAGE_KEY } from 'utils/constants'
|
||||
import storage from 'utils/storage'
|
||||
@@ -9,7 +9,7 @@ import type { Code, LANGUAGE } from 'utils/types'
|
||||
* 管理全局的代码、输入、输出状态
|
||||
*/
|
||||
function savedCodeLanguage() {
|
||||
const saved = normalizeLanguage(storage.get(STORAGE_KEY.LANGUAGE))
|
||||
const saved = parseLanguage(storage.get(STORAGE_KEY.LANGUAGE))
|
||||
return saved && saved !== 'Flowchart' ? saved : null
|
||||
}
|
||||
|
||||
@@ -17,9 +17,6 @@ export const useCodeStore = defineStore('code', () => {
|
||||
// ==================== 状态 ====================
|
||||
const code = reactive<Code>({
|
||||
value: '',
|
||||
// 过一道 normalizeLanguage:上线那一刻学生浏览器的 localStorage 里存的还是
|
||||
// 旧值 Python3,直接拿来用会被后端的契约挡掉(而且报错看不出是这个原因)
|
||||
// 以前的版本会把 Flowchart 也记成语言偏好,那种老值在这里一并不认
|
||||
language: savedCodeLanguage() ?? 'Python',
|
||||
})
|
||||
|
||||
|
||||
@@ -330,15 +330,6 @@ export const admins: RouteRecordRaw = {
|
||||
props: true,
|
||||
meta: { requiresTeacherAdmin: true },
|
||||
},
|
||||
// 原来的「详情」页(题目 / 奖章 / 进度三个页签)并进了一屏编辑,老链接转过去
|
||||
{
|
||||
path: 'problemset/:problemSetId',
|
||||
name: 'admin problemset detail',
|
||||
redirect: (to) => ({
|
||||
name: 'admin problemset edit',
|
||||
params: { problemSetId: to.params.problemSetId },
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: 'ai/reports',
|
||||
name: 'admin ai reports',
|
||||
|
||||
@@ -152,7 +152,6 @@ export const STORAGE_KEY = {
|
||||
ACCESS_TOKEN: 'accessToken',
|
||||
REFRESH_TOKEN: 'refreshToken',
|
||||
LANGUAGE: 'problemLanguage',
|
||||
ADMIN_PROBLEM: 'adminProblem',
|
||||
ADMIN_SQL_PROBLEM: 'adminSqlProblem',
|
||||
ADMIN_CODE_PROBLEM: 'adminCodeProblem',
|
||||
ADMIN_PROBLEM_TAGS: 'adminProblemTags',
|
||||
|
||||
+5
-14
@@ -47,23 +47,14 @@ AI_HINT_DIAGNOSE=
|
||||
# 不设它就等于开一套空数据:空库(站点没有用户没有题)、没有测试点、题面图片 404。
|
||||
DATA_DIR=
|
||||
|
||||
# --- 并行试跑(oj2.xuyue.cc)才需要 ---
|
||||
# --- 可选 ---
|
||||
#
|
||||
# 新站和旧站同时跑的时候,这两个必须设,否则会跟旧栈抢资源。
|
||||
#
|
||||
# 对外端口。旧 backend 占着 8080(机房 81),新栈换一个,NPM 那边把
|
||||
# oj2.xuyue.cc 指到这个端口。**NPM 的记录要打开 Websockets Support**,
|
||||
# 否则学生那边「判题中…」永远不动。
|
||||
# 对外端口,不设是 8080(机房 81)。改了的话 NPM 那边的记录要跟着改,
|
||||
# **NPM 的记录要打开 Websockets Support**,否则学生那边「判题中…」永远不动。
|
||||
WEB_PORT=
|
||||
|
||||
# 判题机的运行状态目录。不设的话是 `$DATA_DIR/judge_server`,和旧判题机同一份 ——
|
||||
# 试跑期间两个 judger 都在跑,必须分开,例如:
|
||||
#
|
||||
# JUDGE_STATE_DIR=/root/OJDeploy/data/judge_server_oj2
|
||||
#
|
||||
# 建完记得 `mkdir -p .../{log,run}`。
|
||||
# `test_case` 和 `public/upload` 仍然共享,那是**故意的** —— 测试点和题面图片
|
||||
# 两边必须看到同一份。
|
||||
# 判题机的运行状态目录(log/、run/),不设是 `$DATA_DIR/judge_server`。
|
||||
# 换目录记得先 `mkdir -p .../{log,run}`。
|
||||
JUDGE_STATE_DIR=
|
||||
|
||||
# --- 只有机房那套需要 ---
|
||||
|
||||
+2
-3
@@ -167,9 +167,8 @@ COPY apps/api/src/db/meta/_journal.json /usr/local/share/oj2/migrations/meta/
|
||||
# 相对路径按 cwd 解析(见 apps/api/src/runtime.ts),所以 workdir 设成 /data;
|
||||
# 但下面的路径全写绝对值,不靠这个巧合。
|
||||
#
|
||||
# 目录布局**照抄旧后端**(test_case、public/upload、public/avatar)。
|
||||
# 这不是审美问题:切换那天不用搬动任何文件,回滚时旧后端也立刻能找到自己的数据。
|
||||
# 少一次几十 GB 的 mv,就少一个在停机窗口里出错的机会。
|
||||
# 目录布局**照抄旧后端**(test_case、public/upload、public/avatar):切换那天不用搬动
|
||||
# 任何文件。旧后端已经下线,但机房和服务器两边的数据目录都是这个布局,别改。
|
||||
WORKDIR /data
|
||||
ENV TEST_CASE_DIRECTORY=/data/test_case \
|
||||
HITOKOTO_DIRECTORY=/data/hitokoto \
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
#
|
||||
# 按容器名 `docker exec`,跟容器是谁起的无关:以前线上的 postgres 由旧栈那份
|
||||
# /root/OJDeploy/docker-compose.yml 起,在 OJ2 目录里 `docker compose exec` 会报
|
||||
# `service "oj-postgres" is not running`。现在它归 compose.debian.yml 了,但按名字找
|
||||
# 照样能用,move-db-into-oj2.sh 搬家时也靠这一点去备份旧容器(CONTAINER=旧容器名)。
|
||||
# `service "oj-postgres" is not running`。现在它归 compose.debian.yml 了,按名字找
|
||||
# 照样能用,不跟着 compose 走。
|
||||
#
|
||||
# ## 用法
|
||||
#
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
# 构建上下文是仓库根,所以 build.context 写 ..(相对本文件)。
|
||||
#
|
||||
# **服务器上的全部容器都由这一个文件起**,包括 postgres 和 redis。以前库和 redis 是旧栈
|
||||
# 那份 /root/OJDeploy/docker-compose.yml 起的、这里通过 DB_HOST 外接过去,升级它们就得去
|
||||
# 改那份文件;2026-10 起收了进来(搬家用 docker/move-db-into-oj2.sh,一次性的)。
|
||||
# 那份 /root/OJDeploy/docker-compose.yml 起的、这里通过 DB_HOST 外接过去,2026-10 起收了进来。
|
||||
#
|
||||
# ⚠️ `DATA_DIR` 默认值 `../data` 解析出来是 **`OJ2/data`**,不是部署目录的 `data/`。
|
||||
# 不设它的话 postgres 会在空目录上初始化一个全新的空库,测试点和题面图片也全都不在,
|
||||
@@ -107,9 +106,7 @@ services:
|
||||
- /tmp
|
||||
volumes:
|
||||
- ${DATA_DIR:-../data}/backend/test_case:/test_case:ro
|
||||
# 判题机的运行状态目录。默认跟着 DATA_DIR 走,和旧栈是同一份 ——
|
||||
# 一次性切换时无所谓(旧判题机已经停了),但**并行试跑时必须单独设 JUDGE_STATE_DIR**,
|
||||
# 否则新旧两个 judger 同时往一个 run/log 目录里写。
|
||||
# 判题机的运行状态目录,默认跟着 DATA_DIR 走
|
||||
- ${JUDGE_STATE_DIR:-${DATA_DIR:-../data}/judge_server}/log:/log
|
||||
- ${JUDGE_STATE_DIR:-${DATA_DIR:-../data}/judge_server}/run:/judger
|
||||
environment:
|
||||
@@ -192,6 +189,5 @@ services:
|
||||
# Caddy 只往这里写访问日志,静态资源在镜像里
|
||||
- ${DATA_DIR:-../data}/backend/log:/data/log
|
||||
ports:
|
||||
# 并行试跑(oj2.xuyue.cc)时换一个端口,8080 还被旧 backend 占着。
|
||||
- "0.0.0.0:${WEB_PORT:-8080}:8000"
|
||||
mem_limit: 256m
|
||||
@@ -48,7 +48,7 @@ services:
|
||||
- /tmp
|
||||
volumes:
|
||||
- ${DATA_DIR:-../data}/backend/test_case:/test_case:ro
|
||||
# 并行试跑时必须单独设 JUDGE_STATE_DIR,否则新旧两个 judger 共用一个运行目录
|
||||
# 判题机的运行状态目录,默认跟着 DATA_DIR 走
|
||||
- ${JUDGE_STATE_DIR:-${DATA_DIR:-../data}/judge_server}/log:/log
|
||||
- ${JUDGE_STATE_DIR:-${DATA_DIR:-../data}/judge_server}/run:/judger
|
||||
environment:
|
||||
@@ -121,6 +121,5 @@ services:
|
||||
volumes:
|
||||
- ${DATA_DIR:-../data}/backend/log:/data/log
|
||||
ports:
|
||||
# 并行试跑时换一个端口,81 还被旧 backend 占着
|
||||
- "${WEB_PORT:-81}:8000"
|
||||
mem_limit: 256m
|
||||
+2
-14
@@ -61,13 +61,6 @@ say "自检"
|
||||
|
||||
[ -f docker/.env ] || die "docker/.env 不存在。内容见手册第三节,别用 .env.example 直接改名(里面的值全是空的)"
|
||||
|
||||
# 以前库和 redis 是外接旧栈的,靠这几个变量指过去;现在 compose 不再读它们。
|
||||
# 留在 .env 里不碍事,但会让人以为还在生效
|
||||
for v in DB_HOST DB_PORT REDIS_HOST REDIS_PORT; do
|
||||
[ -z "$(grep -E "^$v=." docker/.env || true)" ] \
|
||||
|| warn "docker/.env 里的 $v 已经不用了(库和 redis 由本栈起),可以删掉"
|
||||
done
|
||||
|
||||
# --prebuilt 时产物就是构建输入。缺了的话 docker 只会甩一句 COPY 找不到文件,
|
||||
# 这里提前拦下并说清楚该怎么办。
|
||||
if [ "${ARTIFACTS:-build}" = prebuilt ]; then
|
||||
@@ -109,20 +102,15 @@ data_dir=$(grep -E '^DATA_DIR=' docker/.env | tail -1 | cut -d= -f2- || true)
|
||||
[ -n "$data_dir" ] || die "必须显式设 DATA_DIR。
|
||||
默认值 ../data 解析出来是 OJ2/data,那是个空目录,postgres 会在上面初始化一个全新的空库,而且不报任何错。"
|
||||
pgver_file="$data_dir/postgresql/18/docker/PG_VERSION"
|
||||
if [ ! -e "$data_dir/postgresql" ] && [ -d "$data_dir/postgres" ]; then
|
||||
die "$data_dir 下只有 postgres/(PostgreSQL 16 的老库,旧栈 compose 起的),
|
||||
还没有 postgresql/18/docker。先把库搬进本栈:docker/move-db-into-oj2.sh(一次性的,见 docs/deploy.md)"
|
||||
fi
|
||||
[ -f "$pgver_file" ] || die "找不到 $pgver_file —— DATA_DIR 指的不是一个已有的 postgres 数据目录。
|
||||
就这么起下去会得到一个空库。确认 DATA_DIR=$data_dir 对不对。"
|
||||
pgver=$(cat "$pgver_file")
|
||||
[ "$pgver" = 18 ] || die "$data_dir/postgresql 是 PostgreSQL $pgver 的数据目录,但 compose 里是 postgres:18-alpine,起不来。"
|
||||
ok "数据目录 $data_dir/postgresql(PostgreSQL $pgver)"
|
||||
|
||||
# ③ 判题机运行目录必须和旧栈分开,否则两个 judger 往同一个目录里写
|
||||
# ③ 判题机运行目录:不设就是 $DATA_DIR/judge_server
|
||||
judge_dir=$(grep -E '^JUDGE_STATE_DIR=' docker/.env | tail -1 | cut -d= -f2- || true)
|
||||
[ -n "$judge_dir" ] || die "JUDGE_STATE_DIR 没设 —— 试跑期间新旧两个判题机会共用运行目录"
|
||||
ok "判题机运行目录 $judge_dir"
|
||||
ok "判题机运行目录 ${judge_dir:-$data_dir/judge_server}"
|
||||
|
||||
# ④ 判题镜像是自建的(上游 JudgeServer 停更,官方镜像的编译器停在 gcc-13),
|
||||
# registry 上没有这个 tag。忘了 docker load 的话,要到「起栈」那步 compose 去 pull
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 一次性:把服务器上的 postgres / redis 从旧栈那份 /root/OJDeploy/docker-compose.yml
|
||||
# 搬进 OJ2 的 compose.debian.yml,顺带 PostgreSQL 16 → 18、Redis 7 → 8。
|
||||
# 搬完以后服务器上的全部容器都归 OJ2 管,旧 compose 文件里那两个服务就没用了。
|
||||
#
|
||||
# cd /root/OJDeploy/OJ2
|
||||
# docker/move-db-into-oj2.sh # 服务器自己编镜像(慢,但不依赖 CI)
|
||||
# docker/move-db-into-oj2.sh --prebuilt # 用 CI rsync 过来的产物
|
||||
#
|
||||
# ## 跑之前
|
||||
#
|
||||
# 1. **挑没课的时候。** 两个站点共用这个库,中间两边都用不了。镜像先在旧栈照常服务时
|
||||
# 编好,真正停机的只有「备份 → 恢复 → 起栈」那一段。
|
||||
# 2. **机房那台先停应用**(库在这边,机房连着它就恢复不了):
|
||||
# docker compose -f docker/compose.school.yml --env-file docker/.env.school stop oj-api oj-worker
|
||||
# 没停的话脚本会查出来还有谁连着库,报出 IP 后中止。
|
||||
# 3. **docker/.env 的 POSTGRES_PASSWORD 换成强口令**(openssl rand -hex 32)。
|
||||
# 原来的口令是默认的 onlinejudge,而这个账号是超级用户、5445 对公网开着 ——
|
||||
# 知道默认口令的人就能拿到整个库,还能在容器里执行命令。新库直接用新口令建,
|
||||
# 脚本不接受弱口令。机房的 .env.school 要改成同一个值。
|
||||
#
|
||||
# ## 它做什么
|
||||
#
|
||||
# 自检 → 编镜像 → 停本机 api/worker → 确认没人连着旧库 → 记行数 → 备份
|
||||
# → 停掉并删除旧的 postgres / redis 容器 → 起本栈的 PostgreSQL 18 / Redis 8
|
||||
# → 恢复 → 收集统计信息 → 核对行数和网络登录 → docker/deploy.sh --no-build
|
||||
#
|
||||
# 旧库的数据目录 `$DATA_DIR/postgres`(16)**一个字节都不动**,新库在
|
||||
# `$DATA_DIR/postgresql`。Redis 的 `$DATA_DIR/redis` 新旧共用,8 直接读 7 的 dump.rdb。
|
||||
#
|
||||
# ## 恢复时口令为什么要剥掉
|
||||
#
|
||||
# pg_dumpall 会带一句 `ALTER ROLE onlinejudge ... PASSWORD 'md5…'`,原样灌进去会把新库
|
||||
# 的口令改回旧的 md5 哈希。而 18 的镜像新建的库 pg_hba 只认 scram-sha-256,md5 存的
|
||||
# 口令**永远验不过** —— 容器里 docker exec psql 走本地 socket 是 trust,看着一切正常,
|
||||
# 只有 api 和机房从网络连进来才炸。所以恢复时把那个 PASSWORD 子句删掉,库里留下的就是
|
||||
# 初始化时按 POSTGRES_PASSWORD 存的 SCRAM 哈希。
|
||||
#
|
||||
# ## 中途失败
|
||||
#
|
||||
# 停旧容器之前失败:什么都没动,`docker start oj-api oj-worker` 就回去了(容器还是原来的
|
||||
# 配置)。别用 `docker compose ... start`:新的 compose 里 api 依赖本栈的 oj-postgres,
|
||||
# 这时它还不存在,compose 直接拒绝。
|
||||
# 之后失败:旧库目录原样在,回退见 docs/deploy.md「把库搬进 OJ2」。
|
||||
|
||||
[ -n "${BASH_VERSION:-}" ] || exec bash "$0" "$@"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
COMPOSE=(docker compose -f docker/compose.debian.yml --env-file docker/.env)
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--prebuilt) export ARTIFACTS=prebuilt ;;
|
||||
*) echo "未知参数:$arg(可用:--prebuilt)" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
say() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
|
||||
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
||||
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
|
||||
die() { printf '\n\033[1;31m❌ %s\033[0m\n\n' "$*" >&2; exit 1; }
|
||||
envget() { grep -E "^$1=" docker/.env | tail -1 | cut -d= -f2- || true; }
|
||||
|
||||
# ---------------------------------------------------------------- 自检
|
||||
say "自检"
|
||||
|
||||
[ -f docker/.env ] || die "docker/.env 不存在"
|
||||
|
||||
data_dir=$(envget DATA_DIR)
|
||||
[ -n "$data_dir" ] && [ -d "$data_dir" ] || die "DATA_DIR($data_dir)没设或不是目录。服务器上应该是 /root/OJDeploy/data"
|
||||
[ ! -e "$data_dir/postgresql" ] || die "$data_dir/postgresql 已经存在 —— 已经搬过了?
|
||||
如果是上次搬到一半失败留下的,确认里面没有要的东西,删掉它再跑。"
|
||||
ok "数据目录 $data_dir"
|
||||
|
||||
pg_pw=$(envget POSTGRES_PASSWORD)
|
||||
[ "${#pg_pw}" -ge 16 ] && [ "$pg_pw" != onlinejudge ] || die "docker/.env 的 POSTGRES_PASSWORD 太弱(不到 16 位,或者还是默认的 onlinejudge)。
|
||||
这个账号是超级用户、5445 对公网开着。换一个:openssl rand -hex 32
|
||||
机房的 docker/.env.school 要改成同一个值。"
|
||||
[[ "$pg_pw" =~ ^[A-Za-z0-9._~-]+$ ]] || die "POSTGRES_PASSWORD 里有 URL 不安全的字符(它会拼进 DATABASE_URL),用 openssl rand -hex 32 生成"
|
||||
ok "POSTGRES_PASSWORD 是强口令"
|
||||
|
||||
redis_pw=$(envget REDIS_PASSWORD)
|
||||
[ -n "$redis_pw" ] || die "docker/.env 里没有 REDIS_PASSWORD"
|
||||
|
||||
# 旧容器按发布的端口找,不按名字:旧 compose 起的容器叫什么不归我们管
|
||||
old_pg=$(docker ps --filter publish=5445 --format '{{.Names}}' | head -1)
|
||||
old_redis=$(docker ps --filter publish=5446 --format '{{.Names}}' | head -1)
|
||||
[ -n "$old_pg" ] || die "没找到发布 5445 的容器(旧的 postgres)。它没在跑?"
|
||||
[ -n "$old_redis" ] || die "没找到发布 5446 的容器(旧的 redis)。它没在跑?"
|
||||
# 「已经搬过了」靠这里的 16 和上面 postgresql/ 不存在来判断,不看容器是哪个 compose 起的:
|
||||
# 旧栈那份文件原名 docker-compose.debian.yml,按文件名认会把它当成本栈的(线上撞过)
|
||||
pgver=$(docker exec "$old_pg" psql -U onlinejudge -d onlinejudge -tAc 'show server_version_num')
|
||||
[ "${pgver:0:2}" = 16 ] || die "$old_pg 是 PostgreSQL $pgver,这个脚本只认 16"
|
||||
from() { docker inspect -f '{{.Config.Image}},{{index .Config.Labels "com.docker.compose.project.config_files"}}' "$1"; }
|
||||
ok "旧 postgres:$old_pg($(from "$old_pg"))"
|
||||
ok "旧 redis:$old_redis($(from "$old_redis"))"
|
||||
|
||||
redis_src=$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Source}}{{end}}{{end}}' "$old_redis")
|
||||
[ -n "$redis_src" ] || die "$old_redis 的 /data 没挂到宿主机目录上,会话和队列搬不过来"
|
||||
|
||||
db_bytes=$(docker exec "$old_pg" psql -U onlinejudge -d postgres -tAc "select pg_database_size('onlinejudge')")
|
||||
avail=$(( $(df -Pk "$data_dir" | awk 'NR==2 {print $4}') * 1024 ))
|
||||
[ "$avail" -gt $(( db_bytes * 3 )) ] || die "磁盘余量 $((avail >> 20)) MiB,不到库大小($((db_bytes >> 20)) MiB)的三倍:新库一份、备份一份,还得留余地"
|
||||
ok "库 $((db_bytes >> 20)) MiB,磁盘余量 $((avail >> 20)) MiB"
|
||||
|
||||
judge_image=$("${COMPOSE[@]}" config | grep -m1 -E '^[[:space:]]*image: oj2-judge' | awk '{print $2}')
|
||||
docker image inspect "$judge_image" >/dev/null 2>&1 || die "判题镜像 $judge_image 不在本机,见 docker/judge/README.md"
|
||||
|
||||
# ---------------------------------------------------------------- 编镜像
|
||||
# 旧栈这时还在照常服务,编完才开始停机
|
||||
say "编镜像(旧栈照常服务中)"
|
||||
"${COMPOSE[@]}" build oj-api oj-worker oj-web
|
||||
docker pull -q postgres:18-alpine >/dev/null
|
||||
docker pull -q redis:8-alpine >/dev/null
|
||||
ok "postgres:18-alpine、redis:8-alpine 已拉取"
|
||||
|
||||
# ---------------------------------------------------------------- 停机
|
||||
say "停本机的 api / worker(从这里开始站点不可用)"
|
||||
"${COMPOSE[@]}" stop oj-api oj-worker
|
||||
|
||||
# 机房的 api / worker 也连着这个库。停没停只能从库这边看
|
||||
clients=""
|
||||
for _ in $(seq 15); do
|
||||
clients=$(docker exec "$old_pg" psql -U onlinejudge -d postgres -tAc \
|
||||
"select string_agg(distinct coalesce(host(client_addr), 'local'), ', ')
|
||||
from pg_stat_activity
|
||||
where backend_type = 'client backend' and pid <> pg_backend_pid()")
|
||||
[ -z "$clients" ] && break
|
||||
sleep 2
|
||||
done
|
||||
[ -z "$clients" ] || die "还有客户端连着旧库:$clients
|
||||
多半是机房的 api / worker 没停。停掉它们再跑;要先恢复本机服务的话:
|
||||
docker start oj-api oj-worker"
|
||||
ok "没有客户端连着旧库"
|
||||
|
||||
counts_sql="select (select count(*) from submission) || ' ' || (select count(*) from \"user\") || ' ' || (select count(*) from problem)"
|
||||
before=$(docker exec "$old_pg" psql -U onlinejudge -d onlinejudge -tAc "$counts_sql")
|
||||
ok "提交 / 用户 / 题目:$before"
|
||||
|
||||
# ---------------------------------------------------------------- 备份
|
||||
say "备份"
|
||||
backup_dir="${BACKUP_DIR:-$(dirname "$PWD")/backups}"
|
||||
marker=$(mktemp)
|
||||
CONTAINER="$old_pg" BACKUP_DIR="$backup_dir" docker/backup-db.sh
|
||||
backup=$(find "$backup_dir" -maxdepth 1 -name 'db_backup_*.sql.gz' -newer "$marker" | sort | tail -1)
|
||||
rm -f "$marker"
|
||||
[ -n "$backup" ] || die "backup-db.sh 跑完了但找不到新的备份文件($backup_dir)"
|
||||
ok "备份:$backup"
|
||||
|
||||
# ---------------------------------------------------------------- 换容器
|
||||
say "停掉旧的 postgres / redis"
|
||||
docker exec -e REDISCLI_AUTH="$redis_pw" "$old_redis" redis-cli save >/dev/null \
|
||||
|| warn "redis save 失败(口令不对?),会话可能丢,学生重新登录一次就好"
|
||||
docker stop "$old_pg" "$old_redis" >/dev/null
|
||||
# 删掉而不只是停:新容器要用同样的名字和端口
|
||||
docker rm "$old_pg" "$old_redis" >/dev/null
|
||||
ok "已删除容器 $old_pg、$old_redis(数据目录没动)"
|
||||
|
||||
if [ "$redis_src" != "$data_dir/redis" ]; then
|
||||
mkdir -p "$data_dir/redis"
|
||||
[ -e "$data_dir/redis/dump.rdb" ] || cp -a "$redis_src/." "$data_dir/redis/"
|
||||
ok "redis 数据从 $redis_src 拷到 $data_dir/redis"
|
||||
fi
|
||||
|
||||
say "起 PostgreSQL 18 / Redis 8"
|
||||
"${COMPOSE[@]}" up -d oj-postgres oj-redis
|
||||
|
||||
# 不用 pg_isready 等:初始化期间镜像会起一个只听 socket 的临时实例,pg_isready 走
|
||||
# socket 照样说 OK,这时候灌数据会灌进一个马上要被关掉的实例。走网络连就等得到正式的那个,
|
||||
# 还顺带验了新口令能不能从网络登录(api 和机房就是这么连的)
|
||||
tcp_check() {
|
||||
docker exec -e PGPASSWORD="$pg_pw" oj-postgres sh -c \
|
||||
'psql -h "$(hostname -i | cut -d" " -f1)" -U onlinejudge -d onlinejudge -tAc "select 1"' >/dev/null 2>&1
|
||||
}
|
||||
for _ in $(seq 60); do tcp_check && break; sleep 2; done
|
||||
tcp_check || die "PostgreSQL 18 两分钟没起来,看 docker logs oj-postgres"
|
||||
ok "PostgreSQL 18 起来了,新口令能从网络登录"
|
||||
|
||||
# ---------------------------------------------------------------- 恢复
|
||||
say "恢复"
|
||||
log=$(mktemp)
|
||||
started=$(date +%s)
|
||||
gzip -cd "$backup" \
|
||||
| sed -E "/^ALTER ROLE onlinejudge WITH /s/ PASSWORD '[^']*'//" \
|
||||
| docker exec -i oj-postgres psql -q -U onlinejudge -d postgres >/dev/null 2>"$log" || true
|
||||
# 「先删后建」的 dump 往新实例灌,这两条必然出现(见 backup-db.sh 头部),别的都不该有
|
||||
unexpected=$(grep 'ERROR' "$log" | grep -v -e 'current user cannot be dropped' -e 'role "onlinejudge" already exists' || true)
|
||||
[ -z "$unexpected" ] || die "恢复时出了预期之外的错(完整日志 $log):
|
||||
$unexpected"
|
||||
rm -f "$log"
|
||||
ok "恢复完成,用时 $(( $(date +%s) - started ))s"
|
||||
|
||||
docker exec oj-postgres vacuumdb -U onlinejudge --all --analyze-in-stages -q
|
||||
ok "统计信息已收集"
|
||||
|
||||
after=$(docker exec oj-postgres psql -U onlinejudge -d onlinejudge -tAc "$counts_sql")
|
||||
[ "$after" = "$before" ] || die "行数对不上:恢复前 $before,恢复后 $after"
|
||||
ok "提交 / 用户 / 题目:$after,和恢复前一致"
|
||||
|
||||
tcp_check || die "恢复之后新口令从网络登不上了 —— dump 里的口令没剥干净?
|
||||
docker exec -it oj-postgres psql -U onlinejudge -d postgres -c '\\password onlinejudge'"
|
||||
ok "新口令仍能从网络登录"
|
||||
|
||||
# ---------------------------------------------------------------- 起栈
|
||||
say "起栈(交给 deploy.sh)"
|
||||
docker/deploy.sh --no-build
|
||||
|
||||
cat <<EOF
|
||||
|
||||
$(printf '\033[1;32m✅ 搬完了\033[0m')
|
||||
|
||||
还剩三件事:
|
||||
|
||||
1. 机房:docker/.env.school 的 POSTGRES_PASSWORD 改成和这边一样的新值,然后
|
||||
docker compose -f docker/compose.school.yml --env-file docker/.env.school up -d
|
||||
2. /root/OJDeploy/docker-compose.yml 里的 oj-postgres / oj-redis 已经没用了,删掉
|
||||
(整个文件都没别的服务的话就删文件)。留着的话哪天有人 up 一下,会和本栈抢端口。
|
||||
3. 旧库目录 $data_dir/postgres 留着做回退,跑稳一两周再删。
|
||||
备份在 $backup
|
||||
EOF
|
||||
+9
-48
@@ -44,8 +44,7 @@ compose 里 `:?` 强制要求),端口也只发布在 127.0.0.1。2026-09-27
|
||||
`0.0.0.0:5446`、不带口令,从公网直接 PING 得通(实测过)。
|
||||
|
||||
**数据库口令同理**:`onlinejudge` 是超级用户,5445 对公网开着(机房要连),2026-10 以前
|
||||
口令就是默认的 `onlinejudge`。搬进 OJ2 时用 `openssl rand -hex 32` 生成的新口令重建,
|
||||
`move-db-into-oj2.sh` 不接受弱口令。`POSTGRES_PASSWORD` 只在库初始化时生效一次,以后要换:
|
||||
口令就是默认的 `onlinejudge`。搬进 OJ2 时用 `openssl rand -hex 32` 生成的新口令重建。`POSTGRES_PASSWORD` 只在库初始化时生效一次,以后要换:
|
||||
|
||||
```bash
|
||||
docker exec -it oj-postgres psql -U onlinejudge -d postgres -c '\password onlinejudge'
|
||||
@@ -168,57 +167,19 @@ WebSocket)→ 后台判题机列表在线 → 后台题目列表翻页 → 带
|
||||
避开坑 2),换下来的旧库留作 `onlinejudge_prev`。`--files` 顺带同步测试点、题面图片、头像。
|
||||
对服务器只读;拉完本机的账号口令都是线上的。用法和刻意的做法在脚本头部。
|
||||
|
||||
## 把库搬进 OJ2(一次性,顺带 PostgreSQL 16 → 18、Redis 7 → 8)
|
||||
## 把库搬进 OJ2(2026-10-13,已完成)
|
||||
|
||||
`docker/move-db-into-oj2.sh` 把服务器上旧栈 compose 起的 postgres / redis 换成本栈的。
|
||||
大版本不能原地换镜像(18 读不了 16 的目录),所以走「备份 → 新目录起 18 → 恢复」。
|
||||
本机用 10-13 的生产备份搭了一套和线上一样的「之前」状态完整演练过:13 万条提交恢复
|
||||
5 秒,行数一致,登录态保住,提交照常判题,机房那样从外面连 5445 新口令能进、旧口令被拒。
|
||||
服务器上的 postgres / redis 原来由旧栈的 compose 起,那次搬进了本栈,顺带 PostgreSQL 16 → 18、
|
||||
Redis 7 → 8(一次性脚本 `docker/move-db-into-oj2.sh` 用完已删,要看去 git 历史)。
|
||||
留下的坑只有一个,恢复搬家之前(16 时代)的备份时会踩:
|
||||
|
||||
**挑没课的时候做**,两个站点共用这个库,中间两边都用不了(镜像在停机前编好,真正停机
|
||||
的那段不到一分钟)。
|
||||
|
||||
```bash
|
||||
# 0. 代码先推上去。CI 会照常 rsync,然后在 deploy.sh 自检停下(数据目录还不是 18)、
|
||||
# 工作流判红 —— 这是预期的,容器一个没动,旧栈照常服务。
|
||||
|
||||
# 1. 机房:停应用
|
||||
docker compose -f docker/compose.school.yml --env-file docker/.env.school stop oj-api oj-worker
|
||||
|
||||
# 2. 服务器:docker/.env 的 POSTGRES_PASSWORD 换成新生成的强口令(openssl rand -hex 32)
|
||||
|
||||
# 3. 服务器:搬家(CI 已经把产物 rsync 过来了,所以用 --prebuilt)
|
||||
cd /root/OJDeploy/OJ2
|
||||
docker/move-db-into-oj2.sh --prebuilt
|
||||
|
||||
# 4. 机房:docker/.env.school 的 POSTGRES_PASSWORD 改成同一个新值,然后(顺带换上 Redis 8)
|
||||
docker compose -f docker/compose.school.yml --env-file docker/.env.school up -d --build
|
||||
```
|
||||
|
||||
脚本每一步都有自检:机房没停(还有客户端连着旧库)会报出 IP 并中止,口令弱也中止,
|
||||
恢复后行数对不上、新口令从网络登不上也中止。做完按「部署后验证」两边各点一遍。
|
||||
|
||||
之后:`/root/OJDeploy/docker-compose.yml` 里的 `oj-postgres` / `oj-redis` 删掉(留着的话
|
||||
哪天有人 `up` 一下会和本栈抢端口);`docker/.env` 里的 `DB_HOST` / `DB_PORT` /
|
||||
`REDIS_HOST` / `REDIS_PORT` 也删掉(`deploy.sh` 会提醒)。
|
||||
|
||||
**恢复时为什么剥掉 dump 里的口令**:旧库的口令是 md5 哈希存的,dump 原样带着;而 18 的
|
||||
**恢复时要剥掉 dump 里的口令**(上面坑 1 的加重版):16 的库口令是 md5 哈希存的,dump 原样带着;而 18 的
|
||||
镜像新建的库 `pg_hba.conf` 只认 `scram-sha-256`,md5 存的口令**永远验不过**。容器里
|
||||
`docker exec psql` 走本地 socket 是 trust,一切看着正常,只有 api 和机房从网络连进来才报
|
||||
`password authentication failed` —— 演练时就是这么发现的。剥掉之后库里留的是初始化时按
|
||||
`POSTGRES_PASSWORD` 存的 SCRAM 哈希。
|
||||
`password authentication failed`。剥掉之后库里留的是初始化时按 `POSTGRES_PASSWORD` 存的
|
||||
SCRAM 哈希。
|
||||
|
||||
**中途失败**:停旧容器之前失败的,`docker start oj-api oj-worker` 就回去了。之后失败的,
|
||||
旧库目录 `$DATA_DIR/postgres` 一个字节没动:
|
||||
|
||||
```bash
|
||||
docker rm -f oj-postgres oj-redis # 本栈的 18 / 8
|
||||
docker compose -f /root/OJDeploy/docker-compose.yml up -d oj-postgres oj-redis # 旧的 16 / 7
|
||||
# docker/.env 的 POSTGRES_PASSWORD 改回旧口令、DB_HOST 那四个变量填回去,
|
||||
# 代码退回搬家之前那个提交再 docker/deploy.sh
|
||||
```
|
||||
|
||||
搬家之后写进 18 的数据回退时会丢。跑稳一两周再删 `$DATA_DIR/postgres`。
|
||||
服务器上的 `$DATA_DIR/postgres`(16 的老库目录)如果还在,可以删了。
|
||||
|
||||
## 镜像体积
|
||||
|
||||
|
||||
@@ -74,12 +74,12 @@ export const lastVisitSchema = z.object({
|
||||
|
||||
/**
|
||||
* 课堂看板里老师布置这节课的题:按输入顺序给展示题号,最多 20 道,空数组 = 清掉。
|
||||
* `language` 是这份作业用什么语言做;不传当 null(上线时还开着的旧看板页面不带它)
|
||||
* `language` 是这份作业用什么语言做,null = 没选
|
||||
*/
|
||||
export const classLessonRequestSchema = z.object({
|
||||
className: z.string().trim().min(1),
|
||||
problemDisplayIds: z.array(z.string().trim().min(1)).max(20),
|
||||
language: lessonLanguageSchema.optional(),
|
||||
language: lessonLanguageSchema.nullable(),
|
||||
})
|
||||
|
||||
export const classBoardProblemSchema = z.object({
|
||||
|
||||
@@ -18,9 +18,7 @@ import { z } from 'zod'
|
||||
* `apps/api/src/judge/languages.ts` 顶部。
|
||||
*
|
||||
* `Python2` / `Python3` 这两个旧值**已经不在这里了** —— 0019 迁移把库里的
|
||||
* 104530 条提交、937 道题、1235 个用户的成就指标全并成了 `Python`。万一还有旧值
|
||||
* 从别处冒出来(旧客户端的 localStorage、迁移之前排进队列的任务),走下面的
|
||||
* `normalizeLanguage()`,别直接 parse。
|
||||
* 104530 条提交、937 道题、1235 个用户的成就指标全并成了 `Python`。
|
||||
*/
|
||||
export const judgeLanguageSchema = z.enum(['C', 'C++', 'Python', 'Java', 'JavaScript', 'Golang'])
|
||||
|
||||
@@ -62,23 +60,7 @@ export type LessonLanguage = z.infer<typeof lessonLanguageSchema>
|
||||
export type RunnableLanguage = z.infer<typeof runnableLanguageSchema>
|
||||
export type ProblemLanguage = z.infer<typeof problemLanguageSchema>
|
||||
|
||||
/**
|
||||
* 旧语言名 → 现在的值。**库里已经没有旧值了**(0019 迁移清干净并核对过),
|
||||
* 这张表挡的是数据之外的三条路:
|
||||
*
|
||||
* 1. 学生浏览器 localStorage 里存着上次选的 `Python3`(上线那一刻还在那儿);
|
||||
* 2. 迁移之前就排进 BullMQ 的判题任务;
|
||||
* 3. 万一要回滚 —— 旧版后端读 `Python` 会查不到配置,所以**回滚必须连数据一起回**,
|
||||
* 别只滚代码。
|
||||
*/
|
||||
const LANGUAGE_ALIASES: Record<string, ProblemLanguage> = {
|
||||
Python2: 'Python',
|
||||
Python3: 'Python',
|
||||
}
|
||||
|
||||
/** 把可能是旧值的语言名归一化;认不出来返回 null,由调用方决定怎么兜底。 */
|
||||
export function normalizeLanguage(value: unknown): ProblemLanguage | null {
|
||||
if (typeof value !== 'string') return null
|
||||
const parsed = problemLanguageSchema.safeParse(LANGUAGE_ALIASES[value] ?? value)
|
||||
return parsed.success ? parsed.data : null
|
||||
/** 认语言名(地址栏、localStorage 里来的);认不出来返回 null,由调用方决定怎么兜底。 */
|
||||
export function parseLanguage(value: unknown): ProblemLanguage | null {
|
||||
return problemLanguageSchema.safeParse(value).data ?? null
|
||||
}
|
||||
@@ -484,7 +484,7 @@ export const problemListSchema = paginatedSchema(problemListItemSchema)
|
||||
export const problemTypeFilterSchema = z.enum(['sql', 'reference', 'flowchart', 'ast'])
|
||||
|
||||
/**
|
||||
* 题目列表的排序。值沿用原来的(旧链接照样能用):空串 = 最新创建、create_time = 最早创建,
|
||||
* 题目列表的排序:空串 = 最新创建、create_time = 最早创建,
|
||||
* ±submission_number / ±accepted_number 是做过 / 做对的**人数**,带 - 是从多到少
|
||||
*/
|
||||
export const problemListSortSchema = z.enum(['', 'create_time', '-submission_number', 'submission_number', '-accepted_number', 'accepted_number'])
|
||||
|
||||
Reference in new issue
Block a user