项目:interview-platform | 版本:v1.2 | 生成日期:2026-10-06 | 依据:源码分析
本篇是整套文档(00-20)的收官篇,承担三件收口工作。第一,统一语言:前二十篇里反复出现的业务与技术术语,在这里给出以源码为准的唯一定义——术语歧义是多人协作与跨文档阅读时最低级的成本,值得用一个专篇消除。第二,反向索引:既有索引都是「文档 → 内容」方向,本篇补一张「源码文件 → 文档」的对照表,让你拿着一个文件名就能找到讨论它最深入的篇目。第三,任务路径:按四个高频任务(修 AI 链路 bug、加新题型、上线检查、排查限流)给出「读哪几篇、读哪几段、按什么顺序」的具体动线,把 21 篇文档组织成按需取用的工具箱而不是一本必须通读的教材。本篇自身的正文不加新事实,所有定义都标注了源头文件或出处篇目。
题目(Question):全站的数据与体验中枢,模型见 prisma/schema.prisma 的 Question。每道题有题面(content,Markdown 长文)、参考解析(solution)、难度(简单/中等/困难)、题型(questionType:qa/code/judge)、公司与岗位方向等维度;围绕它组织的「按难度是筛选、按时间组织是竞赛、按编排是试卷与路径、按记忆规律是复习」这句话出自《00-项目总览》第 2 节,是理解全站模块关系的总纲。
刷题 / 题目练习:用户与题目交互产生 PracticeHistory(行为事实源)的过程,入口是判题提交(/api/questions/[id]/submit)与手写练习记录(/api/practice)。练习记录的状态映射规则是 accepted → completed、其余 → failed,打卡、热力图、复习调度、排行榜全部消费这一口径(《05-核心流程》流程三)。
掌握度(mastery):每用户每题的三值状态 unsolved/learning/mastered,存储于 UserQuestionState(@@unique([userId, questionId]))。判题通过只把它从 unsolved 单向推进为 learning,绝不回退 mastered;列表接口把它与 isBookmarked 合并进题目响应(《04-数据模型》3.3)。
渐进式提示:题目详情页的三级 AI 提示(1 点拨 / 2 思路框架 / 3 详细思路),按「题目 × 级别」缓存在 QuestionHint 表(@@unique([questionId, level])),首个触发生成的用户为全站付费。三级规则共同约束是「绝不直接透露答案」(《07-模块详解》1.2)。
题解(Solution):用户对某题发布的完整解答(≤20000 字,可标注语言),支持点赞(简单 +1、无去重表,可刷票是已知债务)与编辑;「解析编辑建议」(QuestionEdit)则是另一条众包纠错链——用户提交修改建议、管理员审核后回写 Question.solution(《07-模块详解》1.2 与 10 节)。
精选题集(QuestionSet):运营侧编辑的题目合集,以 slug 访问(/question-sets/[slug]),带浏览计数与发布开关;与用户自建能力无关,管理入口在 /api/admin/question-sets。
组卷 / 试卷(TestPaper):用户自建的题目集合(1-100 题,zod 限制),isPublic 二态可见性;交卷把答案 JSON 快照进 TestPaperSubmission.answers 并自动判分返回 {score, total, correctCount}。题篮(useQuestionBasket,localStorage)是跨页收集题目、跳转组卷的前端机制(《07-模块详解》6.2)。
竞赛(Contest):限时答题活动,类型 weekly/monthly/custom;时间窗由 startTime + duration 动态推导(status 字段只是展示冗余);判题包含隐藏用例(isHidden=true),部分通过按比例折算得分。竞赛没有报名环节——时间窗内提交即参与,ContestSubmission 既是提交档案也是参与记录(《07-模块详解》5.2)。
学习路径(LearningPath)与学习计划(StudyPlan):前者是可预设的模板(isPreset,按 category/difficultyLevel 组织);后者是用户私有的实例(四级结构:计划 → 天 → 条目 → 进度)。「join 学习路径」即把模板按 LearningPathItem 的 dayNumber/sortOrder 复制生成个人 StudyPlan/Day/Item——复制不是引用,路径此后更新不回流已加入的计划(《15-接口全量参考》第 7 节)。
课时(lesson):本项目数据模型中没有名为 lesson 的实体;领域里最接近的概念是学习计划的「天」(StudyPlanDay,@@unique([studyPlanId, dayNumber]),带 targetQuestionCount 默认 5)与学习路径条目的 dayNumber。若从其他系统迁入「课时」概念,映射关系应按「课时 ≈ StudyPlanDay(学习计划语境)或 LearningPathItem 的 dayNumber 分组(路径语境)」理解 ⚠️ 待确认(无官方对应文档,此为按模型结构的最自然映射)。
复习间隔 / 艾宾浩斯阶段(stage):基于遗忘曲线的固定间隔表 EBINGHAUS_INTERVALS = [1,2,4,7,15,30](天),阶段号从练习次数推导(reviewCount = 练习次数 - 1,封顶第 6 阶段)。复习计划不落库,每次从 PracticeHistory 实时推导(《18-代码走读实录》第二部分)。注意全站存在第二份间隔表(/api/reminders 的 [1,3,7,15,30]),两套口径的分叉已列入技术债(《13-技术债务与演进建议》2.4)。
到期复习(due):nextReviewAt ≤ now 的题目集合,daysOverdue(逾期天数)降序展示在首页待复习区;/api/review/due 返回 {dueCount, reviews[]}(《19-数据契约与示例集》7.1)。
打卡 / 连续打卡(streak):以上海时区(UTC+8)为自然日边界的练习统计——currentStreak(当前连续天数)、longestStreak(最长连续)、dailyGoal(每日目标,默认 5)与 goalMet。UserStreak 表是读时物化的快照而非事实源(《05-核心流程》流程五)。
每日一题(daily question):以当天日期字符串作随机种子的确定性抽取,同一天全站所有人看到同一题;登录用户优先从未掌握题中抽,题库为空返回 404(《07-模块详解》7.2)。
能力雷达:按分类正确率绘制的五/六维雷达图(Recharts),数据来自 /api/stats/ability;与之并列的看板图形还有 30 天进度折线、分类/难度分布饼图与 90 天热力图(CalendarHeatmap)。
模拟面试(MockInterview)/ 轮次(turn):一场面试 = 一条 MockInterview(direction 方向 + status)+ 最多 5 条 MockInterviewTurn(aiQuestion/userAnswer/aiFeedback/score/turnOrder)。前端字段映射约定:aiQuestion/userAnswer/aiFeedback → question/answer/feedback(AGENTS.md 约定第 4 条,两边必须同步改)。战报指完结后的 overallScore(0-100)与 summary(《07-模块详解》3.2)。
行为面试 / STAR:direction 为 behavioral 的面试形态,首题取自本地题库 src/lib/behavioral-questions.ts(不耗 AI 配额),评估按 STAR 四维加权(完整性 40%/具体性 25%/逻辑性 20%/表达力 15%)——S 情境、T 任务、A 行动、R 结果(《18-代码走读实录》第 1.4 节)。
AI 面试官(视频/语音模式):火山引擎 SeedRealtime 端到端语音大模型驱动的实时面试官,浏览器经 RTC 直连火山、服务端只签发 Token 与调用 StartVoiceChat 信令;字幕回传后经 /api/video-interview/save 落库(《07-模块详解》4.2)。
经验分享(笔记 Note):/experiences 社区流的内容形态,列表与创建都需登录(返回本人笔记);可关联 questionIds。
社区三件套:关注(Follow,@@unique([followerId, followingId]) + upsert 防重)、通知(Notification,写入口统一走 createNotification——自己不通知自己、失败不阻塞)、举报(Report,多态 target 无外键)。分享短链(ShareLink)以 8 位 slug 承载题目/笔记/面试战报的站外传播(《07-模块详解》第 10 节)。
后台 / 管理员 / editor:/admin 八个子页对应的内容与用户管理面;admin 角色拥有全部 7 个权限点,editor 是 RBAC 种子里预留的内容运营角色(6 个权限点)但因角色接口白名单只有 admin|user 而无法经 API 授予 ⚠️ 待确认是否有意收窄(《08-安全与权限设计》第 3 节)。
Route Handler / 薄控制器:src/app/api/**/route.ts 中与 HTTP 方法同名的导出函数;本项目范式是薄控制器——只做参数提取、鉴权、限流、校验四件事,业务下沉 src/lib(《01-系统架构》2.3)。动态段 [id] 的 params 在 Next.js 16 是 Promise,必须 await params 解构。
proxy.ts(原 middleware):Next.js 16 把中间件更名为 proxy,运行于 Edge Runtime(因此不能 import prisma 或 lib/auth),只做页面级粗筛(受保护前缀 + admin 门禁);数据级权限仍由 API 路由细查,构成「Edge 粗筛 + Node 细查」双层防御(《08-安全与权限设计》第 1 节)。
守卫函数:requireAuth()(返回 401 响应或 null)、optionalAuth()(永不拦截)、requirePermission(code)(返回 401/403 或 null)三件套,出自 src/lib/session.ts 与 permission.ts。本项目没有集中式鉴权中间件,鉴权级别靠每个路由显式调用守卫表达。
权限点(permission code):RBAC 的最小授权单元(如 question:add、admin:access),数据链 User.role → Role.code → RolePermission → Permission.code;User.role 是字符串弱关联(无外键)。角色权限集合有 30 秒 TTL 内存缓存——改权限后最长 30 秒生效窗口(《08-安全与权限设计》第 3 节)。
会话回查:src/lib/auth.ts 的 session 回调每次拉取会话都 findUnique 用户表刷新资料并按 banned 拒签——「无状态令牌 + 有状态校正」的折中,代价是每个带会话请求多一次主键查询(《16-决策记录ADR》ADR-007)。
驱动适配器 / Prisma 单例:src/lib/prisma.ts 用 pg.Pool + PrismaPg 构造的模块级单例(开发态挂 globalThis);全项目无 Repository 层,查询内联在 Route Handler(《16-决策记录ADR》ADR-002)。
迁移 / seed / migrate deploy:schema 变更唯一正道是 pnpm db:migrate(prisma migrate dev)生成迁移文件入库(现 22 个);生产容器 CMD 内建 prisma migrate deploy(只重放、幂等、无 down);seed(tsx prisma/seed.ts)幂等——分类 upsert、题目按 title 去重、只做加法(《06-部署运行》第 7 节)。
复合唯一键 / upsert:@@unique([userId, questionId]) 这类复合键是并发幂等写的兜底;个人状态表全部 upsert(update: {} 空更新实现幂等勾选)。N+1:循环内逐条查询的反模式,正确范式是批量 findMany + Map 合并(《07-模块详解》1.2)。
SSE / 流式转发:text/event-stream 响应,帧格式 data: {JSON}\n\n。服务端用 ReadableStream 逐 chunk 转发 DeepSeek 增量(零额外缓冲),前端用 parseSSEStream(fetch POST + 手工解析,不能用 EventSource)消费。流开始后 HTTP 恒 200,业务错误以 error 事件下发(《19-数据契约与示例集》第 8 节)。
滑动窗口限流:rate-limit.ts 的进程内 Map + 时间戳数组实现,5 分钟定期清理防泄漏;多实例不共享配额,扩容前必须换 Redis(《13-技术债务与演进建议》1.1)。getClientIp 取 x-forwarded-for 最后一跳。
判题包装器(wrapCode):judge.ts 按语言把用户代码包进可执行的 runner——JS/TS 正则提取函数名自动调用、Python 反射实例化 Solution、Java/C++ 检测不到 main 时只注入提示文案(名存实亡的已知局限)。隐藏用例:isHidden=true 的 TestCase 只在竞赛判题中包含,防「面向样例编程」(《07-模块详解》2.2)。
Piston:emkc.org 的公共代码执行 API,5 语言版本映射维护在 code-runner.ts;单用例 15 秒 Promise.race 超时。ASR/TTS/RTC/SeedRealtime:火山引擎的语音识别、语音合成、实时音视频传输、端到端语音大模型四件能力,对应四套互不相同的凭证体系,全部只在服务端持有(《16-决策记录ADR》ADR-008)。
V4 签名 / STS 临时凭证:前者是火山 OpenAPI 的 HMAC-SHA256 推导链签名(kDate → kService → kSigning,用于 StartVoiceChat);后者是阿里云 OSS 的 sts.assumeRole 签发的 1 小时临时上传凭证,密钥收窄的标配做法。
SWR / force-dynamic:前端数据获取缓存库(src/lib/swr.ts 统一 fetcher,401 自动登出);export const dynamic = "force-dynamic" 是 Route Handler 声明动态渲染的约定,依赖会话的接口普遍显式声明(《10-性能设计与优化》第 1 节)。
zod / parseBody:声明式校验 schema 集中在 src/lib/schemas.ts,parseBody 提供三态解析(合法 / 非法 JSON / 校验失败取首个 issue)统一返回 400。旧接口仍是手写校验,新接口必须走 zod(《16-决策记录ADR》ADR-010)。
PWA / 设计令牌 / URL 即状态:PWA 指 public/sw.js(network-first 缓存 /、/questions、/manifest.json)与 SWRegister/OfflineIndicator 组成的离线兜底;设计令牌指 design-tokens.ts 与 CSS 变量(var(--primary)、color-mix())驱动的主题体系,暗色模式用 data-theme + localStorage 防闪烁内联脚本;URL 即状态指题库列表把全部筛选同步进查询串、返回时免重选的交互设计(《02-页面设计》第 3.2 节)。
下表覆盖最常被打开的 42 个源码文件,第三列给出讨论它最深入的篇目与章节(同一文件被多篇提及的,列「主要」篇目):
| 源码文件 | 职责一句话 | 主要篇目 |
|---|---|---|
src/lib/prisma.ts | Prisma 单例 + pg 连接池 | 《01》2.5、《16》ADR-002、《17》第 4 节 |
src/lib/auth.ts | authOptions:Credentials + JWT 回调 | 《08》第 2 节、《16》ADR-007 |
src/lib/session.ts | requireAuth/optionalAuth/getCurrentUser 守卫 | 《08》第 4 节 |
src/lib/permission.ts | requirePermission + 30 秒角色缓存 | 《08》第 3 节、《09》第 3 节 |
src/lib/jwt.ts | 后台 Bearer JWT(jose,7 天) | 《08》第 2 节、《07》8.2 |
src/lib/rate-limit.ts | 进程内滑动窗口限流 | 《08》第 4 节、《16》ADR-003、《18》— |
src/lib/validate.ts + schemas.ts | zod 校验与 schema 登记 | 《08》第 5 节、《16》ADR-010 |
src/lib/api-response.ts | ok/fail/handleApiError 统一响应 | 《16》ADR-010、《09》2.1 |
src/lib/deepseek.ts | DeepSeek 调用封装(116 行) | 《18》第 1.5 节、《17》— |
src/lib/sse.ts | 前端 SSE 解析器 | 《18》第 1.5 节、《09》2.1 |
src/app/api/mock-interview/route.ts | 创建面试 + 首题(SSE) | 《18》第 1.2-1.3 节、《05》流程二 |
src/app/api/mock-interview/[id]/turn/route.ts | 答题评估 + 原子落库(SSE) | 《18》第 1.4 节 |
src/app/api/mock-interview/[id]/route.ts | 删除面试(仅 DELETE) | 《18》第 3.4 节(405 发现) |
src/app/ai/mock-interview/page.tsx | 面试四视图前端(1715 行) | 《18》第三部分、《02》3.4 |
src/lib/review-scheduler.ts | 艾宾浩斯复习推导 | 《18》第二部分、《05》流程五 |
src/lib/streak.ts | 上海时区打卡计算 | 《05》流程五、《09》2.1 |
src/lib/judge.ts | 判题编排 + wrapCode | 《01》第 3 节、《09》第 3 节 |
src/lib/code-runner.ts | Piston 调用 + 语言映射 | 《16》ADR-009、《12》第 7 条 |
src/app/api/questions/route.ts | 题库列表/创建 | 《07》1.2、《19》第 2 节 |
src/app/api/questions/[id]/submit/route.ts | 判题提交 + 状态推进 | 《01》第 3 节、《19》第 4 节 |
src/app/api/questions/[id]/hints/route.ts | 渐进提示(按题缓存) | 《07》1.2、《16》ADR-004 |
src/app/api/contests/[id]/submit/route.ts | 竞赛提交(时间窗/隐藏用例/得分) | 《07》5.2、《19》第 6.2 节 |
src/app/api/review/due/route.ts | 到期复习清单 | 《19》第 7.1 节 |
src/app/api/stats/streak/route.ts | 打卡快照(读时物化) | 《19》第 7.2 节、《07》7.2 |
src/app/api/auth/register/route.ts | 注册(验证码先验 + bcrypt) | 《05》流程一、《09》第 3 节 |
src/lib/verify-code.ts | 验证码生命周期 | 《08》第 2 节 |
src/lib/mailer.ts | SMTP 降级发信 | 《12》第 8 条、《06》第 2 节 |
src/lib/volcengine.ts | 四套火山凭证 + V4 签名 | 《07》4.2、《16》ADR-008 |
src/lib/video-interview.ts | RTC Token + StartVoiceChat | 《07》4.2 |
src/app/api/video-interview/token/route.ts | Token 签发 + HEAD 探测 | 《19》—、《15》第 5 节 |
src/app/api/voice/asr/route.ts | 豆包 ASR WebSocket 代理 | 《07》4.2 |
src/lib/behavioral-questions.ts | 行为面试本地题库 | 《16》ADR-004、《18》1.3 |
src/lib/oss.ts | OSS STS assumeRole | 《08》第 6 节 |
src/lib/notify.ts | 旁路通知(不阻塞) | 《07》第 10 节 |
src/lib/request.ts | 前端请求封装(401 登出) | 《09》2.1、《02》第 5 节 |
src/lib/swr.ts | SWR 统一 fetcher | 《02》第 5 节 |
src/lib/question-types.ts | 题型注册表 | 《07》1.2、《11》SOP 六 |
src/lib/category-cache.ts / category-utils.ts | 分类树缓存与消歧 | 《07》1.1、《09》2.1 |
src/proxy.ts | Edge 页面门禁 | 《01》2.1、《08》第 3 节 |
prisma/schema.prisma | 43 模型单一事实来源 | 《04》全篇 |
prisma/seed.ts | 幂等种子脚本 | 《06》第 7 节、《11》SOP 四 |
prisma/migrations/(22 个目录) | 迁移演进史 | 《04》第 1 节、《16》ADR-005/006 |
next.config.ts / prisma.config.ts | 全站配置与 Prisma CLI 配置 | 《14》第 1-2 节 |
Dockerfile / docker-compose.yml | 构建与编排 | 《06》第 6 节、《14》第 5 节 |
.github/workflows/ci.yml | CI 四步流水线 | 《09》第 5 节、《06》第 9 节 |
动线:《18-代码走读实录》第一部分(第 1.1-1.5 节,先建立链路全景与四个文件的角色)→《05-核心流程》流程二(时序图,补前端交互视角)→ 源码四件套(mock-interview/route.ts、turn/route.ts、deepseek.ts、sse.ts)对照走读 → 《12-FAQ与故障排查》第 5、10 节(503 降级矩阵与日志前缀,判断是故障还是降级)。三个最可能的坑:AI 返回内容无法解析(SSE 内 error 事件,HTTP 200)、上游 502(查服务端日志 DeepSeek API error:)、创建接口 503(Key 未配置或占位值)。若涉及字段映射(前端拿不到反馈),先查 aiQuestion/userAnswer/aiFeedback → question/answer/feedback 映射是否被单边改名(《12》第 9 节)。修完自检:pnpm test:run 中 deepseek.test.ts 与 sse.test.ts 必须保持全绿;错误消息不得透传上游细节(纪律见《11》SOP 三)。
动线:《11-扩展开发指南》SOP 六(主手册,从注册表到发布顺序)→《07-模块详解》1.2 的「题型注册表」段(理解 QUESTION_TYPE_META 为何是唯一事实源)→ 若需要新标签页,《02-页面设计》3.3(详情页五标签页结构)→ 若需要判题,《18》第 2.3 节之外的 judge.ts 与《13》2.7(Java/C++ 包装器现状,避免重复踩坑)。关键约束:defaultTab 必须是 tabs 子集(question-types.test.ts 的自洽性测试会把关);normalizeQuestionType 把脏值归一为 qa,因此发布顺序必须「先发代码、再录数据」;若题型需要长度上限,顺手把创建接口迁入 zod(正题量见《13》1.6)。
动线:《06-部署运行》全篇(部署形态、环境变量、首次部署六步)→《14-配置与环境详解》第 4 节(变量三路径图与四组清单,重点核对 AUTH_URL 三兄弟与 SMTP 组)→《08-安全与权限设计》第 10 节(安全基线速查,逐条过)→《12-FAQ与故障排查》第 0 节决策树(把「这是故障还是降级」的判断装进值班手册)。五个不可省略的动作:DATABASE_URL + AUTH_SECRET ≥32 位就位;首次部署后手工 bootstrap 首管理员(无自动机制)并验证 GET /api/admin/stats 返回 200;SMTP 四变量配置(否则用户收不到验证码);删除或替换 layout.tsx 里硬编码的 Umami 统计脚本;冒烟 curl /api/health 与一次完整的「注册 → 登录 → 判题」人工流程。回滚预案:《06》第 10 节第 7 条(migrate deploy 不支持 down,需反向迁移或备份恢复)。
动线:《08-安全与权限设计》第 4 节(限流矩阵全表 + 标识符选择逻辑)→《12-FAQ与故障排查》第 6 条(429 排障三要点:内存实现重启清零、按 userId 共享配额、上海时区护栏重置点)→ src/lib/rate-limit.ts 源码(112 行全读,重点 rateLimitHeaders 与 getClientIp)→ 若涉及扩容规划,《13-技术债务与演进建议》1.1 与《10-性能设计与优化》第 6 节建议 9(Redis 化改造)。两个高频误判:「重启后好了」说明是限流而非权限(内存态被清);「深夜被限额」说明撞上了每日 20 场的上海时区护栏(UTC 服务器跨 16:00 才重置)。给客户端的规范:429 响应的 X-RateLimit-Reset 是秒级 Unix 时间戳,应据此退避(《19》第 8 节)。
任何任务开始前建议先读《00-项目总览》第 4 节(目录导览与分层约定)与《11-扩展开发指南》收尾的七条纪律;动手改代码前过一遍《16-决策记录ADR》第「收尾」节的三条决策语法,确认改动不与既有取舍(单实例优先、外购基建、渐进演进)冲突。本篇与《00》第 6 节的阅读路线建议互补:那篇按「角色」给路径(接手后端/前端/运维),本篇按「任务」给路径,两层索引合用可以覆盖绝大多数进入场景。
至此文档集共 21 篇(00-20)。维护约定沿用《13-技术债务与演进建议》末段的声明:源码是唯一事实来源,文档是带时间戳的快照——当文档与源码冲突时一律以源码为准,并请在对应篇目标注修订;无法从源码确认的事项保持「⚠️ 待确认」标记,核销时逐条更新。术语表(第 1-2 节)在新增业务概念时同步增补,反向索引(第 3 节)在新文件进入核心链路时登记一行——这份索引的价值随维护纪律线性增长,而它的失效同样随疏于维护线性发生。