心理测评系统的“未完成答卷”如何处理?前端本地存储与后端定时保存的取舍

单次提交在重型量表中是一场用户体验灾难。结合IndexedDB与后端Redis异步刷盘,探讨高并发下断点续填的最佳前中后三级容灾架构。

在开发大型心理测评软件时,经常会遇到一份包含 500 多道题的重型量表(如 MMPI 明尼苏达多项人格测验)。受测者往往需要耗费一个多小时才能做完。在这个极其漫长的过程中,网络波动、手机突然没电、浏览器意外崩溃等突发状况防不胜防。

如果系统要求用户在所有题目做完后一次性提交,一旦发生意外,受测者将丢失所有进度,导致极其强烈的挫败感,甚至拒绝重新配合筛查。为了确保用户体验和数据的可靠回收,“断点续答”机制成为了重型心理 SaaS 的标配。

本文将深度探讨处理未完成答卷的两种核心架构方案:前端本地持久化与后端定时轮询持久化,并解析它们在实战中的取舍之道。

方案一:前端本地存储(LocalStorage / IndexedDB)

在追求极高并发承载力、希望尽可能减轻服务器压力的场景下,很多团队会首选将用户的答题进度缓存到浏览器本地。

实现机制与优势

利用 HTML5 的 localStorage,每当用户点击一个选项时,前端应用会立刻将 [QuestionID: OptionID] 序列化为 JSON 并存入本地。当用户下次刷新页面或重新打开链接时,前端代码优先检查 localStorage 中是否存在未提交的缓存记录,如果存在,则直接进行状态水合(Hydration),恢复到之前的答题位置。

这种方式的优势极其明显:

  1. 零后端开销:在最终提交前,后端服务器完全不会收到任何关于进度的碎片化请求,完美规避了写放大的问题。
  2. 极佳的弱网适应性:即便受测者处于没有信号的地下室,他依然可以继续点击答题,进度稳稳保存在本地,直到有网时再一键上报。

致命的缺陷:跨设备断层

正如橙星云技术团队在实战落地中指出的,纯前端存储的最大软肋是“无法跨设备漫游”。如果学生在机房用电脑做到一半下课了,随后掏出手机想继续做,本地缓存就彻底失效了。此外,一旦用户在微信内使用了“清理缓存”功能,或者在隐身模式下答题,本地数据也会瞬间灰飞烟灭。对于极其关键的高级别评估,这种数据丢失风险是难以容忍的。

方案二:后端定时长轮询与心跳保存

为了实现真正的跨设备多端同步,并将数据的命运牢牢掌控在系统手里,更为硬核的做法是依靠后端的持续状态同步。

防抖(Debounce)与批量提交策略

如果用户每做一题就向后端发起一次 POST 请求,在一个万人同时在线的大型普查中,服务器的 TPS(每秒事务处理量)会瞬间被几万次无意义的点击打穿。

成熟的架构必须在前端引入请求防抖与队列打包机制。
可以设立一个长度为 10 的队列,或者设定一个 15 秒的定时器心跳。用户在前端的高频点击,仅在内存队列中更新。当队列满或者心跳时间到达时,前端将其打包成一个轻量级的 Payload 发送给后端的专门接口(如 /api/v1/answers/draft)。

后端存储介质的选型:为什么不建议直接写 MySQL?

对于这种高频写入但数据价值密度相对较低的“草稿数据”,直接写入关系型数据库(如 MySQL)是极度消耗 IO 性能的。

更聪明的做法是引入 Redis 作为缓冲层。利用 Redis 的 Hash 结构,以 UserID:ScaleID 作为 Key,将题目进度快速更新入内存。当受测者最终点击“正式提交”时,后端才从 Redis 中拉取完整答案序列,执行计分逻辑,并将最终结果持久化到 MySQL。如果用户迟迟未提交,也可以通过定时任务,在半夜系统低峰期将 Redis 里的僵尸草稿异步持久化归档。

实战演进:前中后三级容灾架构

在医疗级或极度严苛的政务心理平台中,往往采用前端缓存加后端异步队列的“混合双打”模式:

  1. 第一层容灾:用户每点一题,极速写入 IndexedDB,提供毫无延迟的本地安全感。
  2. 第二层同步:每隔 30 秒,前端后台线程静默将差异化进度(Delta)推送至后端 Redis 集群。
  3. 第三层兜底:如果在推送过程中检测到彻底断网,前端立刻停止网络重试,进入纯离线模式,直至网络恢复后触发全量补偿同步。

总结

未完成答卷的处理逻辑,看似只是一个小小的用户体验优化,背后却隐藏着前端性能、弱网容灾、高并发写操作和数据库架构等多维度的技术博弈。

抛弃简陋的单次提交,建立一套高可用、抗压且多端同步的草稿守护引擎,是所有严肃 B 端软件开发者对用户数据最基本的敬畏之心。这种在细节深处死磕的极客精神,正是打造企业级软件不可或缺的核心底色。

Leave a Reply

Your email address will not be published. Required fields are marked *