本文基于「码上面试平台」当前代码整理,重点是区分整场会话的生命周期和题目流程的游标状态。
一场面试,为什么会有两套状态?
阅读这条状态链路时,很容易把两个问题混在一起:题目 flow 已经COMPLETED,是否意味着会话记录也已FINISHED?或者会话可以恢复,是否意味着 Redis 里一定有当前题游标?
这类现象不一定意味着某个枚举写错了。先要确认代码里管理的是不是同一个对象。
项目里至少有两套状态:
| 状态对象 | 表达什么 | 主要字段 |
|---|---|---|
InterviewSessionStatus | 整场面试的生命周期 | 会话是否在上传简历、可开始、进行中、结束或被放弃 |
InterviewFlowState | 当前题目流程走到哪里 | 当前题号、题目索引、追问次数、流程阶段 |
会话主记录保存在 MongoDB 的interview_session集合。题目 flow 是面试运行态,通过缓存服务保存在 Redis。它们的存储位置和更新时机都不同。
会话状态回答“这场面试还能不能继续”
会话状态目前包含:
DRAFT -> RESUME_UPLOADING -> READY -> IN_PROGRESS -> FINISHED \______________________________________________-> ABANDONED代码并不把它当作可任意改写的显示标签。InterviewSessionServiceImpl创建会话时设为DRAFT;上传简历前变为RESUME_UPLOADING;成功提题后写成READY;首次取题或答题后,READY才转为IN_PROGRESS;正式收尾会设置FINISHED和结束时间。
canResume()当前只把READY和IN_PROGRESS视为可继续状态。DRAFT、RESUME_UPLOADING属于活动状态的一部分,但不代表用户可以直接进入答题。创建新会话时,服务还会先把该用户已有的活动会话标记为ABANDONED。这是一项产品行为,意味着“再开一场”会影响之前尚未结束的会话,前端交互和恢复策略要和它保持一致。
Flow 状态回答“当前题应该发生什么”
题目 flow 有自己的状态:
INIT -> ASKING | COMPLETED ASKING -> EVALUATING | FOLLOW_UP | COMPLETED EVALUATING -> ASKING | FOLLOW_UP | COMPLETED FOLLOW_UP -> EVALUATING | ASKING | COMPLETED COMPLETED -> (terminal)实际合法转换由InterviewFlowStateMachine的映射表约束。状态机负责检查转换是否合法,并委托题目缓存服务读写 Redis 状态、推进题号;当推进后的题号越界时,再将 flow 标记为COMPLETED。
InterviewFlowState除了 status,还保存currentIndex、currentQuestionNumber、totalQuestions和追问计数。追问题号可能是1-F1这样的形式,所以“下一题”不能简单理解成currentIndex + 1。追问仍然属于当前主问题的子流程。
两套状态在代码里何时更新?
简历提取成功后,InterviewSessionFacade把会话状态设为READY;提取服务则把题目放入缓存并初始化 flow。开始答题时,会话状态变成IN_PROGRESS,flow 仍然按当前题目的进度更新。
最后一道题答完后,答题流水线可以先把题目 flow 标记成COMPLETED并返回结束响应。正式收尾走另一条路径:InterviewRecordServiceImpl.saveInterviewRecordFromRedis在 finalize 锁下保存记录;如果会话还没结束,会把主记录改为FINISHED,然后再保存一次报告记录。
因此,看到 flow 是COMPLETED,只能说明题目流程走到了末尾;它本身不等于报告已经收口,也不等于 MongoDB 会话主记录已经更新为FINISHED。反过来,主记录状态也不能单独告诉我们 Redis flow 里当前题号和追问游标是什么。
阅读和排障时我会先确认四件事
- 先确认状态属于哪个对象。
session.status和flow.status的同名值也不能混为一谈。 - 再确认是谁写状态。会话服务写 Mongo 主记录,状态机委托缓存服务推进 Redis flow,报告服务负责正式 finalize。
- 恢复时不要只看一个缓存 key。Redis flow 缺失时,代码还有从快照、答题轮次和题目材料重建的路径。
- 关注非法状态如何处理。
InterviewFlowStatus.from遇到空值或未知字符串会回退到INIT。这提高了读取时的容错,但也可能掩盖存储数据异常;如果线上依赖这个回退,最好保证日志和恢复诊断能暴露原始值。
我的复盘:状态机应该描述事实,而不是页面文案
一开始我也容易把所有“进行中/已完成”收进一个 session status。读完调用链后,更清楚地看到这两个状态的职责不同:一个服务于会话管理和恢复入口,一个服务于题目推进和并发控制。
拆开之后,代码会多几处状态同步点。收益是每个状态能表达更准确的事实;代价是 finalize、恢复和异常补偿都要考虑两边是否暂时不一致。排障时与其问“为什么状态不对”,不如先问“哪个状态对象、在哪个步骤、由谁更新”。
源码入口
- 会话状态枚举:
admin/src/main/java/com/hewei/hzyjy/xunzhi/interview/service/model/InterviewSessionStatus.java - 会话状态更新:
admin/src/main/java/com/hewei/hzyjy/xunzhi/interview/service/impl/InterviewSessionServiceImpl.java - 题目流程状态机:
admin/src/main/java/com/hewei/hzyjy/xunzhi/interview/application/flow/InterviewFlowStateMachine.java - Redis flow 读写:
admin/src/main/java/com/hewei/hzyjy/xunzhi/interview/service/impl/InterviewQuestionCacheServiceImpl.java - 正式结束与记录收口:
admin/src/main/java/com/hewei/hzyjy/xunzhi/interview/flow/report/InterviewRecordServiceImpl.java
发布标签
JavaSpring Boot状态机RedisMongoDB源码分析