From 7b6bfeb6fb414041aeaafce16c1439834da074f5 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Wed, 26 Aug 2026 00:22:05 -0600 Subject: [PATCH] =?UTF-8?q?fix(=E6=A0=87=E7=AD=BE):=20/problem-tags=20?= =?UTF-8?q?=E8=AE=A1=E6=95=B0=E6=BC=8F=E8=BF=87=E6=BB=A4=E9=9A=90=E8=97=8F?= =?UTF-8?q?=E9=A2=98=E5=92=8C=E8=B5=9B=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 标签列表按题目数 > 0 过滤,但计数没有像 /problems 那样限制 visible=true 且非赛题,导致标签在首页出现、点进去却一道题都没有。 同时把旧仓库冻结政策收紧写进 CLAUDE.md:从今天起旧仓库零改动, 不再有内部小修的例外,所有后续工作只落在 OJ2。 Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 19 ++++++++----------- apps/api/src/routes/problem.ts | 7 +++++++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9654982..9f136df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,19 +2,16 @@ OJ2 是判题狗(Online Judge)的后端重写:Django 6 → Bun + TypeScript,前后端同仓。 上一代在 `../OnlineJudge/`(Django)和 `../ojnext/`(Vue SPA),**两者都是回滚路径, -默认冻结。** +完全冻结、一行都不改。** -冻结的目的是「回滚那天旧站能原样起来、和新站看到同一份数据」,不是「一个字节都不许动」。 -所以红线是**外部可观测的东西**: +> **2026-08-26 起**:旧仓库零改动,没有例外——包括修 bug、包括不影响外部接口的 +> 内部小修(日志、缓存实现之类,以前允许,现已收紧)。所有后续工作,包括在旧仓库 +> 里发现的 bug,都只落在 OJ2:先确认 OJ2 是否有对应逻辑、是否重现了同样的问题, +> 只在 OJ2 里修;旧仓库那边如实告知用户"未处理,按当前政策不动旧仓库",不要顺手改掉。 -- **不许动**:接口路径与响应结构、数据库 schema、磁盘上的数据布局、密码哈希格式、 - 依赖版本。这些一动,回滚就不再是「把上游切回去」那么简单。 -- **可以动**:纯内部实现的小修(缓存放哪、日志、注释),前提是签名、异常、 - 返回结构逐一对齐不变,且 `uv run ruff check` 通过。改完在 commit message 里 - 写清楚为什么值得破例。 - -已发生的破例:`utils/cache.py` 的 `JsonDataLoader` 把一言数据集塞 Redis,每次请求 -都要把 323KB 的 pickle 拉过网络再反序列化,改成了进程内缓存。响应结构没动。 +冻结的目的是「回滚那天旧站能原样起来、和新站看到同一份数据」——改 OJ2 时,接口路径 +与响应结构、数据库 schema 这些外部可观测的东西都要小心,一动回滚就不再是「把上游 +切回去」那么简单。 设计文档:`docs/specs/2026-08-06-bun-backend-rewrite-design.md` 切换手册:`docs/specs/phase5-cutover-runbook.md` ← 上线当天照这份走 diff --git a/apps/api/src/routes/problem.ts b/apps/api/src/routes/problem.ts index 0743cf9..a4c7063 100644 --- a/apps/api/src/routes/problem.ts +++ b/apps/api/src/routes/problem.ts @@ -148,9 +148,16 @@ problemRoutes.get("/problems", optionalAuth, async (c) => { problemRoutes.get("/problem-tags", async (c) => { const keyword = c.req.query("keyword")?.trim() + // 只数公开题库里可见的题:隐藏的题和比赛题都不算,否则标签会出现在 + // 首页列表里,点进去却一道题都筛不出来(对齐 /problems 的过滤条件) const rows = await db.select({ id: schema.problemTag.id, name: schema.problemTag.name, problemCount: countDistinct(schema.problemTags.problemId) }) .from(schema.problemTag) .innerJoin(schema.problemTags, eq(schema.problemTags.problemtagId, schema.problemTag.id)) + .innerJoin(schema.problem, and( + eq(schema.problem.id, schema.problemTags.problemId), + eq(schema.problem.visible, true), + isNull(schema.problem.contestId), + )) .where(keyword ? ilike(schema.problemTag.name, `%${keyword}%`) : undefined) .groupBy(schema.problemTag.id, schema.problemTag.name).having(sql`count(${schema.problemTags.problemId}) > 0`) .orderBy(asc(schema.problemTag.name))