清理旧后端下线后的残留
Some checks failed
Deploy / deploy (push) Has been cancelled

旧 Django 后端 2026-08-26 下线、回滚路径作废,代码里还留着几处以它为前提的
死代码和过期注释。

- PUBLIC_WS_URL / PUBLIC_OJ_URL 全部删除。BaseWebSocket 的
  `${PUBLIC_WS_URL}/${path}/` fallback 是 Channels 时代的写法,而三个子类
  早就各自传 url,等于永不执行;config.vue 里 PUBLIC_OJ_URL 只是个会被
  getWebsiteConfig() 立刻覆盖、且指着 :8000 的假初值。WebSocketConfig.path
  随之去掉,url 改成必填。
- vite.config:删掉 /api2、/ws2 的迁移期注释,换成还成立的约束(代理这三段
  要和 docker/Caddyfile 同步);newBackend 改名 backend。
- 六处 “回滚时旧后端还要读” 换成真实理由:snake_case 保留的结论不变,但因为
  判题机按这套键名写、存量 JSONB 就是这形状。
- CLAUDE.md:数据库一节开头「不写迁移 + 先想清楚回滚」与下一节的 drizzle
  migration 自相矛盾,改掉;判题状态码不再要求同步到 OnlineJudge,理由换成
  历史 submission.result 和沙箱用的就是这套编码。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 09:09:26 -06:00
parent 64cb7b62ef
commit 7793a2fe4c
11 changed files with 33 additions and 51 deletions

View File

@@ -95,11 +95,11 @@ dev 直接起不来。
加完路由跑 `bun run --filter '@oj2/api' check:routes` 加完路由跑 `bun run --filter '@oj2/api' check:routes`
### 判题状态码要三处同步 ### 判题状态码不能改
`apps/api/src/judge/status.ts``apps/web/src/utils/constants.ts` `apps/api/src/judge/status.ts``apps/web/src/utils/constants.ts` 必须一致。
以及上一代的 `../OnlineJudge/submission/models.py`(回滚时要对得上)。 这些整数是**落库的值**12 万条历史提交的 `submission.result` 就是它们,判题沙箱回的也是
题目表情 reaction 的语义 key 同理。 这套编码,所以只能新增、不能改已有的含义。题目表情 reaction 的语义 key 同理。
### 比赛只有 ACM 模式 ### 比赛只有 ACM 模式
@@ -112,9 +112,11 @@ dev 直接起不来。
## 数据库 ## 数据库
Drizzle schema 是从生产库 `drizzle-kit pull` 出来的,**不写迁移**。 Drizzle schema 最初`drizzle-kit pull` 从生产库拉出来的,所以它长得像 Django 建的表
新旧后端跑在同一套表结构上(阶段 5 演练逐列比对过,零差异),这是回滚能成立的前提 —— 表名、bigint/int4 混用、外键全是 NO ACTION`schema.ts` 顶部记了哪些地方是手工修的。
所以改 schema 前先想清楚回滚怎么办。
**schema 现在归 OJ2 独占。** 旧后端已下线,「改 schema 要考虑回滚」这条约束不再存在,
结构变更走下面的 migration 正常演进即可。
### 改 schema 走 drizzle migration ### 改 schema 走 drizzle migration

View File

@@ -1,8 +1,6 @@
PUBLIC_ENV=xuyue.cc PUBLIC_ENV=xuyue.cc
PUBLIC_MAXKB_URL=https://maxkb.xuyue.cc/chat/api/embed?protocol=https&host=maxkb.xuyue.cc&token=dd37457027c40b39 PUBLIC_MAXKB_URL=https://maxkb.xuyue.cc/chat/api/embed?protocol=https&host=maxkb.xuyue.cc&token=dd37457027c40b39
PUBLIC_OJ_URL=https://oj.xuyue.cc
PUBLIC_CODE_URL=https://code.xuyue.cc PUBLIC_CODE_URL=https://code.xuyue.cc
PUBLIC_JUDGE0_URL=https://judge0api.xuyue.cc PUBLIC_JUDGE0_URL=https://judge0api.xuyue.cc
PUBLIC_SIGNALING_URL=wss://signaling.xuyue.cc PUBLIC_SIGNALING_URL=wss://signaling.xuyue.cc
PUBLIC_WS_URL=wss://oj.xuyue.cc/ws
PUBLIC_ICONIFY_URL=https://icon.xuyue.cc PUBLIC_ICONIFY_URL=https://icon.xuyue.cc

