项目:interview-platform | 版本:v1.0 | 生成日期:2026-10-05 | 修订:2026-10-06 一致性回刷 | 依据:源码分析
本篇是整套文档的运维落地篇,与第 00 篇第 5 节「快速上手」、第 01 篇第 1 节「外部依赖拓扑」、第 04 篇第 6 节「数据生命周期/迁移纪律」相互呼应:前三篇回答「系统是什么、怎么跑起来」,本篇回答「如何在一台干净的机器上把它部署到生产、如何迁移与备份数据库、质量门禁如何把守」。全部命令与配置项均取自仓库真实文件:Dockerfile、docker-compose.yml、prisma.config.ts、next.config.ts、package.json、.github/workflows/ci.yml、vitest.config.ts、eslint.config.mjs、.env.example 等,未在源码中出现的事实一律标注「⚠️ 待确认」。
interview-platform 的部署形态是「单容器全栈单体 + 外部 PostgreSQL + 一组可选外部 SaaS」:整个应用(React 页面、101 个 REST API、AI 编排、判题编排、数据访问层)打包进一个基于 node:22-slim 的 Docker 镜像,以非 root 的 node 用户监听 4300 端口;容器启动时先执行 prisma migrate deploy(幂等)再 next start。数据库不在 compose 编排内(docker-compose.yml 没有 db 服务),必须自备外部 PostgreSQL 实例;DeepSeek、Piston、火山引擎、阿里云 OSS、SMTP 均为可选外部依赖——最小可运行配置只需 DATABASE_URL 与 AUTH_SECRET 两个变量,缺失的 AI Key 只会让对应接口返回 503 并提示配置项名,不影响站点其余功能。
这张拓扑图对应四条部署基线:① 无状态应用层——除 /public/uploads 之外进程不落本地盘(.gitignore 注释标明 uploads 为运行时生成目录),理论上可水平扩容,但限流器是进程内存实现,多实例前需先改造(见第 10 节);② 数据库外部化——连接串唯一入口是 DATABASE_URL(src/lib/prisma.ts 与 prisma.config.ts 两处读取);③ 密钥不进镜像——.dockerignore 排除了 .env、.env.local、.env.example 等全部 env 文件,运行期变量只能由 compose environment 段或容器运行时注入;④ 探针无外部依赖——src/app/api/health/route.ts 只返回 {status:"ok", timestamp},不查数据库,保证「进程活着」与「依赖可用」两类故障被区分对待(依赖故障由业务接口的 5xx 体现)。
| 项 | 要求 | 依据 |
|---|---|---|
| Node.js | 22.x(CI 与 Docker 镜像统一为 22;package.json 未声明 engines 字段,本地 20+ 一般可运行但未经门禁验证) | .github/workflows/ci.yml 的 node-version: 22、Dockerfile 的 FROM node:22-slim |
| 包管理器 | pnpm 10.34.5(packageManager 字段精确锁定;Docker 内经 corepack enable 激活) | package.json、Dockerfile |
| 数据库 | PostgreSQL(Prisma schema provider = "postgresql";仓库未锁定版本下限 ⚠️ 待确认,建议采用数据库官方仍在维护的版本并自行回归 22 个迁移) | prisma/schema.prisma |
| 构建内存 | 建议 ≥4GB 堆内存(Docker 构建期显式设置 NODE_OPTIONS=--max-old-space-size=4096) | Dockerfile |
| 出站网络 | 应用服务器需能访问 api.deepseek.com(AI)、emkc.org(判题)、*.volcengineapi.com 与 RTC WebSocket(语音视频)、*.aliyuncs.com(OSS/STS)、SMTP 主机;全部为 HTTPS/WSS 出站,无入站回调 | next.config.ts CSP connect-src 白名单与 src/lib/* 集成代码 |
| npm 镜像源 | .npmrc 固定 registry=https://registry.npmmirror.com/,海外环境部署可自行替换 | .npmrc |
各外部服务的可选性矩阵(决定你要准备哪些账号):DeepSeek Key 缺失 → AI 对话/模拟面试/渐进提示/代码点评全部不可用,其余功能正常;火山六项凭证不全 → 视频面试入口自动隐藏(HEAD /api/video-interview/token 探测返回不可用),普通语音识别同样降级;OSS 四项(OSS_ACCESS_KEY_ID/SECRET、OSS_ROLE_ARN、OSS_REGION/BUCKET)不全 → 头像上传接口报「环境变量 xxx 未配置」;SMTP 三项(SMTP_HOST/USER/PASS)不全 → 注册验证码降级为打印在服务端控制台(src/lib/mailer.ts),本地联调可无邮件服务跑通注册。
以下命令在克隆仓库后的根目录执行,与第 00 篇第 5 节一致,这里补充每一步背后的机制:
# 0) 启用 pnpm(已装可跳过):corepack 会读取 packageManager 字段锁定版本
corepack enable
# 1) 安装依赖(--frozen-lockfile 保证与 pnpm-lock.yaml 一致,CI 同款)
pnpm install
# 2) 准备环境变量:最少 DATABASE_URL + AUTH_SECRET
cp .env.example .env.local # 编辑填入真实值
# 3) 生成 Prisma 客户端(必须手动执行,见下方说明)
pnpm prisma generate
# 4) 建表:创建/应用开发迁移(首次即全量 22 个迁移)
pnpm db:migrate # 等价 prisma migrate dev
# 5)(可选)填充种子数据:6 个领域分类 + 四大题库 + 示例题集/学习路径
pnpm prisma db seed
# 6) 启动开发服务器(Turbopack,默认 3000 端口)
pnpm dev三个容易踩坑的细节:
prisma generate 必须手动执行。package.json 没有 postinstall 钩子,而生成产物 src/generated/prisma/ 被 .gitignore 排除(/src/generated/prisma),克隆后不存在;不执行则所有 import @/generated/prisma/client 的文件类型报错。generator 配置在 prisma/schema.prisma 头部(provider = "prisma-client",output = "../src/generated/prisma")。.env 与 .env.local 的加载者不同。Next.js 进程(dev/start)按标准优先级加载 .env.local;而 Prisma CLI 由 prisma.config.ts 驱动,该文件第一行 import "dotenv/config" 只加载根目录 .env。因此若你只建了 .env.local,pnpm db:migrate/db:studio/db seed 会因读不到 DATABASE_URL 失败。最省心的做法是 .env 与 .env.local 各放一份 DATABASE_URL,或直接在 shell 里 export。next.config.ts 的 allowedDevOrigins: ["192.168.1.4", "localhost"] 放行了指定局域网 IP,用于手机等外设真机调试语音/视频面试;换网络环境需同步修改此数组,否则跨源请求被 Next 开发服务器拒绝。开发常用脚本全表(package.json scripts):
| 命令 | 实际执行 | 用途 |
|---|---|---|
pnpm dev | next dev | 开发服务器(Turbopack,3000 端口) |
pnpm build / pnpm start | next build / next start | 生产构建 / 生产启动(可用 -p 指定端口) |
pnpm lint | eslint | 代码检查(flat config) |
pnpm typecheck | tsc --noEmit | 全量类型检查 |
pnpm test / pnpm test:run | vitest / vitest run | 单测(watch / 单次) |
pnpm test:coverage | vitest run --coverage | 单测 + 覆盖率门槛检查 |
pnpm db:migrate | prisma migrate dev | 生成并应用开发迁移 |
pnpm db:studio | prisma studio | 可视化查库 |
pnpm prepare | husky | 安装 git hooks(pnpm install 时自动执行) |
第 00 篇已按「功能视角」介绍过部分变量,这里给出面向部署的完整清单,变量名逐一核对自源码中的 process.env.* 读取点(grep 全量扫描结果),并将仓库内三份 env 样例的差异一并说明。
| 变量 | 读取位置 | 说明 |
|---|---|---|
DATABASE_URL | src/lib/prisma.ts(运行时连接池)、prisma.config.ts(CLI:migrate/seed/studio) | PostgreSQL 连接串,如 postgresql://user:pass@host:5432/interview_platform?schema=public;运行进程缺失时 prisma.ts 直接抛错 |
AUTH_SECRET | src/lib/auth.ts(NextAuth 会话签名)、src/lib/jwt.ts(后台 Bearer JWT,生产模式未设置即抛错)、src/proxy.ts(NEXTAUTH_SECRET ?? AUTH_SECRET 兜底) | ≥32 位随机串(openssl rand -hex 32),更换会使全部会话失效 |
NEXTAUTH_SECRET | src/proxy.ts 兜底第二优先级 | compose 中注入为 ${AUTH_SECRET} 双保险,避免依赖 next-auth 内部解析顺序 |
AUTH_URL / NEXTAUTH_URL | 业务源码未直接读取,由 next-auth 库内部解析 | 站点对外地址;compose 默认 http://localhost:4300,生产必须改为真实域名,否则 OAuth 回调与 baseUrl 判定出错 |
| 变量 | 读取位置 | 说明 |
|---|---|---|
DEEPSEEK_API_KEY | src/lib/deepseek.ts(3 处) | 值为占位串 sk-your-deepseek-api-key-here 或 fake 时视同未配置,AI 接口返回错误提示 |
VOLC_API_KEY | src/lib/volcengine.ts | 豆包 ASR/TTS 的 X-Api-Key;单独决定语音识别/合成可用性 |
VOLC_TTS_RESOURCE_ID / VOLC_ASR_RESOURCE_ID / VOLC_TTS_VOICE_TYPE | 同上 | 有代码级默认值:seed-tts-2.0、volc.bigasr.sauc.duration、zh_female_shuangkuaisisi_uranus_bigtts,可不配 |
VOLC_S2S_APP_ID / VOLC_S2S_ACCESS_TOKEN / VOLC_RTC_APP_ID / VOLC_RTC_APP_KEY / VOLC_ACCESS_KEY / VOLC_SECRET_KEY | src/lib/volcengine.ts(六项齐全性校验) | 视频面试(RTC + 端到端语音大模型)凭证;StartVoiceChat 的 V4 签名用 AK/SK |
| 变量 | 读取位置 | 说明 |
|---|---|---|
OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET / OSS_ROLE_ARN | src/lib/oss.ts(requireEnv 逐项强校验) | STS assumeRole 签发 1 小时临时上传凭证,缺一即抛错 |
OSS_REGION / OSS_BUCKET | src/app/api/upload/avatar/route.ts | 有默认值 oss-cn-shanghai / xiaoye-leaf(建议显式覆盖默认值) |
OSS_HOSTNAME | next.config.ts | images.remotePatterns 放行的图片域名,默认 xiaoye-leaf.oss-cn-shanghai.aliyuncs.com |
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS | src/lib/mailer.ts | PORT 默认 465;HOST/USER/PASS 三者齐备才真实发信,否则验证码打印到控制台。注意 .env.example 未列出该组变量,需对照 mailer.ts 手工补齐 |
NEXT_PUBLIC_APP_URL | src/app/robots.ts、src/app/sitemap.ts(默认 https://interview-platform.example.com)、src/app/api/share、/api/reminders/digest(默认 http://localhost:3000) | 站点公开地址,生产必填;同时作为 Docker 构建参数(见 4.4) |
TZ | compose 注入 Node 进程时区 | 打卡/复习的业务日界在代码中硬编码 Asia/Shanghai(src/lib/streak.ts),不依赖 TZ;TZ 只影响日志时间戳等系统行为 |
CRON_SECRET | src/app/api/reminders/digest/route.ts | 未配置时该端点 503;调用须带请求头 x-cron-secret,供外部 cron 每日发复习提醒摘要(示例:curl -X POST -H "x-cron-secret: $CRON_SECRET" https://<host>/api/reminders/digest) |
NODE_ENV | src/lib/prisma.ts 等 | production 时 Prisma 单例不再挂 globalThis;Dockerfile 运行阶段已固定设置 |
Dockerfile 构建阶段给 DATABASE_URL 塞的是假串 postgresql://build:build@localhost:5432/build_dummy、AUTH_SECRET=build-only-dummy——仅为通过 prisma.config.ts 的非空校验,prisma generate/next build 均不连库;文件注释明确警告绝不通过 build-arg 传入真实连接串。NEXT_PUBLIC_APP_URL 的双重身份:当前仅服务端代码读取它(robots/sitemap/share/digest),运行期注入即可生效;但它带 NEXT_PUBLIC_ 前缀,一旦未来被客户端组件引用就会在构建期内联,届时必须经 docker-compose.yml 的 build.args 传入并重新构建镜像(链路已预留:compose args: NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL} → Dockerfile ARG/ENV)。STORAGE_DRIVER、NEXT_PUBLIC_OSS_REGION、NEXT_PUBLIC_OSS_BUCKET 三个变量在 docker-compose.yml 中透传,但全源码 grep 无任何引用 ⚠️ 待确认(疑为早期浏览器直传 OSS 方案遗留),可不配置。.env.example 中的 GITHUB_ID/GITHUB_SECRET:源码未读取(src/lib/auth.ts 仅配置 CredentialsProvider,无 OAuth),属遗留示例,忽略即可。.env.example 是权威变量字典(pnpm-lock 之外的唯一入库样例,.gitignore 用 !.env.example 放行);.env.local.example 是早期 Supabase 方案的历史残留,与现行架构无关;真实 .env/.env.local 均被 .gitignore 与 .dockerignore 双重排除。若目标机已具备 Node 22 + pnpm 环境,可绕过 Docker 直接以传统方式运行(适合系统盘常驻部署或低配环境):
pnpm install --frozen-lockfile
pnpm exec prisma generate
# 生产库迁移:只应用已提交的迁移,不生成新迁移、不产生 shadow database
pnpm exec prisma migrate deploy
#(可选)首次部署填充种子
pnpm prisma db seed
NODE_ENV=production pnpm build # 产物在 .next/
NODE_ENV=production pnpm start -p 4300要点:① next build 不需要数据库连接(与 Docker 构建同理),但需要 DATABASE_URL/AUTH_SECRET 满足模块加载期的非空校验;② migrate deploy 与 migrate dev 的区别是前者只重放 prisma/migrations/ 中已登记的迁移且幂等,这正是容器 CMD 采用它的原因;③ pnpm start 默认 3000 端口,生产建议与 Docker 保持一致的 -p 4300,由前置反向代理做 TLS 终结;④ 服务器需暴露的入站端口只有应用端口,语音/视频面试的 WebSocket(wss://rtc.volcengineapi.com 等)是浏览器直连火山的出站连接,无需在服务端开额外入站端口。
Dockerfile 第一段 FROM node:22-slim AS build)构建链路共九步,每步都有明确意图:corepack enable 激活 pnpm;apt-get install openssl ca-certificates 安装 Prisma 引擎运行所需的 openssl 与出站 HTTPS 根证书;WORKDIR /app 后先只拷贝 package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc 五个清单文件再 pnpm install --frozen-lockfile——利用 Docker 层缓存,依赖未变时不重装;随后 COPY . .(受 .dockerignore 约束,见 6.4)并设置 NODE_OPTIONS=--max-old-space-size=4096;最后 pnpm exec prisma generate 生成客户端到 src/generated/prisma/,pnpm build 产出 .next/。注意 ENV CI=true 全程生效,避免交互式提示卡住非 TTY 构建。
FROM node:22-slim AS run)运行镜像同样安装 openssl 与根证书,然后从构建阶段拷入七样东西:node_modules(全量,含 devDependencies——next.config.ts 未启用 output: "standalone",且运行期 prisma migrate deploy 需要 CLI,而 prisma 包声明在 dependencies 中)、.next 构建产物、public/(/career 页服务端要读 public/knowledge/*.md,不能省)、prisma/(schema + 22 个迁移)、prisma.config.ts、package.json、next.config.ts 与 pnpm-workspace.yaml。之后 USER node 降权、EXPOSE 4300、声明 HEALTHCHECK(30 秒间隔、5 秒超时、20 秒启动宽限、3 次重试,探针即 GET /api/health),最终 CMD 一行完成「先迁移后启动」:
CMD ["sh", "-c", "pnpm exec prisma migrate deploy && pnpm start -p 4300"]这意味着每次容器重启都会自动把数据库推进到最新迁移——发布新版本 = 推新镜像 + 重启容器,无需独立的迁移作业;代价是迁移失败会导致容器起不来(编排层按 restart: unless-stopped 反复拉起),因此重大迁移务必先在预发库演练。
docker-compose.yml 编排解析单服务编排(compose 项目名 mianshi,构建产出的镜像 tag 为 mianshi:latest):build.args 传入 NEXT_PUBLIC_APP_URL;ports "4300:4300";restart: unless-stopped;healthcheck 与镜像内 HEALTHCHECK 等参数一致(双重声明);logging 用 json-file 驱动并限制 max-size: 20m、max-file: 5(约 100MB 日志上限,防裸 Docker 日志膨胀)。environment 段是全部运行期变量的注入点,来源是 compose 同目录 .env 文件的变量替换(${DATABASE_URL} 语法),其中 AUTH_URL/NEXTAUTH_URL 有 :- 默认值链,最终兜底 http://localhost:4300。文件头注释强调了设计原则:数据库连接串只作为运行期环境变量注入,构建期不接收。
# 1) 拉代码并准备 compose 变量文件(compose 自动读取同目录 .env 做替换)
git clone <repo> interview-platform && cd interview-platform
cp .env.example .env # 编辑:至少填 DATABASE_URL、AUTH_SECRET、AUTH_URL、NEXT_PUBLIC_APP_URL
# 2) 构建并启动(首次构建含 pnpm install + next build,耗时较长)
docker compose up -d --build
# 3) 观察启动日志:应看到 migrate deploy 的迁移回放,然后 Next.js ready
docker compose logs -f mianshi
# 4) 冒烟验证
curl -s http://localhost:4300/api/health # {"status":"ok",...}
# 5)(可选,建议生产)填充题库种子:容器内执行,读同一 DATABASE_URL
docker compose exec mianshi pnpm exec prisma db seed
# 6) 设置首个管理员(容器内无 psql,需在数据库侧执行;Role.code='admin' 已随迁移就位)
# psql -h <db-host> -U <user> -d interview_platform -c \
# "UPDATE users SET role='admin' WHERE email='you@example.com';"关于第 6 步:RBAC 的角色/权限数据由迁移 prisma/migrations/20260824020000_seed_rbac_data/migration.sql 携带(admin/editor/user 三角色 + 7 个权限点,ON CONFLICT DO NOTHING 幂等),migrate deploy 后即存在;但源码没有「首个管理员引导」机制(无 bootstrap 环境变量、seed 亦不提升任何用户),因此首管理员必须手工改库,之后的角色调整走后台接口 PATCH /api/admin/users/[id]/role(src/app/api/admin/users/[id]/role/route.ts,白名单 admin|user)。
.dockerignore 的裁剪作用上下文裁剪分三类:安全类排除全部 .env*、certs;体积类排除 node_modules、.next、src/generated(生成物必须在镜像内重新 prisma generate,避免宿主平台产物污染)、docs、coverage、backups、screenshots;正确性类排除 *.test.ts(x)、*.spec.ts、vitest.config.ts、tsconfig.test.json、prisma/seed.sql 等非运行物——这也意味着测试文件不进镜像,质量门禁完全前移到 CI(第 9 节)。
迁移纪律(与第 04 篇第 6 节呼应):schema 变更的唯一正道是本地 pnpm db:migrate(prisma migrate dev)生成迁移文件并入库,禁止手写/手改迁移 SQL;当前 prisma/migrations/ 共 22 个迁移目录 + migration_lock.toml,涵盖从 20260607105724_init 到 20260924200000_add_question_hints 的全部演进(含 per-user 状态两连迁、热路径索引、评论回复、封禁字段、RBAC 三表 + 数据种子)。生产应用一律 prisma migrate deploy(容器 CMD 已内建)。
种子数据:prisma.config.ts 将 seed 命令定义为 tsx prisma/seed.ts(Prisma 7 起 package.json 的 prisma.seed 写法已废弃),执行 pnpm prisma db seed 即可。脚本自身幂等:6 个领域分类按固定 id(domain-前端 等)upsert;题目从 src/data/ 八个题库文件(算法×3、前端×1、后端×2、系统设计×2)导入,按 title 查重存在即跳过;示例题集/学习路径存在即跳过。因此 seed 可在生产重复执行而不产生重复数据,但也要注意它只做加法——删除线上题目前不必担心被 seed 复活,反过来改了 src/data 里的旧题目内容也不会同步到已存在的题目。
备份与恢复:仓库未内置任何备份脚本(backups/、sql 备份/ 均被 .gitignore 排除,历史遗留的 prisma/seed.sql 亦已弃用),以下为标准运维惯例建议(非源码行为):每日全量逻辑备份 pg_dump "$DATABASE_URL" -Fc -f "backup_$(date +%F).dump",恢复用 pg_restore -d "$DATABASE_URL" --clean --if-exists <file>;托管数据库(RDS 等)建议叠加 WAL 归档实现时点恢复(PITR)。恢复演练时注意版本配套:先用 migrate deploy 建好空库结构再导入,或直接恢复后跑一次 migrate deploy 补齐镜像内新迁移。此外业务数据中只有头像上传落在 OSS,其余全部在库内,数据库备份即完整备份;public/uploads 目录是本地直传的历史兼容路径,生产走 OSS 后通常为空。
健康探针:GET /api/health(src/app/api/health/route.ts)无任何外部依赖,恒定快速返回 {status:"ok", timestamp};镜像 HEALTHCHECK 与 compose healthcheck 均以「响应 ok」为健康判据,失败 3 次交由 restart: unless-stopped 重启。它刻意不探数据库——数据库故障应表现为业务接口 5xx 与日志报错,而不是把应用容器打进重启循环。
日志:应用日志即 stdout/stderr(接口层未捕获异常由 handleApiError(src/lib/api-response.ts)统一打印 [API Error] 前缀后返回 500),compose 侧用 json-file 驱动限制在 20MB×5 个文件;生产检索建议接 Docker logging driver 或采集中间件,源码未内置任何 APM/结构化日志 ⚠️ 待确认(无相关依赖)。
访问统计:src/app/layout.tsx 硬编码引入了一个 Umami 统计脚本(https://analytics.zhangzhengyang.com/script.js + 固定 website id),next.config.ts 的 CSP 则放行 analytics.umami.is 域名(且 CSP 整体处于 Report-Only 观察模式,不会实际拦截)。二次部署到自有域名时这块统计不会指向你的 Umami 实例,应自行替换或删除该 <script> 并同步调整 CSP 白名单。
质量门禁分「本地提交钩子」与「CI 流水线」两层,全部通过才允许合入。
测试分布:单测共 27 个文件(Vitest + Testing Library + jsdom),覆盖三圈——服务层 src/lib/__tests__/ 14 个(api-response、category-cache、category-utils、deepseek、judge、jwt、permission、question-types、rate-limit、request、review-scheduler、sse、streak、validate)加 src/lib/design-tokens.test.ts、src/lib/utils.test.ts;API 路由 7 个(auth/register、questions 及其 edits/encounter/hints 子资源、search-history、test-papers);UI 组件 4 个(ui/Button、ui/Input、ui/Badge、SpeakButton)。devDependencies 里虽有 @playwright/test,但仓库内不存在 playwright.config.* 与任何 e2e 用例 ⚠️ 待确认——E2E 体系尚未落地,当前门禁止步于单测。
Vitest 配置(vitest.config.ts):jsdom 环境 + globals: true + 全局 setup src/test/setup.ts;覆盖率用 v8 provider,只统计 src/lib/**(核心业务逻辑层),阈值 statements/lines 40%、functions/branches 30%,源码注释标明这是「2026-09 设定基线、防止覆盖率回退」的保底线——pnpm test:coverage 低于阈值即非零退出。
静态检查:pnpm lint 走 ESLint 9 flat config(eslint.config.mjs):eslint-config-next 的 core-web-vitals + typescript 两套预设之上,显式关闭了三条与项目形态冲突的规则(react-hooks/set-state-in-effect——客户端组件数据获取的标准模式;@next/next/no-img-element——动态外域头像无法枚举进 next/image;错误边界组件的 no-html-link-for-pages),并把超大数据文件 src/data/backend-questions-supplement.ts 与 scripts/** 加入忽略。pnpm typecheck 为 tsc --noEmit 全量检查(tsconfig strict;测试文件另有 tsconfig.test.json 注入 vitest/jest-dom 类型)。
提交钩子:package.json 的 prepare: "husky" 在 install 时装配钩子,.husky/pre-commit 只有一行 pnpm exec lint-staged,对暂存的 *.{ts,tsx,js,mjs} 执行 eslint --fix——脏文件要么被自动修复、要么因残留 error 而提交失败。
CI 流水线(.github/workflows/ci.yml):触发于 push 到 master/main 与全部 PR,单 job verify 串行四步——pnpm/action-setup@v4(version 10)+ actions/setup-node@v4(node 22,开启 pnpm 缓存)→ pnpm install --frozen-lockfile → pnpm lint → pnpm typecheck → pnpm test:run。注意 CI 不跑 build、不连数据库、不做覆盖率门槛(test:run 而非 test:coverage),这三项分别由 Docker 构建、部署环境与本地/手动 test:coverage 承担。依赖更新由 .github/dependabot.yml 每周各开一批 npm 与 github-actions 升级 PR。
src/lib/rate-limit.ts 是进程内 Map 滑动窗口(模拟面试 10 次/分、AI 对话 20 次/分等),多副本部署时配额互不共享,登录防爆破(IP 5 次/分、邮箱失败 10 次/时)同样被稀释——在限流器完成 Redis 化改造(《13-技术债务与演进建议》1.1)之前,多副本水平扩容是不受支持的部署形态(源码注释自认的局限;2026-10-06 回刷按台账建议升级为显式警告)。output: "standalone" 且运行期需要 Prisma CLI 迁移,运行镜像携带全量 node_modules;对体积敏感可评估 standalone + 独立迁移作业的改造,但需同步解决 public/、prisma/、prisma.config.ts 的随行拷贝。emkc.org):可用性与排队不可控,应用侧已有 15 秒/用例超时兜底(src/lib/judge.ts),内网隔离环境部署时在线判题整体不可用,需自建 Piston 兼容端点 ⚠️ 待确认(源码未提供自建切换配置)。next.config.ts 注入的是 Content-Security-Policy-Report-Only(注释写明观察两周后切强制),上线后应关注上报并在确认无误报后移除 -Report-Only 后缀。TZ 无关,跨时区部署不会导致「今天」漂移;但日志与数据库 now() 的时区语义仍受基础设施影响,建议统一 TZ=Asia/Shanghai(compose 已预留注入)。-p 4300),对外映射由 compose ports 控制;本地开发默认 3000。AUTH_URL/NEXTAUTH_URL/NEXT_PUBLIC_APP_URL 三者必须与用户实际访问的协议域名一致,否则出现登录回调错误与 sitemap/分享链接指向 localhost 的问题。docker compose up -d --build 旧版本或指回旧 tag;注意若新版本迁移已执行,旧镜像启动时 migrate deploy 不会回滚迁移(Prisma 不支持自动 down),需按第 04 篇的迁移纪律准备反向迁移或从备份恢复。