测评平台(三):批量保存、草稿试答与可追溯发布
配置后台需要允许半成品存在。题目编辑、评分配置和报告模板通常分几次完成,保存过程还会遇到浏览器刷新和并发修改。预览要尽量接近正式作答,发布后又必须冻结内容。把这几个动作压进一个 status 字段,容易出现草稿进入统计或审核后内容仍被修改。
草稿状态和发布状态分开
草稿版本可以有 editing、ready_for_review、rejected;发布包只有 building、published、retired。试答引用草稿版本,正式任务只引用已发布的 AssessmentRelease。发布状态改变后,原发布包不再接受编辑。
图中的 Trial 不是发布包状态,它表示运行入口。试答可以使用草稿的 FormDSL,但评分和答案解析仍由服务端完成,避免页面自己解释规则。
批量保存的边界
90 道题的工作台不适合每次输入都提交整份 JSON。前端发送带路径的补丁:题目标识、字段路径、客户端序号和内容摘要。服务端校验路径属于当前草稿版本,再按版本号合并。两个窗口同时编辑时,后到的旧版本返回冲突,调用方选择重新加载或覆盖,不能静默丢失另一方修改。
批量保存接口返回服务端版本、已接受项和拒绝项。部分成功时,页面逐项显示错误,不能把整个请求显示为“保存失败”后让用户猜测哪些题已经写入。服务端保存时限制数组深度、字符串长度和单次补丁数量,防止一条请求耗尽解析资源。
草稿复制使用新版本 ID,复制源、操作人和时间进入审计。发布包不能通过改数据库指针“变成”另一个版本,所有关系用显式外键和状态校验保护。
预览和正式作答共用解析器
试答页面调用和正式作答相同的 FormDSL 校验、可见性计算和 FrozenFormAnswerResolver。NUMBER 类型从 numberValue 解析,不能从展示字符串 rawValue 评分。试答结果标记为临时,不写入正式参与者报告和统计。
预览数据使用合成参与者 ID,提交令牌只在开发或测试范围有效。服务端返回给浏览器的快照递归移除 score、scoreRangeJson、标准答案和其他评分字段,键名大小写变化也要过滤。清理逻辑不能只处理顶层对象,否则嵌套报告规则会泄漏。
发布是一个事务边界
发布请求先确认草稿版本仍是审核通过状态,再编译 FormDSL、检查引用、写入题目快照和报告配置,计算 checksum,末步插入 AssessmentRelease。事务提交前,运行入口看不到这条发布记录。发布完成后,运行入口只接受 release_id,不接受前端传来的任意版本号。
正式入口由 access_mode 决定:PUBLIC 允许匿名启动,TASK_BASED 要求任务身份,BOTH 同时保留两类入口。AssessmentSubmitToken 绑定任务、发布包、过期时间和可选用户 ID。旧格式令牌在兼容窗口内解析,但新提交仍需校验当前登录用户,不能把客户端传入的 userId 当作身份来源。
回滚的真实含义
回滚发布指向旧的不可变发布包,不能把已发布包的内容改回去。已开始的运行实例继续使用创建时的 release_id,新任务才切换到旧包。若评分器已经升级,回滚还要确认旧包所需的评分器版本和报告模板仍可用。
审核记录保存草稿版本、发布包 checksum、审核人、发布时间和差异摘要。发现错误时先停用新包,再决定是否回滚入口;已经产生的结果保留原版本,不通过覆盖数据制造“看起来一致”。
验证重点
前端用组件测试覆盖快速切换资源、批量保存部分失败、草稿试答和发布后复制。后端测试覆盖状态迁移、checksum、快照字段清理、令牌用户绑定和旧令牌兼容。真实项目的后端测评模块曾用 -DskipTests=false 跑过 431 个测试,前端测评相关测试 87 个通过;这些数字说明验证链路存在,不代表任意新增规则无需补测。
这套设计保证草稿可以安全试错,发布包可以复现,正式运行有明确身份边界。它不能替代审核人员对题目内容和评分口径的判断。