View File

@@ -1,8 +1,6 @@
PUBLIC_ENV=school PUBLIC_ENV=school
PUBLIC_MAXKB_URL=http://10.13.114.114:92/chat/api/embed?protocol=http&host=10.13.114.114:92&token=dd37457027c40b39 PUBLIC_MAXKB_URL=http://10.13.114.114:92/chat/api/embed?protocol=http&host=10.13.114.114:92&token=dd37457027c40b39
PUBLIC_OJ_URL=http://10.13.114.114:81
PUBLIC_CODE_URL=http://10.13.114.114:82 PUBLIC_CODE_URL=http://10.13.114.114:82
PUBLIC_JUDGE0_URL=http://10.13.114.114:8082 PUBLIC_JUDGE0_URL=http://10.13.114.114:8082
PUBLIC_ICONIFY_URL=http://10.13.114.114:8098 PUBLIC_ICONIFY_URL=http://10.13.114.114:8098
PUBLIC_SIGNALING_URL=ws://10.13.114.114:8085 PUBLIC_SIGNALING_URL=ws://10.13.114.114:8085
PUBLIC_WS_URL=ws://10.13.114.114:81/ws

View File

@@ -1,8 +1,6 @@
PUBLIC_ENV=test PUBLIC_ENV=test
PUBLIC_MAXKB_URL=http://10.13.114.114:92/chat/api/embed?protocol=http&host=10.13.114.114:92&token=dd37457027c40b39 PUBLIC_MAXKB_URL=http://10.13.114.114:92/chat/api/embed?protocol=http&host=10.13.114.114:92&token=dd37457027c40b39
PUBLIC_OJ_URL=http://10.13.114.114:93
PUBLIC_CODE_URL=http://10.13.114.114:82 PUBLIC_CODE_URL=http://10.13.114.114:82
PUBLIC_JUDGE0_URL=http://10.13.114.114:8082 PUBLIC_JUDGE0_URL=http://10.13.114.114:8082
PUBLIC_ICONIFY_URL=http://10.13.114.114:8098 PUBLIC_ICONIFY_URL=http://10.13.114.114:8098
PUBLIC_SIGNALING_URL=ws://10.13.114.114:8085 PUBLIC_SIGNALING_URL=ws://10.13.114.114:8085
PUBLIC_WS_URL=ws://10.13.114.114:93/ws

View File

