在线考试系统这个领域,我从早期做单体架构的教务模块开始,到后来参与过几个中大型的分布式考试平台搭建,踩过的坑不算少。今天聊的是管理员服务这一块,很多人觉得管理员服务无非就是增删改查,没什么技术含量,但真正做过考试系统的人都知道,管理员服务是整个系统的控制中枢,它承载的不只是数据维护,更是考试公平性、数据安全性和业务灵活性的底层支撑。一个设计得好的管理员服务,能让后续的运维成本降低一半以上;设计得不好,考务人员天天找你救火。
这篇文章主要面向正在做或准备做在线考试系统的后端开发、产品经理和测试人员,尤其是那些需要从零搭建管理员服务模块的团队。我会从业务功能拆解、权限模型设计、核心操作流程、数据一致性保障、常见问题排查几个维度展开,把管理员服务里那些文档不会写、但实际开发中一定会遇到的细节讲清楚。全文基于常见的在线考试系统实践来展开,不同项目的具体实现可能有差异,但核心思路是通用的。
1. 管理员服务的定位与整体设计思路
1.1 为什么管理员服务不是简单的CRUD
很多人拿到需求文档,看到管理员服务里写着“用户管理、考试管理、题库管理、成绩管理”,第一反应就是建几张表,写几套RESTful接口,前端调一下就完事了。这种思路在小型项目里勉强能用,但一旦考试规模上去,问题就会集中爆发。
管理员服务的本质是什么?它是整个考试系统的配置中心和调度中心。一场考试能不能顺利开考,取决于管理员服务里的考试配置是否正确、考生分配是否到位、试卷策略是否生效、时间窗口是否准确。它不像考生端那样直接面对高并发答题请求,但它决定了考生端能不能正常工作。
我经历过一个典型场景:某次模拟考试,考务人员在管理员后台修改了考试结束时间,但考生端缓存没有及时刷新,导致部分考生提前被强制交卷。这个问题表面上是缓存问题,根子上是管理员服务的配置变更没有走统一的发布流程。所以管理员服务的设计,不能只考虑“能不能改”,还要考虑“改了之后怎么让所有依赖方感知到”。
从架构层面看,管理员服务通常需要承担以下几类职责:
- 基础数据维护:用户、角色、部门、科目、知识点等基础信息的增删改查。
- 考试生命周期管理:考试创建、发布、开始、结束、归档的全流程状态流转。
- 题库与试卷管理:题目录入、审核、组卷策略配置、试卷生成与预览。
- 考务安排:考场分配、考生编排、监考人员指派、考试时间窗口设置。
- 成绩与报表:成绩查询、导出、统计分析、异常成绩处理。
- 系统配置:参数配置、通知模板、日志审计、数据字典维护。
这些职责之间不是孤立的,它们通过考试这个核心实体串联起来。设计的时候如果只按功能模块切分,很容易出现数据不一致的问题。比如考试已经发布了,但题库里某道题被删除了,考生答题时就会遇到题目加载失败。所以管理员服务的设计,必须把状态流转和数据依赖放在第一位考虑。
1.2 权限模型选型:RBAC还是更细粒度的方案
管理员服务的权限设计是绕不开的话题。最常见的方案是RBAC(基于角色的访问控制),也就是用户关联角色,角色关联权限,权限对应具体的操作或资源。这个模型足够简单,也足够应对大多数场景。
但在考试系统里,RBAC有时候不够用。举个例子:某位老师只能管理自己创建的考试,不能修改其他老师创建的考试。这是典型的数据级权限需求,单纯的RBAC只能控制“能不能访问考试管理接口”,没法控制“能访问哪些考试的数据”。
常见的做法是在RBAC基础上叠加数据权限规则。具体实现方式有几种:
| 方案 | 实现方式 | 适用场景 | 优缺点 |
|---|---|---|---|
| 角色+数据范围 | 角色上绑定数据范围(全部/本部门/本人) | 组织结构清晰的场景 | 实现简单,但灵活性一般 |
| 角色+资源标签 | 资源打标签,角色绑定标签 | 资源分类明确的场景 | 灵活度高,但标签维护成本大 |
| 角色+自定义规则 | 通过规则引擎动态计算 | 复杂权限场景 | 最灵活,但开发和维护成本高 |
| 角色+创建者校验 | 接口层校验资源创建者 | 简单的归属控制 | 实现最快,但扩展性差 |
我的建议是:如果项目初期,用“角色+数据范围”就够了,把数据范围定义清楚,比如全部、本部门、本人三级。等到业务复杂到一定程度,再考虑引入更细粒度的方案。不要一上来就搞规则引擎,那是给自己找麻烦。
还有一个容易被忽略的点:权限的继承与覆盖。比如一个用户同时拥有“出题老师”和“考务管理员”两个角色,出题老师只能看自己的题,考务管理员能看所有题,那最终这个用户能看到什么?通常的做法是取并集,也就是权限叠加。但有些系统会设计成“拒绝优先”,也就是只要有一个角色拒绝,就拒绝。这两种策略没有绝对的好坏,关键是要在系统里统一,不能有的地方取并集,有的地方拒绝优先,否则后患无穷。
1.3 状态机设计:考试生命周期的核心
考试的状态流转是管理员服务里最容易出bug的地方。我见过不少系统,考试状态用几个字段拼凑,比如is_published、is_started、is_ended,然后靠代码逻辑去判断当前到底处于什么状态。这种设计在状态少的时候还能应付,一旦状态多起来,组合爆炸,维护成本极高。
正确的做法是引入状态机。把考试的生命周期定义成有限个状态,每个状态之间的流转有明确的触发条件和副作用。一个典型的考试状态机大概是这样:
- 草稿:考试刚创建,信息还不完整,可以随意修改。
- 待发布:考试信息已完善,等待发布。
- 已发布:考试对考生可见,但还没到开始时间。
- 进行中:考试已开始,考生可以答题。
- 已结束:考试时间到,考生不能再答题。
- 阅卷中:主观题需要人工阅卷。
- 已归档:成绩已发布,考试数据只读。
每个状态流转都要考虑几个问题:谁有权限触发这个流转?流转时需要做哪些校验?流转后需要通知哪些下游系统?比如“已发布”到“进行中”的流转,通常是由定时任务触发的,触发时要检查当前时间是否到了考试开始时间,同时要通知考生端刷新考试列表。
注意:状态机的流转一定要做成幂等的。也就是说,同一个流转请求重复执行多次,结果应该是一样的。这在分布式环境下尤其重要,因为定时任务可能重复触发,消息可能重复消费。
状态机的实现方式有很多种,简单的可以用枚举加switch-case,复杂的可以用Spring StateMachine或者自己写一个轻量级的状态机引擎。我的经验是,如果状态不超过10个,流转规则不超过30条,自己写一个配置化的状态机就够了,没必要引入重型框架。关键是要把状态定义、流转规则、副作用处理这三部分分离清楚,方便后续扩展。
2. 核心业务功能拆解与实操要点
2.1 用户与角色管理:别小看这张表
用户管理看起来简单,但考试系统里的用户管理和普通后台管理系统不太一样。考试系统通常涉及几类用户:系统管理员、考务管理员、出题老师、阅卷老师、考生、监考人员。每类用户的属性字段和业务规则都不一样。
考生有学号、班级、专业等属性,老师有工号、所属院系、可授课程等属性。如果把这些都塞到一张user表里,字段会膨胀得很快,而且大量字段对某些用户类型是空的。常见的做法是主表+扩展表的设计:user表存公共字段(ID、用户名、密码、手机号、状态),考生扩展表存考生特有字段,教师扩展表存教师特有字段。
这种设计的好处是字段清晰,查询时按需join。坏处是新增用户类型时要加表,但考试系统的用户类型相对稳定,这个代价可以接受。
用户管理里还有一个高频操作:批量导入。考务人员通常会在开学时把全校学生信息批量导入系统。这个功能看起来简单,但实际做起来坑很多:
- 数据格式校验:Excel模板里的字段类型、长度、必填项都要校验,而且要给出行号级的错误提示,不能只说“导入失败”。
- 重复数据处理:学号已存在的怎么处理?是跳过、更新还是报错?通常要提供选项让用户选择。
- 事务控制:批量导入要么全成功,要么全失败,不能导入一半留一半脏数据。但数据量大时,长事务又会锁表,需要分批提交加补偿机制。
- 异步处理:几千条数据同步导入可能超时,通常要走异步任务,导入完成后通知用户。
我踩过的一个坑是:导入时没有做字符集校验,结果有些学生的姓名里有生僻字,导入后变成乱码。后来在导入前加了字符集检测和转换,才解决这个问题。所以批量导入这种功能,一定要在测试环境用真实数据跑几遍,别用几条干净数据测完就上线。
角色管理方面,除了前面说的权限模型,还要注意角色的互斥与约束。比如“出题老师”和“阅卷老师”可以是同一个人,但“考生”和“监考人员”通常不能是同一个角色。这种互斥规则要在角色分配时校验,避免出现逻辑冲突。
2.2 题库管理:题目录入的效率和准确性
题库是考试系统的核心资产。题库管理模块的设计,直接影响到出题老师的工作效率。我见过一些系统,录入一道题要填十几个字段,点五六次保存,老师怨声载道。好的题库管理,应该在保证数据完整性的前提下,尽可能减少操作步骤。
题目通常分为几种类型:单选题、多选题、判断题、填空题、简答题、编程题。每种类型的字段要求不同,但有一些公共字段是必须的:
- 题干:支持富文本,可能包含图片、公式、代码块。
- 选项:选择题特有,每个选项有内容和是否正确。
- 答案:标准答案,主观题可能是参考答案。
- 解析:题目解析,用于考后复盘。
- 难度:通常分易、中、难三级。
- 知识点:关联到知识点树,用于组卷和统计分析。
- 分值:默认分值,组卷时可覆盖。
题目录入的难点在于富文本和特殊内容的处理。数学公式、化学方程式、代码片段,这些内容用普通的文本编辑器很难处理好。常见的方案是集成数学公式编辑器(如MathJax或KaTeX)和代码高亮编辑器(如CodeMirror)。但集成之后,存储和渲染又成了问题:公式存LaTeX还是存图片?代码存纯文本还是存HTML?
我的建议是:公式存LaTeX源码,渲染时前端用KaTeX或MathJax解析;代码存纯文本,渲染时用代码高亮库处理。这样存储的是结构化数据,后续修改和迁移都方便。如果存图片,题目迁移时图片路径容易失效;如果存HTML,不同端的渲染效果可能不一致。
题目录入还有一个高频需求:批量导入。很多老师习惯在Word里出题,然后导入系统。这就需要一个Word解析工具,把Word里的题目结构提取出来。这个功能的难点在于Word格式的多样性,不同老师的排版习惯不一样,解析规则很难覆盖所有情况。实际项目中,通常是提供一个标准模板,要求老师按模板排版,然后按模板解析。虽然限制了灵活性,但保证了成功率。
提示:题库管理一定要有审核流程。出题老师录入的题目,应该经过审核才能进入正式题库。审核不通过的题目要能退回并附上修改意见。这个流程在考试系统里非常重要,因为题目的质量直接关系到考试的公平性。
2.3 试卷与组卷策略:固定试卷还是随机组卷
组卷是管理员服务里逻辑最复杂的模块之一。组卷方式通常分为两种:固定试卷和随机组卷。
固定试卷就是手动选题,每道题都指定好,考生拿到的试卷完全一样。这种方式适合题目数量少、需要精确控制试卷内容的场景。优点是可控性强,缺点是出题工作量大,而且试卷容易泄露。
随机组卷则是设定组卷规则,系统从题库里按规则抽题。规则通常包括:知识点分布、题型分布、难度分布、每题分值、总分等。比如“从第一章抽5道单选题,每题2分,难度中等;从第二章抽3道多选题,每题3分,难度不限”。系统根据这些规则,从符合条件的题目中随机抽取。
随机组卷的核心是抽题算法。最简单的做法是按条件查询题目,然后随机排序取前N条。但这种方式有个问题:如果符合条件的题目刚好只有N条,那每次抽出来的试卷都一样,失去了随机性。更好的做法是给每道题维护一个使用次数,抽题时优先抽取使用次数少的题目,这样能保证题目轮换。
还有一种更复杂的组卷方式:多套试卷随机分发。系统生成多套难度相当的试卷,考生登录时随机分配一套。这种方式既保证了考试公平性,又降低了试卷泄露的风险。实现上,可以先用组卷算法生成N套试卷,存入试卷池,考生开考时从池中分配。
组卷策略的配置界面也很关键。我见过一些系统,组卷规则用JSON配置,考务人员根本看不懂。好的做法是提供可视化的规则配置界面,用表单和拖拽的方式让用户配置规则,后台再转换成JSON。这样考务人员能自己调整组卷策略,不需要开发介入。
| 组卷方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 固定试卷 | 小规模考试、精确控制 | 可控性强 | 工作量大、易泄露 |
| 随机组卷 | 大规模考试、题库充足 | 省时省力、降低泄露风险 | 难度控制复杂 |
| 多套随机 | 高保密要求考试 | 公平性好、安全性高 | 实现复杂、需要试卷池 |
2.4 考务安排:考场、考生与时间的三角关系
考务安排是管理员服务里最贴近实际业务的部分。一场考试要顺利进行,需要把考场、考生、时间三个要素安排好。
考场管理包括考场的创建、容量设置、设备信息维护。一个考场通常有固定的座位数,安排考生时不能超过容量。有些系统还支持考场分组,比如按楼层或区域分组,方便管理。
考生编排是把考生分配到具体的考场和座位。编排方式有几种:
- 手动编排:考务人员逐个分配,适合小规模考试。
- 自动编排:按规则自动分配,比如按班级、按学号顺序、随机分配。
- 混合编排:先自动分配,再手动调整。
自动编排的算法需要考虑几个约束:考场容量不能超、同一班级的考生尽量分散(防止作弊)、特殊考生(如残疾考生)需要特殊安排。这些约束用简单的贪心算法就能解决,不需要复杂的优化算法。
时间管理是考务安排里最容易出问题的部分。考试时间包括:报名时间、准考证打印时间、考试开始时间、考试结束时间、迟到截止时间、提前交卷时间。这些时间点之间的关系要严格校验,比如考试结束时间必须晚于开始时间,迟到截止时间不能晚于考试结束时间。
注意:时间管理一定要考虑时区问题。如果系统只在一个时区使用,可以忽略;但如果涉及跨时区考试,所有时间都要统一存储为UTC,展示时再转换成当地时间。我见过一个系统因为没处理时区,导致海外考生看到的考试时间差了8小时,差点错过考试。
还有一个细节:考试时间的延长。特殊情况下,考务人员可能需要给某些考生延长考试时间。这个功能要在考生端和管理员端同时支持,而且延长的时间要记录在案,方便审计。
3. 实操过程与核心环节实现
3.1 考试创建与发布的完整流程
考试创建看起来就是填个表单,但实际涉及的步骤和校验很多。我把一个典型的考试创建流程拆解一下:
第一步:基本信息填写。包括考试名称、考试描述、考试类型(正式考试/模拟考试/补考)、关联的课程或科目。这一步的校验主要是必填项和格式校验。
第二步:试卷配置。选择组卷方式,配置组卷规则或手动选题。如果是随机组卷,要预览组卷结果,确认题目分布符合预期。这一步的校验包括:题目总分是否等于试卷总分、题型分布是否合理、知识点覆盖是否全面。
第三步:考试时间设置。设置考试的开始时间、结束时间、答题时长、迟到截止时间、提前交卷时间。校验规则包括:结束时间晚于开始时间、答题时长不超过考试时间窗口、迟到截止时间在开始时间之后。
第四步:考生分配。选择参加考试的考生范围,可以按班级、按部门、按标签筛选,也可以手动指定。分配完成后要预览考生名单,确认人数和名单正确。
第五步:考场安排。如果是在线考试,这一步可能不需要;如果是线下机房考试,需要分配考场和座位。自动编排后要检查是否有考场超容或考生未分配的情况。
第六步:发布考试。发布前要做一次全面校验,确认所有必填项都已填写、所有配置都合法。发布后考试状态变为“已发布”,考生端可以看到考试信息。
这个流程里,发布前的校验是最关键的。我建议把校验规则做成配置化的,每条规则对应一个校验器,发布时依次执行所有校验器,收集所有错误后一次性返回。这样考务人员能一次看到所有问题,而不是改一个报一个。
# 校验器示例(伪代码) class ExamValidator: def validate(self, exam): errors = [] errors.extend(self.check_basic_info(exam)) errors.extend(self.check_paper_config(exam)) errors.extend(self.check_time_settings(exam)) errors.extend(self.check_candidate_assignment(exam)) return errors发布操作本身要幂等。考务人员可能因为网络问题重复点击发布按钮,系统要保证重复发布不会产生副作用。通常的做法是给发布操作加一个状态判断:只有“待发布”状态的考试才能发布,已发布的考试再次发布直接返回成功。
3.2 成绩管理与数据导出
成绩管理是考试结束后考务人员最常用的功能。成绩数据通常包括:考生信息、试卷信息、各题得分、总分、排名、交卷时间、答题时长。
成绩查询的难点在于查询条件的灵活性。考务人员可能按考试查、按班级查、按考生查、按分数段查,还可能组合多个条件。如果每个条件都写一个查询接口,接口数量会爆炸。常见的做法是提供一个统一的查询接口,用查询对象(Query Object)封装所有条件,后台动态拼接SQL。
但动态拼接SQL有SQL注入的风险,所以一定要用参数化查询,不能直接拼接字符串。另外,查询条件多的时候,索引的设计很关键。通常需要在考试ID、考生ID、班级ID这些高频查询字段上建索引。
成绩导出是另一个高频需求。考务人员通常需要把成绩导出成Excel,用于存档或上报。导出功能有几个注意点:
- 数据量大时用异步导出:几万条数据同步导出会超时,通常要生成异步任务,导出完成后提供下载链接。
- 导出格式要规范:表头、列宽、单元格格式都要设置好,方便考务人员直接使用。
- 敏感信息脱敏:如果导出文件会外发,考生手机号、身份证号等敏感信息要脱敏处理。
我踩过的一个坑是:导出时没有限制单次导出的数据量,结果有考务人员导出了全量数据,直接把服务器内存打满了。后来加了单次导出上限,超过上限提示分批导出,才解决这个问题。
成绩管理还有一个重要功能:成绩修改。有时候因为题目有问题或系统故障,需要给考生补分或改分。这个操作一定要有审计日志,记录谁在什么时候改了谁的成绩,改前改后分别是多少。而且成绩修改通常需要审批流程,不能由一个人说了算。
3.3 系统配置与数据字典
系统配置模块看起来不起眼,但它是管理员服务的“控制面板”。很多业务规则都通过配置项来控制,比如:是否允许考生提前交卷、是否显示题目解析、是否开启防作弊检测、成绩发布后是否允许修改。
配置项的管理要注意几点:
- 配置分类:按模块分类,比如考试配置、安全配置、通知配置,方便查找。
- 配置类型:字符串、数字、布尔、JSON,不同类型的配置项渲染不同的编辑控件。
- 配置缓存:配置项读取频繁,通常要加缓存,但修改后要及时刷新缓存。
- 配置审计:谁改了配置、改了什么、什么时候改的,都要记录。
数据字典是另一个基础模块,用于维护枚举值,比如考试类型、题目类型、用户状态。数据字典的好处是,新增枚举值时不需要改代码,只需要在字典里加一条记录。但要注意,字典值一旦被业务数据引用,就不能随意删除,只能禁用。
提示:系统配置和数据字典的修改,一定要有二次确认。我见过有人误删了一个字典项,导致关联的业务数据全部显示异常。后来加了删除前的引用检查,才避免了类似问题。
4. 常见问题与排查技巧实录
4.1 数据一致性问题的排查思路
管理员服务里最常见的问题就是数据不一致。比如考试状态显示“进行中”,但考生端显示“已结束”;或者考生名单里有人,但考场安排里没有。这类问题的排查,我通常按以下思路走:
第一步:确认数据源。先搞清楚这个数据是从哪个表、哪个缓存、哪个接口来的。管理员服务和考生端可能读的是不同的数据源,比如管理员读主库,考生端读从库,主从延迟就会导致不一致。
第二步:检查状态流转。如果涉及状态,先看状态机的流转日志,确认状态是什么时候变的、由谁触发的、有没有异常。状态流转日志是排查这类问题的关键,所以系统设计时一定要记录。
第三步:检查缓存。如果数据源没问题,那很可能是缓存没刷新。检查缓存的过期时间和刷新策略,确认修改数据后有没有主动清除缓存。
第四步:检查消息通知。如果涉及下游系统,检查消息有没有正常发送和消费。消息队列的堆积、消费失败、重复消费都可能导致数据不一致。
下面是一个常见数据一致性问题的速查表:
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 考试状态不一致 | 缓存未刷新 | 对比缓存和数据库 | 修改后主动清缓存 |
| 考生名单缺失 | 主从延迟 | 检查主从同步状态 | 关键读走主库 |
| 成绩数据错误 | 并发写冲突 | 检查更新日志 | 加乐观锁或分布式锁 |
| 配置不生效 | 配置缓存过期 | 检查配置缓存 | 缩短缓存时间或主动刷新 |
| 消息重复消费 | 消费幂等未做 | 检查消费日志 | 加幂等校验 |
4.2 权限问题的典型表现与修复
权限问题在管理员服务里也很常见,典型表现有几种:
- 越权访问:低权限用户能访问高权限接口。这通常是接口层没有做权限校验,只在前端隐藏了按钮。修复方法是所有接口都要做权限校验,不能依赖前端。
- 数据越权:用户能访问不属于自己的数据。这通常是数据权限没做,或者做错了。修复方法是在查询时强制加上数据范围条件。
- 权限缓存不一致:用户权限变了,但缓存没刷新,导致权限判断错误。修复方法是权限变更后主动清除相关用户的权限缓存。
我踩过的一个坑是:权限校验用了注解,但有些接口忘了加注解,导致这些接口实际上没有权限控制。后来加了一个全局的权限校验拦截器,所有接口默认需要权限,只有显式标注为公开的接口才放行。这样虽然麻烦一点,但安全得多。
注意:权限校验一定要在服务端做,前端的权限控制只是提升用户体验,不能作为安全屏障。我见过有系统只在前端做了权限控制,结果有人直接调接口就绕过了。
4.3 性能问题的定位与优化
管理员服务虽然并发不高,但有些操作很重,比如批量导入、成绩导出、组卷。这些操作如果没优化好,很容易拖垮整个系统。
批量导入的优化:用批量插入代替逐条插入,用异步任务代替同步处理,用分批提交代替大事务。我实测过,一万条数据用逐条插入要几分钟,用批量插入只要几秒。
成绩导出的优化:用流式查询代替一次性加载,用SXSSF代替XSSF(POI的流式API),用异步导出代替同步导出。如果数据量特别大,还可以考虑用数据库的导出工具直接生成文件。
组卷的优化:组卷算法的时间复杂度主要取决于题库大小和抽题条件。如果题库很大,查询条件很复杂,可以考虑预生成试卷池,考试时直接分配,而不是实时组卷。
还有一个通用的优化点:接口的响应时间。管理员服务的接口通常不要求毫秒级响应,但也不能太慢。我一般要求列表查询在500ms以内,详情查询在200ms以内,复杂操作在3秒以内。超过这个标准就要排查慢查询、检查索引、优化SQL。
4.4 操作审计与日志管理
管理员服务的所有写操作都应该记录审计日志。审计日志的内容包括:操作人、操作时间、操作类型、操作对象、操作前后的数据快照、操作结果。
审计日志的作用有几个:一是排查问题时可以追溯操作历史;二是满足合规要求;三是发现异常操作。比如某个账号在凌晨批量删除了大量题目,审计日志就能及时发现。
审计日志的存储要注意几点:
- 异步写入:审计日志的写入不能影响主业务,通常用异步方式。
- 独立存储:审计日志量大,通常存到独立的表或独立的存储系统。
- 定期归档:审计日志保留一定时间后要归档,避免占用过多空间。
- 防篡改:审计日志一旦写入就不能修改,通常只允许追加。
我见过一个系统,审计日志和业务数据存在同一个库,结果业务数据量大了之后,审计日志的查询变得非常慢。后来把审计日志拆到独立的库,才解决了这个问题。所以审计日志的存储方案,一开始就要规划好。
5. 管理员服务与其他模块的协作边界
5.1 与考生端的接口约定
管理员服务和考生端之间的接口,核心是考试信息的同步。管理员发布考试后,考生端要能及时看到;管理员修改考试配置后,考生端要能感知到变化。
常见的同步方式有两种:推和拉。推的方式是管理员服务主动通知考生端,比如通过消息队列发送考试变更事件。拉的方式是考生端定期查询考试列表,或者考生登录时查询。
实际项目中,通常是推拉结合:考生登录时拉取一次考试列表,考试有变更时通过消息推送通知考生端刷新。这样既保证了实时性,又避免了频繁推送。
接口约定里要特别注意字段的兼容性。管理员服务新增一个字段,考生端可能不认识,但不能因此报错。通常的做法是考生端忽略不认识的字段,管理员服务新增字段时保证向后兼容。
还有一个容易忽略的点:考试信息的缓存时间。考生端缓存考试信息可以提升性能,但缓存时间太长会导致信息更新不及时。我一般建议缓存时间不超过5分钟,关键操作(如开始考试)时强制刷新。
5.2 与阅卷服务的协作
阅卷服务负责主观题的批改。管理员服务和阅卷服务的协作主要在几个环节:
- 考试结束后:管理员服务通知阅卷服务,某场考试已结束,可以开始阅卷。
- 阅卷任务分配:管理员服务把需要阅卷的题目和考生答案推送给阅卷服务,阅卷服务分配给阅卷老师。
- 阅卷进度查询:管理员服务可以查询阅卷进度,用于成绩发布的决策。
- 成绩回写:阅卷完成后,阅卷服务把主观题得分回写给管理员服务。
这个协作过程中,数据的一致性是关键。阅卷服务回写成绩时,要保证成绩数据的准确性和完整性。通常的做法是阅卷服务回写时带上阅卷任务ID,管理员服务根据任务ID做幂等校验,避免重复回写。
5.3 与统计分析模块的数据对接
统计分析模块需要从管理员服务获取基础数据,用于生成各种报表。对接方式通常有两种:实时查询和数据同步。
实时查询是统计分析模块直接查管理员服务的数据库或接口。这种方式实现简单,但会给管理员服务带来查询压力,而且复杂的统计查询可能很慢。
数据同步是管理员服务把数据同步到统计分析模块的数据库,统计分析模块查自己的库。这种方式性能好,但数据有延迟,而且需要维护同步逻辑。
我的建议是:如果统计需求简单、数据量不大,用实时查询;如果统计需求复杂、数据量大,用数据同步。同步方式可以用定时任务,也可以用CDC(变更数据捕获)工具。定时任务实现简单但实时性差,CDC实时性好但维护成本高。
6. 一些实操心得与避坑建议
6.1 关于接口设计的经验
管理员服务的接口设计,我总结了几条经验:
接口要按业务场景设计,不要按表设计。比如“创建考试”是一个业务场景,可能涉及多张表的写入,那就提供一个接口,而不是让前端分别调多个接口。这样能保证业务逻辑的内聚性,也方便做事务控制。
接口的入参和出参要稳定。一旦发布,就不要轻易改字段名或字段类型。如果必须改,要提供版本兼容。我见过一个系统,接口出参改了一个字段名,导致前端全部报错,排查了半天才发现。
批量操作要提供异步接口。批量导入、批量删除、批量修改,这些操作如果数据量大,同步接口很容易超时。提供一个异步接口,返回任务ID,前端轮询任务状态,体验更好。
接口要有幂等设计。创建、更新、删除操作都要考虑幂等。创建操作可以用唯一键约束,更新和删除可以用版本号或状态判断。
6.2 关于数据库设计的经验
管理员服务的数据库设计,有几个点要特别注意:
软删除还是硬删除。考试系统的数据通常不能硬删除,因为涉及成绩和审计。常见的做法是软删除,用is_deleted字段标记。但软删除会导致唯一索引失效,比如用户名软删除后,新用户还能用同样的用户名。解决办法是把唯一索引改成“唯一索引+is_deleted”的组合索引,或者软删除时把唯一字段改名。
大字段的存储。题干、解析这些字段可能很大,如果和其他字段存在同一张表,会影响查询性能。常见的做法是把大字段拆到扩展表,主表只存ID和常用字段。
历史数据的归档。考试系统的数据会逐年累积,几年后数据量会很大。要提前规划归档策略,比如把两年前的考试数据归档到历史库,主库只保留近期数据。
6.3 关于测试的经验
管理员服务的测试,不能只测正常流程,异常流程和边界条件更重要。我通常会重点测这几类:
- 权限测试:不同角色的用户访问同一接口,结果是否符合预期。
- 状态测试:考试在不同状态下执行同一操作,结果是否符合预期。
- 并发测试:多个用户同时操作同一数据,是否会出现脏写或死锁。
- 数据量测试:大数据量下的查询、导出、导入性能是否达标。
- 异常测试:网络中断、服务重启、数据库连接失败等异常情况下,系统是否能正确处理。
我踩过的一个坑是:测试时用的都是干净数据,上线后遇到脏数据就出问题了。比如某个考试的考生名单里有重复的考生ID,导致编排考场时出现异常。后来在关键操作前都加了数据清洗和校验,才避免了类似问题。
6.4 关于上线的经验
管理员服务上线时,有几个点要特别注意:
数据初始化。系统上线前要初始化基础数据,比如默认角色、默认权限、数据字典、系统配置。这些数据要提前准备好,上线时直接导入。
灰度发布。如果是对已有系统的升级,最好先灰度发布,让部分用户试用,确认没问题后再全量。灰度期间要密切监控日志和错误率。
回滚方案。上线前要准备好回滚方案,包括数据库回滚、代码回滚、配置回滚。一旦出现问题,能快速回滚到上一个稳定版本。
监控告警。上线后要配置监控告警,关注接口响应时间、错误率、数据库连接数、缓存命中率等指标。一旦异常,及时处理。
我个人在实际操作中的体会是,管理员服务的复杂度不在于技术,而在于业务。技术方案可以选型,可以替换,但业务逻辑的梳理和沉淀需要时间。一个好的管理员服务,一定是业务驱动设计的,而不是技术驱动设计的。先把业务流程理清楚,把状态流转定义好,把权限模型设计对,剩下的就是按部就班的实现。反过来,如果业务没理清就急着写代码,后面返工的成本会非常高。
最后再分享一个小技巧:管理员服务的每个操作,都问自己三个问题——谁可以操作?操作的前置条件是什么?操作后会影响什么?把这三个问题回答清楚,大部分设计问题就迎刃而解了。