|
|
a8408c0bb5
|
docs: 文档整理,CLAUDE.md 瘦身一半,删掉重写期已完成的 22 份阶段产物
CLAUDE.md 从 490 行降到 228 行:只留日常要当场记住的约束,展开拆成五份专题
文档 —— docs/deploy.md(部署与备份恢复)、database.md(迁移执行器、基线、
drizzle-kit 的坑)、timezone.md(时区口径与那次成就订正)、contract.md
(出参不 parse 的四次故障)、ast-rules.md(AST 规则与 C++ 的调用形态)。
删掉的是阶段 0–5 那批一次性产物:4 份实施计划、10 份评审/核验/修复报告、
endpoint-inventory.md(110 端点是 2026-08 的快照,现在 363 条路由)、
docs/spikes/ 的 spike 与提取脚本(结论早已落进代码)。phase5 切换手册删之前
先把仍然有效的部分提炼进 docs/deploy.md:拓扑、deploy.sh、部署后验证清单、
NPM 那两个不能关的开关、pg_dumpall 恢复的两个坑、镜像体积;演练报告与回滚
两节随旧栈下线一并作废。
两份设计文档保留,补上状态行说明它们是「当初为什么这么定」而不是现状。
apps/web/CLAUDE.md 顺手订正过期内容:PUBLIC_OJ_URL / PUBLIC_WS_URL 两个变量
早已不存在(baseURL 写死 /api,dev 走 vite proxy、线上由 Caddy 同源伺服),
store 与 composable 清单补齐到与目录一致。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-16 08:03:26 -06:00 |
|
|
|
cc51e305cf
|
refactor(时区): 常量收进契约、SQL 统一走 localTime,去掉会话时区与 TZ 兜底
Deploy / deploy (push) Has been cancelled
- TIME_ZONE / TIME_ZONE_OFFSET_MINUTES 移到 packages/contract/src/time.ts,
前后端共用一份,不再各写一遍靠注释对齐。
- apps/api/src/time.ts 新增 localTime(列),替换散落 7 处的
`at time zone ${TIME_ZONE_SQL}`;ac-trend 的 where 复用同一个 year 表达式。
- /problems/:displayId/yearly-ac 漏写了时区、按 UTC 切年,被会话时区兜底掩盖;
改为按东八区切(只影响每年 12-31 北京 0–8 点的提交归年)。
- 删掉数据库连接的 TimeZone 和 Dockerfile 的 TZ:正确代码不依赖它们,
它们只会在线上掩盖漏写处、让 dev 与线上答案不同。
- time.ts:calendarDayYearsAgo/pad 并入 shiftMonthsByCalendar,startOfCalendarDay
并入 todayStart,localWeekday 改用 getUTCDay,删掉历史叙述注释。
- 前端 zonedParts 改为固定偏移 + getUTC*(与后端、日期选择器同一写法),
去掉 Intl formatToParts;10 万次 299ms → 11ms。zonedYear 去掉按浏览器时区的兜底。
- 两份 CLAUDE.md 同步;n-date-picker 那条过时说明改成现用法。
验证:新旧「近两年起点」21359 个时刻 0 差异、localWeekday 0 差异、
前端固定偏移与 Intl 在 America/New_York 下 47821 个时刻 0 差异;
localTime 在 select/group by/where 复用可用;fix-achievement-hours 预演仍为 148 / 60。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1d8B3f4SXJwDvUY625eQd
|
2026-09-14 06:06:52 -06:00 |
|
|
|
4c0c38445c
|
fix(时区): 日历口径收回东八区,读出的时刻统一成 ISO 并保留微秒
旧栈 Django 按 Asia/Shanghai 算日历,OJ2 重写时这个锚点丢了:容器和数据库会话
都是 UTC,于是「今日提交」在北京时间 0–8 点是空的,「凌晨/早起提交次数」整体偏
8 小时,热力图、AC 趋势年份、近两年活跃人数也各按进程时区切。
- 新增 apps/api/src/time.ts 作为唯一锚点(固定 +8 偏移,不依赖进程 TZ / tzdata),
todayStart、成就小时/日期键、热力图、月份平移、年份夹逼全部改走它;
SQL 里按日历切的一律显式 at time zone。
- db/index.ts:连接会话时区设为东八区(兜底);给 timestamptz(1184) 挂 parser,
读出统一成 ISO 8601 UTC,撤掉为拿 PG 文本形状写的 ::text。parser 保留微秒 ——
生产库 12.3 万条提交几乎全带微秒,截成毫秒会让翻页分界行和班级 AC 排名的
<= min(create_time) 把自己排除(翻页每页丢一条、排名少 1)。
- 前端 parseTime/zonedParts/zonedYear 按 Asia/Shanghai 渲染,n-date-picker 做
toPickerValue/fromPickerValue 平移,站内不再按浏览器时区取时间部件。
- Dockerfile 设 TZ=Asia/Shanghai 作为第二道兜底。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1d8B3f4SXJwDvUY625eQd
|
2026-09-14 05:55:17 -06:00 |
|
|
|
fdfb064d0c
|
fix(备份): 磁盘余量检查在老 mawk 上崩掉
Deploy / deploy (push) Has been cancelled
服务器上跑 backup-db.sh 报:
docker/backup-db.sh: 第 132 行:[: 4.29685e+10: 需要整数表达式
`df -Pk | awk 'NR==2 { print $4 * 1024 }'` 让 awk 做了那个乘法。**mawk 1.3.3**
(老 Debian 的 /usr/bin/awk)打印超过 2^31 的整数时会退回 OFMT(%.6g),42968547328
就成了 `4.29685e+10`,丢回 [ ] 比大小当场退出。用 debian:buster-slim 复现出了
一模一样的字符串;mawk 1.3.4(现在的 stable)和 busybox awk 都正常打整数,
我本机是 gawk,所以测不出来。
改成原样取 df 的第 4 列(KB),乘 1024 放到 bash 的 64 位算术里做,谁的 awk 都一样。
另外加一层 digits_or_zero:df 或 psql 返回的东西不是纯数字就当「读不出来」,
**跳过余量检查照常备份** —— 余量检查是提醒,不该因为读不到它就不备份了。
(顺带把「磁盘剩 0.0 B」那句误导的输出并进警告里。)
clear-sessions.sh 里那句 awk 求和也过了一遍:Redis 键数不可能到 2^31,不受影响。
验证:正常路径照旧;把 free_kb 强行喂成 `4.29685e+10`,现在是一条警告加一次
完整备份,不再退出。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqjE6qPo67fqVDKn6Bx7yd
|
2026-09-08 05:26:09 -06:00 |
|
|
|
2bf0e7bfc1
|
ops(备份): 加 backup-db.sh,按容器名导出、校验完整性、带保留策略
Deploy / deploy (push) Has been cancelled
原来手敲的那条
docker compose exec -T oj-postgres pg_dumpall -c -U onlinejudge > db_backup_xxx.sql
在服务器上报 `service "oj-postgres" is not running`。容器明明在跑 —— 线上是外接
形态,postgres 由 /root/OJDeploy/docker-compose.yml 起,不归 OJ2 这套 compose 管;
compose.debian.yml 里那个 oj-postgres 挂着 `profiles: ["local-data"]`,只在自带
数据形态下启动。所以脚本直接按容器名 `docker exec`,跟谁起的无关,两种形态都能用。
比原来那条命令多做的事:
- **先写 .partial,验完才改名**。`> file.sql` 中途失败(容器挂了、盘满了、
pg_dumpall 报错)会留个半截文件,看着像备份,等到要恢复那天才发现不是。
中断也清掉。
- **验完整性**。gzip -t,再检查结尾有没有「PostgreSQL database cluster dump
complete」,缺了就当失败。
- **umask 077 + chmod 600**。备份里有 user.raw_password(明文密码,本来就是留给
老师查的)和角色口令散列,不该落成 644。
- **自检真连一次库**(psql -c 'select 1' 而不是 pg_isready)。后者用户名写错照样
说 OK,要到 pg_dumpall 才炸出一句 role does not exist。
- **默认存到仓库外面**。deploy.sh 头部那条 rsync 带 --delete,备份放仓库里下次
部署就没了;真放进去了会警告。
- **保留策略带兜底**。删超期的,但最新 3 份永远保留 —— 时钟错乱或者 --keep-days
手滑填 0,都不该把手头唯一的备份删掉。
- **磁盘余量检查**。剩余空间比库还小就拒绝,--force 才继续。备份把生产盘写满比
没备份更糟。
默认 gzip,--plain 关掉。头部写了 cron 的写法,以及恢复时那几条
`does not exist` / `already exists` 是 pg_dumpall -c 的正常噪音。
本机 dev 库(245 MiB)实跑:正常路径 38.9 MiB gz / 2 秒;容器名写错、用户名写错都在
自检拦住;导出中途失败后 .partial 被清掉;保留策略造 5 份 30 天前的 → 删 5 留 3,
把全部文件做旧 → 仍然保住最新 3 份。**恢复也真跑了**:起一个干净的 postgres:16-alpine
灌进去,submission=12 / user=11 / problem=20 / 28 张表,和原库一致。
只管数据库。判题测试点在 data/backend/test_case,不在库里,那份还没有备份手段。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqjE6qPo67fqVDKn6Bx7yd
|
2026-09-08 05:21:13 -06:00 |
|
|
|
ddc3f05bc3
|
fix(运维脚本): clear-sessions.sh 补上 dash 垫片
Deploy / deploy (push) Has been cancelled
`sh docker/clear-sessions.sh` 在 Debian 上会用 dash 跑(/bin/sh 就是它),
第 34 行的 `set -euo pipefail` 里 pipefail 是 bash 专有的,脚本一上来就死在
`Illegal option -o pipefail`,正事一件没干。deploy.sh 里早就有同一道垫片,
写这个脚本时漏抄了。
实跑验证(dev 栈,`sh docker/clear-sessions.sh`):垫片生效后跑到容器探活
那步正常报错;`CONTAINER=oj2-redis YES=1 sh docker/clear-sessions.sh` 全程
走通 —— session:* 删 101 个、user-sessions:* 删 4 个、bull:* 前后都是 57 个
(dbsize 162 → 57,判题队列一个键没动)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XvmqDsZNyUo9P3sFQtoWVB
|
2026-09-07 18:49:24 -06:00 |
|
|
|
01d7924faa
|
ops(会话): 加一次性脚本清掉全部会话,闭掉存量会话吊销不掉的空窗
反向索引 user-sessions:<uid> 是 498fc1c 才加的,在那之前签发的会话不在索引里,
revokeUserSessions 靠 SMEMBERS 找不到它们 —— 改密码、重置密码、禁用账号对这批会话
统统无效,只能等最长一个 SESSION_TTL_SECONDS(默认 7 天)自然过期。学生密码是明文
存着给老师查的,改密码正是密码泄露后唯一的补救手段,这个空窗不留。代价是所有人重新
登录一次。跑过一次就不用再跑,此后签发的会话都带索引。
**两个站点都要跑。** 机房和服务器共用一个数据库,但各有各的 Redis,会话存在各自的
Redis 里,只清一边等于只解决一半。脚本结尾会提醒这件事。
只删 session:* 和 user-sessions:*,不用 FLUSHALL:同一个 Redis 里还装着 BullMQ 的
判题队列,清掉等于把还在队列里的提交全丢了,那些 submission 会永远停在 PENDING。
删完会核对 bull:* 的键数没变,变了就报错让人工检查。
扫描用 SCAN 不用 KEYS(KEYS 会阻塞住整个 Redis,而这上面还挂着所有人的会话读写),
xargs 分批 DEL 避免顶到命令行长度上限。
拿一次性容器验过:1200 条 session + 50 条索引全删(跨过分批边界),30 个 bull 键和
throttling 桶原样不动;空库上重跑是幂等的;容器名写错时报错退出不做任何事。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XvmqDsZNyUo9P3sFQtoWVB
|
2026-09-07 07:45:47 -06:00 |
|
|
|
f00c941ede
|
perf(构建): builder 分开拷源码,只改后端不再陪编一次前端
Deploy / deploy (push) Has been cancelled
builder 阶段是 `COPY . .` 一把梭,而 docker 的层缓存是链式的:那一层的校验和
覆盖整个仓库,改任何一个文件都会把它作废,后面 vite build 和 bun compile 全部
重跑。前端那 160s 一次都躲不掉,改一篇 markdown 也要陪着编。
按各自的输入分开拷,慢的那条放前面(缓存链上越靠前越不容易被碰到):
只改 apps/api 前端两层命中缓存,只重编后端 ~1s
只改 apps/web 前端重编,后端被链条带着 ~160s
改 packages/contract 两边都重编(本来就都依赖它,是对的)
改 docs / 根上的杂项 builder 根本看不见 0
代价是 builder 只看得见这里点名拷进来的四样,新加顶层目录要同步加 COPY。
顺带堵掉两个同源的洞:
· apps/web/docs、tests、**/CLAUDE.md 进了 .dockerignore。它们在 apps/web 里面,
整个目录拷进去的话,改一篇文档也会让 vite build 那层失效。
· .dockerignore 末尾为 --prebuilt 开的例外(!dist/oj2-api、!apps/web/dist)让本地
产物照样进构建上下文 —— 这是比 `COPY . .` 更隐蔽的一条:在本机编一次后端,
80MB 的 dist/oj2-api 变了,rsync 上去,服务器就重编一次前端,源码一行没动。
dist/oj2-api 现在不再被 builder 拷贝;apps/web/dist 仍在 COPY apps/web 里面,
所以 deploy.sh 头上那条手动 rsync 补了 --exclude dist。CI 走 --prebuilt,
builder 整个不进构建图,不受影响。
验证:用老 Dockerfile 和新 Dockerfile 各构建一次 --target artifacts 导出对比,
后端二进制 sha256 相同,前端 dist 500 个文件逐个 sha256 一致。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-26 10:25:15 -06:00 |
|
|
|
5f04f3283f
|
fix(部署): 迁移那步的服务名写成了 api,实际是 oj-api
Deploy / deploy (push) Has been cancelled
`docker compose run ... api oj2-api migrate` 直接报 `no such service: api`。
compose 里六个服务全带 oj- 前缀(oj-api / oj-worker / oj-web / oj-judge /
oj-postgres / oj-redis),没有叫 api 的。
顺带修掉失败提示:原来不管迁移因为什么失败,都说「破坏性迁移需要
OJ2_ALLOW_DESTRUCTIVE=1 显式放行」——而这次的真实原因是服务名不存在,
提示把人往完全错误的方向带。现在改成让人去看 migrate 自己的输出
(它对每种情况都打印了具体该做什么),只把三种常见原因列成线索。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-26 08:32:19 -06:00 |
|
|
|
19054718d5
|
fix(部署): 迁移那步去掉 --no-deps,否则自带数据形态下起不来库
Deploy / deploy (push) Has been cancelled
上一个提交让 deploy.sh 支持「自带数据」形态,但迁移那步还留着 --no-deps。
`run --rm --no-deps api oj2-api migrate` 不会拉起 oj-postgres,于是这一步
直接连不上库 —— 而它正好排在 `up -d` 之前,postgres 这时候还没起。
oj-api 的依赖恰好就是 oj-postgres / oj-redis,两种形态都是对的:
- 自带数据:靠 run 把 postgres 起来,depends_on 的 condition: service_healthy
还顺带保证库真的就绪了才开始迁移
- 外接:那两个服务不在启用的 profile 里,depends_on 上的 required: false 让
compose 跳过,不会多起东西
worker / web 不是 api 的依赖,两种形态下都不会被带起来,去掉 --no-deps
不会有副作用。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-26 08:25:17 -06:00 |
|
|
|
e6c8c3c155
|
feat(部署): deploy.sh 识别「自带数据」形态,不再写死试跑假设
Deploy / deploy (push) Has been cancelled
旧 Django 后端下线后要把 postgres / redis 收回 OJ2 自己的 compose 管。
compose 那边本来就备好了(oj-postgres / oj-redis 挂在 local-data profile 下,
镜像和旧栈同版本),但 deploy.sh 三处都写死了「并行试跑」的假设,会直接挡住:
- COMPOSE 没有 --profile local-data,自带的两个容器永远不会被启动
- 自检② 见到 DATABASE_URL 指向 oj-postgres 就 die —— 而合并后它必然指向那里
- 自检④ 要求已经有 oj-postgres 在跑 —— 旧的停了、新的还没起,正好卡死
现在按 DB_HOST 是否为空自动判形态。判据用 DB_HOST 而不是另加开关,是因为它本来
就决定了 DATABASE_URL 指向谁;两个变量各说各话迟早会出现「起了自带的库、却连到
别处」这种自相矛盾的状态。
自检② 改成两个方向都查:外接形态下不许指向 oj-postgres,自带形态下必须指向
`oj-postgres:5432`。连端口一起查是因为清了 DB_HOST 却忘了清 DB_PORT 会拼出
`oj-postgres:5445` —— 那是宿主机映射端口,容器网络里不通。
新增自检②b:自带形态下检查 $DATA_DIR/postgres/PG_VERSION 存在且是 16。
这是整个部署里唯一会**静默**走歪的地方 —— DATA_DIR 不对的话 postgres 当成全新
部署,在空目录上初始化一个空库,站点起得来、能注册能登录,就是一道题都没有。
验证:用模拟的 env 跑 `docker compose config` 确认
--profile local-data → 服务多出 oj-postgres / oj-redis
DATABASE_URL → postgres://onlinejudge:...@oj-postgres:5432/onlinejudge
REDIS_URL → redis://oj-redis:6379
卷 → /root/OJDeploy/data/{postgres,redis}(旧栈同一批目录)
并验证了「清了 DB_HOST 但忘清 DB_PORT」会被自检②拦下、正常配置会放行。
形态判定对空值 / 非空值 / 缺失三种情况都试过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-26 08:19:28 -06:00 |
|
|
|
586c88f629
|
build(数据库): 改用 drizzle migration,加提交列表索引、清掉 Django 残留
Deploy / deploy (push) Has been cancelled
一条线上的三件事:让 drizzle 的迁移机制真正可用 → 用它加索引 → 用它清掉
不再需要的 Django 表,最后接进部署和 CI。
## 1. 让 drizzle-kit generate 可用
原本以为不能用:只加一个索引,generate 却吐出一堆噪音,其中 5 条
`DROP SEQUENCE auth_*/django_*` 打到生产库上会直接搞坏旧后端。
逐个查下来全是 `pull` 出的基线自己不能 round-trip,都是可修的:
- **快照里的 Django 序列**:tablesFilter 只过滤表、不过滤它们的序列。
已从 0000_snapshot.json 清掉。
- **bigint 上限精度**:pull 生成的 `maxValue: 9223372036854775807` 是 JS
number 字面量,round-trip 成 ...776000,每次 generate 都多出 10 条
ALTER COLUMN。改成字符串。
- **表达式索引的 opclass**:problem_tag_name_ci_unique 在快照里带
opclass,drizzle 自己序列化不出来,导致每次 drop + recreate。已去掉。
改完 `generate` 是干净的 no-op。
两个改不掉、只能绕的写进了 CLAUDE.md:索引 `.desc()` 生成 SQL 时会被丢
(单列索引不写方向即可,Postgres 用 Index Scan Backward 服务 ORDER BY
DESC,实测同样 0.08ms);migrator 把所有语句包一个事务,
CREATE INDEX CONCURRENTLY 跑不了。
最容易吃亏的是 drizzle 没有 --fake-initial:对已有数据的库直接 migrate
会从 0000 跑起、撞表回滚,**而且 exit 1 但一个错误都不打印**。
## 2. 0001 提交列表索引
`WHERE contest_id IS NULL ORDER BY create_time DESC LIMIT n` 用不上现有的
contest_create_time_idx (contest_id, create_time DESC) —— Postgres 不把
`IS NULL` 当成能吃掉首列、从而继承第二列有序性的等值条件。把
enable_seqscan / enable_bitmapscan 全关掉逼它用也不肯,宁可走单列
contest_id 索引再全量排序。于是每翻一页都 Parallel Seq Scan 扫完整张表。
换成部分索引后谓词由索引自己保证,索引序就是查询要的排序序。生产快照
(12.3 万条提交)实测首页取 10 行:61.8ms / 读 18936 blocks →
0.22ms / 读 34 blocks。端到端 94ms → 6ms。
真正要命的不是单次 61ms,是每个请求都要把 169MB 的表刷一遍
shared_buffers —— 一节课几十个学生同时开提交列表,磁盘和缓存直接被打穿。
## 3. 0002 删掉 Django 残留
确认旧 Django 后端不再使用、也不再作为回滚路径。删前核实过:没有任何
OJ2 保留的表引用这 7 张,3 条外键全在它们内部(所以不用 CASCADE,真有
漏网的会报错而不是被悄悄级联掉);5 个序列都由各自的表 owned,随
DROP TABLE 一并消失;数据全是 Django 自身元数据。tablesFilter 随之移除。
**回滚路径就此作废** —— CLAUDE.md 开头和 runbook 的「回滚保证」「七、回滚」
都改了。这条迁移已在本机 dev 库和生产快照副本上跑通,**生产库尚未执行**。
## 4. migrate 接进部署与 CI
deploy.sh 在构建之后、起栈之前跑 `oj2-api migrate`,失败就中止部署(旧
容器原样还在跑)。CI 走的也是 deploy.sh,所以不用给 GitHub 配数据库凭据,
也不用把生产库对外开放。
迁移文件**不内嵌进二进制**,随镜像装在 /usr/local/share/oj2/migrations。
这样 drizzle 的 migrate() 能原样用 —— 靠 _journal.json 自动发现,新增迁移
不用改任何代码,和 Django 扫 migrations/ 是一回事。内嵌就得为每条迁移
手写一行 import,那是迟早会漏的账。(CLAUDE.md 里「单二进制不能读文件」
那条讲的是 node_modules 和 import.meta.dir 推路径,按显式绝对路径读一个
数据目录不在此列。)
三道闸门,都是写完测出来才补上的:
- **破坏性迁移拦截**:DROP TABLE / DROP COLUMN / DROP SCHEMA /
ALTER COLUMN ... TYPE / TRUNCATE 命中就退出 4,需要
`OJ2_ALLOW_DESTRUCTIVE=1` 显式放行。DROP INDEX / DROP CONSTRAINT 不算,
拦了只会让人习惯性带上放行开关。扫描前先剥注释,避免误报。
- **基线缺失**:退出 3 并直接打印该敲的 SQL。注意判的是
`max(created_at) < 0` 而不是「表不存在」—— 表存在而为空(上次迁移失败
留下的)同样是没基线。
- **迁移目录读不到**:这是最可能犯的错(Dockerfile 漏拷),原本是 drizzle
的堆栈,现在直接说该检查哪一行。
另外发现 0000_crazy_gateway.sql 是 pull 的产物,**整份被 /* */ 包着,
可执行语句 0 条**,所以这个库根本不能靠迁移自举建表。原先写的「空库就从
0000 建」跑起来会炸在一个和真实原因毫不相干的 unterminated /* comment 上。
现在如实说明:结构只能来自 docs/specs/schema.sql 或生产 dump。
验证:镜像内编译(ARTIFACTS=build)出的真实镜像跑完五种场景 —— 拦截、
放行、幂等、无基线、漏拷目录,全部符合预期;dev 形态同样五种场景全过。
tsc / 路由遮蔽 / deploy.sh 语法 / generate no-op 都通过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-26 07:56:30 -06:00 |
|
|
|
dd6a6a04eb
|
refactor(密码): 删掉 PASSWORD_HASH_UPGRADE,无条件写 argon2
Deploy / deploy (push) Has been cancelled
上一个 commit 把五个写入点收进 hashPassword,顺手把开关默认值翻成了 true 并
留下 false 分支当退路。这个退路是假的,删掉。
**开关只能拦住将来,修不了已经发生的。** 等真到了要把旧站拉回来的那天,活跃
账号早就都被登录/重置密码升成 argon2 了,这时候设 `=false` 一个也救不回来。
正经退路是切换手册「万一已经改坏了」那节的脚本 —— 拿 `raw_password` 重算
Django 的 `make_password`,能修已经变成 argon2 的账号。留着一个永远轮不到它
上场的开关,只是给后来人多一件要读懂的东西。
删掉的:`config.passwordHashUpgrade`、`hashDjangoPbkdf2` 和它那套 Django 参数
(1200000 迭代 / 22 位 salt / RANDOM_STRING_CHARS)、两个 compose 里的
`PASSWORD_HASH_UPGRADE` env。auth.ts 的登录升级变成无条件。
保留的:
- `hashPassword()` —— 虽然现在只是一行 argon2,但「只有一个地方写密码」正是上
一个 commit 的重点。五处各写各的才是当初那个 bug 的根。
- `verifyPassword` 的 **pbkdf2 分支** —— 生产库 1710 个账号全是 Django 写的
pbkdf2(迭代次数 120000~1200000),只会在各自下次登录时才迁移。删了就是
全站登不上。代码注释里写死了这句话。
- 切换手册里的 `raw_password` 恢复脚本,以及回滚流程里新加的「密码要额外处理」
那一步。
## 验证
改完重跑了一遍关键路径(临时造一个 Django 生成的 120000 迭代老哈希):
- 存量账号登录 200 → 哈希变成 argon2 → 再登 200 → 错密码 401
- 后台重置密码 → argon2 → 新密码能登录
- 注册 → argon2 → 能登录
tsc(apps/api) 0 error、check:routes 168 条无遮蔽、vue-tsc 0 error、build 通过、
两个 compose `config -q` 通过。测试用户已删干净。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-25 23:50:55 -06:00 |
|
|
|
ed9d9f65bf
|
chore(部署): deploy.sh 结尾别再把 NPM 当待办事项
Deploy / deploy (push) Has been cancelled
反代早就配好了,每次部署完还提示「还差 NPM 那一步」是噪音。改成只报站点
地址,把 Websockets Support / client_max_body_size 200M 降级成「别关」的
提醒 —— 只有改 WEB_PORT 时才需要回 NPM 动一下。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-25 18:36:08 -06:00 |
|
|
|
6356643cc9
|
ci(部署): 部署走 GitHub Actions,产物挪到 runner 上编
Deploy / deploy (push) Has been cancelled
服务器性能差,镜像 builder 阶段(1238 个包的 bun install + bun compile +
vite build)首次约 5 分钟,光前端就 160s。改成在 runner 上编好 dist/oj2-api
和 apps/web/dist,rsync 过去,服务器那边只剩几条 COPY。
服务器自己编的能力没有砍掉。Dockerfile 顶部加了全局 ARG ARTIFACTS:
build(默认) builder 阶段自己编,只要有 docker 就能手动部署
prebuilt 用构建上下文里现成的产物,builder 不进构建图
实现是两个 scratch 阶段归一成 /artifacts/ 布局,再用变量阶段名
FROM artifacts-${ARTIFACTS} 选一个,运行时阶段不关心产物是谁编的。
deploy.sh 加 --prebuilt:产物缺失在自检就 die;顺带检测 buildx —— 没装的话
compose 会退回 classic builder,那个不看依赖图、所有阶段挨个跑,产物白编
(结果正确但不省时间),黄字提醒不阻断。
.dockerignore 末尾放行两个产物路径,必须在 dist/ 那些规则之后,
dockerignore 是最后一条匹配说了算。
顺手删掉 apps/web/.github/ —— 从 ojnext 抄来的残留,路径不对(GitHub 只认
仓库根的 .github/workflows/)从来没跑过,内容也是 ojnext 的。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-25 18:30:22 -06:00 |
|
|
|
604857b25e
|
fix(一言): 接回真数据集,之前线上返回的是三条硬编码
/quotes/random 在阶段 3 基线里只铺了个桩(三条写死的句子),读盘逻辑没移植,
所以 oj2.xuyue.cc 上一直是假数据——和 DATA_DIR 无关,哪儿部署都一样。
数据本来就在服务器上、位置也已经对上:旧栈挂 ./data/backend:/data,Django 的
HITOKOTO_DIR 就是它下面的 hitokoto/;OJ2 的 api 挂的是同一个目录。所以
Dockerfile 里跟着 TEST_CASE_DIRECTORY 写死 /data/hitokoto 即可,不用搬文件。
按分类懒加载并常驻,但只留前端用得上的 hitokoto/from 两个字段:原样缓存
7160 条要 2.5MB,裁完 1.4MB,而 api 容器只有 512m。读不到数据集时回落到
内置三条,不影响启动(本机 dev 默认就走这条)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-16 10:20:00 -06:00 |
|
|
|
face92a5dc
|
fix(登录): 默认不再把 Django 的 pbkdf2 升级成 argon2 —— 这是道单向门
试跑第一天就出事了:在 oj2 上登录过的账号,回旧站 oj.xuyue.cc 登不上,WS 也连不上
(旧站的 Channels 认 session,没登录自然连不上,两个症状同一个根因)。
`routes/auth.ts` 原来在登录成功后把 pbkdf2 哈希重算成 argon2id 写回库。问题是:
- Bun 写出来的是 `$argon2id$v=19$…`,Django 存 argon2 是 `argon2$argon2id$v=19$…`
(多一个算法标签、没有开头的 `$`)。Django 按 `$` 切第一段当算法名,拿到空串,认不出。
- 更彻底的一点:旧后端的 pyproject.toml 里**根本没有 argon2-cffi**,
格式对了也验不了。
所以这不只是双跑的问题 —— 一次性切换后的回滚窗口内同样成立:学生登录过新站,
回滚之后就登不上了。演练时比对的是表结构「逐列一致」,比不出这种语义上的单向门。
改成 `config.passwordHashUpgrade` 控制,**默认关闭**,并在两份 compose 的
environment 里显式声明 `PASSWORD_HASH_UPGRADE`(原来根本没透传,想开也开不了)。
等确定不会再回滚了再打开。
## 验证(真库真容器,不是看代码)
schema.sql 起库 + 造一个 Django 格式的 pbkdf2 用户(密码已知),跑新栈实测:
- 默认形态:登录 200,库里的哈希**仍是** `pbkdf2_sha256$260000$…`,容器里
PASSWORD_HASH_UPGRADE 为空
- 对照组 PASSWORD_HASH_UPGRADE=true:登录 200,哈希变成 `$argon2id$v=19$m=655…`
—— 正是生产库现在的样子,锁死可复现
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-16 09:57:43 -06:00 |
|
|
|
e060327de4
|
fix(部署脚本): sh docker/deploy.sh 会挂在 pipefail,自己换 bash 重跑
Debian 的 /bin/sh 是 dash,没有 pipefail,一上来就 `Illegal option -o pipefail`。
在 set 之前加一行守卫,非 bash 就 exec 换 bash 重新执行(只用 dash 也认的语法)。
实测:本机 /bin/sh 指向 bash,所以这条得在容器里的真 dash 上验 ——
带守卫的版本正常往下跑(停在 docker/.env 不存在,因为只挂了脚本没挂仓库);
去掉守卫的对照版本在 dash 下直接 Bad substitution + 语法错误。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-16 09:37:06 -06:00 |
|
|
|
eadfa0b04a
|
fix(部署脚本): 改成在服务器上直接跑,加 --check
上一版是从本机 rsync + ssh 过去的,不是想要的形态。改成服务器上跑:
代码怎么上去(rsync / 以后的 git pull)不归它管,脚本只管起栈。
五道自检,前两道是这次在服务器上真撞到的失败:
compose 版本 ≥ 2.20 depends_on.required 是那版才有的字段
DATA_DIR 卷指向 OJ2/data → 中止(空数据,且不报错)
DB_HOST DATABASE_URL 还指着 oj-postgres → 中止
JUDGE_STATE_DIR 没设的话新旧两个判题机共用运行目录
旧栈还活着 oj-postgres / oj-redis 是新栈的上游
--check 只做只读校验,不建目录不动容器(mkdir 挪到起栈那一段了)。
## 跑出来的两个问题
1. 版本比较写反了。原来用 `sort -V -C` 判「这两行本来就有序」,结果本机
compose 5.4.0 被判成「太老」。改成取 min(2.20, ver),在 1.29.2 / 2.19.9 /
2.20.0 / 2.24.5 / 5.4.0 / 0 六个值上逐个验过分界。
2. --check 里的 mkdir 会真的建目录,本机跑直接撞权限。挪走了。
自检全路径实跑过:旧栈没起时正确拦下;造两个同名容器冒充旧栈后全绿通过;
抹掉 DATA_DIR / DB_HOST 两条守卫都按预期中止并打出可操作的提示。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-16 09:34:39 -06:00 |
|
|
|
eb849ca5ab
|
fix(镜像): apt 换清华源、去掉用不上的 ca-certificates;补部署脚本
## apt-get update 在服务器上卡死
不是慢,是挂住不返回 —— 本机构建从来没事,所以演练没暴露。换清华源。两个细节:
- trixie 的源是 deb822 格式,在 `/etc/apt/sources.list.d/debian.sources`,
老的 `sources.list` 在这个基底里是**空文件**,改它没有任何效果。
- **只能换主机名、必须保持 http。** https 源会在这一步失败,因为镜像里还没有根证书。
`ARG APT_MIRROR` 可覆盖。
## ca-certificates 是多余的
原注释写「给 AI 接口的 https 出站用」,不成立:Bun 和 Node 一样内嵌了一份根证书,
走自己那份,不读系统的 /etc/ssl/certs。
实测:把探针二进制丢进裸 debian:trixie-slim(没有 ca-certificates)请求
api.deepseek.com,握手正常、返回 401(没带 key),和装了的镜像行为一致。
去掉之后重新构建,clang-format / ruff 都在,https 出站照旧,487MB → 483MB。
哪天镜像里加了用 OpenSSL 做 TLS 的东西(curl、wget 之类),这条要重新考虑,
注释里写了。
## docker/deploy.sh
没有 git remote 时的部署路径:本机 rsync 推代码 → 服务器上构建 → 起栈 → 冒烟。
默认不推 docker/.env(两边不是一回事,覆盖掉是静默故障),要同步显式 --env。
起栈前两道守卫,就是今天在服务器上真撞到的那两种失败:DATA_DIR 没生效(卷指向
OJ2/data)、DB_HOST 没生效(DATABASE_URL 还指着试跑形态下并不存在的 oj-postgres)。
两道守卫都用当天那份坏 env 正反跑过:齐全时放行,抹掉这两个变量时都触发。
两半的语法也都 `bash -n` 过(远端那半是 heredoc,单独渲染后再查的)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-16 09:30:50 -06:00 |
|
|
|
1a059d0b88
|
feat(切换): 支持并行试跑,新站先挂 oj2.xuyue.cc 跑几天
设计文档写的是「不双跑不灰度」,这次改主意了。理由:「只换前后端」形态下新栈
本来就不碰数据库进程,双跑的增量风险只剩「两个后端同时写同一个库」这一条;
换来的是**正式切换退化成改一行 NPM 上游**,比停机切换更稳,回滚也不用停任何容器。
## 两个新变量
WEB_PORT 对外端口。8080 还被旧 backend 占着(机房是 81)
JUDGE_STATE_DIR 判题机运行状态目录
JUDGE_STATE_DIR 是必须的:不设的话新旧两个 judger 会同时往
`data/judge_server/{run,log}` 里写。默认值用嵌套写法跟着 DATA_DIR 走
(`${JUDGE_STATE_DIR:-${DATA_DIR:-../data}/judge_server}`,compose 支持嵌套默认值,
试过),所以一次性切换那条路径完全不受影响。
test_case 和 public/upload 仍然共享 —— 那是故意的,测试点和题面图片两边必须
看到同一份。
## 手册
新增第四节「并行试跑」,后面章节顺移(原四~九 → 五~十),两处交叉引用一并改了。
第五节拆成两条路径:试跑过的只需改 NPM 上游 + 事后停旧栈;没试跑的走原来那套。
第七节回滚同理。
试跑那节写明了三件容易踩的:NPM 的 Websockets Support 必须打开(漏了的话页面
一切正常,唯独「判题中…」永远不动,而刷新一下结果就出来,自测很难发现)、
两边登录态不互通、以及双写的是真实数据不是沙盒(别在 oj2 上办正式比赛)。
## 验证
试跑形态 `config` 解析:判题机目录落到 judge_server_oj2、端口 8090、test_case
仍指向共享的那份;不设新变量时默认值一个没变(judge_server / 8080)。
四份 compose `config -q` 全通过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-16 09:04:16 -06:00 |
|
|
|
cbff292551
|
feat(切换): compose 支持「只换前后端」,接着用旧栈的 postgres/redis
演练把一件事盖住了:当时是把生产 dump 恢复进 `OJ2/data/postgres` 的,
所以没人发现新 compose 在 `OJ2/docker/` 下,`../data` 解析出来是 `OJ2/data`,
而旧栈用的是 `<部署目录>/data`(服务器上是 `/root/OJDeploy/data`)。
照原手册切过去,postgres 会在一个空目录上初始化一个全新的空库 —— 站点起得来,
但没有用户没有题、判题全挂、题面图片 404。旧数据完好,回滚正常,但当天会白吓一场。
## 改法
三组 env 旋钮,默认值保持原样,不影响本机和演练那条路:
DATA_DIR 所有数据卷的根,默认 ../data
DB_HOST/PORT 默认 oj-postgres:5432
REDIS_HOST/PORT 默认 oj-redis:6379
postgres 和 redis 挪进 `profiles: ["local-data"]`,默认不起 —— 否则会跟旧栈
那两个抢 5445 / 5446。配套给 depends_on 加 `required: false`:实测严格的
depends_on 碰上未启用的 profile 会让整个 project 直接 invalid,不是可选项。
代价写进注释了:自带数据形态下 postgres 起不来时 compose 只警告不中止。
给 api / worker 加 host-gateway 映射,DB_HOST 填 host.docker.internal 就行,
不用去猜 docker0 的网段。数据库流量不出本机。
school 那套的 7 个挂载点同样换成 DATA_DIR —— 机房那台也有自己的旧数据目录,
测试点和题面图片都在里面,同一个坑。
## 验证
用 docs/specs/schema.sql 起了个发布在宿主机 5445 的 postgres 冒充旧栈:
正好 4 个容器(没有 postgres/redis)、oj-api healthy、首页与 /api/site
/api/problems 200、未登录进后台 401。读写两个方向都验了 —— 那个库的
pg_stat_activity 里有来自 172.17.0.1 的 postgres.js 连接,judge_server 表里
也出现了新判题机写进去的心跳行。
四份 compose 的 `config -q` 全通过。
手册第三、四、六节按这个形态重写:停旧栈改成只 stop oj-backend / oj-judge
(旧判题机会争 data/judge_server/run,旧 backend 占着 8080),回滚变成把这两个
再 start 起来,数据库进程全程不停。
**DATA_DIR 漏填不会报错**(它有默认值),是切换当天唯一会静默走歪的地方,
手册里给了 `config | grep source:` 的自查和两种症状的区分。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-16 08:57:25 -06:00 |
|
|
|
f6bc42f534
|
docs: 给 OJ2 补一份自己的 CLAUDE.md;Dockerfile 拷上 bunfig.toml
## bunfig.toml 没拷进容器,才是那批「本地能过、容器过不了」的根因
bunfig.toml 里设了 `linker = "hoisted"`,但 Dockerfile 只拷了 package.json 和
bun.lock,于是容器里用的是默认的 isolated 布局(包都在 node_modules/.bun/ 下),
本地靠"提升"才解析得到的包在容器里一律 Could not resolve —— 而本地构建始终是好的,
只有镜像构建才炸。之前是一个一个补直接依赖补过去的(那是对的、该保留),
这里让两边布局一致,是第二道保险。
带上之后装 622 个包(isolated 是 1238),镜像重建通过,二进制在容器里
serve + healthcheck 正常。
顺手补了 main.ts 帮助文本里漏掉的 healthcheck 子命令(compose 里把 command
写错时,看到的就是这行)。
## OJ2/CLAUDE.md
OJ2 是独立仓库,之前没有自己的项目指引。写了一份,重点是几条「不知道就会踩」的:
- **本机 Docker 可用**,整套依赖和上线演练都能在本机跑 —— 别沿用上一代
"本机跑不起来后端"的旧假设
- 单二进制不能依赖 node_modules,`.node` 资源导入 dev 和编译两种形态行为不同,
**改完两种形态都要跑**
- SQL 判题 spawn 的是二进制自己,入口必须有 argv 分发,那道递归闸不能删
- 判题状态码三处同步、raw_password 要保留、比赛只有 ACM、前端要兼容老 Chrome
- 不写迁移:新旧后端跑同一套表结构,这是回滚能成立的前提
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-08 03:37:45 -06:00 |
|
|
|
fb22b7d49f
|
docs(阶段5): 用生产快照跑完整切换演练
演练用 compose.debian.yml **本身**在本机 Docker 里跑,不是简化版。
数据是 2026-08-07 的 pg_dumpall 快照:1710 用户 / 956 题 / 123140 提交。
## 出口标准达成
停旧栈 11s,起新栈 34s(镜像预先构建好),全链路验证约 2 分钟 ——
**停机不到 1 分钟**,远在 30 分钟内。真正的时间风险在构建镜像(首次约 5 分钟),
所以手册里第一条就是「镜像必须在停机窗口之前构建好」。
验证到位的:首页、站点配置、题目列表、标签、公告、登录(argon2 新哈希和
Django pbkdf2 旧哈希都支持)、个人页、排行榜、后台四个接口、判题机自动注册,
以及**完整判题**(提交 Python A+B → AC,1.2 秒,两个测试点全过)。
## 两件原以为要做、实测不用做的事
- **不需要任何 DDL**:生产 dump 和新后端在用的库逐列对比,两边都是 278 列,
零差异。新后端直接跑在现有结构上。
- **不需要重置序列**:我在 phase3-coverage.md 里记的那条「切换必做:重置序列」
**是错的**,来自我手工按显式 id 导入、又没补 setval 的本地库。真实的
pg_dumpall 带 30 条 setval,且把快照里所有序列和 max(id) 逐个对过,错位 0 个。
已在原文档上标注更正,没有删掉原文 —— 错误结论本身也是信息。
## 回滚保证已实测
新栈跑完登录、提交、判题之后,再和生产 dump 比一次结构:逐列一致,零差异。
加上数据目录布局照抄旧后端,回滚 = 停新栈 + 起旧栈,约 20 秒,不动任何数据。
(未实测的部分也写明了:本机没构建旧 Django 镜像,「起旧栈」这一步没跑过。)
## 演练抓到的真问题
**pg_dumpall 备份会覆盖数据库口令。** 恢复完快照,新后端立刻报
`password authentication failed` —— 因为 dump 里带
`ALTER ROLE onlinejudge ... PASSWORD 'md5…'`,把角色口令覆盖成了备份时生产的那个。
正常切换不受影响(根本不恢复备份),但灾难恢复时这一条不写下来,
现场会被一个看起来毫不相干的报错卡住。
**恢复备份前必须先停应用**,否则 dump 里的 DROP DATABASE 失败。演练时因为
目标库是空的,数据照样进去了 —— 那是运气,目标库有数据就是满屏主键冲突。
## 镜像体积没达标,写明了原因
api 镜像 487MB,设计文档写的是「数十 MB」。一半以上(269MB)是 clang-format
拖进来的 LLVM,光 libLLVM.so 就 124MB。旧 Python 镜像同样装了 clang-format,
所以新镜像仍明显更小,但当初估「数十 MB」时没把它算进去。
瘦身路径也记了(换静态 clang-format 可砍 265MB),暂不做。
## 清理
演练在 data/postgres 留下了一份完整的生产数据副本,含 1710 名学生的
raw_password 明文列,已删除。手册里留了提醒 —— 那不是测试数据。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-08 02:13:59 -06:00 |
|
|
|
ea521e7b0a
|
feat(阶段5): 镜像、三套 compose,前端切回 /api 与 /ws
## 镜像
一个 Dockerfile 两个 target:api(单二进制)和 web(Caddy + 前端产物)。
旧后端是一个容器里用 supervisord 跑 caddy+gunicorn+dramatiq,这里拆成
oj-web / oj-api / oj-worker 三个容器 —— Docker 本身就是进程管理器,
拆开之后 worker 挂了能单独重启、日志也分得开,少一层 supervisord 要维护。
同一个镜像换个子命令就是 worker,镜像里只有一份运行时。
新增 healthcheck 子命令:运行镜像是 debian-slim,没有 curl/wget,
让二进制自己打 /health(只打 /health 不碰库 —— 库挂了该由库的 healthcheck 报,
不该让 api 跟着被判不健康然后被重启)。
**数据目录照抄旧后端**(test_case、public/upload、public/avatar)。
不是审美问题:切换那天不用搬动任何文件,回滚时旧后端立刻能找到自己的数据。
少一次几十 GB 的 mv,就少一个在停机窗口里出错的机会。
构建路上踩到三个坑,都是「本地能过、容器里过不了」那一类:
- 构建上下文吸进了 data/,judge_server/run 是判题沙箱用别的 uid 建的,
docker 连 stat 都做不了,构建直接失败 → 补 .dockerignore
- mermaid@9.4.3(机房老 Chrome 的 legacy 依赖,不能砍)从容器里连
registry.npmjs.com 稳定失败,主机上没问题 → 换 npmmirror,并重试两次
- 容器里 bun 用 isolated 布局,本地是扁平的。靠「提升」才能解析到的包
在容器里一律解析不到:@node-rs/jieba-linux-x64-gnu(编译要 import 它的 .node)、
以及前端的 @codemirror/{language,state,view} 和 @lezer/highlight。
这些本来就是代码直接 import 的,补成直接依赖。顺手写了个脚本扫全仓,
确认只有这 4 个。
## 前端切回 /api、/ws
迁移期用 /api2、/ws2 指新后端,/api、/ws 还指着 Django。端点已全部搬完,
临时前缀去掉。改动只有三处(api2.ts 的 baseURL、websocket.ts 的两个 URL),
`api2` 那些 import 是模块名不是路径,不动。vite 代理同步收敛成三条。
## compose
- debian:全套(含 postgres,对外开 5445 给机房连)
- school:**没有 postgres**,连服务器的库;本地 Redis + 本地判题沙箱
- 两边共用一个库但各有各的队列和 WS 推送,和旧后端的 Dramatiq/Channels 拓扑一致
密钥一律走 env 且带 `:?`,没设置就直接报错退出,不静默用弱默认值。
COOKIE_SECURE 在机房必须是 false(http 直连 IP,带 Secure 的 Cookie 发不回来,
表现是「登录成功但立刻又变未登录」)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-07 23:41:20 -06:00 |
|
|
|
b7adf2993d
|
fix(阶段3): 去掉判题机 token 的弱默认值
F5:token 校验实现本身是对的(用了 timingSafeEqual),问题是缺省值
"oj2-dev-token" 写死在仓库里 —— 写死在仓库里的 token 等于没有 token。
后端改为对齐旧后端 options/options.py:93 的 fail-safe:
env 缺失时生成随机值并在启动日志里告警,判题机连不上,但不会静默用弱默认值。
compose 改为 ${OJ2_JUDGE_TOKEN:?...},未设置直接报错退出。
本地开发怎么设写在 docker/compose.dev.yml 顶部与 .env.example 里:
两个变量名不同(判题机镜像认 TOKEN,后端认 JUDGE_SERVER_TOKEN)但值必须相同。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-07 01:59:27 -06:00 |
|
|
|
ec274419c3
|
Build Phase 2 judge vertical slice
|
2026-08-06 22:42:39 -06:00 |
|
|
|
901906328d
|
chore: 本机开发依赖 compose(postgres 16 + redis)
PostgreSQL 固定 16-alpine 与生产 16.10 对齐,首次启动自动灌入
docs/specs/schema.sql(仅结构)。端口错开为 5433 / 6380。
实测:34 张表与 schema.sql 逐一对应,差集 0,两个服务健康。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-06 20:32:48 -06:00 |
|