1. 项目背景与核心价值
这个基于微信小程序的学生考勤管理系统,是我去年为本地一所职业院校开发的实战项目。当时学校面临几个痛点:传统纸质签到效率低下(200人课堂点名要15分钟)、代签现象严重、数据统计滞后。市面上成熟的考勤系统又存在两个问题:要么功能过于复杂(带薪资模块等学校用不上的功能),要么就是按年订阅费用高昂。
这个系统用微信小程序作为前端(学生和老师都在用微信),后端采用经典的SSM框架(Spring+SpringMVC+MyBatis),实现了几个关键创新:
- 动态二维码防伪:每次生成的签到码包含时间戳+课程ID的MD5加密值,10秒自动刷新
- 多重定位验证:结合GPS定位、Wi-Fi指纹和蓝牙信标(教室安装iBeacon)三重校验
- 离线模式:考虑到学校部分区域网络不稳定,小程序端采用IndexedDB暂存数据
实测数据:200人课堂签到时间从15分钟降至28秒,代签率从17%降到0.3%
2. 技术架构详解
2.1 微信小程序端设计要点
采用微信原生框架+TypeScript开发,关键配置在app.json中需要特别注意:
json复制{
"permission": {
"scope.userLocation": {
"desc": "用于校验签到位置真实性"
}
},
"requiredBackgroundModes": ["location"]
}
几个踩坑经验:
- 获取定位时必须处理
wx.authorize的异步响应,我封装了如下工具方法:
typescript复制async function getLocationWithRetry(retry = 3): Promise<LocationData> {
try {
const res = await wx.getSetting()
if (!res.authSetting['scope.userLocation']) {
await wx.authorize({ scope: 'scope.userLocation' })
}
return await wx.getLocation({ type: 'gcj02' })
} catch (err) {
if (retry > 0) return getLocationWithRetry(retry - 1)
throw new Error('定位授权失败')
}
}
- 蓝牙信标扫描要注意Android和iOS的差异:
- iOS需要先
wx.openBluetoothAdapter - Android 10+需要
ACCESS_FINE_LOCATION权限
2.2 后端SSM框架关键配置
数据库采用MySQL 8.0,特别注意事务处理的设计:
java复制@Transactional(rollbackFor = Exception.class)
public void handleSignIn(SignRequest request) {
// 1. 验证二维码有效性
if (!qrcodeService.validate(request.getCode())) {
throw new BizException("无效签到码");
}
// 2. 验证地理位置
LocationVerifyDTO dto = buildVerifyDTO(request);
if (!locationService.verify(dto)) {
throw new BizException("位置校验失败");
}
// 3. 记录考勤
attendanceMapper.insert(buildAttendance(request));
// 4. 更新课程统计
courseStatService.update(request.getCourseId());
}
遇到的性能瓶颈及解决方案:
- 并发签到时的锁竞争:采用Redis分布式锁+课程ID分片
- 历史数据查询慢:按月分表+ES索引
3. 防作弊机制实现
3.1 动态二维码生成策略
采用时间窗口算法,核心逻辑:
java复制public String generateQRCode(String courseId) {
long timestamp = System.currentTimeMillis() / 10000; // 10秒窗口
String raw = courseId + "|" + timestamp + "|" + SECRET_KEY;
return DigestUtils.md5Hex(raw);
}
验证时需要注意时间漂移问题,我采用NTP时间同步:
java复制boolean validate(String code, String courseId) {
long currentWindow = getNtpTime() / 10000;
// 检查当前窗口及前一个窗口(兼容网络延迟)
for (int i = 0; i <= 1; i++) {
String expected = generateQRCode(courseId, currentWindow - i);
if (expected.equals(code)) return true;
}
return false;
}
3.2 多重定位校验算法
结合三种定位方式的权重计算:
python复制def verify_location(gps, wifi, beacon):
# GPS权重40%(要求500米内)
gps_score = 0.4 if distance(gps, class_gps) < 500 else 0
# Wi-Fi指纹权重35%(匹配至少3个AP)
wifi_score = 0.35 if len(set(wifi) & class_wifi) >=3 else 0
# 蓝牙信标权重25%(信号强度>-70dBm)
beacon_score = 0.25 if beacon.rssi > -70 else 0
return gps_score + wifi_score + beacon_score >= 0.7
实测中发现华为手机的特殊情况:
- EMUI系统会主动扫描周边Wi-Fi,可能导致wifi_score异常偏高
- 解决方案:对华为设备降低wifi_score权重到25%,增加GPS权重
4. 数据统计与分析
4.1 实时考勤看板
采用WebSocket推送数据更新,前端用ECharts实现:
javascript复制// 小程序端订阅
wx.connectSocket({
url: 'wss://yourdomain.com/ws',
success() {
wx.onSocketMessage(res => {
const data = JSON.parse(res.data)
this.updateDashboard(data)
})
}
})
// 后端推送逻辑
@ServerEndpoint("/ws")
public class AttendanceWebSocket {
@OnOpen
public void onOpen(Session session) {
String courseId = getCourseIdFromSession(session);
Executor.scheduleAtFixedRate(() -> {
Stats stats = statService.getRealtimeStats(courseId);
session.getBasicRemote().sendText(JSON.toJSONString(stats));
}, 0, 5, TimeUnit.SECONDS);
}
}
4.2 离线数据处理方案
小程序端存储策略:
typescript复制// 初始化数据库
const db = wx.cloud.database()
const cacheDB = wx.getStorageSync('indexedDB') || {}
function cacheData(data) {
// 先存内存
cacheDB[data.courseId] = data
// 尝试同步到云端
wx.request({
url: 'api/sync',
method: 'POST',
data,
success: () => delete cacheDB[data.courseId],
fail: () => wx.setStorageSync('indexedDB', cacheDB)
})
}
后端处理离线数据的合并策略:
java复制public void handleOfflineData(List<Attendance> offlineData) {
offlineData.forEach(data -> {
// 基于事件时间的去重
if (!attendanceMapper.exists(data.getStudentId(), data.getCourseId(), data.getTimestamp())) {
attendanceMapper.insert(data);
}
});
// 批量更新统计
courseStatService.batchUpdate(offlineData.stream()
.map(Attendance::getCourseId)
.distinct()
.collect(Collectors.toList()));
}
5. 部署与性能优化
5.1 微信小程序审核要点
特别注意这些审核规则:
- 用户隐私协议必须包含位置信息使用说明
- 若使用web-view,域名必须备案
- 蓝牙权限需明确说明用途(如"用于教室信标识别")
我的隐私协议模板节选:
code复制我们收集的位置信息仅用于:
- 验证签到时的实际地理位置
- 统计各区域考勤情况
数据保留期限:自学期结束起6个月
5.2 服务器配置建议
最低配置要求:
- 2核4G(实测支持并发500+签到请求)
- 带宽5M(主要消耗在WebSocket推送)
关键JVM参数:
bash复制-server -Xms2048m -Xmx2048m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
Nginx优化配置:
nginx复制location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
# WebSocket支持
if ($http_upgrade = "websocket") {
proxy_pass http://backend;
}
}
6. 源码结构与使用说明
项目目录结构:
code复制├── mini-program # 小程序端
│ ├── components # 通用组件
│ │ ├── qrcode-scanner # 扫码组件
│ │ └── location-validator # 定位校验组件
├── server # 后端服务
│ ├── src/main/java
│ │ ├── config # SSM配置
│ │ ├── controller # 接口层
│ │ ├── service # 业务逻辑
│ │ └── util # 工具类
│ └── sql # 数据库脚本
└── docs # 文档
├── api.md # 接口文档
└── deploy-guide.md # 部署指南
快速启动步骤:
- 数据库初始化(执行server/sql下的脚本)
- 修改server/src/main/resources/application.yml中的数据库配置
- 小程序端修改project.config.json中的appid
- 后端启动:
mvn spring-boot:run - 小程序开发工具导入mini-program目录
常见问题解决方案:
- 二维码生成异常:检查服务器时间是否同步
- 定位偏差过大:在教室四个角部署蓝牙信标
- WebSocket连接失败:检查Nginx的proxy_set_header配置
这个项目最让我自豪的是在没有任何商业SDK的情况下,仅用开源技术栈就实现了99.7%的准确率。有个实用建议:在教室布置至少4个蓝牙信标(前后左右各一个),通过三角定位可以显著提升室内定位精度。
