Flutter+springboot实现考勤码+gps考勤签到功能
- 实现步骤和思路:
- 设计
- 发布考勤任务
- 签到考勤
- 环境准备
- 后端步骤:
- 1.教师发布考勤任务接口
- 2.rabbitmq收到教师发布考勤执行的步骤代码接口
- 3.考勤过期后,rabbitmq执行的代码
- 4.websocket推送,以及离线消息缓存,上线取出发送,删除缓存
- 5.学生考勤接口
- 前端flutter app
- 需要用到的插件
- 2. 发布成功,通过provide更新UI,把考勤记录记入本地换成,并且利用计时器倒计时
- websocket接收考勤消息推送和处理
- 1.websocket接收数据
- 2.websocket处理解析数据
- 3. 利用NotificationsUtil通知用户,并使用EventBus消息总线将处理好的数据推送到相关的UI界面
- 4. 要显示跟新的UI界面监听消息总线通道,取出收到的数据写入本地并通过provide刷新UI
- 效果预览图
- 总结
本人正在做一个智慧课堂辅助app的毕设作品,刚好有一个功能是考勤签到,正好实现了,分享一下经验。
主要实现有 1.app教师用户发布数字或gps考勤任务,gps考勤可以设置考勤地点和距离范围内有效,并且能设置考勤失效时间。后端将考勤任务通过websocket推送到该课程的所有学生,学生收到后在指定时间地点内输入考勤码完成签到,过期不签到的同学将被系统设置为旷课。利用redis做离线消息缓存七天,离线用户的将消息存储到redis中,用户上线后取出redis离线消息推送。
实现步骤和思路:
后端springboot:
1.教师通过restful风格接口创建考勤任务,生成数据库记录
2. 在该创建考勤接口中使用rabbitmq来执行三个步骤:
1.生成需要签到的学生名单记录到数据库,赋默认状态值-1,表示需要签到;2.将需要考勤的消息通过websocket推送给前端app,3.使用redis把教师创建的考勤任务记入,并设置过期时间为教师设置的有效时间。
3.在创建考勤接口最后一步使用rabbitmq的消息延迟队列去跟新数据库状态为-1(需要考勤)的记录修改为缺勤的状态,并把缺勤的记录通过websocket推送到学生前端。 消息延迟队列时间为考勤有效时间
4. 利用redis做离线消息缓存
前端flutter:
1.教师使用高德地图插件定位获取考勤位置,设置考勤距离和有效时间,通过http请求发送到后端;
3. 成功通过Timer计时器来显示剩余时间,并且使用provide状态管理来保持时间显示;
4. 考勤期间收到完成考勤的学生数据,使用provide插入本地并更新名单记录;
5. 考勤结束收到后端websocket推送的学生缺勤名单,provide更新数据;
学生:1.收到教师发布的考勤任务,前往考勤页面点击考勤,完成考勤更新UI,如果失败,也跟新UI界面
使用到的工具和框架有:
springboot
spring-data-jpa
mysql
redis
rabbitmq
rabbitmq消息延迟队列插件
设计
发布考勤任务
课堂创建者可以发布考勤任务让学生用户在指定的时间内完成考勤。发布的考勤主要分两种方式,第一种是数字考勤,学生只需要在指定的时间内通过考勤码完成考勤任务即可;第二种为GPS考勤,是在第一种考勤的方式上,设置指定考勤的地点范围,只有在指定的地点范围内完成考勤,才算成功,否则系统视为缺勤。该功能主要涉及的工具和技术有Reids非关系型数据库、RabbitMQ消息中间件、消息延时队列、Websocket。服务端具体的设计过程如下:
(1) 客户端通过请求Restful风格接口创建考勤任务,生成数据库记录;
(2) 在该创建考勤任务接口中利用RabbitMQ消息中间件来异步完成三个步骤:
①生成需要签到的学生名单(userId)批量写入到数据库,考勤状态(status)赋默认状态值-1,表示需要完成签到任务;
②将考勤消息通过websocket推送给需要完成考勤任务的所有用户App客户 端;
③把教师用户创建的考勤任务记录写入Redis,并设置过期时间为教师用户指 定的时间(expireTime)。
(3) 利用RabbitMQ的延时队列去修改学生考勤记录状态(status)为-1的记录统一修改为状态为0,表示学生缺勤,未能在指定时间内完成考勤任务,并用Websocket推送缺勤名单给考勤任务创建者;
(4) 需要完成考勤的学生用户在考勤有效时间内完成考勤,数据库学生考勤状态(status)为-1的修改为1,表示出勤, 并用Websocket推送缺勤名单给考勤任务创建者。
Flutter
App客户端Gps考勤使用到高德地图的定位接口,主要分为以下几个步骤:
(1)在Flutter的pubspec.yaml文件中引入高德地图插件:
amap_location_fluttify: 0.8.11+481e45c #高德地图
(2) 到高德地图开放平台(https://lbs.amap.com/)注册开发者,并创建应用;
(3) 在Android的AndroidManifest.xml文件下添加高德地图创建应用生成的Key
(4) 在调用高德地图接口插件前,调用以下代码,初始化接口:
WidgetsFlutterBinding.ensureInitialized();
Await AmapCore.init(‘21f35eb097c0cd048f7668194525ba7a’);
发布考勤任务功能的流程如图。
签到考勤
学生用户在收到教师用户发布的考勤任务后,将收到系统提示的考勤消息。在相应的课堂考勤列表中出现需要完成的考勤记录,如果发布的是数字考勤,通过教师告诉学生的考勤码完成签到,如果是GPS考勤,则需要手机定位后在输入考勤码签到。如果考勤任务时间过期,或者考勤用户的定位地点不在指定地点范围内将直接视为缺勤,无法再签到。具体实现流程如图所示。
环境准备
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.1-jre</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
后端步骤:
1.教师发布考勤任务接口
2.rabbitmq收到教师发布考勤执行的步骤代码接口
3.考勤过期后,rabbitmq执行的代码
4.websocket推送,以及离线消息缓存,上线取出发送,删除缓存
用户上线后,websocket取出redis离线消息推送,并且清除离线缓存的数据
@OnPen接口
5.学生考勤接口
///学生考勤
@RequestMapping(value = "AttendanceStudentCheck", method = {RequestMethod.GET, RequestMethod.POST})
public BaseResult AttendanceStudentCheck(@Valid @RequestBody AttendanceStudentDto attendanceStudentDto,
HttpServletRequest request) {
String key = RedisKeyUtil.Attendance.getAttendanceExpireKey(attendanceStudentDto.getAttendanceId());
Attendance attendance = (Attendance) redisService.get(key);
String studentId = (String) request.getAttribute("X-AUTH-ID");
if (attendance == null) {//考勤失败,缺勤
attendanceStudentService.updateStudentStatus(studentId, attendanceStudentDto, 3, 0);
return BaseResult.ok(3);
}
boolean isSuccess = false;
double d = 0;
if (attendance.getAttendCode().equals(attendanceStudentDto.getAttendCode())) {
if (attendanceStudentDto.getType() == 0) {//数字考勤
//考勤成功
isSuccess = true;
} else {//gps
BigDecimal lat = attendance.getLatitude();
BigDecimal lon = attendance.getLongitude();
double distance = attendance.getDistance();
attendanceStudentDto.getLatitude();
attendanceStudentDto.getLongitude();
d = DistanceUtil.earthDis(lat.doubleValue(), lon.doubleValue(),
attendanceStudentDto.getLatitude().doubleValue(),
attendanceStudentDto.getLongitude().doubleValue());
System.out.println("distance: " + d);
if (d <= distance) {
isSuccess = true;
}
}
}
if (isSuccess) {
AttendanceStudent attendanceStudent =
attendanceStudentService.updateStudentStatus(studentId, attendanceStudentDto, 0, d);
Map<String, Object> map = new HashMap<>();
map.put("method", "teacher/createAttendance");
map.put("publisherId", attendance.getPublisherId());
try {
redisRabbitProvider.websocketSend(attendanceStudent, map);
} catch (IOException e) {
e.printStackTrace();
}
return BaseResult.ok(0);
} else {
return BaseResult.ok(-1);
}
}
前端flutter app
需要用到的插件
dio: ^3.0.8
provide: ^1.0.2 #状态管理
shared_preferences: ^0.5.1
flutter_local_notifications: ^1.1.5+1 #
permission_handler: ^4.2.0+hotfix.3 #权限库
amap_location_fluttify: 0.8.6+356f11c #高德地图
flutter_event_bus: ^0.0.2
主要思路
教师通过dio 发送http请求创建考勤任务,
经过封装的发布考勤接口,
2. 发布成功,通过provide更新UI,把考勤记录记入本地换成,并且利用计时器倒计时
自定义的provide+计时器工具类
class TimeExpire{
int expire=0;
double minutes = 0;
int second = 0;
}
///倒计时器
class ExpireTimerProvide with ChangeNotifier{
Map<int, Timer> timerMap = Map();
Map<int, TimeExpire> timeExpire=Map();
///
void init(){
timerMap.clear();
timeExpire.clear();
}
//开始计时方法
startCountdownTimer(Duration d, int id) {
int counterTime = d.inSeconds;
print('进来 ${counterTime}');
TimeExpire expireT=TimeExpire();
expireT.expire=counterTime;
expireT.minutes= counterTime / 60;
expireT.second = counterTime % 60;
timeExpire.putIfAbsent(id, ()=>expireT);
var oneMinuute = Duration(seconds: 1);
var callback = (Timer timer) async {
// print(timer.tick);
if (timeExpire[id].expire < 1) {
timerMap[id].cancel();
if (timerMap.containsKey(id)) {
timerMap.remove(id);
}
print('remove :${timerMap}');
} else {
int temp=timeExpire[id].expire;
timeExpire[id].expire =temp- 1;
timeExpire[id].minutes = temp / 60;
timeExpire[id].second = temp % 60;
}
notifyListeners();
};
timerMap.putIfAbsent(id, ()=>Timer.periodic(oneMinuute, callback));
}
}
显示计时UI代码
websocket接收考勤消息推送和处理
1.websocket接收数据
2.websocket处理解析数据
3. 利用NotificationsUtil通知用户,并使用EventBus消息总线将处理好的数据推送到相关的UI界面
推送总线消息
Application.eventBus.publish(要推送的数据);
4. 要显示跟新的UI界面监听消息总线通道,取出收到的数据写入本地并通过provide刷新UI
完整代码已经上传到github项目,做为项目的某部分,项目将继续完善
github:coureapp源码
效果预览图
总结
最终圆满完成考勤功能,推送消息没有问题。本人做后端的,不擅长前端,UI做的太丑了,求手下留情!