这个课堂考勤系统是我去年为某高校计算机学院开发的实际项目,经过一个学期的实际运行检验,系统稳定性与功能性都得到了验证。相比传统的手工签到或简单的Excel记录方式,这套系统通过技术手段解决了三个核心痛点:实时性差(无法立即获取缺勤名单)、数据统计繁琐(教师需要手动汇总多个表格)以及防伪能力弱(代签现象频发)。
系统采用前后端分离架构,前端使用Vue.js+ElementUI实现响应式界面,后端基于SpringBoot 2.7.x构建RESTful API,数据持久层采用MyBatis-Plus 3.5.x。特别值得一提的是,我们创新性地引入了多重验证机制:除了常规的账号密码登录外,还集成了动态二维码签到和地理位置校验,有效杜绝了90%以上的代签行为。数据库方面支持MySQL和SQLServer双引擎,实测在300人同时签到的场景下,平均响应时间保持在800ms以内。
在技术选型阶段,我们对比了多种方案:
最终选择SpringBoot+MyBatis组合主要基于:
spring-boot-starter-web就自动配置好了Tomcat和SpringMVCBaseMapper提供了大量开箱即用的CRUD方法关键依赖示例(pom.xml节选):
xml复制<dependencies>
<!-- SpringBoot基础starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus增强 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<!-- 数据库驱动(多数据源支持) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
系统采用模块化设计,主要包含以下核心模块:
| 模块名称 | 技术实现 | 关键特性 |
|---|---|---|
| 身份认证 | Spring Security | JWT令牌、RBAC权限模型 |
| 签到管理 | WebSocket+Redis | 实时广播、并发控制 |
| 数据统计 | EasyExcel+Quartz | 定时任务、Excel导出 |
| 系统监控 | SpringBoot Actuator | 健康检查、性能指标 |
其中签到管理模块的设计最具挑战性,我们采用了三级缓存策略:
在早课签到高峰期,系统需要处理300+的并发请求。我们通过以下方案保证系统稳定性:
技术实现:
java复制@RestController
@RequestMapping("/attendance")
public class AttendanceController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@PostMapping("/sign")
public R sign(@RequestBody SignDTO dto, HttpServletRequest request) {
// 1. 获取用户ID
String userId = JwtUtil.getUserId(request);
// 2. 获取分布式锁(防止重复签到)
String lockKey = "lock:sign:" + dto.getCourseId() + ":" + userId;
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (!locked) {
return R.error("操作太频繁,请稍后重试");
}
try {
// 3. 业务处理(伪代码)
AttendanceEntity entity = new AttendanceEntity();
entity.setUserId(userId);
entity.setCourseId(dto.getCourseId());
entity.setSignTime(new Date());
entity.setLocation(dto.getLocation());
// 4. 异步写入数据库
attendanceService.asyncSave(entity);
return R.ok("签到成功");
} finally {
// 5. 释放锁
redisTemplate.delete(lockKey);
}
}
}
踩坑经验:
为解决代签问题,我们设计了时效性二维码方案:
生成规则:
技术实现:
java复制public class QrCodeUtil {
private static final String SECRET_KEY = "your_secure_key_here";
public static String generateCode(String courseId) {
long timeWindow = System.currentTimeMillis() / (60 * 1000);
String rawData = courseId + "|" + timeWindow;
return HmacUtils.hmacSha256Hex(SECRET_KEY, rawData);
}
public static boolean verifyCode(String courseId, String code) {
long currentWindow = System.currentTimeMillis() / (60 * 1000);
// 检查当前时间窗口
String expected1 = HmacUtils.hmacSha256Hex(SECRET_KEY,
courseId + "|" + currentWindow);
// 检查前一个时间窗口(容错)
String expected2 = HmacUtils.hmacSha256Hex(SECRET_KEY,
courseId + "|" + (currentWindow - 1));
// 检查后一个时间窗口(容错)
String expected3 = HmacUtils.hmacSha256Hex(SECRET_KEY,
courseId + "|" + (currentWindow + 1));
return code.equals(expected1)
|| code.equals(expected2)
|| code.equals(expected3);
}
}
注意事项:
使用MyBatis-Plus的Lambda表达式构建复杂查询:
java复制public PageUtils queryPage(Map<String, Object> params,
Long courseId, LocalDate startDate, LocalDate endDate) {
QueryWrapper<AttendanceEntity> wrapper = new QueryWrapper<>();
// 基础条件
wrapper.eq(courseId != null, "course_id", courseId)
.ge(startDate != null, "sign_time", startDate.atStartOfDay())
.le(endDate != null, "sign_time", endDate.plusDays(1).atStartOfDay());
// 动态排序
if (params.containsKey("sort") && params.containsKey("order")) {
String sortField = (String) params.get("sort");
boolean isAsc = "asc".equalsIgnoreCase((String) params.get("order"));
wrapper.orderBy(true, isAsc, sortField);
}
IPage<AttendanceEntity> page = this.page(
new Query<AttendanceEntity>().getPage(params),
wrapper
);
return new PageUtils(page);
}
实现步骤:
java复制@Data
public class AttendanceExportDTO {
@ExcelProperty("学号")
private String studentNo;
@ExcelProperty("姓名")
private String studentName;
@ExcelProperty("签到时间")
private String signTime;
@ExcelProperty("状态")
private String status; // 正常/迟到/缺勤
}
java复制public void export(HttpServletResponse response,
Long courseId, LocalDate date) throws IOException {
// 1. 查询数据
List<AttendanceEntity> list = listByCourseAndDate(courseId, date);
// 2. 转换为DTO
List<AttendanceExportDTO> exportList = list.stream()
.map(entity -> {
AttendanceExportDTO dto = new AttendanceExportDTO();
// 属性拷贝...
return dto;
}).collect(Collectors.toList());
// 3. 设置响应头
String fileName = URLEncoder.encode(
"考勤报表_" + date.toString(), "UTF-8");
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
response.setHeader("Content-disposition",
"attachment;filename=" + fileName + ".xlsx");
// 4. 写入Excel
EasyExcel.write(response.getOutputStream(), AttendanceExportDTO.class)
.sheet("考勤数据")
.doWrite(exportList);
}
性能优化点:
采用SpringBoot的Profile机制管理不同环境配置:
code复制application.yml # 公共配置
application-dev.yml # 开发环境
application-test.yml # 测试环境
application-prod.yml # 生产环境
关键配置示例:
yaml复制# application-prod.yml
spring:
datasource:
url: jdbc:mysql://master.db:3306/attendance?useSSL=false
username: prod_user
password: ${DB_PASSWORD} # 从环境变量读取
hikari:
maximum-pool-size: 20
connection-timeout: 30000
redis:
cluster:
nodes:
- redis-node1:6379
- redis-node2:6379
timeout: 5000
通过Actuator暴露监控端点:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
prometheus:
enabled: true
自定义健康检查指标:
java复制@Component
public class AttendanceHealthIndicator
implements HealthIndicator {
@Autowired
private RedisTemplate redisTemplate;
@Override
public Health health() {
try {
// 检查Redis连接
redisTemplate.getConnectionFactory().getConnection().ping();
// 检查数据库连接
// ...
return Health.up()
.withDetail("redis", "available")
.build();
} catch (Exception e) {
return Health.down()
.withException(e)
.build();
}
}
}
在实际运行半年后,我们收集了教师和学生的反馈,规划了以下改进方向:
这个项目给我的深刻体会是:教育信息化系统开发必须紧密贴合实际教学场景。比如我们最初设计的严格签到策略(精确到秒级)在实际应用中反而造成了排队拥堵,后来调整为分钟级精度+动态容差机制后,既保证了考勤严肃性又提升了用户体验。技术方案的选择永远应该服务于业务需求,而不是相反。