@@ -13,7 +13,7 @@ interface Props {
/** /**
* ACM 助手行。`acInfo` 的**内容**是 acm_contest_rank.submission_info 的 JSONB 原文, * ACM 助手行。`acInfo` 的**内容**是 acm_contest_rank.submission_info 的 JSONB 原文,
* 键名保持 snake_case —— 回滚时旧后端还要读 * 键名保持 snake_case —— 判题写进去的就是这套键名,见 utils/types.ts 的 SubmissionInfo
*/ */
type HelperItem = Omit<AcmHelperItem, "acInfo"> & { type HelperItem = Omit<AcmHelperItem, "acInfo"> & {
acInfo: SubmissionInfo acInfo: SubmissionInfo

View File

@@ -117,7 +117,7 @@ const abnormalServers = computed(() =>
) )
const websiteConfig = reactive<WebsiteConfig>({ const websiteConfig = reactive<WebsiteConfig>({
websiteBaseUrl: import.meta.env.PUBLIC_OJ_URL, websiteBaseUrl: "",
websiteName: "判题狗", websiteName: "判题狗",
websiteNameShortcut: "判题狗", websiteNameShortcut: "判题狗",
websiteFooter: "所有权归属于徐越,感谢青岛大学开源 OJ 系统,感谢开源社区", websiteFooter: "所有权归属于徐越,感谢青岛大学开源 OJ 系统,感谢开源社区",

View File

@@ -3,12 +3,10 @@
interface ImportMetaEnv { interface ImportMetaEnv {
readonly PUBLIC_ENV: string readonly PUBLIC_ENV: string
readonly PUBLIC_MAXKB_URL: string readonly PUBLIC_MAXKB_URL: string
readonly PUBLIC_OJ_URL: string
readonly PUBLIC_CODE_URL: string readonly PUBLIC_CODE_URL: string
readonly PUBLIC_JUDGE0_URL: string readonly PUBLIC_JUDGE0_URL: string
readonly PUBLIC_ICONIFY_URL: string readonly PUBLIC_ICONIFY_URL: string
readonly PUBLIC_SIGNALING_URL: string readonly PUBLIC_SIGNALING_URL: string
readonly PUBLIC_WS_URL: string
} }
interface ImportMeta { interface ImportMeta {

View File

@@ -18,10 +18,8 @@ export interface WebSocketMessage {
* WebSocket 配置 * WebSocket 配置
*/ */
export interface WebSocketConfig { export interface WebSocketConfig {
/** WebSocket 路径(如 '/ws/submission/' */ /** 完整 URL。后端只认 /ws/submissions 和 /ws/config 两条,按当前页面的协议与 host 拼 */
path: string url: string
/** 完整 URL提供后覆盖 PUBLIC_WS_URL + path */
url?: string
/** 最大重连次数,默认 5 */ /** 最大重连次数,默认 5 */
maxReconnectAttempts?: number maxReconnectAttempts?: number
/** 重连延迟(毫秒),默认 1000 */ /** 重连延迟(毫秒),默认 1000 */
@@ -61,7 +59,7 @@ export class BaseWebSocket<T extends WebSocketMessage = WebSocketMessage> {
public status: Ref<ConnectionStatus> = ref<ConnectionStatus>("disconnected") public status: Ref<ConnectionStatus> = ref<ConnectionStatus>("disconnected")
constructor(config: WebSocketConfig) { constructor(config: WebSocketConfig) {
this.url = config.url ?? `${import.meta.env.PUBLIC_WS_URL}/${config.path}/` this.url = config.url
this.maxReconnectAttempts = config.maxReconnectAttempts ?? 5 this.maxReconnectAttempts = config.maxReconnectAttempts ?? 5
this.reconnectDelay = config.reconnectDelay ?? 1000 this.reconnectDelay = config.reconnectDelay ?? 1000
@@ -306,10 +304,7 @@ class SubmissionWebSocket extends BaseWebSocket<SubmissionUpdate> {
constructor() { constructor() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:" const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
super({ super({ url: `${protocol}//${window.location.host}/ws/submissions` })
path: "submission",
url: `${protocol}//${window.location.host}/ws/submissions`,
})
} }
/** /**
@@ -387,7 +382,8 @@ export function useSubmissionWebSocket(
* *
* class NotificationWebSocket extends BaseWebSocket<NotificationMessage> { * class NotificationWebSocket extends BaseWebSocket<NotificationMessage> {
* constructor() { * constructor() {
* super({ path: 'notification' }) * const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
* super({ url: `${protocol}//${window.location.host}/ws/notifications` })
* } * }
* } * }
* *
@@ -448,10 +444,7 @@ export interface FlowchartEvaluationUpdate extends WebSocketMessage {
class FlowchartWebSocket extends BaseWebSocket<FlowchartEvaluationUpdate> { class FlowchartWebSocket extends BaseWebSocket<FlowchartEvaluationUpdate> {
constructor() { constructor() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:" const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
super({ super({ url: `${protocol}//${window.location.host}/ws/submissions` })
path: "flowchart",
url: `${protocol}//${window.location.host}/ws/submissions`,
})
} }
/** /**
@@ -518,10 +511,7 @@ export interface ConfigUpdate extends WebSocketMessage {
class ConfigWebSocket extends BaseWebSocket<ConfigUpdate> { class ConfigWebSocket extends BaseWebSocket<ConfigUpdate> {
constructor() { constructor() {
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:" const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
super({ super({ url: `${protocol}//${window.location.host}/ws/config` })
path: "config",
url: `${protocol}//${window.location.host}/ws/config`,
})
} }
/** /**

View File

@@ -19,7 +19,8 @@ import type {
/** /**
* 个人主页数据。`acmProblemsStatus` 的**内容**保持 snake_case —— * 个人主页数据。`acmProblemsStatus` 的**内容**保持 snake_case ——
* 它是 user_profile.acm_problems_status 的 JSONB 原文,回滚时旧后端还要读。 * 它是 user_profile.acm_problems_status 的 JSONB 原文,库里存量数据就是这套键名,
* 改名等于要把全部历史行迁一遍。
*/ */
export type Profile = Omit<UserProfile, "user" | "acmProblemsStatus"> & { export type Profile = Omit<UserProfile, "user" | "acmProblemsStatus"> & {
user: SessionUser user: SessionUser
@@ -281,7 +282,7 @@ interface Info {
/** /**
* 判题产出的统计。**键名保持 snake_case** —— 这是 submission.statistic_info * 判题产出的统计。**键名保持 snake_case** —— 这是 submission.statistic_info
* JSONB 的原文,判题机写进去、回滚时旧后端还要读,不能跟着响应字段一起改名。 * JSONB 的原文,判题机按这套键名写进去,不能跟着响应字段一起改名。
*/ */
export interface StatisticInfo { export interface StatisticInfo {
score?: number score?: number
@@ -368,7 +369,7 @@ export type BlankContest = Omit<
/** /**
* acm_contest_rank.submission_info 的 JSONB 内容。**键名保持 snake_case** —— * acm_contest_rank.submission_info 的 JSONB 内容。**键名保持 snake_case** ——
* 判题写进去、回滚时旧后端还要读,不能跟着响应字段一起改名。 * 判题按这套键名写进去,历史比赛的榜单行也是这个形状,不能跟着响应字段一起改名。
*/ */
export interface SubmissionInfo { export interface SubmissionInfo {
is_ac: boolean is_ac: boolean
@@ -380,7 +381,7 @@ export interface SubmissionInfo {
/** /**
* 榜单行。`submissionInfo` 的**内容**仍是 snake_case —— 它是 acm_contest_rank * 榜单行。`submissionInfo` 的**内容**仍是 snake_case —— 它是 acm_contest_rank
* 表的 JSONB 原文,回滚时旧后端还要读,见 SubmissionInfo。 * 表的 JSONB 原文,见 SubmissionInfo。
*/ */
export type ContestRank = Omit< export type ContestRank = Omit<
import("@oj2/contract").ContestRankItem, import("@oj2/contract").ContestRankItem,

View File

@@ -92,9 +92,8 @@ function injectMaxkb(maxkbUrl: string | undefined): Plugin {
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "PUBLIC_") const env = loadEnv(mode, process.cwd(), "PUBLIC_")
// 开发时一律指向本机后端。PUBLIC_OJ_URL / PUBLIC_WS_URL 是迁移期指向 // 开发时一律指向本机后端apps/api3000
// 旧 Django 用的,端点搬完后不再参与代理。 const backend = {
const newBackend = {
target: "http://localhost:3000", target: "http://localhost:3000",
changeOrigin: true, changeOrigin: true,
} }
@@ -162,12 +161,11 @@ export default defineConfig(({ mode }) => {
server: { server: {
port: 5173, port: 5173,
proxy: { proxy: {
// 迁移期间 /api2、/ws2 是「新后端」的临时前缀,/api、/ws 还指着 Django。 // 只有这三段过后端,和生产的 Caddy 认的是同一套docker/Caddyfile
// 端点已全部搬完,临时前缀去掉、统一指向新后端 —— 生产的 Caddy 也只认 // 别在这里加代理规则而不同步改 Caddyfile。
// /api、/ws、/public 这三段docker/Caddyfile "/api": backend,
"/api": newBackend, "/public": backend,
"/public": newBackend, "/ws": { ...backend, ws: true },
"/ws": { ...newBackend, ws: true },
}, },
}, },
} }

View File

@@ -11,9 +11,8 @@ export const problemDifficultySchema = z.enum(["Low", "Mid", "High"])
/** /**
* SQL 题配置与展示数据。两者都是 `problem.sql_config` / `problem.sql_display` * SQL 题配置与展示数据。两者都是 `problem.sql_config` / `problem.sql_display`
* 的 **JSONB 原文**,所以键名保持 snake_case —— 回滚时旧后端要读同一份, * 的 **JSONB 原文**,所以键名保持 snake_case —— 生产库 9 道 SQL 题存的就是这个形状
* 旧后端 `judge/sql_runner.py:build_display` 产出的就是这个形状 * (移植自旧后端 `judge/sql_runner.py:build_display`,逐条比对过,键集完全一致)。
* (生产库 9 道 SQL 题逐条比对过,键集完全一致)。
* *
* 写成精确 schema 而不是 `z.record(z.unknown())`:前端原来得自己手抄一份 * 写成精确 schema 而不是 `z.record(z.unknown())`:前端原来得自己手抄一份
* SQLDisplay 接口才能渲染表格,抄错了没人拦得住。 * SQLDisplay 接口才能渲染表格,抄错了没人拦得住。