移动端考勤功能开发

dev_xd
lj7788@126.com 2025-09-04 17:46:13 +08:00
parent 0a4ca3f570
commit 71d85458a4
14 changed files with 161 additions and 28 deletions

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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>

View File

@ -195,7 +195,6 @@
<dependency>
<groupId>org.openpnp</groupId>
<artifactId>opencv</artifactId>
<version>4.5.1-2</version>
</dependency>
</dependencies>

View File

@ -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 + ")");
}
}
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -482,3 +482,14 @@ export function getMobileAttendanceConfigById(id) {
method: "get",
});
}
/**
* 移动端考勤
*/
export function mobileAttendance(data) {
return request({
url: "/manage/mobileAttendConfig/attendance",
method: "post",
data: data,
});
}

View File

@ -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);
}
});
},
});

View File

@ -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>

View File

@ -49,7 +49,6 @@ Page({
* 获取用户当前位置
*/
getUserLocation() {
debugger;
wx.getLocation({
type: "gcj02", // 使用国测局坐标
success: (res) => {

View File

@ -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>

View File

@ -276,7 +276,7 @@ export default {
}
});
},
DelViewpoint(item, index) {debugger
DelViewpoint(item, index) {
let that = this;
ElMessageBox.confirm(`确定要删除漫游 “${item.name}” 吗?`, "提示", {
confirmButtonText: "确定",