项目:interview-platform | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
本篇是配置参考手册:对 next.config.ts、prisma.config.ts、ESLint/husky/CI 配置做逐文件走读,给出环境变量四组全清单与三份样例文件的差异说明,解析 Dockerfile 两阶段构建的每一段,最后给出本地/生产差异矩阵。与《06-部署运行》的分工:旧篇讲「部署流程怎么走」,本篇讲「每个配置文件里每一项是什么、为什么」。全部条目逐行核对自仓库真实文件。
文件共 59 行,五个配置块。这是全仓库「体量最小、杠杆最大」的文件——安全响应头、图片域白名单、打包边界、开发联调白名单全部由它决定,任何一行改动都会全站生效,因此每次修改都应在本地完整回归一次「首页渲染、头像显示、语音面试权限弹窗」三个场景(分别对应 headers、images、Permissions-Policy 三块配置)。
| 配置项 | 值 | 作用与理由(源码注释归纳) |
|---|---|---|
images.remotePatterns | https://${OSS_HOSTNAME}(默认 xiaoye-leaf.oss-cn-shanghai.aliyuncs.com) | 放行 OSS 域名给 next/image 优化;换桶需改 OSS_HOSTNAME |
headers() | 5 个安全头(:path* 全站) | X-Content-Type-Options: nosniff、X-Frame-Options: SAMEORIGIN、Referrer-Policy: strict-origin-when-cross-origin、Permissions-Policy: camera=(self), microphone=(self), geolocation=()(语音视频面试需要摄像头麦克风,显式只授本站)、Content-Security-Policy-Report-Only(观察模式,白名单见《08-安全与权限设计》第 8 节) |
serverExternalPackages | ["ali-oss"] | ali-oss 是纯 Node SDK,其可选依赖 proxy-agent 会被 Turbopack 静态分析报错;排除出打包交运行时 require |
allowedDevOrigins | ["192.168.1.4", "localhost"] | 允许指定局域网 IP 访问开发服务器(手机真机联调语音面试);换网络需同步修改 |
experimental.viewTransition | true | 启用 React View Transition(配合 src/components/ViewTransitionWrapper.tsx 的页面切换动画,实验性特性) |
注意两点:OSS_HOSTNAME 在构建期读取(决定 remotePatterns),因此改桶名必须重新构建镜像(与 NEXT_PUBLIC_APP_URL 同理,见第 4 节构建参数链路);未启用 output: "standalone",这是镜像携带全量 node_modules 的直接原因(《13-技术债务与演进建议》第 3.7 条)。
Prisma 7 起CLI 由独立配置文件驱动(package.json 的 prisma 块写法已废弃,文件头注释明确说明)。这个文件只有 15 行,却是本地开发体验的分水岭——它决定了「哪些命令读哪份 env、seed 怎么跑、迁移放哪里」。四项配置:
import "dotenv/config":决定 Prisma CLI 只读根目录 .env——这是「.env 与 .env.local 加载者不同」问题的根源(《12-FAQ与故障排查》第 1 条),也是 .env 必须存在的唯一理由。schema: "prisma/schema.prisma":模型唯一事实来源位置。migrations.path: "prisma/migrations" + migrations.seed: "tsx prisma/seed.ts":迁移目录与 seed 命令(用 tsx 直接跑 TS 脚本,Prisma 7 的 seed 新写法)。pnpm prisma db seed 即执行它。datasource.url: process.env["DATABASE_URL"]:CLI 连接串唯一来源;为空时 prisma generate/next build 会因非空校验失败——这正是 Dockerfile 构建期注入假串的原因(见第 5 节)。与之配套的 prisma/schema.prisma 头部:generator 为 prisma-client(新架构),output = "../src/generated/prisma"(生成物进 src,被 .gitignore 排除);datasource 只声明 provider = "postgresql",连接串完全交给运行时(src/lib/prisma.ts 的 pg Pool)。这种「CLI 与运行时各自取连接串」的分离设计有一个安全收益:CLI 永远只在你显式执行命令时连库,应用进程的连接池配置(如未来加 max、SSL 参数)不会与 CLI 行为互相干扰;代价则是上文反复强调的双份 env 维护。若未来想统一,可让 prisma.config.ts 追加读取 .env.local(dotenv 支持 path 参数),但这属于对既有约定的修改,需同步更新《12-FAQ与故障排查》相关条目。
理解这些配置的正确姿势是「每一条豁免都有一个事故或权衡背书」:本项目不是从模板里抄来的配置,而是开发过程中逐条谈判的结果,读配置就是读决策史。
eslint.config.mjs(ESLint 9 flat config):预设为 eslint-config-next/core-web-vitals + eslint-config-next/typescript 两套;globalIgnores 排除 .next/out/build/next-env.d.ts(框架默认)、scripts/**(数据生成脚本,用 require)、src/data/backend-questions-supplement.ts(超大数据文件,跳过解析提速)。三条显式关闭的规则各带理由注释:react-hooks/set-state-in-effect(客户端组件 useEffect 内 setState 是本项目数据获取标准模式,全量 RSC 化不现实)、@next/next/no-img-element(头像为动态外域图片,域名不可枚举)、错误边界文件的 no-html-link-for-pages(错误边界内不能假设 next/link 可加载)。规则豁免全部可审计——修改前先读注释。
husky 与 lint-staged:package.json 的 "prepare": "husky" 在 install 时装配;.husky/pre-commit 一行 pnpm exec lint-staged;lint-staged 配置(package.json 内)对 *.{ts,tsx,js,mjs} 执行 eslint --fix。效果:格式与 lint 问题挡在提交前,自动修复的文件回写暂存区。注意 pre-commit 是四道门禁中最快的一层,但不跑测试与类型检查——它假设你会把这两件事交给 CI;如果本地跳过钩子(--no-verify),CI 会在推送后拦截。
.github/workflows/ci.yml:单 job verify,push 到 master/main 与全部 PR 触发。四步:checkout@v4 → pnpm/action-setup@v4(version 10)+ setup-node@v4(node 22,cache: pnpm)→ pnpm install --frozen-lockfile → 串行 pnpm lint → pnpm typecheck → pnpm test:run。刻意不做的三件事(build、连库、覆盖率)及影响见《09-测试与质量保障》第 5 节。配套 .github/dependabot.yml:npm 与 github-actions 两个生态各每周一批升级 PR——依赖不过期是这类单人维护项目最容易失守的防线,dependabot 的价值在持续提醒而非自动合并,建议保留「每周批量处理一次」的节奏。
vitest.config.ts 与 tsconfig.test.json:jsdom 环境、globals: true、setup 文件 src/test/setup.ts、css: false 提速;覆盖率 v8 provider、include: ["src/lib/**"]、阈值 statements/lines 40、functions/branches 30(2026-09 基线注释)。测试专属 tsconfig 继承主配置,types 注入 vitest/globals 与 @testing-library/jest-dom,include 只收测试文件。别名 @ → ./src 与主 tsconfig 保持一致——两处别名必须同步改,漏一处会出现「IDE 能跳转但测试找不到模块」的怪象。
tsconfig.json 要点:strict 全开、moduleResolution: "bundler"、paths: {"@/*": ["./src/*"]};exclude 除默认外显式排除 src/data/backend-questions-supplement.ts 与 prisma/import-techcodex.ts(历史导入脚本)。Next.js 16 下注意 params/useParams 均为 Promise 类型,类型系统会强制你异步解构——这是故意的破坏性变更(AGENTS.md 头部警告)。include 里包含 .next/types/**/*.ts 与 .next/dev/types/**/*.ts,即 Next 生成的路由类型校验参与 typecheck——因此 typecheck 必须在至少一次 dev/build 之后才完整(冷克隆仓库直接 typecheck 会漏掉路由类型层)。
在列清单之前,先理解变量抵达进程的三条路径——本项目的配置问题绝大多数源于对这张图的误解:
三条路径的要点:Next 加载器(本地)优先 .env.local;Prisma CLI 只读 .env——这是「网站能跑但 migrate 失败」的唯一根因;Docker 链路里镜像内没有 env 文件(.dockerignore 全排除),运行期变量只能由 compose environment 注入,构建期只有 NEXT_PUBLIC_APP_URL 经 build.args 传入、其余凭证用假值占位。以下清单以源码 process.env.* 读取点为准(grep 全量核对)。「必需」组缺失会阻断启动或核心功能;三组可选变量缺失只降级对应功能(503/降级打印/入口隐藏)。
| 变量 | 读取位置 | 说明 |
|---|---|---|
DATABASE_URL | src/lib/prisma.ts(运行时 Pool)、prisma.config.ts(CLI) | PostgreSQL 连接串;运行进程缺失即抛错,CLI 缺失则 migrate/seed 失败;两处加载器不同,.env 需各有其份 |
AUTH_SECRET | src/lib/auth.ts(会话签名,显式指定)、src/lib/jwt.ts(后台 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,生产必须为真实协议+域名,否则登录回调出错 |
| 变量 | 读取位置 | 说明 |
|---|---|---|
DEEPSEEK_API_KEY | src/lib/deepseek.ts(3 处)及 mock-interview 路由内联校验 | 占位串 sk-your-deepseek-api-key-here 与 fake 视同未配置(防照抄样例) |
VOLC_API_KEY | src/lib/volcengine.ts | 豆包 ASR/TTS 的 X-Api-Key;单独决定语音识别/合成可用性 |
VOLC_TTS_RESOURCE_ID | 同上 | 默认 seed-tts-2.0(代码级默认值,可不配) |
VOLC_ASR_RESOURCE_ID | 同上 | 默认 volc.bigasr.sauc.duration |
VOLC_TTS_VOICE_TYPE | 同上 | 默认 zh_female_shuangkuaisisi_uranus_bigtts |
VOLC_S2S_APP_ID / VOLC_S2S_ACCESS_TOKEN | 同上 | 端到端实时语音大模型(旧版 AppId+Token 鉴权) |
VOLC_RTC_APP_ID / VOLC_RTC_APP_KEY | 同上 | RTC「AI 音视频互动」应用凭证(AppKey 参与 Token 签名) |
VOLC_ACCESS_KEY / VOLC_SECRET_KEY | 同上 | 账号级 AK/SK,StartVoiceChat 的 V4 签名 |
六项 S2S/RTC/AKSK 全部就绪才开放视频面试(isSeedRealtimeConfigured())。
| 变量 | 读取位置 | 说明 |
|---|---|---|
OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET / OSS_ROLE_ARN | src/lib/oss.ts(requireEnv 逐项强校验) | STS assumeRole 三件套,缺一上传即抛错;临时凭证 1 小时 |
OSS_REGION | src/app/api/upload/avatar、upload/oss | 默认 oss-cn-shanghai(建议显式覆盖) |
OSS_BUCKET | 同上 | 默认 xiaoye-leaf(作者私有桶名,务必覆盖!) |
OSS_HOSTNAME | next.config.ts | next/image 放行域名,默认同上;构建期读取 |
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS | src/lib/mailer.ts | PORT 默认 465(secure: true);HOST/USER/PASS 齐备才真实发信,否则验证码打印控制台;.env.example 未列出该组 |
NEXT_PUBLIC_APP_URL | src/app/robots.ts、sitemap.ts(默认示例域名)、/api/share、/api/reminders/digest(默认 http://localhost:3000) | 站点公开地址,生产必填;同时是 Docker 构建参数(见第 5 节) |
TZ | compose 注入 | 业务日界硬编码 Asia/Shanghai 不依赖它;只影响日志时间戳 |
CRON_SECRET | src/app/api/reminders/digest/route.ts | 未配置该端点 503;调用带 x-cron-secret 头 |
NODE_ENV | src/lib/prisma.ts 等 | production 时 Prisma 单例不挂 globalThis;Docker 运行段已固定 |
三份样例文件的分工与差异:.env.example 是权威变量字典(.gitignore 用 !.env.example 唯一放行),但含两处遗留——GITHUB_ID/GITHUB_SECRET(源码无 GitHub OAuth,照配无效)与缺失的 SMTP 组;.env.local.example 是早期 Supabase 方案的历史残留(NEXT_PUBLIC_SUPABASE_*、浏览器直传 OSS 变量),与现行架构完全无关,忽略或删除;真实 .env/.env.local 被 .gitignore 与 .dockerignore 双重排除,只存在于部署环境。另有三个 compose 透传但源码未读取的变量:STORAGE_DRIVER、NEXT_PUBLIC_OSS_REGION、NEXT_PUBLIC_OSS_BUCKET(docker-compose.yml),疑为浏览器直传方案遗留,可不配置(《13-技术债务与演进建议》第 3.1 条)。
读这个 Dockerfile 的主线是「构建期与运行期的职责切分」:构建段要依赖(安装、编译、生成),运行段只要产物(代码、迁移、配置),两段之间通过 COPY --from=build 精确交接七样东西——凡是交接清单之外的东西(源码、测试、文档、假凭证)都不进运行镜像。理解了这个切分逻辑,下面逐段的每个动作都有明确的归属。
构建阶段(FROM node:22-slim AS build,第 5-22 行),九步:① corepack enable + apt 安装 openssl ca-certificates(Prisma 引擎需要 openssl;出站 HTTPS 需要根证书)并清理 apt 缓存;② WORKDIR /app;③ ENV CI=true 全程生效防交互提示卡住非 TTY 构建;④ ARG NEXT_PUBLIC_APP_URL + ENV 固化——为将来客户端代码引用该变量预留构建期内联链路(当前仅服务端读取,运行期注入即可,但链路已就位);⑤ 注入构建期假凭证 DATABASE_URL=postgresql://build:build@localhost:5432/build_dummy 与 AUTH_SECRET=build-only-dummy,仅满足 prisma.config.ts 非空校验,generate 与 build 均不连库,注释明令禁止传真实串;⑥ 先只拷贝 package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc 五个清单文件再 pnpm install --frozen-lockfile——Docker 层缓存的关键:依赖未变时重构建跳过安装;⑦ COPY . .(受 .dockerignore 裁剪:全部 .env*、node_modules、.next、src/generated、测试文件、docs 等均不进上下文);⑧ ENV NODE_OPTIONS=--max-old-space-size=4096(next build 内存需求);⑨ pnpm exec prisma generate + pnpm build,产出 src/generated/prisma/ 与 .next/。
运行阶段(FROM node:22-slim AS run,第 24-45 行):① ENV NODE_ENV=production + CI=true;② 同构建段安装 openssl 与根证书(运行期 Prisma 引擎与出站 HTTPS 仍需要);③ WORKDIR /app 后从构建段拷入七样(全部 --chown=node:node 配合降权):node_modules(全量含 devDependencies,原因双重——未启用 standalone 且运行期 migrate deploy 需要 prisma CLI)、.next、public/(career 页服务端读 public/knowledge/*.md,不能省)、prisma/(schema + 22 个迁移)、prisma.config.ts、package.json、next.config.ts + pnpm-workspace.yaml;④ USER node 降权为非 root;⑤ EXPOSE 4300;⑥ HEALTHCHECK(30s 间隔 / 5s 超时 / 20s 启动宽限 / 3 次重试,探针为容器内 node 原生 fetch 调 /api/health);⑦ CMD 一行完成「先迁移后启动」:pnpm exec prisma migrate deploy && pnpm start -p 4300——发布新版本等于推镜像加重启,迁移失败则容器不启动(失败模式见《12-FAQ与故障排查》第 3 条)。
docker-compose.yml 与镜像内 HEALTHCHECK 参数完全一致(双重声明),environment 段是全部运行期变量的注入点,来自 compose 同目录 .env 的变量替换(AUTH_URL/NEXTAUTH_URL 有 :- 默认值链兜底 http://localhost:4300);build.args 把 NEXT_PUBLIC_APP_URL 传入构建期(对应上文第 ④ 步);logging 限 20MB × 5 文件。使用 compose 时有三个容易忽略的行为值得点名:其一,compose 的变量替换只读 compose 文件同目录的 .env,与 Next/Prisma 的加载逻辑互不相干——同一台机器上三份 .env 各司其职的场景完全正常;其二,environment 段中值为空的变量(如未配置的 DEEPSEEK_API_KEY)会以空字符串注入而非不注入,应用侧的「未配置」判断(假值检查)恰好兼容空串,这是 validateDeepSeekApiKey 把占位值与空值一并拦截的原因;其三,compose 项目名固定为 mianshi(文件头 name: 字段),容器与镜像的命名都跟随它,多套环境并行部署时应通过 -p 参数或复制 compose 文件改名隔离,避免共享项目名导致互相覆盖。
读这张矩阵的正确方式是按「为什么会不同」分组,而不是死记条目。第一组差异源于运行环境本身(端口、NODE_ENV、健康检查、日志去向),它们由 Dockerfile 与 compose 固化,本地无对应物;第二组差异源于加载器(env 文件、Prisma 客户端生成、迁移命令形态),根源都是第 2 节讲的 Prisma 7 新架构与 dotenv 单文件加载;第三组差异源于部署安全(构建期假凭证、env 不进镜像、SMTP 必配),它们是刻意设计而非偶然。真正需要人工动作的是矩阵最后几行:AUTH_URL 三兄弟的生产覆盖、首管理员的手工 bootstrap、SMTP 的生产配置——历次部署事故几乎全部集中在这三行。
| 维度 | 本地开发 | 生产(Docker) |
|---|---|---|
| 端口 | 3000(pnpm dev) | 4300(CMD 写死 -p 4300),对外由 compose ports 映射 |
| env 加载 | Next 读 .env.local(优先)/.env;Prisma CLI 只读 .env | 全部经 compose environment 注入;镜像内无任何 env 文件(.dockerignore 排除) |
| Prisma 客户端 | 手动 pnpm prisma generate(无 postinstall) | 构建阶段显式 generate |
| 迁移 | pnpm db:migrate(migrate dev,可生成新迁移) | 容器 CMD migrate deploy(只重放已登记迁移,幂等) |
| 种子数据 | 可选 pnpm prisma db seed | 可选:docker compose exec mianshi pnpm exec prisma db seed |
| SMTP | 未配置即降级控制台打印验证码(联调友好) | 应配置真实 SMTP,否则用户收不到验证码(降级行为相同) |
| NODE_ENV / Prisma 单例 | development,实例挂 globalThis(热重载复用) | production,模块级单例不挂全局 |
| 开发源访问 | allowedDevOrigins 限制局域网 IP | 无该配置(生产构建不启用) |
| AUTH_URL/NEXTAUTH_URL | http://localhost:3000 | 必须为真实对外地址(compose 默认值是 localhost:4300,须覆盖) |
| 首个管理员 | 本地改库或后门自便 | 手工 UPDATE users SET role='admin'(无 bootstrap 机制,见《08-安全与权限设计》第 3 节) |
| 健康检查 | 无 | HEALTHCHECK + compose healthcheck 双重探 /api/health |
| 日志 | 终端直出 | json-file 20MB×5 轮转,docker compose logs 检索 |
配置变更的三条铁律收尾:改了 OSS_HOSTNAME 或未来被客户端引用的 NEXT_PUBLIC_* 变量必须重新构建镜像(构建期内联);改 AUTH_SECRET 全员会话失效(安排在低峰并周知);任何新环境变量必须三处同步——.env.example(字典)、docker-compose.yml environment 段(生产注入)、《14-配置与环境详解》本篇清单(文档),三缺一都会制造下一个人的排障故事。补充第四条留给依赖升级:pnpm-lock.yaml 与 packageManager 字段共同锁定依赖宇宙,CI 的 --frozen-lockfile 与 Docker 的同参数安装都会拒绝锁文件漂移——升级依赖的正确动作是本地 pnpm update / 改 package.json 后重新 pnpm install 生成新锁文件并一起提交,而不是在 CI 里放宽参数。