移动端考勤功能开发
parent
0a4ca3f570
commit
71d85458a4
|
@ -849,7 +849,7 @@ export default {
|
|||
this.$message.error("暂无模型,请先关联模型");
|
||||
} else {
|
||||
bimTools.addModelList(window.bimMgrApi, this.bimCfg, this.models, (hideParts) => {
|
||||
console.log(":--->", hideParts); debugger
|
||||
console.log(":--->", hideParts);
|
||||
this.loadDevicePosition();
|
||||
setTimeout(() => {
|
||||
bimTools.setDefaultViewPoint(window.bimMgrApi, this.bimCfg, this.viewPoint)
|
||||
|
|
|
@ -34,15 +34,45 @@ public class ImageSimilarityUtils {
|
|||
// OpenCV人脸检测器
|
||||
private static CascadeClassifier faceDetector;
|
||||
|
||||
// OpenCV是否成功加载的标志
|
||||
public static boolean openCVLoaded = false;
|
||||
|
||||
static {
|
||||
try {
|
||||
// 加载OpenCV库(使用openpnp的OpenCV依赖)
|
||||
// 尝试加载OpenCV库(使用openpnp的OpenCV依赖)
|
||||
System.out.println("开始加载OpenCV库...");
|
||||
nu.pattern.OpenCV.loadLocally();
|
||||
// 初始化人脸检测器(使用相对路径)
|
||||
String cascadePath = ImageSimilarityUtils.class.getClassLoader().getResource("opencv/haarcascade_frontalface_default.xml").getPath();
|
||||
faceDetector = new CascadeClassifier(cascadePath);
|
||||
openCVLoaded = true;
|
||||
System.out.println("OpenCV库加载成功");
|
||||
|
||||
try {
|
||||
// 初始化人脸检测器(使用相对路径)
|
||||
String cascadePath = ImageSimilarityUtils.class.getClassLoader().getResource("opencv/haarcascade_frontalface_default.xml").getPath();
|
||||
System.out.println("人脸检测器模型路径: " + cascadePath);
|
||||
faceDetector = new CascadeClassifier(cascadePath);
|
||||
|
||||
if (faceDetector.empty()) {
|
||||
System.err.println("人脸检测器初始化失败,将使用基础相似度计算方法");
|
||||
faceDetector = null;
|
||||
} else {
|
||||
System.out.println("人脸检测器初始化成功");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("人脸检测器初始化失败: " + e.getMessage());
|
||||
faceDetector = null;
|
||||
}
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
System.err.println("OpenCV库加载失败( UnsatisfiedLinkError),将使用基础相似度计算方法: " + e.getMessage());
|
||||
openCVLoaded = false;
|
||||
faceDetector = null;
|
||||
} catch (Exception e) {
|
||||
System.err.println("OpenCV加载失败,将使用基础相似度计算方法: " + e.getMessage());
|
||||
openCVLoaded = false;
|
||||
faceDetector = null;
|
||||
} catch (Throwable t) {
|
||||
System.err.println("OpenCV加载失败(未知错误),将使用基础相似度计算方法: " + t.getMessage());
|
||||
t.printStackTrace();
|
||||
openCVLoaded = false;
|
||||
faceDetector = null;
|
||||
}
|
||||
}
|
||||
|
@ -190,6 +220,7 @@ public class ImageSimilarityUtils {
|
|||
private static double calculateImageSimilarity(BufferedImage image1, BufferedImage image2) {
|
||||
// 如果OpenCV初始化失败,直接使用基础方法
|
||||
if (faceDetector == null) {
|
||||
System.out.println("使用基础相似度计算方法");
|
||||
return calculateImageSimilarityBasic(image1, image2);
|
||||
}
|
||||
|
||||
|
@ -272,6 +303,12 @@ public class ImageSimilarityUtils {
|
|||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
// 发生异常时使用原始方法
|
||||
System.out.println("OpenCV处理出现异常,使用基础相似度计算方法: " + e.getMessage());
|
||||
return calculateImageSimilarityBasic(image1, image2);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
e.printStackTrace();
|
||||
// OpenCV链接错误时使用原始方法
|
||||
System.out.println("OpenCV链接错误,使用基础相似度计算方法: " + e.getMessage());
|
||||
return calculateImageSimilarityBasic(image1, image2);
|
||||
}
|
||||
}
|
||||
|
@ -306,6 +343,11 @@ public class ImageSimilarityUtils {
|
|||
* @return 人脸区域矩形,如果未检测到人脸则返回null
|
||||
*/
|
||||
private static Rect detectFace(BufferedImage image) {
|
||||
// 如果OpenCV未正确加载,直接返回null
|
||||
if (faceDetector == null || !openCVLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 将BufferedImage转换为OpenCV Mat
|
||||
Mat mat = bufferedImageToMat(image);
|
||||
|
@ -335,9 +377,12 @@ public class ImageSimilarityUtils {
|
|||
return largestFace;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
System.err.println("OpenCV链接错误,无法检测人脸: " + e.getMessage());
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
System.err.println("人脸检测出现异常: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,16 @@ public class ProMobileAttendanceData extends BaseEntity
|
|||
@Excel(name = "考勤时间", width = 30, dateFormat = "yyyy-MM-dd")
|
||||
private Date attDate;
|
||||
|
||||
private String basePath;
|
||||
|
||||
public String getBasePath() {
|
||||
return basePath;
|
||||
}
|
||||
|
||||
public void setBasePath(String basePath) {
|
||||
this.basePath = basePath;
|
||||
}
|
||||
|
||||
/** 考勤照片 */
|
||||
@Excel(name = "考勤照片")
|
||||
private String attImg;
|
||||
|
|
|
@ -80,7 +80,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<if test="userId != null "> and userId = #{userId}</if>
|
||||
<if test="admitGuid != null and admitGuid != ''"> and admitGuid = #{admitGuid}</if>
|
||||
<if test="userName != null and userName != ''"> and userName like concat('%', #{userName}, '%')</if>
|
||||
<if test="inTime != null "> and date(inTime) = date(#{inTime})</if>
|
||||
<if test="inTime != null ">
|
||||
and (date(inTime) = date(#{inTime}) or date(outTime) = date(#{inTime}))
|
||||
</if>
|
||||
<if test="outTime != null "> and date(outTime) = date(#{outTime})</if>
|
||||
<if test="deviceNo != null and deviceNo != ''"> and deviceNo = #{deviceNo}</if>
|
||||
</where>
|
||||
|
|
|
@ -195,7 +195,6 @@
|
|||
<dependency>
|
||||
<groupId>org.openpnp</groupId>
|
||||
<artifactId>opencv</artifactId>
|
||||
<version>4.5.1-2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ import com.yanzhu.common.log.annotation.Log;
|
|||
import com.yanzhu.common.log.enums.BusinessType;
|
||||
import com.yanzhu.common.security.annotation.RequiresPermissions;
|
||||
import com.yanzhu.manage.domain.ProMobileAttendanceData;
|
||||
import com.yanzhu.manage.service.IAttendanceUbiDataService;
|
||||
import com.yanzhu.manage.service.IProMobileAttendanceDataService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
@ -39,6 +41,10 @@ public class ProMobileAttendanceConfigController extends BaseController
|
|||
@Autowired
|
||||
private IProMobileAttendanceConfigService proMobileAttendanceConfigService;
|
||||
|
||||
@Autowired
|
||||
private IAttendanceUbiDataService attendanceUbiDataService;
|
||||
@Autowired
|
||||
private IProMobileAttendanceDataService proMobileAttendanceDataService;
|
||||
/**
|
||||
* 查询移动端考勤配置列表
|
||||
*/
|
||||
|
@ -123,21 +129,24 @@ public class ProMobileAttendanceConfigController extends BaseController
|
|||
@PostMapping("/attendance")
|
||||
public AjaxResult attendance(@RequestBody ProMobileAttendanceData attData){
|
||||
//根据用户上传的照片与用户信息的照片计算相似度
|
||||
String userPicture=attData.getUserPicture();
|
||||
String attImg=attData.getAttImg();
|
||||
String userPicture =attData.getBasePath()+ attData.getUserPicture();
|
||||
String attImg=attData.getBasePath()+attData.getAttImg();
|
||||
|
||||
// 使用专门为人脸识别考勤优化的相似度计算
|
||||
double similarity = ImageSimilarityUtils.calculateFaceSimilarity(userPicture, attImg);
|
||||
|
||||
if (similarity>=0.8) {
|
||||
// 相似度达标,增加考勤数据
|
||||
// TODO: 增加考勤数据逻辑
|
||||
// TODO: 增加考勤历史记录逻辑
|
||||
|
||||
return AjaxResult.success("考勤成功,人脸匹配");
|
||||
// 根据是否使用OpenCV调整阈值
|
||||
// OpenCV场景下阈值为0.8,基础算法场景下阈值为0.75
|
||||
double threshold = ImageSimilarityUtils.openCVLoaded ? 0.8 : 0.75;
|
||||
|
||||
if (similarity >= threshold) {
|
||||
// 相似度达标,允许考勤
|
||||
int cnt=attendanceUbiDataService.addMobiileAttendanceData(attData);
|
||||
cnt+=proMobileAttendanceDataService.insertProMobileAttendanceData(attData);
|
||||
return AjaxResult.success("考勤成功,人脸匹配 (相似度: " + String.format("%.2f", similarity) + ")");
|
||||
} else {
|
||||
// 相似度不达标,拒绝考勤
|
||||
return AjaxResult.error("考勤失败,人脸不匹配");
|
||||
return AjaxResult.error("考勤失败,人脸不匹配 (相似度: " + String.format("%.2f", similarity) + ", 阈值: " + threshold + ")");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import java.util.Map;
|
|||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.yanzhu.manage.domain.AttendanceUbiData;
|
||||
import com.yanzhu.manage.domain.ProMobileAttendanceData;
|
||||
import com.yanzhu.manage.domain.ProProjectInfoSubdeptsUsers;
|
||||
|
||||
/**
|
||||
|
@ -116,4 +117,9 @@ public interface IAttendanceUbiDataService
|
|||
* @return
|
||||
*/
|
||||
List<AttendanceUbiData> getRealAttendance(Long prjId);
|
||||
|
||||
/**
|
||||
* 增加移动端考勤数据
|
||||
*/
|
||||
int addMobiileAttendanceData(ProMobileAttendanceData attData);
|
||||
}
|
||||
|
|
|
@ -12,10 +12,12 @@ import com.yanzhu.common.core.utils.DateUtils;
|
|||
import com.yanzhu.common.core.utils.StringUtils;
|
||||
import com.yanzhu.common.security.utils.SecurityUtils;
|
||||
import com.yanzhu.manage.domain.AttendanceUbiData;
|
||||
import com.yanzhu.manage.domain.ProMobileAttendanceData;
|
||||
import com.yanzhu.manage.domain.ProProjectInfoSubdeptsUsers;
|
||||
import com.yanzhu.manage.mapper.AttendanceUbiDataMapper;
|
||||
import com.yanzhu.manage.mapper.ProProjectInfoSubdeptsUsersMapper;
|
||||
import com.yanzhu.manage.service.IAttendanceUbiDataService;
|
||||
import com.yanzhu.manage.service.IProProjectInfoSubdeptsUsersService;
|
||||
import com.yanzhu.system.mapper.SysDictDataMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
@ -42,6 +44,8 @@ public class AttendanceUbiDataServiceImpl implements IAttendanceUbiDataService
|
|||
@Autowired
|
||||
private ProProjectInfoSubdeptsUsersMapper proProjectInfoSubdeptsUsersMapper;
|
||||
|
||||
@Autowired
|
||||
private IProProjectInfoSubdeptsUsersService proProjectInfoSubdeptsUsersService;
|
||||
/**
|
||||
* 查询考勤管理
|
||||
*
|
||||
|
@ -309,5 +313,49 @@ public class AttendanceUbiDataServiceImpl implements IAttendanceUbiDataService
|
|||
return attendanceUbiDataMapper.getRealAttendance(prjId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int addMobiileAttendanceData(ProMobileAttendanceData attData) {
|
||||
AttendanceUbiData attendance = new AttendanceUbiData();
|
||||
ProProjectInfoSubdeptsUsers user=proProjectInfoSubdeptsUsersService.findProSubDeptsUserInfo(attData.getProjectId(),attData.getUserId());
|
||||
if(user==null){
|
||||
return 0;
|
||||
}
|
||||
attendance.setProjectId(attData.getProjectId());
|
||||
attendance.setUserId(user.getUserId());
|
||||
attendance.setIsDel(0L);
|
||||
attendance.setComId(user.getComId());
|
||||
attendance.setComName(user.getComName());
|
||||
attendance.setProjectName(user.getProjectName());
|
||||
attendance.setSubDeptId(user.getSubDeptId());
|
||||
attendance.setSubDeptName(user.getSubDeptName());
|
||||
attendance.setUserName(user.getUserName());
|
||||
attendance.setSubDeptGroup(user.getSubDeptGroup());
|
||||
attendance.setSubDeptName(user.getSubDeptName());
|
||||
attendance.setCraftType(user.getCraftType());
|
||||
attendance.setCraftPost(user.getCraftPost());
|
||||
attendance.setDeviceNo("mobile");
|
||||
if("in".equals(attData.getInOut())){
|
||||
attendance.setInTime(attData.getAttDate());
|
||||
attendance.setInPhoto(attData.getAttImg());
|
||||
}else{
|
||||
attendance.setOutTime(attData.getAttDate());
|
||||
attendance.setOutPhoto(attData.getAttImg());
|
||||
}
|
||||
attendance.setCreateBy(SecurityContextHolder.getUserName());
|
||||
attendance.setCreateTime(DateUtils.getNowDate());
|
||||
|
||||
AttendanceUbiData where = new AttendanceUbiData();
|
||||
where.setProjectId(attData.getProjectId());
|
||||
where.setUserId(attData.getUserId());
|
||||
where.setInTime(attData.getAttDate());
|
||||
List<AttendanceUbiData> list=queryAttendaceInfo(where);
|
||||
if(list.size()>0){
|
||||
attendance.setId(list.get(0).getId());
|
||||
return updateAttendanceUbiData(attendance);
|
||||
}else{
|
||||
return insertAttendanceUbiData(attendance);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -482,3 +482,14 @@ export function getMobileAttendanceConfigById(id) {
|
|||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动端考勤
|
||||
*/
|
||||
export function mobileAttendance(data) {
|
||||
return request({
|
||||
url: "/manage/mobileAttendConfig/attendance",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ import fmt from "../../../utils/date.js";
|
|||
import { getToken, getUserInfo } from "../../../../utils/auth.js";
|
||||
import { securityFileUpload } from "../../../../utils/request.js"; // 导入文件上传工具
|
||||
const app = getApp();
|
||||
import { getMobileAttendanceConfigById } from "../../../../api/project.js";
|
||||
import { mobileAttendance } from "../../../../api/project.js";
|
||||
import { calculateDistance } from "../../../../utils/location.js"; // 导入计算距离的工具函数
|
||||
|
||||
import config from "../../../../config.js";
|
||||
Page({
|
||||
/**
|
||||
* 页面的初始数据
|
||||
|
@ -224,7 +224,6 @@ Page({
|
|||
});
|
||||
},
|
||||
doSave() {
|
||||
debugger;
|
||||
if (!this.data.faceImage) {
|
||||
app.toast("未获取到照片!");
|
||||
return;
|
||||
|
@ -374,6 +373,16 @@ Page({
|
|||
attDate: cfgData.attDate,
|
||||
attImg: this.data.faceImageUrl,
|
||||
cfgInfo: cfgData,
|
||||
basePath: config.baseImgUrl,
|
||||
};
|
||||
console.log("考勤数据", postData);
|
||||
mobileAttendance(postData).then((res) => {
|
||||
if (res.code == 200) {
|
||||
app.toast("考勤成功");
|
||||
this.returnToPage();
|
||||
} else {
|
||||
app.toast("考勤失败: " + res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
<view class="row-content">{{cfgData.attAddress}}</view>
|
||||
</view>
|
||||
<view class="box_map">
|
||||
<map id="attendanceMap" longitude="{{cfgData.attLongitude || cfgData.longitude}}" latitude="{{cfgData.attLatitude || cfgData.latitude}}" scale="14"
|
||||
<map id="attendanceMap" longitude="{{cfgData.attLongitude || cfgData.longitude}}" latitude="{{cfgData.attLatitude || cfgData.latitude}}" scale="15"
|
||||
show-location markers="{{mapMarkers}}" circles="{{mapCircles}}" style="width: 100%; height: 400rpx;">
|
||||
</map>
|
||||
</view>
|
||||
|
|
|
@ -49,7 +49,6 @@ Page({
|
|||
* 获取用户当前位置
|
||||
*/
|
||||
getUserLocation() {
|
||||
debugger;
|
||||
wx.getLocation({
|
||||
type: "gcj02", // 使用国测局坐标
|
||||
success: (res) => {
|
||||
|
|
|
@ -72,11 +72,6 @@
|
|||
<view class="inline-block"><rich-text nodes="{{distance.formatDistance(item.distance)}}"></rich-text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 显示考勤点的地址信息 -->
|
||||
<view class="content-row" wx:if="{{item.attAddress}}">
|
||||
当前位置:
|
||||
{{item.attAddress}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
|
@ -276,7 +276,7 @@ export default {
|
|||
}
|
||||
});
|
||||
},
|
||||
DelViewpoint(item, index) {debugger
|
||||
DelViewpoint(item, index) {
|
||||
let that = this;
|
||||
ElMessageBox.confirm(`确定要删除漫游 “${item.name}” 吗?`, "提示", {
|
||||
confirmButtonText: "确定",
|
||||
|
|
Loading…
Reference in New Issue