课堂签到和在线考试这类需求,几乎每个高校和培训机构都在做。但很多团队第一次做出来的系统,要么老师用起来繁琐,要么学生端体验割裂,要么一到考试高峰就崩。我这次用Java做后端、微信小程序做前端,把签到、在线测试、考试这三块整合在一起,从需求梳理到最终部署走了一遍完整的流程,过程中踩了不少坑,也沉淀了一些可以复用的设计思路。这篇文章就把整个系统的设计过程、关键实现和实战经验分享出来,给正在做同类项目的朋友一个参考。
先说结论:这套系统的核心价值,是把“课前签到”和“课中/课后考试”这两个高频教学场景统一到一个微信生态闭环里。学生不用额外装App,打开微信扫码或点小程序就能完成签到和答题;教师端通过后台可以管理课程、发起签到、配置试卷、查看成绩。对开发者来说,技术上没有特别高深的东西,难的是把各个环节的边界划分清楚,尤其是签到防作弊、考试断网恢复、并发扣减这类细节,稍不注意就会在真实课堂上翻车。
1. 课堂签到与在线考试,为什么要用微信小程序来做
1.1 教学场景里的真实痛点
传统课堂点名,五十人的班级至少要花五到十分钟,遇到合班课直接翻倍。而纸质考试从出题、印卷、监考、收卷到批改,整个周期太长,老师出一次单元测验的工作量极大。还有一些实训类课程,需要频繁进行随堂测验来验证学生的掌握程度,纸质方式完全跟不上节奏。
市面上其实已经有不少在线考试系统,但普遍存在三个问题。第一,学习成本高,系统和学校现有的教学管理流程割裂,老师需要重新维护一套学生账号体系;第二,体验不够轻,很多系统需要学生下载App或者通过浏览器访问,在课堂上临时使用很不方便;第三,考试防作弊能力弱,很多所谓在线考试系统仅仅是“在线答题”,切换应用、复制题目这些行为完全没有约束。
1.2 微信生态带来的天然优势
微信小程序能解决上面这些问题的核心原因,在于它吃透了“流量入口”和“身份识别”这两件事。
- 学生端零安装:微信本身就是学生日常使用频率最高的应用,小程序即扫即用。老师把签到码投到屏幕上,学生微信扫一扫就完成签到,整个动作不会超过十秒。
- 身份天然绑定:小程序调用微信登录后,后端通过code换openid,这个openid是微信用户的唯一标识,天然解决了“我是谁”的问题,不需要再让学生去注册账号。这点对Java后端来说实现成本很低,但价值极高。
- 订阅消息通知:小程序可以给用户发送课程签到提醒、考试结果通知等订阅消息,形成教学环节的闭环。
当然,微信小程序也有它的局限。比如代码包大小限制在2M以内(主包),复杂的图表展示和大规模的题库管理不适合全部放在小程序端。我的方案是:小程序只承担“签到”“答题”“查看成绩”这些高频轻量操作,完整的题库管理、学生管理、成绩分析都放在管理后台完成,通过HTTP接口与小程序交互。
text复制小程序端(学生/教师) ——HTTPS/JSON——> Java后端API ——> MySQL + Redis
|
管理后台(教师出题、查成绩)
这套架构下,小程序端越轻越好,重逻辑尽量后移,这是我在项目一开始就定下的原则。后面所有设计和实现,都围绕这个原则展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体技术选型与工程结构设计
2.1 后端选型:版本、框架与关键依赖
Java后端我选择了Spring Boot 2.7.x + MyBatis-Plus + MySQL 8.0 + Redis 6.x的组合。这套组合在校园项目和中小型生产环境中非常成熟,资料多、排错成本低。
- JDK版本:我用了JDK 1.8,不是不能用17,而是考虑到很多高校服务器上部署环境比较旧,1.8兼容性最稳妥。如果你的环境是全新的,直接用17也没问题,但要注意Spring Boot版本对JDK 17的支持情况(Spring Boot 2.7以上才支持JDK 17)。
- 认证方案:小程序端不需要维护Session,直接使用JWT(JSON Web Token)。登录时前端调用wx.login()拿到code,后端拿着code去微信接口换取openid,然后签发JWT返回给前端。后续所有请求带上token即可。
- 持久层:MyBatis-Plus的代码生成器可以在几分钟内生成基础的CRUD代码,省下的时间可以用来打磨核心业务逻辑。
- Redis的用途:存放签到码、试卷缓存、答题进度、接口限流计数等等。后面会详细说。
以下是pom.xml中核心依赖的片段,供参考:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2.2 小程序端选型:原生框架还是uni-app
这个小程序端我用了原生框架,也就是WXML + WXSS + JS + 微信开发者工具直接开发。原因有三点:一是原生框架性能最好,启动速度最快;二是微信的API更新是原生优先支持的;三是原生框架没有额外的编译层,出问题排查起来简单直接。
如果你是跨端需求,比如同时要出H5和App,那选择uni-app会更合适。但纯做微信小程序这个场景,原生框架是省心省事的选择。
原生小程序的项目结构大致如下:
text复制miniprogram/
├── pages/
│ ├── login/ // 登录页
│ ├── index/ // 首页(课程列表)
│ ├── sign/ // 签到页(扫码或输入码)
│ ├── exam/ // 考试页(答题主页面)
│ ├── result/ // 成绩查看页
│ └── mine/ // 个人中心
├── utils/
│ ├── request.js // 封装wx.request请求,统一携带token
│ ├── auth.js // 登录与token获取逻辑
│ └── util.js // 常用工具函数
└── app.js
2.3 接口设计规范
接口统一以/api/开头,遵循RESTful风格。返回统一格式,方便前端处理:
json复制{
"code": 200,
"message": "success",
"data": {}
}
所有接口都需要在拦截器中验证JWT,只有登录接口和微信回调接口是白名单。这个统一返回结构看起来简单,但在实际联调中能省很多事,前端不用为每个接口单独处理错误逻辑。
3. 签到模块的关键设计:从GPS定位到动态二维码
签到是课堂场景下使用频率最高、稳定性要求最高的功能。如果一个签到功能在课堂关键时刻失效,那这套系统的口碑就全完了。所以我在签到模块上花的心思最多。
3.1 签到方式的选择与取舍
市面上的签到方式大致有三种:GPS定位签到、二维码签到、人脸识别签到。人脸识别对硬件要求高,普通教室的前置摄像头角度不一定拍得到所有学生,我没有采用。GPS定位签到在室外还行,但室内定位漂移严重,容易误判,我也没有把它作为主要方式。最终选择了“动态二维码 + 辅助GPS校验”的方案。
核心逻辑是:教师端在管理后台或教师小程序中开启签到,后端生成一个含随机数的二维码,二维码中只包含签到活动ID和一个短时有效的签名串,学生扫一扫即可完成签到。关键设计如下表:
| 设计点 | 实现方式 | 原因 |
|---|---|---|
| 二维码有效期 | 默认30秒,可配置 | 防止学生截图转发给室友 |
| 二维码刷新机制 | 前端每20秒轮询一次获取新码 | 保证屏幕上的码始终是新的 |
| 签到时间窗口 | 教师可设定签到起止时间 | 避免课程结束后学生还能签到 |
| 签到次数限制 | 同一学生同一签到活动只允许一次 | 防止重复签到刷记录 |
| GPS辅助校验 | 签到时间时上报经纬度,后台比对教师设定的范围 | 辅助判断是否在教室内 |
3.2 动态二维码签名算法
这里有一个细节需要注意:二维码不是简单的随机字符串,而是要带签名,防止学生自己伪造签到活动ID。我的实现方式是:
- 教师开启签到时,后端生成signId(UUID),并写入Redis,过期时间30秒。
- 后端同时生成签名串:
md5(signId + secretKey + expireTime)。 - 二维码内容为:
{"signId":"xxx","expireTime":1699999999,"sign":"xxx"}。 - 小程序扫码后解析JSON,把signId和expireTime传给后端。
- 后端校验expireTime是否过期、sign是否合法、当前时间是否在签到时间窗内。
java复制public class SignCodeUtil {
private static final String SECRET_KEY = "your-sign-secret-key";
/**
* 生成签名
* @param signId 签到活动ID
* @param expireTime 过期时间戳
*/
public static String generateSign(String signId, long expireTime) {
String raw = signId + SECRET_KEY + expireTime;
return DigestUtils.md5Hex(raw);
}
/**
* 验证签名是否合法且未过期
*/
public static boolean verifySign(String signId, long expireTime, String sign) {
if (System.currentTimeMillis() / 1000 > expireTime) {
return false;
}
String expected = generateSign(signId, expireTime);
return expected.equals(sign);
}
}
注意,SECRET_KEY绝对不能出现在前端代码中,每次生成签名只能在后端完成。小程序端只负责展示和转发。
3.3 扫码签到的并发处理
一个两百人的合班课,大家同时扫一个码,后端会瞬间收到大量签到请求。如果直接查数据库判断是否已签到,然后insert,很容易出现并发问题,特别是数据库连接池压力会很大。
我的优化方案是:用Redis做第一道防重闸门。签到请求进来时,先用SETNX sign:userId:activityId 1命令尝试写入,只有写入成功的学生才允许继续查库入库。这个操作是原子的,Redis单线程执行,天然防并发重复。
java复制public boolean signIn(String userId, String activityId) {
String key = "sign:" + activityId + ":" + userId;
Boolean first = stringRedisTemplate.opsForValue()
.setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(first)) {
// 已签过到
return false;
}
// 继续执行数据库插入
try {
signRecordMapper.insert(...);
return true;
} catch (Exception e) {
// 插入失败要删除Redis标记,允许重试
stringRedisTemplate.delete(key);
throw e;
}
}
注意有一个坑:如果数据库insert异常,必须删除Redis中的标记,否则学生会被锁住无法重新签到。这个我在测试阶段真实遇到过,数据回滚了但Redis标记还在,导致学生无法补签。
3.4 补签与异常处理
真实课堂一定会出现学生手机没电、没网、或者扫不到码的情况。所以补签逻辑是必须的。教师端在后台可以看到每个签到活动的已签名单和未签名单,对异常情况可以手动补签。
这里要强调一点:签到记录需要带上IP、扫码时间、二维码ID,便于后续有争议时追溯。这些字段不需要展示给学生,但后台一定要能查得到。
4. 在线测试与考试模块:考点建模和防作弊设计
在线考试模块是另一个核心,也是工作量最大的部分。出题、组卷、答题、评分、防作弊,每个环节都有不少细节。我这里说说主要的思路。
4.1 题库与组卷策略
题库设计要满足“老师可以分类管理题目”和“考试随机抽题”两个需求。我设计了以下核心表:
question_bank:题库表,包含题目类型(单选、多选、判断)、难度(1-5)、所属课程、题干、选项内容、正确答案、分值。paper:试卷表,包含试卷名称、所属课程、考试时长、总分、及格分。paper_question:试卷与题目关联表,包含试卷ID、题目ID、题目在试卷中的顺序、分值。
组卷策略我实现了两种:手工选题和随机抽题。手工选题适合期中期末等正式考试,老师自己决定每道题。随机抽题适合随堂测验,老师只需设定每个知识点的题目数量和难度分布,后端按规则从题库中抽取。
随机抽题需要特别注意:每个学生的试卷不要完全一样。我的方案是按难度分层抽题,每个知识点从题目池中随机选取,这样相邻座位上两个学生的试卷至少是不同的题目组合,在一定程度上降低抄袭概率。
java复制public List<PaperQuestion> autoGeneratePaper(PaperConfig config) {
// 1. 根据知识点和难度查询可用题目
// 2. 对每个知识点,按难度分组
// 3. 每组内用Random随机选取指定数量
// 4. 组装试卷顺序,打乱选项
// 5. 返回试卷题目列表
}
4.2 答题流程与数据上报
在线答题最大的风险是什么?是学生答到一半断网、误关小程序、手机没电关机。如果答题数据没有实时保存,一次意外就可能导致整场考试白费。
所以答题数据必须“随做随传”。学生的每一次选择/取消选择都实时提交到后端,后端存储到Redis中(key为exam:progress:{userId}:{paperId}),考试结束后统一落库到MySQL。这样做有两个好处:
- 实时保存进度,断网重进后可以继续答题。
- 减少数据库写入频次,考试过程中高并发下不会压垮数据库。
学生交卷时,后端从Redis取出答题记录,计算成绩,更新exam_result表,同时清理Redis中的进度缓存。
题目的展示顺序上,我刻意使用了随机顺序。每个学生拿到的题序不同,选项顺序也做了随机化。选项乱序的实现很简单:存储选项时本身是一个数组,返回给前端前用Collections.shuffle()打乱一次即可。
4.3 防作弊:切换后台检测与答题时间校验
小程序防作弊的能力比Web端强,因为小程序有比较严格的生命周期控制,页面的onHide和onShow事件可以捕获到用户切走的行为。
我在考试页面做了这样一个逻辑:进入考试时记录时间戳,页面隐藏(onHide)时记录离开时间,页面重新显示(onShow)时计算本次离开的时长。如果离开超过一定阈值(我设置为3次、每次超过30秒),则弹窗提示并在后台记录“异常行为日志”。超过5次则强制交卷。
javascript复制Page({
data: { leaveCount: 0 },
onHide() {
this.leaveStartTime = Date.now();
},
onShow() {
if (this.leaveStartTime) {
const leaveDuration = (Date.now() - this.leaveStartTime) / 1000;
if (leaveDuration > 30) {
this.setData({ leaveCount: this.data.leaveCount + 1 });
// 上报异常行为
wx.request({ url: `${apiBase}/exam/behavior`, data: {...} });
}
this.leaveStartTime = null;
}
}
});
另一个防作弊手段是答题时长校验。正常情况下,一道单选题不可能在2秒内完成阅读和选择。提交试卷时,后端会对比每题的最短答题时间和实际耗时,把异常数据标记给教师端参考。这个方案不能完全杜绝作弊,但至少能给老师提供数据依据。
4.4 暂停考试与断点恢复
长时段的考试中,学生可能会遇到来电、微信消息提醒等情况,导致小程序被系统挂起。如果系统很不友好地直接退出,学生体验会很差。所以我在考试页面做了“断点恢复”机制:学生重新进入考试页面时,先从后端拉取当前Redis中保存的答题进度,恢复到离开时的状态。
这个功能实现起来不难,但非常影响口碑。学生在真实考试中因为一个来电就丢失所有答案,下次就不会再用你的系统了。
5. 数据库与缓存设计:这些表和缓存是系统的地基
5.1 核心表结构
数据库设计是整个系统的根基。我按“用户体系、课程体系、签到体系、考试体系”四个维度划分,核心表如下:
| 表名 | 说明 | 关键字段 |
|---|---|---|
sys_user |
用户表 | id, openid, name, avatar, role(teacher/student) |
course |
课程表 | id, name, teacher_id, start_date, end_date |
course_student |
选课关系表 | id, course_id, student_id |
sign_activity |
签到活动表 | id, course_id, teacher_id, start_time, end_time, status |
sign_record |
签到记录表 | id, activity_id, student_id, sign_time, ip, sign_code |
question_bank |
题库表 | id, course_id, type, difficulty, content, options, answer, score |
exam |
考试表 | id, course_id, paper_id, name, start_time, end_time, duration |
exam_record |
答题记录表 | id, exam_id, student_id, question_id, user_answer, is_correct, duration |
exam_result |
考试成绩表 | id, exam_id, student_id, score, submit_time, status |
exam_behavior_log |
异常行为日志表 | id, exam_id, student_id, behavior_type, create_time |
单看表结构可能不够直观,我展开说明几个比较关键的设计点。
选课关系表不是简单的学生和课程多对多,它还承担了“这个学生能不能参加这门课的签到和考试”的权限校验。后端在签到、考试接口中都要校验学生是否属于该课程,防止串课。
答题记录表中的is_correct字段,我建议在交卷时统一计算,而不是每题保存时就算好。这样如果老师中途修改了题目答案(极少数情况,但有),还能重新计算成绩。
考试成绩表中加了一个status字段,用来标记成绩的确认状态。比如教师可以设定“考试结束后统一公布成绩”,在公布之前的分数学生端不可见。
5.2 Redis缓存的使用场景
Redis在本系统中的角色主要有四个:
- 动态签到码缓存:前面提到了,签到码过期时间30秒,存在Redis中。
- 考试答题进度缓存:
exam:progress:{userId}:{paperId},考试期间实时保存答题进度。 - 接口防刷限流:对登录接口和签到接口做简单的限流,防止恶意请求。
- 热点数据缓存:课程列表、已结束的考试成绩等热点数据,可以缓存到Redis,减少数据库压力。
数据库和缓存的配合上,一条原则:考试期间读写走Redis,考试结束后统一落MySQL;签到期间通过Redis做防重,记录落MySQL。这样既保证了性能,也保证了数据的持久化和可追溯性。
5.3 数据库索引设计实战
索引这块很容易被忽视,但恰恰是上线后性能瓶颈的根源。我建索引的经验是这样的:
sign_record表的activity_id和student_id都建索引,因为查询“谁没签到”和“某人签到了哪些课”都是高频操作。exam_result表的exam_id建索引,因为成绩列表和排名统计都靠它。exam_record表的(exam_id, student_id)建联合索引,查单个人的答题明细时快。- 不要给所有字段都建索引,写多读少的表索引过多只会拖慢插入。
sql复制ALTER TABLE sign_record ADD INDEX idx_activity_id (activity_id);
ALTER TABLE sign_record ADD INDEX idx_student_id (student_id);
ALTER TABLE exam_result ADD INDEX idx_exam_id (exam_id);
ALTER TABLE exam_record ADD INDEX idx_exam_student (exam_id, student_id);
6. 小程序端到后端联调时踩过的那些坑
6.1 获取登录用户信息失败
热搜词里有“小程序获取登录后的微信用户失败:wx1cb4398e1413dce7”,这个问题太经典了。微信官方对用户信息授权策略调整了好几次,现在wx.getUserProfile接口返回的昵称和头像已经是匿名化的,直接展示会变成“微信用户”和灰色头像。
我遇到的实际问题是:开发阶段一切正常,上线后突然获取不到用户信息。排查半天发现是基础库版本不同导致的兼容性问题。最终方案是:正常登录流程只依赖wx.login换取openid,不强制用户点击授权;如果业务需要展示头像昵称,采用“资料填写页”的方式引导用户自主填写或选择微信头像,而不是在登录时强弹授权框。
6.2 真机测试时net::ERR_CONNECTION_RESET
另一个高频问题是真机调试时报net::ERR_CONNECTION_RESET。这通常不是代码问题,而是网络环境问题。开发者在电脑上能通,手机连同一个WiFi却报连接被重置,十有八九是:
- 后端服务绑定的是127.0.0.1,手机访问不到,必须绑定0.0.0.0或局域网IP。
- 手机和电脑不在同一网段。
- 公司/学校网络开启了AP隔离,设备间无法互通。
我的经验是:开发阶段直接用“微信开发者工具-详情-本地设置-不校验合法域名”选项,真机调试时把后端的IP改为局域网IP,并且一定要把request合法域名配置到小程序后台。如果是个人开发且没有备案的HTTPS域名,只能在开发者工具中调试,真机预览受限。
6.3 Spring Boot后端时间与JSON序列化的坑
小程序端和后端的时间格式不一致,是个非常隐蔽的坑。Java后端默认返回的LocalDateTime序列化为"2024-11-25T14:30:00",而小程序端的new Date()解析这种格式在iOS上会报错。
我的统一处理方案是:在全局配置中指定Jackson序列化格式:
yaml复制spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
同时,后端存储时间统一使用时间戳(long)或MySQL的datetime,对外接口返回统一格式的字符串。这个约定从项目第一天就定下来,后面就不会出现各写各的、联调时互相甩锅的情况。
6.4 小程序代码包体积控制
微信小程序主包不能超过2M,考试页面引用了不少组件,稍不注意就超了。我的处理方式:
- 图片资源全部走CDN,不打进代码包。
- 考试页面、成绩页面等低频页面用分包加载。
- 公共组件尽量抽象复用,比如单选、多选、判断题用同一个组件渲染。
json复制{
"pages": [
"pages/index/index",
"pages/login/login"
],
"subPackages": [
{
"root": "pages/exam",
"pages": ["pages/exam/detail"]
}
]
}
分包之后,首屏加载速度快了不少,也根治了代码包超标问题。
6.5 考试倒计时的误差问题
前端用setInterval做倒计时时,会遇到两个问题。第一,小程序切到后台后,定时器会被系统挂起甚至清除。第二,长时间运行后setInterval存在累积误差。
我的做法是:倒计时的基准不放在前端,而是后端在考试开始时返回endTime时间戳,前端根据当前时间与endTime的差值来计算剩余时间。每次页面onShow时重新校准。这样即使定时器被挂起,恢复后也能立即跳到正确的剩余时间。
javascript复制setInterval(() => {
const remain = Math.max(0, Math.floor((this.endTime - Date.now()) / 1000));
if (remain <= 0) {
this.submitExam(); // 时间到自动交卷
} else {
this.setData({ remainTime: remain });
}
}, 1000);
注意,自动交卷这个动作只能靠后端兜底。前端倒计时归零时发送交卷请求,但后端也需要在提交时校验考试时间是否已过期,防止前端绕过倒计时在时间结束后继续答题。
7. 后续扩展与真实运营中的体会
7.1 从“能用”到“好用”的差异
这套系统如果只在技术维度打转,做到“能用”其实不难——CRUD谁都会写。但从“能用”到“好用”,差别往往在细节里。比如签到结束后教师端看到的“出勤率统计图”,比如考试结束后自动给缺考学生打上的标记,比如错题本功能,比如学生端的历史成绩趋势曲线。这些功能单看不复杂,但合在一起才是一个完整的教学工具,而不是一个单纯的答题软件。
7.2 按阶段分批实施的思路
如果你也想做同类系统,我建议按这样的顺序分批实施:
- 第一阶段:只做签到功能,把用户体系、课程体系、扫码签到跑通。这个阶段能覆盖老师日常最痛的点。
- 第二阶段:加入题库管理和在线考试,支持手动组卷和自动评分。这个阶段系统开始有完整闭环。
- 第三阶段:加分页统计、成绩分析、异常行为记录、订阅消息推送。这些是拉开体验差距的功能。
不要一上来就想做一个“大而全”的系统。先跑通最小的闭环,让老师和学生真正用起来,再根据反馈迭代,效果远比闭门造车好。
7.3 关于部署和运维
线上部署我选择了单台云服务器:Spring Boot应用 + MySQL + Redis,用Docker Compose编排。配置了HTTPS证书,小程序后台绑定域名后所有接口走正规的https://。如果后续用户量大,再把MySQL和Redis拆到独立节点,加一层Nginx负载均衡。
日志方面,用logback按天滚动,保留了最近30天的日志。这个非常重要,课堂上出了纠纷,比如“学生说签到了老师不认”,日志能帮你还原现场。
7.4 最后分享一个小技巧
小程序端的接口请求封装,一定要在request.js里统一做token过期处理和错误弹窗,不要在每一个页面里写重复的wx.request。我在utils里封装了一个request函数,所有业务页面统一调用,代码量减少一半,排查问题时也只需要在一个地方找原因。
javascript复制function request({ url, method = 'POST', data = {} }) {
return new Promise((resolve, reject) => {
wx.request({
url: `${apiBase}${url}`,
method,
data,
header: { 'Authorization': 'Bearer ' + getToken() },
success: (res) => {
if (res.data.code === 200) {
resolve(res.data.data);
} else if (res.data.code === 401) {
// token过期,重新登录
reLogin().then(() => reject('login expired'));
} else {
wx.showToast({ title: res.data.message, icon: 'none' });
reject(res.data);
}
},
fail: (err) => {
wx.showToast({ title: '网络异常', icon: 'none' });
reject(err);
}
});
});
}
这个函数的价值不仅在于代码复用,更在于它把“token过期自动重新登录”“错误统一提示”“loading统一控制”这些问题收敛到了一个地方。联调阶段省下来的时间,远比你想象的多。
做这个系统最大的感受是:技术本身不难,难的是把教学场景理解透。同样一个签到功能,你如果不理解“学生上课是坐在一起的,扫码就是一瞬间的事情”,你就不会想到解决二维码转发和并发防重的问题;同样一个考试功能,如果你不理解“真实考试一定会有人断网、有人切后台、有人误关页面”,你就不会认真设计答题进度缓存和断点恢复。把这些真实场景里的问题一个个解决掉,这个系统才算真正落地,而不是看起来能跑。
