人脸考勤签到进阶篇
目录
签到业务流程说明
一、需求介绍
Emos系统的人脸签到模块包含的功能非常丰富,不仅仅只有人脸识别的签到功能,而且还可以根据用户签到时候的地理定位,计算出该地区是 新冠疫情 的 高风险 还是 低风险 地区。如果员工是在疫情高风险地区签到的,Emos系统会立即向公司人事部门发送告警邮件。
二、如何获取地理信息?
微信小程序提供了获取地理定位的接口方法,我们调用该方法就能获取到地理坐标。但是我们得到的仅仅是坐标而已,我们还需要把地理坐标转换成地址信息,例如什么省份、什么城市、什么街道等等。
腾讯位置服务提供了把地理坐标转换成地址这个功能,只需要我们注册之后就可以免费使用了。并且还提供了JS调用接口,我们在小程序中可以很简单的把地理坐标转换成地址信息。
三、如何判定某地区新冠疫情的风险等级?
本地宝这个网站提供了新冠疫情地区风险等级的查询,我们输入自己的地址,就能看到具体的风险等级。
既然我们已经把地理坐标转换成了地址信息,那么就可以根据地址信息去查询风险等级了。但是本地宝并没有提供Web接口让我们调用,所以我们只能URL地址传参的方式获取本地宝返回的响应。而且响应的内容是HTML,我们还要从HTML中解析出我们想要的风险等级信息。
开通腾讯位置服务
一、开通腾讯位置服务步骤
因为Emos签到流程中要获取用户当前所在地址的信息,所以需要把定位坐标缓存成地址,恰好腾讯位置服务提供了这个功能。所以我们按照提示开通这个服务即可,该服务对开发者来说是免费的,所以我们可以放心使用。
首先我们用浏览器访问 腾讯位置服务 官网,然后在页面的右上角点击注册按钮,并且填写注册信息。
在 应用管理 〉我的应用 栏目中,可以看到已经创建的密钥。如果是新注册的用户,这里没有任何密钥,需要你自己创建一个新的密钥。
根据提示填写密钥的信息。密钥创建成功之后,你要把密钥字符串记录下来,在小程序开发当中会用到。
把该密钥和咱们的小程序关联在一起,在界面中填写小程序的授权ID。
二、腾讯位置服务SDK
腾讯位置服务提供了多种SDK程序包,其中的JavaScript版本的SDK适用于微信小程序,所以我们下载这个SDK包。
登陆微信公众平台里面,在“开发管理” -> “开发设置”中设置request合法域名,添加https://apis.map.qq.com 。
在小程序项目中,创建 lib 目录,把SDK文件放入其中。
把定位坐标转换成真实地址
一、获取定位坐标
可以通过用户授权API来判断用户是否给应用授予定位权限。
uni.authorize(OBJECT)
uni.authorize({
scope: 'scope.userLocation',
success() {
uni.getLocation()
}
})
注意:scope.userLocation 权限需要在 manifest.json 配置 permission
微信小程序提供了定位接口,只需要我们调用方法即可。uni-app框架的uni对象里面也封装了地理定位的方法,我们来看一下。
uni.getLocation(OBJECT)
获取当前的地理位置和速度。 在微信小程序中,当用户离开应用后,此接口无法调用,除非申请后台持续定位权限;当用户点击“显示在聊天顶部”时,此接口可继续调用。
// 示例
uni.getLocation({
type: 'wgs84',
success: function (res) {
console.log('当前位置的经度:' + res.longitude);
console.log('当前位置的纬度:' + res.latitude);
}
});
二、编辑签到页面
我们首先要获取用户签到时的地理定位
uni.showLoading({
title: '签到中请稍后' 3. });
setTimeout(function() { 5. uni.hideLoading();
}, 30000);
//获取地理定位
uni.getLocation({
type: 'wgs84',
success: function(resp) {
let latitude = resp.latitude;
let longitude = resp.longitude;
}
})
接下来我们根据定位坐标,换算成真实地址,先引用腾讯位置SDK文件
var QQMapWX = require('../../lib/qqmap-wx-jssdk.min.js');
var qqmapsdk;
然后在 onLoad() 生命周期函数中,初始化 qqmapsdk 对象
onLoad: function() {
qqmapsdk = new QQMapWX({
key: 'KSFBZ-####-####-####-37KUE-W3FLZ'
});
},
编写JS代码把GPS坐标转换成地址
qqmapsdk.reverseGeocoder({
location: {
latitude: latitude,
longitude: longitude
},
success: function(resp) {
// console.log(resp.result);
let address = resp.result.address;
let addressComponent = resp.result.address_component;
let nation = addressComponent.nation;
let province = addressComponent.province;
let city = addressComponent.city;
let district = addressComponent.district;
}
})
在Docker中安装人脸识别镜像
安装Docker程序
执行下面的指令,稍等片刻,Docker程序就安装好了
yum install docker -y
管理Docker程序的命令也非常简单,如下:
service docker start
service docker stop
service docker restart
导入人脸识别镜像
把 face.tar.gz 文件上传到CentOS系统
把镜像导入Docker环境
#导入镜像文件
docker load < face.tar.gz
#查看安装的镜像
docker images
#删除镜像
docker rmi face
运行人脸识别程序
一、创建Docker容器
上节课我们在Docker中安装了人脸识别镜像,因为人脸识别程序是用Python写的,而且需要很多依赖库,安装起来非常麻烦,所以我就把依赖环境和人脸识别程序封装成Docker镜像,只要你在本地Docker上面导入镜像,创建出容器,就能运行Python人脸程序了。
把 demo.tar 文件上传到Linux根目录,然后解压缩
tar -xvf demo.tar
解压缩之后,demo文件夹中就包含了人脸识别Python程序,我们只需要把demo文件夹挂载到Docker容器,那么在容器中就能访问Linux主机的demo文件夹了。下面开始创建容器,映射端口号,挂载目录。
#创建容器,把容器3000端口映射到宿主机3000端口,把/demo映射到宿主机的/demo
docker run -d -it -p 3000:3000 -v /demo:/demo --name node face
#查看容器运行状态
docker ps -a
#进入到node容器
docker exec -it node bash
二、运行人脸识别程序
进入到node容器之后,然后进入 /demo 目录,运行人脸识别程序
cd /demo
#把Python程序挂起到后台运行
nohup python3 -c "from app import app;" > log.out 2>&1 &
ps -aux
kill -9 进程ID
三、接口调用
人脸识别程序程序结合了Flask框架,提供Web接口,具体如下
1. 创建人脸模型数据
当Emos系统的MySQL数据库中不存在签到员工的人脸模型数据,这时候应该调用人脸识别程序的Web接口,上传照片文件,然后由Python程序识别照片中的人脸,返回人脸模型数据。Java系统接收到人脸模型数据之后,把数据保存在MySQL数据表里面。
接口名称:/create_face_model
请求类型:POST
传入参数:icode
返回结果:人脸模型数据
2. 执行人脸签到识别
接口名称:/checkin
请求类型:POST
传入参数:icode
返回结果:人脸识别结果
实现人脸签到(持久层)
一、维护员工人脸模型数据
在 TbFaceModelDao.xml 文件中添加SQL语句
<select id="searchFaceModel" parameterType="int" resultType="String">
SELECT face_model FROM tb_face_model
WHERE user_id=#{userId}
</select>
<insert id="insert" parameterType="com.example.emos.wx.db.pojo.TbFaceModel">
INSERT INTO tb_face_model
SET user_id=#{userId},
face_model=#{faceModel}
</insert>
<delete id="deleteFaceModel" parameterType="int">
DELETE FROM tb_face_model
WHERE user_id=#{userId}
</delete>
在 TbFaceModelDao.java 接口中添加DAO方法
@Mapper
public interface TbFaceModelDao {
public String searchFaceModel(int userId);
public void insert(TbFaceModel faceModel);
public int deleteFaceModel(int userId);
}
二、保存签到记录
在 TbCheckinDao.xml 文件中添加INSERT语句
<insert id="insert" parameterType="com.example.emos.wx.db.pojo.TbCheckin">
INSERT INTO tb_checkin
SET user_id=#{userId},
<if test="address!=null">
address=#{address},
</if>
<if test="country!=null">
country=#{country},
</if>
<if test="province!=null">
province=#{province},
</if>
<if test="city!=null">
city=#{city},
</if>
<if test="district!=null">
district=#{district},
</if>
status=#{status},
<if test="risk!=null">
risk=#{risk},
</if>
date=#{date},
create_time=#{createTime}
</insert>
在 TbCheckinDao.java 中添加抽象方法
@Mapper
public interface TbCheckinDao {
……
public void insert(TbCheckin entity);
}
实现人脸签到(业务层)
一、判断签到用户是否存在人脸模型
在 application.yml 文件中,添加值注入信息
emos:
……
face:
createFaceModelUrl: http://CentOS的IP地址:3000/create_face_model
checkinUrl: http://CentOS的IP地址:3000/checkin
code: HelloWorld
创建 CheckinForm.java 表单类,接收小程序提交的签到数据
@Data
@ApiModel
public class CheckinForm {
private String address;
private String country;
private String province;
private String city;
private String district;
}
在 CheckinService.java 接口中添加抽象的签到方法
public interface CheckinService {
……
public void checkin(HashMap param);
}
在 CheckinServiceImpl.java 中实现抽象方法
@Service
@Scope("prototype")
@Slf4j
public class CheckinServiceImpl implements CheckinService {
@Autowired
private TbFaceModelDao faceModelDao;
@Value("${emos.face.checkinUrl}")
private String checkinUrl;
@Autowired
private SystemConstants constants;
@Value("${emos.code}")
private String code;
@Override
public void checkin(HashMap param) {
Date d1=DateUtil.date();
Date d2=DateUtil.parse(DateUtil.today()+" "+constants.attendanceTime);
Date d3=DateUtil.parse(DateUtil.today()+" "+constants.attendanceEndTime);
int status=1;
if(d1.compareTo(d2)<=0){
status=1;
}
else if(d1.compareTo(d2)>0&&d1.compareTo(d3)<0){
status=2;
}
else{
throw new EmosException("超出考勤时间段,无法考勤");
}
int userId= (Integer) param.get("userId");
String faceModel=faceModelDao.searchFaceModel(userId);
if(faceModel==null){
throw new EmosException("不存在人脸模型");
}
else{
String path=(String)param.get("path");
HttpRequest request= HttpUtil.createPost(checkinUrl);
request.form("photo", FileUtil.file(path),"targetModel",faceModel);
request.form("code",code);
HttpResponse response=request.execute();
if(response.getStatus()!=200){
log.error("人脸识别服务异常");
throw new EmosException("人脸识别服务异常");
}
String body=response.body();
if("无法识别出人脸".equals(body)||"照片中存在多张人脸".equals(body)){
throw new EmosException(body);
}
else if("False".equals(body)){
throw new EmosException("签到无效,非本人签到");
}
else if("True".equals(body)){
//TODO 查询疫情风险等级
//TODO 保存签到记录
}
}
}
}
查询签到所在地区新冠疫情风险等级
@Data
public class TbCheckin implements Serializable {
private String date;
private Date createTime;
}
延伸:date字段是日期类型,createTime字段是Datetime类型。Java中没有Datetime类型,所以映射时用了日期类型-Date类。数据表中date类型就是date类型,保存的数据就是日期不包含时间。如果映射成Java中的日期类型,Java中日期类型还会有小时分钟秒毫秒,这些信息不应该存在。所以一个正确的ORM映射,就是把数据表中date类型字段映射到Java的string变量上。这样就只保存了日期数据,并不包含小时分钟秒毫秒之类的。
一、利用本地宝查询地区风险等级
本地宝H5网页提供了新冠疫情风险等级查询,在网页上面直接输入地区,就能查询到疫情的风险等级。
Java程序想要查询用户签到地区的风险等级,不能到页面里面点来点去的,所以我们要用URL传参的方式,把地址信息传入本地宝的H5页面。
你可以在浏览器地址栏填写下方的URL连接,就能查询到北京市西城区当前的新冠疫情风险等级。
http://m.bj.bendibao.com/news/yqdengji/?qu=西城区
从上面的案例推断,URL地址要传入两个参数: 城市编码 和 区县 。
城市编码可以从 tb_city 表中查询到,其中的code字段就是城市对应的编号。
我们可以用小程序提交过来的签到城市,然后到 tb_city 表中根据城市名称查询到城市编号。接下来,就可以把参数添加到URL上面。
我们想要提取查询到的风险等级结果应该怎么办呢?这个很简单,用Java程序解析本地宝HTML页面的标签,提取我们想要的结果信息即可。在Java领域中 jsoup 提供了解析HTML标签的功能,所以我们要在Java项目中引入 jsoup 库。
在 pom.xml 文件中添加 jsoup 依赖,然后重新reload项目
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
</dependency>
二、编写持久层代码
在 TbCityDao.xml 文件中添加查询语句
<select id="searchCode" parameterType="String" resultType="String">
SELECT code
FROM tb_city
WHERE city = #{city}
</select>
在 TbCityDao.java 接口中添加抽象方法
@Mapper
public interface TbCityDao {
public String searchCode(String city);
}
三、补充签到业务层代码
在 CheckinServiceImpl.java 文件中继续补充查询疫情风险等级的代码
@Autowired
private TbCityDao cityDao;
@Override
public void checkin(HashMap param) {
……
String faceModel=faceModelDao.searchFaceModel(userId);
if(faceModel==null){
throw new EmosException("不存在人脸模型");
}
else{
……
if("无法识别出人脸".equals(body)||"照片中存在多张人脸".equals(body)){
throw new EmosException(body);
}
else if("False".equals(body)){
throw new EmosException("签到无效,非本人签到");
}
else if("True".equals(body)){
//查询疫情风险等级
int risk=1;
String city= (String) param.get("city");
String district= (String) param.get("district");
String address= (String) param.get("address");
String country= (String) param.get("country");
String province= (String) param.get("province");
if(!StrUtil.isBlank(city)&&!StrUtil.isBlank(district)){
String code=cityDao.searchCode(city);
try{
String url = "http://m." + code + ".bendibao.com/news/yqdengji/?qu=" + district;
Document document=Jsoup.connect(url).get();
Elements elements=document.getElementsByClass("list-content");
if(elements.size()>0){
Element element=elements.get(0);
String result=element.select("p:last-child").text();
// result="高风险";
if("高风险".equals(result)){
risk=3;
//发送告警邮件
}
else if("中风险".equals(result)){
risk=2;
}
}
}catch (Exception e){
log.error("执行异常",e);
throw new EmosException("获取风险等级失败");
}
}
//保存签到记录
TbCheckin entity=new TbCheckin();
entity.setUserId(userId);
entity.setAddress(address);
entity.setCountry(country);
entity.setProvince(province);
entity.setCity(city);
entity.setDistrict(district);
entity.setStatus((byte) status);
entity.setRisk(risk);
entity.setDate(DateUtil.today());
entity.setCreateTime(d1);
checkinDao.insert(entity);
}
}
}
}
发送疫情高风险地区告警邮件
一、为什么要采用异步发送邮件?
因为在签到过程中,执行人脸识别和查询疫情风险等级,都比较消耗时间。如果发送邮件再做成同步执行的,势必导致签到执行时间过长,影响用户体验。由于要把签到结果保存到签到表,所以人脸识别和疫情风险等级查询必须是同步执行的。发送邮件跟保存签到数据没有直接关联,所以做成异步并行执行的程序更好一些,这样也能缩短用户签到时候等待的时间。
二、导入Email邮件库
编辑 pom.xml 文件,添加依赖库
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
三、设置SMTP服务器信息
发送邮件是通过SMTP服务器来完成的,所以我们要配置一下SMTP服务器的连接信息。这里我以163的SMTP服务器为例,并且提前已经开启了163邮箱的SMTP功能。
spring:
……
mail:
default-encoding: UTF-8
host: smtp.163.com
username: *************@163.com
password: 此处是密码
接下来我们把系统内的常用邮箱声明一下,以后会用到这些邮箱往外发送邮件,或者给这些邮箱发送内部邮件。例如,员工签到地点是疫情高风险地区,那么就应该向HR邮箱发送邮件,告知人事总监有员工需要隔离。
emos:
……
email:
system: *********@163.com
hr: **********@qq.com
二、实现异步发送邮件
在SpringBoot项目中开启异步多线程非常简单,只需要下面几个步骤即可。
在主类上面开启 @EnableAsync 注解
……
@EnableAsync
public class EmosWxApiApplication {
……
}
在 com.example.emos.wx.config 中创建 ThreadPoolConfig 类,声明Java线程池
@Configuration
public class ThreadPoolConfig {
@Bean("AsyncTaskExecutor")
public AsyncTaskExecutor taskExecutor(){
ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();
// 设置核心线程数
executor.setCorePoolSize(8);
// 设置最大线程数
executor.setMaxPoolSize(16);
// 设置队列容量
executor.setQueueCapacity(32);
// 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(60);
// 设置默认线程名称
executor.setThreadNamePrefix("task-");
// 设置拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
// 线程池对象自动注册给Spring项目了。
在 com.example.emos.wx.task 中创建 EmailTask 类,定义线程任务
@Component
@Scope("prototype")
public class EmailTask implements Serializable {
@Autowired
private JavaMailSender javaMailSender;
@Value("${emos.email.system}")
private String mailbox;
@Async
public void sendAsync(SimpleMailMessage message){
message.setFrom(mailbox);
// message.setCc(mailbox); // 抄送给自己
javaMailSender.send(message);
}
}
// @Component
// @Scope("prototype")
// Serializable
// @Async
// 都是必须的
查询员工的姓名和部门名称,在 TbUserDao.xml 文件中声明查询语句
<select id="searchNameAndDept" parameterType="int" resultType="HashMap">
SELECT u.name, d.dept_name
FROM tb_user u LEFT JOIN tb_dept d ON u.dept_id=d.id
WHERE u.id = #{userId} AND u.status = 1
</select>
在 TbUserDao 接口中定义抽象方法
public HashMap searchNameAndDept(int userId);
定义值注入变量,用来接收人员隔离告警邮件
@Value("${emos.email.hr}")
private String hrEmail;
@Autowired
private EmailTask emailTask;
@Autowired
private TbUserDao userDao;
编写发送告警邮件的代码
HashMap<String,String> map=userDao.searchNameAndDept(userId);
String name = map.get("name");
String deptName = map.get("dept_name");
deptName = deptName != null ? deptName : "";
SimpleMailMessage message=new SimpleMailMessage();
message.setTo(hrEmail);
message.setSubject("员工" + name + "身处高风险疫情地区警告");
message.setText(deptName + "员工" + name + "," + DateUtil.format(new Date(), "yyyy年MM月dd日") + "处于" + address + ",属于新冠疫情高风险地区,请及时与该员工联系,核实情况!");
emailTask.sendAsync(message);
实现人脸签到(Web层)
一、设置上传图片存储的路径
因为签到自拍照是临时使用,所以不需要存储在腾讯云对象存储中,我们只需要在本地找个文件夹存放这些签到照片,签到业务执行完,就立即删除该文件即可。
在 application.yml 文件中,设置图片存放路径
emos:
……
image-folder: D:/emos/image
在主类中添加初始化代码,项目启动时候自动创建图片文件夹
……
public class EmosWxApiApplication {
……
@Value("${emos.image-folder}")
private String imageFolder;
……
@PostConstruct
public void init(){
……
new File(imageFolder).mkdirs();
}
}
二、编辑Controller类
编辑 CheckinController.java 类,定义 checkin() 方法
@RequestMapping("/checkin")
@RestController
@Api("签到模块Web接口")
@Slf4j
public class CheckinController {
@Value("${emos.image-folder}")
private String imageFolder;
@PostMapping("/checkin")
@ApiOperation("签到")
public R checkin(@Valid CheckinForm form,@RequestParam("photo") MultipartFile file,@RequestHeader("token") String token){
if(file==null){
return R.error("没有上传文件");
}
int userId=jwtUtil.getUserId(token);
String fileName=file.getOriginalFilename().toLowerCase();
if(!fileName.endsWith(".jpg")){
return R.error("必须提交JPG格式图片");
}
else{
String path=imageFolder+"/"+fileName;
try{
file.transferTo(Paths.get(path));
HashMap param=new HashMap();
param.put("userId",userId);
param.put("path",path);
param.put("city",form.getCity());
param.put("district",form.getDistrict());
param.put("address",form.getAddress());
param.put("country",form.getCountry());
param.put("province",form.getProvince());
checkinService.checkin(param);
return R.ok("签到成功");
}catch (IOException e){
log.error(e.getMessage(),e);
throw new EmosException("图片保存错误");
}
finally {
FileUtil.del(path);
}
}
}
}
// 防止照片重名,加上时间戳
if (file != null) {
//获取上传文件名
fileName = file1.getOriginalFilename();
//获取后缀名
String sname = fileName.substring(fileName.lastIndexOf("."));
//时间格式化格式
SimpleDateFormat simpleDateFormat =new SimpleDateFormat("yyyyMMddHHmmssSSS");
//获取当前时间并作为时间戳
String timeStamp=simpleDateFormat.format(new Date());
//拼接新的文件名
String newName ="人脸识别"+timeStamp+sname;
//指定上传文件的路径
String path = "F:\" + newName;
//上传保存
file.transferTo(new File(path));
//保存当前文件路径
request.getSession().setAttribute("currFilePath", path);
}
创建新员工人脸模型数据(业务层)
一、编写抽象方法
如果用户是第一次签到,checkin方法检测到数据库中没有该员工的人脸模型数据,移动端会收到异常消息,所以要重新发送HTTP请求,让后端项目用签到照片创建人脸模型数据。所以我们先来把创建人脸模型的业务层抽象方法声明一下。
在 CheckinService 接口中,声明抽象方法
public interface CheckinService {
……
public void createFaceModel(int userId, String path);
}
二、编写创建人脸模型方法
在 CheckinServiceImpl 类中,实现抽象方法
……
public class CheckinServiceImpl implements CheckinService {
……
@Value("${emos.face.createFaceModelUrl}")
private String createFaceModelUrl;
……
@Override
public void createFaceModel(int userId, String path) {
HttpRequest request=HttpUtil.createPost(createFaceModelUrl);
request.form("photo",FileUtil.file(path));
request.form("code",code);
HttpResponse response=request.execute();
String body=response.body();
if("无法识别出人脸".equals(body)||"照片中存在多张人脸".equals(body)){
throw new EmosException(body);
}
else{
TbFaceModel entity=new TbFaceModel();
entity.setUserId(userId);
entity.setFaceModel(body);
faceModelDao.insert(entity);
}
}
}
创建新员工人脸模型数据(Web层)
在 CheckinController 类中创建 createFaceModel() 方法
@RequestMapping("/checkin")
@RestController
@Api("签到模块Web接口")
@Slf4j
public class CheckinController {
……
@PostMapping("/createFaceModel")
@ApiOperation("创建人脸模型")
public R createFaceModel(@RequestParam("photo") MultipartFile file,@RequestHeader("token") String token){
if(file==null){
return R.error("没有上传文件");
}
int userId=jwtUtil.getUserId(token);
String fileName=file.getOriginalFilename().toLowerCase();
if(!fileName.endsWith(".jpg")){
return R.error("必须提交JPG格式图片");
}
else{
String path=imageFolder+"/"+fileName;
try{
file.transferTo(Paths.get(path));
checkinService.createFaceModel(userId,path);
return R.ok("人脸建模成功");
}catch (IOException e){
log.error(e.getMessage(),e);
throw new EmosException("图片保存错误");
}
finally {
FileUtil.del(path);
}
}
}
}
实现人脸签到(移动端)
每人每天只可签到一次,调试时要删掉数据表数据。
163邮箱反垃圾邮件级别提升,会拦截咱们项目发送邮件,推荐使用阿里邮箱个人版。
application.yml 中修改 spring.mail 和 emos.email 项