多步骤问卷最终提交时应使用DB::transaction包裹完整校验逻辑,先读全数据、再统一断言规则、最后原子写入,确保强一致性。

在多步骤问卷调查中,把所有字段校验放到事务闭包内一次性完成,核心是“先读全、再断言、后写入”,避免分步提交导致中间状态不一致。关键不在“多步骤”本身,而在最终落库前的原子性校验闭环。
一、用 DB::transaction 包裹完整校验逻辑
不要在每一步保存时单独 commit,而是等用户提交最终页时,统一开启事务,在闭包里完成全部字段读取、规则判断与数据写入。
- 从数据库查出该问卷实例当前所有已填字段(包括历史步骤数据),用
withTrashed()或显式 JOIN 确保覆盖软删除项 - 将问卷配置中定义的必填项、格式约束(如邮箱正则)、逻辑依赖(如“选了A则B必填”)全部加载为规则数组
- 遍历规则逐条校验:对每个字段,检查值是否存在、是否符合类型、是否满足跨字段条件;任一失败就 throw new ValidationException
- 全部通过后,调用
$survey->update([...])或批量插入 answer 行,不拆成多次 save()
二、校验阶段禁止任何写操作干扰快照
事务内校验必须基于同一时刻的数据快照,否则并发填写会导致校验依据失效。
- 使用
SELECT ... FOR UPDATE锁定主问卷记录(如 surveys 表的 id 行),防止他人同时修改同一份问卷 - 避免在闭包里调用其他会触发写入的模型方法(如 $user->increment('fill_count')),这些应放在事务外或作为最后一步
- 若需关联校验外部数据(如“所选课程是否仍在开课”),用
DB::table('courses')->where(...)->value('status')只读查询,并加锁必要时也锁对应 course 行
三、结构化错误反馈需保留原始字段路径
一次性校验失败时,不能只抛通用异常,要让前端清楚知道哪一步哪一题出错。
- 校验循环中收集错误信息为
['step_2.question_5' => '邮箱格式不正确']这类带层级的键名 - 捕获异常后,用
response()->json(['errors' => $errors], 422)返回,前端可直接映射到对应表单项 - 不要用 Laravel 默认的
$request->validate(),它面向单次请求,无法跨步骤聚合校验上下文
四、支持草稿态与终稿态分离设计
多步骤场景下,用户可能反复修改中间页,但只有最终提交才触发强一致性校验。
- 平时保存走普通 update,不进事务,也不校验全局规则(仅做基础格式过滤)
- 终稿提交接口单独路由(如 POST /surveys/{id}/submit),此端点强制启用事务闭包校验
- 可在 surveys 表加 status 字段(draft/submitting/submitted),事务内先置为 submitting,成功后再改 submitted,失败则回滚到 draft

















