[M-QUIZ-01] Attempt 生命周期与答案可见性控制 #336
Closed
opened 2026-06-23 20:27:54 +08:00 by wangdl
·
3 comments
Labels
Clear labels
adr
Area: adr
architecture
Area: architecture
area:activity
活动/统计
area:admin
管理后台
area:admin-api
area:ai
AI/RAG
area:ai-runtime
AI Runtime / AI 分析体系相关
area:analytics
area:api
API 接口
area:auth
认证与授权
area:cos
对象存储
area:database
数据库/Migration
area:import
文件导入/解析
area:knowledge
知识库/知识点
area:learning-info
area:learning-session
area:quiz
测验/自测
area:reading-event
area:reading-progress
area:review
复习系统
area:security
安全相关
audit
Area: audit
audit:api-admin-info
audit:api-info
audit:planned
已完成宏观规划,尚未代码审查
audit:reviewed
backend
Area: backend
billing
Area: billing
blocked-by:api-info-aggregation
blocked-by:api-info-core
blocked-by:api-info-ops
blocked-by:api-info-schema
blocked-by:processor
blocked-by:schema
compatibility
Area: compatibility
gate
Area: gate
infrastructure
Area: infrastructure
priority:p0
最高优先级,阻塞发布
priority:p1
高优先级,里程碑必需
priority:p2
中优先级,后续版本
prisma
Area: prisma
release
Area: release
repo:api
API 仓库 Issue
status:blocked
被阻塞
status:done
已完成
status:partial
status:todo
type:aggregation
type:bug
缺陷修复
type:design
设计
type:docs
文档
type:feature
新功能
type:migration
type:refactor
重构
type:test
work:admin-api
work:aggregation
work:api
work:artifact
题目/卡片产物
work:audit
work:circuit-breaker
熔断
work:contract
work:design
架构/协议设计工作
work:docs
work:export
work:extend-existing
work:internal-api
Runtime 内部接口
work:job
Job 调度相关
work:new-module
work:new-table
work:ops
work:query
work:quota
额度/限流
work:schema
Prisma Schema 设计
work:security
work:service
Service 层实现
work:snapshot
Snapshot 构建
work:test
Milestone
No items
No Milestone
M-QUIZ-01 Quiz 答题闭环与跨端契约收口
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: wangdl/api-server#336
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
类型 / 标签
backendquizattemptsecurityp0依赖
无(M-AI-07 Quiz 生成已就绪)
背景
审计发现(
m-ai-07-quiz-generation-migration-contract.md §1.7):getQuizQuestions()在 SELECT 中包含answer和explanation字段——任何已认证用户可在未开始答题前获取正确答案findOne()include questions 同样泄漏答案目标
POST /api/quiz/:id/attempts),每次答题启动新 Attemptanswer和explanationanswer和explanation非目标
quiz.service.ts)影响范围
src/modules/quiz/quiz.controller.tssrc/modules/quiz/quiz.service.tssrc/modules/ai-runtime/user-ai.service.ts:546-556getQuizQuestions()脱敏QuestionForAttemptvsQuestionWithAnswer验收标准
POST /api/quiz/:id/attempts创建独立 Attempt,返回attemptIdanswer/explanationanswer/explanationanswer/explanation建议执行顺序
#1 — 最先执行。Attempt 是整个答题闭环的基础。
开发完成评论
完成内容
POST /quizzes/:id/attempts端点——创建独立 Attempt,每次答题启动新 AttemptGET /quizzes/attempts/:id/questions端点——按 Attempt 加载题目,未提交前不返回answer和explanationquiz.service.ts:findOne()——增加userId校验,跨用户访问返回 403quiz.service.ts:start()——增加 quiz 所有权校验,跨用户创建 Attempt 返回 403quiz.service.ts:getAttemptQuestions()——per-attempt 答案可见性控制,新 Attempt 不受历史 Attempt 影响quiz.service.ts:getResults()——增加userId校验user-ai.service.ts:getQuizQuestions()——支持可选attemptId参数,改为 per-attempt 答案可见性(不再基于"任意历史已完成 Attempt")user-ai.controller.ts:getQuizQuestions()——透传可选attemptIdquery 参数修改文件
src/modules/quiz/quiz.service.tssrc/modules/quiz/quiz.controller.tssrc/modules/ai-runtime/user-ai.service.tssrc/modules/ai-runtime/user-ai.controller.tssrc/modules/ai-runtime/user-ai.controller.spec.ts(测试签名修复)代码证据
quiz.service.ts:91:findOne()增加if (quiz.userId !== userId) throw new ForbiddenException(...)quiz.service.ts:100:start()增加 quiz 所有权校验ForbiddenExceptionquiz.service.ts:108-130:新增getAttemptQuestions()——按 attempt.finishedAt 控制answer/explanation返回quiz.service.ts:166:getResults()增加userId校验user-ai.service.ts:550-563:getQuizQuestions()改为接收可选attemptId,仅当该 attempt 已完成才返回答案;未提供 attemptId 时不返回答案(安全默认)quiz.controller.ts:43-47:新增POST :id/attempts端点quiz.controller.ts:56-60:新增GET attempts/:id/questions端点测试情况
npx jest src/modules/ai-job/quiz-generation-*.spec.ts src/modules/ai-runtime/quiz-execution-router.spec.ts src/modules/ai-runtime/user-ai.service.spec.ts src/modules/ai-runtime/user-ai.controller.spec.ts关键行为变更
未完成 / 风险
quiz.service.ts:create()保留原样)是否建议进入 Review
第二轮:N1/B1 修复 + 测试补齐
N1 修复:getResults 信息泄漏
quiz.service.ts:159-165:userId进入 Prisma 查询条件,跨用户统一返回 404(与submit()一致)B1 修复:getQuizQuestions 跨 Quiz 答案泄漏
user-ai.service.ts:555:attempt.findFirst()增加quizId过滤,防止用 Quiz B 的 Attempt 查 Quiz A 的答案测试补齐(N2/N3 解决)
quiz.service.spec.ts(新增 18 tests)
findOne:正常/不存在/跨用户 403start:正常创建/不存在/跨用户 403getAttemptQuestions:未提交无答案/已提交有答案/不存在 404/跨用户 403/历史 Attempt 不影响新 AttemptgetResults:正常/N1 修复(跨用户 404)/不存在submit:正常判分/跨用户/已提交拒绝getHistory:正常quiz.controller.spec.ts(新增 7 tests)
POST :id/attempts创建 AttemptGET attempts/:id/questions返回题目GET :id/GET :id/results传入 userIduser-ai.service.spec.ts(新增 5 tests)
测试统计
src/modules/quiz/quiz.service.spec.ts:18 tests ✅src/modules/quiz/quiz.controller.spec.ts:7 tests ✅src/modules/ai-runtime/user-ai.service.spec.ts:+5 tests(新增 describe 块)✅修改文件(本轮新增)
src/modules/quiz/quiz.service.ts(N1 修复)src/modules/ai-runtime/user-ai.service.ts(B1 修复)src/modules/quiz/quiz.service.spec.ts(新建)src/modules/quiz/quiz.controller.spec.ts(新建)src/modules/ai-runtime/user-ai.service.spec.ts(新增 getQuizQuestions 测试套件)遗留(非阻塞)
getAttemptQuestions与getQuizQuestions功能重叠,建议在后续里程碑中统一为单一入口N2/N3 修复:统一跨用户信息泄漏模式
修改
getAttemptQuestions:userId进入 Prisma 查询条件,删除独立ForbiddenException,跨用户统一 404findOne:同上,where: { id, userId }start:同上,where: { id: quizId, userId }ForbiddenExceptionimport最终一致性
所有查询方法统一为
findUnique({ where: { ..., userId } })模式,跨用户访问不再暴露 403 vs 404 差异:findOne{ id, userId }start{ id: quizId, userId }getAttemptQuestions{ id: attemptId, userId }getResults{ id: attemptId, userId }submit{ id: attemptId }+ 内联 userId 检查测试更新
ForbiddenException→NotFoundExceptionwhere条件中userId的断言验证