目录
签到业务流程说明
一、需求介绍
二、如何获取地理信息?
三、如何判定某地区新冠疫情的风险等级?
开通腾讯位置服务
二、腾讯位置服务SDK
把定位坐标转换成真实地址
一、获取定位坐标
二、编辑签到页面
在Docker中安装人脸识别镜像
安装Docker程序
导入人脸识别镜像
运行人脸识别程序
一、创建Docker容器
二、运行人脸识别程序
三、接口调用
实现人脸签到(持久层)
一、维护员工人脸模型数据
二、保存签到记录
实现人脸签到(业务层)
一、判断签到用户是否存在人脸模型
查询签到所在地区新冠疫情风险等级
一、利用本地宝查询地区风险等级
二、编写持久层代码
三、补充签到业务层代码
发送疫情高风险地区告警邮件
一、为什么要采用异步发送邮件?
二、导入Email邮件库
三、设置SMTP服务器信息
二、实现异步发送邮件
实现人脸签到(Web层)
一、设置上传图片存储的路径
二、编辑Controller类
创建新员工人脸模型数据(业务层)
一、编写抽象方法
二、编写创建人脸模型方法
创建新员工人脸模型数据(Web层)
实现人脸签到(移动端)
签到业务流程说明
一、需求介绍
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 startservice docker stopservice 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映射到宿主机的/demodocker 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 -auxkill -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_modelWHERE user_id=#{userId}</select><insert id="insert" parameterType="com.example.emos.wx.db.pojo.TbFaceModel">INSERT INTO tb_face_modelSET user_id=#{userId},face_model=#{faceModel}</insert><delete id="deleteFaceModel" parameterType="int">DELETE FROM tb_face_modelWHERE 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_checkinSET 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_modelcheckinUrl: http://CentOS的IP地址:3000/checkincode: 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 {@Autowiredprivate TbFaceModelDao faceModelDao;@Value("${emos.face.checkinUrl}")private String checkinUrl;@Autowiredprivate SystemConstants constants;@Value("${emos.code}")private String code;@Overridepublic 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 codeFROM tb_cityWHERE 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-8host: smtp.163.comusername: *************@163.compassword: 此处是密码
接下来我们把系统内的常用邮箱声明一下,以后会用到这些邮箱往外发送邮件,或者给这些邮箱发送内部邮件。例如,员工签到地点是疫情高风险地区,那么就应该向HR邮箱发送邮件,告知人事总监有员工需要隔离。
emos:……email:system: *********@163.comhr: **********@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 {@Autowiredprivate JavaMailSender javaMailSender;@Value("${emos.email.system}")private String mailbox;@Asyncpublic 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_nameFROM tb_user u LEFT JOIN tb_dept d ON u.dept_id=d.idWHERE 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; ……@PostConstructpublic 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;……@Overridepublic 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 项