目录
签到业务流程说明
一、需求介绍
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;
} 
 
一、利用本地宝查询地区风险等级
本地宝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 项










