[add] 获取身份证、银行卡信息,人员实名、打卡,获取打卡信息

This commit is contained in:
lcj
2025-07-29 08:56:19 +08:00
parent e79c6b1ed3
commit c6efc08650
35 changed files with 1350 additions and 130 deletions

View File

@ -80,7 +80,7 @@ public class AuthController {
* @param body 登录信息
* @return 结果
*/
@ApiEncrypt
// @ApiEncrypt
@PostMapping("/login")
public R<LoginVo> login(@RequestBody String body) {
LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
@ -183,7 +183,7 @@ public class AuthController {
/**
* 用户注册
*/
@ApiEncrypt
// @ApiEncrypt
@PostMapping("/register")
public R<Void> register(@Validated @RequestBody RegisterBody user) {
if (!configService.selectRegisterEnabled(user.getTenantId())) {

View File

@ -8,7 +8,7 @@ ruoyi:
copyrightYear: 2024
captcha:
enable: true
enable: false
# 页面 <参数设置> 可开启关闭 验证码校验
# 验证码类型 math 数组计算 char 字符验证
type: MATH

View File

@ -0,0 +1,18 @@
package org.dromara.common.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author lilemy
* @date 2025-07-28 20:07
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GeoPoint {
private double lng;
private double lat;
private double alt;
}

View File

@ -1,8 +1,10 @@
package org.dromara.common.utils;
import cn.hutool.json.JSONUtil;
import org.dromara.common.constant.GeoJsonConstant;
import org.dromara.common.core.constant.HttpStatus;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.constant.GeoJsonConstant;
import org.dromara.common.domain.GeoPoint;
import org.dromara.facility.domain.dto.geojson.FacFeatureByPlane;
import org.dromara.facility.domain.dto.geojson.FacFeatureByPoint;
import org.dromara.facility.domain.dto.geojson.FacGeometry;
@ -267,4 +269,48 @@ public class JSTUtil {
return nearestPolygon;
}
/**
* 判断一个点是否在多个区域中,返回第一个匹配区域的点集合(否则 null
*
* @param lat 纬度
* @param lng 经度
* @param rangeListJson 多边形列表,每个为 JSON 数组(多边形的点数组)
* @return 匹配区域的 List<Point>,否则 null
*/
public static List<GeoPoint> findMatchingRange(String lat, String lng, List<String> rangeListJson) {
double latitude = Double.parseDouble(lat);
double longitude = Double.parseDouble(lng);
Point point = geometryFactory.createPoint(new Coordinate(longitude, latitude));
for (String rangeJson : rangeListJson) {
List<GeoPoint> polygonPoints = JSONUtil.toList(rangeJson, GeoPoint.class);
if (polygonPoints.size() < 3) continue; // 不是有效多边形
Polygon polygon = buildPolygon(polygonPoints);
if (polygon.contains(point)) {
return polygonPoints; // 找到匹配范围
}
}
return null;
}
/**
* 将点集合转换为 JTS 多边形
*/
private static Polygon buildPolygon(List<GeoPoint> points) {
Coordinate[] coordinates = points.stream()
.map(p -> new Coordinate(p.getLng(), p.getLat()))
.toArray(Coordinate[]::new);
// 需要闭合坐标环(首尾相连)
if (!coordinates[0].equals2D(coordinates[coordinates.length - 1])) {
Coordinate[] closed = new Coordinate[coordinates.length + 1];
System.arraycopy(coordinates, 0, closed, 0, coordinates.length);
closed[closed.length - 1] = coordinates[0];
coordinates = closed;
}
LinearRing shell = geometryFactory.createLinearRing(coordinates);
return geometryFactory.createPolygon(shell);
}
}

View File

@ -1,12 +1,13 @@
package org.dromara.common.utils.baiduUtil;
import cn.hutool.json.JSONUtil;
import com.google.gson.Gson;
import org.dromara.common.utils.baiduUtil.entity.AccessTokenResponse;
import org.glassfish.jaxb.runtime.v2.runtime.reflect.opt.Const;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
@ -20,7 +21,7 @@ import static org.dromara.common.constant.businessConstant.REDIS_BAIDU_KEY;
* @Author 铁憨憨
* @Date 2025/7/18 9:46
* @Version 1.0
*
* <p>
* 获取百度AccessToken
*/
@Service
@ -88,7 +89,7 @@ public class BaiDuCommon {
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
AccessTokenResponse tokenResponse = gson.fromJson(response.body(), AccessTokenResponse.class);
AccessTokenResponse tokenResponse = JSONUtil.toBean(response.body(), AccessTokenResponse.class);
return tokenResponse.getAccessToken();
} else {
throw new IOException("获取AccessToken失败状态码: " + response.statusCode());

View File

@ -1,12 +1,12 @@
package org.dromara.common.utils.baiduUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dromara.common.utils.baiduUtil.entity.face.ComparisonRes;
import org.dromara.common.utils.baiduUtil.entity.face.HumanFaceReq;
import org.dromara.common.utils.baiduUtil.entity.face.HumanFaceRes;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.IOException;
import java.net.URI;

View File

@ -1,6 +1,8 @@
package org.dromara.common.utils.baiduUtil;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.utils.baiduUtil.entity.ocr.IDCardInfo;
import org.dromara.common.utils.baiduUtil.entity.ocr.OcrReq;
import org.dromara.common.utils.baiduUtil.entity.ocr.Result;
@ -9,20 +11,18 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
/**
* @Author 铁憨憨
* @Date 2025/7/18 10:45
* @Version 1.0
*
* <p>
* 处理百度OCR相关逻辑身份证识别、银行卡识别
*/
@Slf4j
@Service
public class BaiDuOCR {
@Autowired
@ -30,20 +30,19 @@ public class BaiDuOCR {
@Autowired
private HttpClient httpClient;
@Autowired
private ObjectMapper objectMapper; //ObjectMapper 是 Java 处理 JSON 的核心工具,项目中使用它进行 JSON 与 Java 对象的相互转换。
private ObjectMapper objectMapper; //ObjectMapper 是 Java 处理 JSON 的核心工具,项目中使用它进行 JSON 与 Java 对象的相互转换。
String baseUrlTemplate = "https://aip.baidubce.com/rest/2.0/ocr/v1/bankcard?access_token=%s";
String BASE_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard?access_token=%s";
String baseUrlTemplate = "https://aip.baidubce.com/rest/2.0/ocr/v1/bankcard";
String BASE_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard";
/**
* @param vr 请求参数
* @return 识别结果Result
* @description 银行卡OCR识别
* @author 铁憨憨
* @date 2025/7/18 11:50
* @param vr 请求参数
* @return 识别结果Result
**/
public Result bankCardOCRRecognition(OcrReq vr) {
// 先从缓存里面捞取token若为空直接抛出异常
@ -53,62 +52,41 @@ public class BaiDuOCR {
}
try {
// 构建请求URL包含token参数
String requestUrl = String.format(baseUrlTemplate, URLEncoder.encode(atStr, StandardCharsets.UTF_8));
// 准备请求体将请求参数转为JSON
String requestBody = objectMapper.writeValueAsString(vr);
if (requestBody == null) {
throw new RuntimeException("请求参数序列化失败");
String param;
if (vr.getImage() != null) {
String imgParam = URLEncoder.encode(vr.getImage(), StandardCharsets.UTF_8);
param = "image=" + imgParam;
} else if (vr.getUrl() != null) {
param = "url=" + vr.getUrl();
} else {
throw new RuntimeException("请传入图片或图片URL");
}
// 构建HTTP请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(requestUrl))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
String resultStr = HttpUtil.post(baseUrlTemplate, atStr, param);
// 发送请求并获取响应
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
// 处理响应状态码非200直接抛出异常
if (response.statusCode() != 200) {
throw new RuntimeException("接口请求失败,状态码:" + response.statusCode() + ",响应信息:" + response.body());
}
// 解析响应结果(若解析失败抛出异常)
Result result = objectMapper.readValue(response.body(), Result.class);
log.info("百度OCR识别结果{}", resultStr);
Result result = JSON.parseObject(resultStr, Result.class);
if (result == null) {
throw new RuntimeException("响应结果解析失败");
throw new RuntimeException("未识别到银行卡信息");
}
// // 若接口返回错误码如百度API的error_code抛出异常
// if (result.getErrorCode() != null && result.getErrorCode() != 0) {
// throw new RuntimeException("接口返回错误:" + result.getErrorMessage() + "(错误码:" + result.getErrorCode() + "");
// }
return result;
} catch (IOException e) {
// IO异常如序列化失败、网络IO错误
throw new RuntimeException("IO处理异常" + e.getMessage(), e);
} catch (InterruptedException e) {
// 线程中断异常
Thread.currentThread().interrupt(); // 恢复中断状态
throw new RuntimeException("请求被中断:" + e.getMessage(), e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param request 请求参数
* @return 识别结果WordsResult
* @description 身份证OCR识别
* @author 铁憨憨
* @date 2025/7/18 16:53
* @param request 请求参数
* @return 识别结果WordsResult
**/
public WordsResult idCardOCR(OcrReq request) {
// 获取AccessToken为空直接抛异常
@ -117,41 +95,28 @@ public class BaiDuOCR {
throw new RuntimeException("获取访问令牌失败token为空");
}
// 构建请求URL
String requestUrl = String.format(BASE_URL, accessToken);
try {
// 准备请求体(序列化失败抛异常)
String requestBody = objectMapper.writeValueAsString(request);
if (requestBody == null) {
throw new RuntimeException("请求参数序列化失败");
}
// 构建HTTP请求
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create(requestUrl))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
// 发送请求并获取响应
HttpResponse<String> response = httpClient.send(
httpRequest,
HttpResponse.BodyHandlers.ofString()
);
// 处理响应状态码非200直接抛异常
if (response.statusCode() != 200) {
throw new RuntimeException("接口请求失败,状态码:" + response.statusCode() + ",响应信息:" + response.body());
String param = "id_card_side=" + request.getIdCardSide();
if (request.getImage() != null) {
String imgParam = URLEncoder.encode(request.getImage(), StandardCharsets.UTF_8);
param = param + "&image=" + imgParam;
} else if (request.getUrl() != null) {
param = param + "&url=" + request.getUrl();
} else {
throw new RuntimeException("请传入图片或图片URL");
}
// 处理响应内容(去除空格并解析)
String responseBody = response.body().replaceAll("\\s+", "");
IDCardInfo idCardInfo = objectMapper.readValue(responseBody, IDCardInfo.class);
// 构建HTTP请求
String result = HttpUtil.post(BASE_URL, accessToken, param);
// 处理响应内容
IDCardInfo idCardInfo = JSON.parseObject(result, IDCardInfo.class);
if (idCardInfo == null) {
throw new RuntimeException("响应结果解析失败");
}
// 检查身份证状态状态异常由validateImageStatus抛异常
validateImageStatus(idCardInfo.getImageStatus());
@ -166,20 +131,18 @@ public class BaiDuOCR {
} catch (IOException e) {
// IO异常序列化、网络IO等
throw new RuntimeException("IO处理异常" + e.getMessage(), e);
} catch (InterruptedException e) {
// 线程中断异常
Thread.currentThread().interrupt(); // 恢复中断状态
throw new RuntimeException("请求被中断:" + e.getMessage(), e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param imageStatus 图片状态
* @throws RuntimeException 当状态不正常时抛出运行时异常
* @description 验证身份证图片状态
* @author 铁憨憨
* @date 2025/7/18 17:08
* @param imageStatus 图片状态
* @throws RuntimeException 当状态不正常时抛出运行时异常
**/
private void validateImageStatus(String imageStatus) {
if (imageStatus == null || "normal".equals(imageStatus)) {

View File

@ -0,0 +1,77 @@
package org.dromara.common.utils.baiduUtil;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
/**
* http 工具类
*/
public class HttpUtil {
public static String post(String requestUrl, String accessToken, String params)
throws Exception {
String contentType = "application/x-www-form-urlencoded";
return HttpUtil.post(requestUrl, accessToken, contentType, params);
}
public static String post(String requestUrl, String accessToken, String contentType, String params)
throws Exception {
String encoding = "UTF-8";
if (requestUrl.contains("nlp")) {
encoding = "GBK";
}
return HttpUtil.post(requestUrl, accessToken, contentType, params, encoding);
}
public static String post(String requestUrl, String accessToken, String contentType, String params, String encoding)
throws Exception {
String url = requestUrl + "?access_token=" + accessToken;
return HttpUtil.postGeneralUrl(url, contentType, params, encoding);
}
public static String postGeneralUrl(String generalUrl, String contentType, String params, String encoding)
throws Exception {
URL url = new URL(generalUrl);
// 打开和URL之间的连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
// 设置通用的请求属性
connection.setRequestProperty("Content-Type", contentType);
connection.setRequestProperty("Connection", "Keep-Alive");
connection.setUseCaches(false);
connection.setDoOutput(true);
connection.setDoInput(true);
// 得到请求的输出流对象
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.write(params.getBytes(encoding));
out.flush();
out.close();
// 建立实际的连接
connection.connect();
// 获取所有响应头字段
Map<String, List<String>> headers = connection.getHeaderFields();
// 遍历所有的响应头字段
for (String key : headers.keySet()) {
System.err.println(key + "--->" + headers.get(key));
}
// 定义 BufferedReader输入流来读取URL的响应
BufferedReader in = null;
in = new BufferedReader(
new InputStreamReader(connection.getInputStream(), encoding));
String result = "";
String getLine;
while ((getLine = in.readLine()) != null) {
result += getLine;
}
in.close();
System.err.println("result:" + result);
return result;
}
}

View File

@ -1,10 +1,11 @@
package org.dromara.common.utils.baiduUtil.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serial;
import java.io.Serializable;
/**
* @Author 铁憨憨
* @Date 2025/7/18 9:56
@ -12,7 +13,10 @@ import lombok.experimental.Accessors;
*/
@Data
@Accessors(chain = true)
public class AccessTokenResponse {
public class AccessTokenResponse implements Serializable {
@Serial
private static final long serialVersionUID = 5429166046159819095L;
private String accessToken;
private int expiresIn;
private String scope;

View File

@ -1,13 +1,14 @@
package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import org.dromara.system.domain.vo.SysOssVo;
/**
* @Author 铁憨憨
* @Date 2025/7/18 10:58
* @Version 1.0
*
* <p>
* 银行卡具体信息
*/
@Data
@ -22,18 +23,23 @@ import lombok.Data;
public class BankData {
@JSONField(name = "bank_card_number",label = "银行卡卡号")
@JSONField(name = "bank_card_number", label = "银行卡卡号")
private String bankCardNumber;
@JSONField(name = "valid_date",label = "有效期")
@JSONField(name = "valid_date", label = "有效期")
private String validDate;
@JSONField(name = "bank_card_type",label = "银行卡类型0不能识别; 1借记卡; 2贷记卡原信用卡大部分为贷记卡; 3准贷记卡; 4预付费卡")
@JSONField(name = "bank_card_type", label = "银行卡类型0不能识别; 1借记卡; 2贷记卡原信用卡大部分为贷记卡; 3准贷记卡; 4预付费卡")
private int bankCardType;
@JSONField(name = "bank_name",label = "银行名,不能识别时为空")
@JSONField(name = "bank_name", label = "银行名,不能识别时为空")
private String bankName;
@JSONField(name = "holder_name",label = "持卡人姓名,不能识别时为空")
@JSONField(name = "holder_name", label = "持卡人姓名,不能识别时为空")
private String holderName;
/**
* 银行卡图片信息
*/
private SysOssVo image;
}

View File

@ -1,5 +1,6 @@
package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
/**
@ -11,6 +12,8 @@ import lombok.Data;
*/
@Data
public class Field {
@JSONField(name = "location")
private Location location; // 位置信息
@JSONField(name = "words")
private String words; // 识别文字
}

View File

@ -1,6 +1,6 @@
package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
/**

View File

@ -1,18 +1,26 @@
package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
/**
* @Author 铁憨憨
* @Date 2025/7/18 10:56
* @Version 1.0
*
* <p>
* 位置信息(坐标)
*/
@Data
public class Location {
private int top; // 顶部坐标
private int left; // 左侧坐标
private int width; // 宽度
private int height; // 高度
@JSONField(name = "top")
private int top;
@JSONField(name = "left")
private int left;
@JSONField(name = "width")
private int width;
@JSONField(name = "height")
private int height;
}

View File

@ -1,24 +1,30 @@
package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson2.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @Author 铁憨憨
* @Date 2025/7/18 10:47
* @Version 1.0
*
* <p>
* OCR请求参数身份证/银行卡通用)
*/
@Data
public class OcrReq {
public class OcrReq implements Serializable {
@Serial
private static final long serialVersionUID = -8670697823104813789L;
private String image; // 图像base64与url二选一
private String url; // 图像URL与image二选一
@JSONField(name = "id_card_side")
@JsonProperty("id_card_side")
private String idCardSide; // 身份证正反面front/back银行卡无需
@JSONField(name = "detect_photo")
@JsonProperty("detect_photo")
private boolean detectPhoto;// 是是否开启银行卡质量类型(清晰模糊、边框/四角不完整检测功能默认不开启false。
}

View File

@ -1,6 +1,6 @@
package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
/**

View File

@ -1,33 +1,38 @@
package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson2.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import org.dromara.system.domain.vo.SysOssVo;
/**
* @Author 铁憨憨
* @Date 2025/7/18 10:50
* @Version 1.0
*
* <p>
* 身份证识别字段集合
*/
@Data
public class WordsResult {
@JSONField(name = "姓名")
private Field name; // 姓名
private Field name;
@JSONField(name = "民族")
private Field nation; // 民族
private Field nation;
@JSONField(name = "住址")
private Field address; // 住址
private Field address;
@JSONField(name = "公民身份号码")
private Field citizenIdentification; // 身份证号
private Field citizenIdentification;
@JSONField(name = "出生")
private Field birth; // 出生日期
private Field birth;
@JSONField(name = "性别")
private Field gender; // 性别
@JSONField(name = "失效日期")
private Field expirationDate; // 失效日期
@JSONField(name = "签发机关")
private Field issuingAuthority; // 签发机关
@JSONField(name = "签发日期")
private Field issueDate; // 签发日期
private Field gender;
/**
* 身份证图片信息
*/
private SysOssVo image;
}

View File

@ -0,0 +1,77 @@
package org.dromara.contractor.controller.app;
import jakarta.annotation.Resource;
import org.dromara.common.core.domain.R;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.utils.baiduUtil.entity.ocr.BankData;
import org.dromara.common.utils.baiduUtil.entity.ocr.WordsResult;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.domain.dto.constructionuser.SubConstructionUserAuthenticationReq;
import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserVo;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 施工人员app
*
* @author lilemy
* @date 2025-07-23 18:44
*/
@Validated
@RestController
@RequestMapping("/app/contractor/constructionUser")
public class SubConstructionUserAppController {
@Resource
private ISubConstructionUserService constructionUserService;
/**
* 获取当前登录用户实名信息
*/
@GetMapping("/loginUser")
public R<SubConstructionUserVo> getLoginUserInfo() {
SubConstructionUser constructionUser = constructionUserService.getBySysUserId(LoginHelper.getUserId());
return R.ok(constructionUserService.getVo(constructionUser));
}
/**
* 根据身份证图片获取身份证信息
*/
@Log(title = "施工人员", businessType = BusinessType.OTHER)
@PostMapping("/idCard")
public R<WordsResult> getIdCardMessage(@RequestParam("file") MultipartFile file, String idCardSide) {
return R.ok(constructionUserService.getIdCardMessageByPic(file, idCardSide));
}
/**
* 根据银行卡图片获取银行卡信息
*/
@Log(title = "施工人员", businessType = BusinessType.OTHER)
@PostMapping("/bankCard")
public R<BankData> getBankCardMessage(@RequestParam("file") MultipartFile file) {
return R.ok(constructionUserService.getBankCardMessageByPic(file));
}
/**
* 施工人员实名认证
*/
@Log(title = "施工人员", businessType = BusinessType.INSERT)
@PostMapping("/authentication")
public R<Long> insertByAuthentication(@RequestParam("file") MultipartFile file, @Validated SubConstructionUserAuthenticationReq req) {
return R.ok(constructionUserService.insertByAuthentication(file, req));
}
/**
* 施工人员人脸比对
*/
@Log(title = "施工人员", businessType = BusinessType.OTHER)
@PostMapping("/face/comparison")
public R<Boolean> faceComparison(@RequestParam("file") MultipartFile file) {
return R.ok(constructionUserService.faceComparison(file));
}
}

View File

@ -34,6 +34,11 @@ public class SubConstructionUser extends BaseEntity {
*/
private String facePic;
/**
* 系统用户id
*/
private Long sysUserId;
/**
* 人员姓名
*/

View File

@ -0,0 +1,150 @@
package org.dromara.contractor.domain.dto.constructionuser;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.dromara.common.core.constant.RegexConstants;
import java.io.Serial;
import java.io.Serializable;
/**
* @author lilemy
* @date 2025-07-24 10:48
*/
@Data
public class SubConstructionUserAuthenticationReq implements Serializable {
@Serial
private static final long serialVersionUID = 1001024234468070802L;
/**
* 人员姓名
*/
@NotBlank(message = "人员姓名不能为空")
private String userName;
/**
* 项目id
*/
@NotNull(message = "项目id不能为空")
private Long projectId;
/**
* 分包公司id
*/
@NotNull(message = "分包公司id不能为空")
private Long contractorId;
/**
* 联系电话
*/
@NotBlank(message = "联系电话不能为空")
@Pattern(regexp = RegexConstants.MOBILE, message = "手机号格式不正确")
private String phone;
/**
* 0:保密 1:男 2女
*/
@NotBlank(message = "性别不能为空")
private String sex;
/**
* 民族
*/
@NotBlank(message = "民族不能为空")
private String nation;
/**
* 身份证正面图片
*/
@NotBlank(message = "身份证正面图片不能为空")
private String sfzFrontPic;
/**
* 身份证反面图片
*/
@NotBlank(message = "身份证反面图片不能为空")
private String sfzBackPic;
/**
* 身份证号码
*/
@NotBlank(message = "身份证号码不能为空")
private String sfzNumber;
/**
* 身份证有效开始期
*/
@NotBlank(message = "身份证有效开始期不能为空")
private String sfzStart;
/**
* 身份证有效结束期
*/
@NotBlank(message = "身份证有效结束期不能为空")
private String sfzEnd;
/**
* 身份证地址
*/
@NotBlank(message = "身份证地址不能为空")
private String sfzSite;
/**
* 身份证出生日期
*/
@NotBlank(message = "身份证出生日期不能为空")
private String sfzBirth;
/**
* 籍贯
*/
@NotBlank(message = "籍贯不能为空")
private String nativePlace;
/**
* 银行卡图片
*/
@NotBlank(message = "银行卡图片不能为空")
private String yhkPic;
/**
* 银行卡号
*/
@NotBlank(message = "银行卡号不能为空")
private String yhkNumber;
/**
* 开户行
*/
private String yhkOpeningBank;
/**
* 持卡人
*/
private String yhkCardholder;
/**
* 工种
*/
@NotBlank(message = "工种不能为空")
private String typeOfWork;
/**
* 工资计量单位
*/
private String wageMeasureUnit;
/**
* 特种工作证图片
*/
private String specialWorkPic;
/**
* 备注
*/
private String remark;
}

View File

@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.utils.baiduUtil.entity.ocr.BankData;
import org.dromara.common.utils.baiduUtil.entity.ocr.WordsResult;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.domain.dto.constructionuser.*;
import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserVo;
@ -12,6 +14,7 @@ import org.dromara.contractor.domain.exportvo.BusConstructionUserExportVo;
import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserAttendanceMonthVo;
import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserAttendanceTotalVo;
import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserGisVo;
import org.springframework.web.multipart.MultipartFile;
import java.util.Collection;
import java.util.List;
@ -172,4 +175,47 @@ public interface ISubConstructionUserService extends IService<SubConstructionUse
Page<SubConstructionUserAttendanceTotalVo> getAttendanceTotalVoPage(SubConstructionUserAttendanceQueryReq req,
PageQuery pageQuery);
/**
* 获取施工人员身份证信息
*
* @param file 图片文件
* @param idCardSide 身份证正反面front/back
* @return 身份证信息
*/
WordsResult getIdCardMessageByPic(MultipartFile file, String idCardSide);
/**
* 获取施工人员银行卡信息
*
* @param file 图片文件
* @return 银行卡信息
*/
BankData getBankCardMessageByPic(MultipartFile file);
/**
* 实名认证
*
* @param file 人脸图片
* @param req 身份信息认证对象
* @return 是否认证成功
*/
Long insertByAuthentication(MultipartFile file, SubConstructionUserAuthenticationReq req);
/**
* 人脸识别
*
* @param file 图片文件
* @return 是否匹配成功
*/
Boolean faceComparison(MultipartFile file);
/**
* 根据系统用户id查询施工人员
*
* @param sysUserId 系统用户id
* @return 施工人员
*/
SubConstructionUser getBySysUserId(Long sysUserId);
}

View File

@ -1,6 +1,7 @@
package org.dromara.contractor.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.IdcardUtil;
import cn.hutool.core.util.PhoneUtil;
@ -18,8 +19,17 @@ import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.oss.core.OssClient;
import org.dromara.common.oss.exception.OssException;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.utils.IdCardEncryptorUtil;
import org.dromara.common.utils.baiduUtil.BaiDuFace;
import org.dromara.common.utils.baiduUtil.BaiDuOCR;
import org.dromara.common.utils.baiduUtil.entity.face.HumanFaceReq;
import org.dromara.common.utils.baiduUtil.entity.ocr.BankData;
import org.dromara.common.utils.baiduUtil.entity.ocr.OcrReq;
import org.dromara.common.utils.baiduUtil.entity.ocr.WordsResult;
import org.dromara.contractor.constant.SubConstructionUserConstant;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.domain.SubConstructionUserFile;
@ -48,7 +58,12 @@ import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.YearMonth;
import java.util.*;
import java.util.stream.Collectors;
@ -99,6 +114,12 @@ public class SubConstructionUserServiceImpl extends ServiceImpl<SubConstructionU
@Resource
private IdCardEncryptorUtil idCardEncryptorUtil;
@Resource
private BaiDuOCR baiDuOCR;
@Resource
private BaiDuFace baiDuFace;
/**
* 查询施工人员
*
@ -956,4 +977,193 @@ public class SubConstructionUserServiceImpl extends ServiceImpl<SubConstructionU
return constructionUserAttendanceTotalPage;
}
/**
* 获取施工人员身份证信息
*
* @param file 图片文件
* @param idCardSide 身份证正反面front/back
* @return 身份证信息
*/
@Override
public WordsResult getIdCardMessageByPic(MultipartFile file, String idCardSide) {
if (file == null) {
throw new ServiceException("请上传图片", HttpStatus.BAD_REQUEST);
}
if (StringUtils.isBlank(idCardSide)) {
throw new ServiceException("请选择身份证正反面", HttpStatus.BAD_REQUEST);
}
if (!"front".equals(idCardSide) && !"back".equals(idCardSide)) {
throw new ServiceException("请选择正确的身份证正反面", HttpStatus.BAD_REQUEST);
}
String base64;
try {
// 获取文件字节数组
byte[] bytes = file.getBytes();
// Base64 编码
base64 = Base64.getEncoder().encodeToString(bytes);
} catch (IOException e) {
throw new ServiceException("图片转换失败,请重新上传");
}
OcrReq request = new OcrReq();
request.setImage(base64);
request.setIdCardSide(idCardSide);
WordsResult wordsResult = baiDuOCR.idCardOCR(request);
if (wordsResult == null) {
throw new ServiceException("识别失败,请重新上传");
}
// 获取数据成功,保存图片信息
SysOssVo upload = ossService.upload(file);
wordsResult.setImage(upload);
return wordsResult;
}
/**
* 获取施工人员银行卡信息
*
* @param file 图片文件
* @return 银行卡信息
*/
@Override
public BankData getBankCardMessageByPic(MultipartFile file) {
if (file == null) {
throw new ServiceException("请上传图片", HttpStatus.BAD_REQUEST);
}
String base64;
try {
// 获取文件字节数组
byte[] bytes = file.getBytes();
// Base64 编码
base64 = Base64.getEncoder().encodeToString(bytes);
} catch (IOException e) {
throw new ServiceException("图片转换失败,请重新上传");
}
OcrReq request = new OcrReq();
request.setImage(base64);
request.setDetectPhoto(false);
BankData res = baiDuOCR.bankCardOCRRecognition(request).getRes();
if (res == null) {
throw new ServiceException("识别失败,请重新上传");
}
// 获取数据成功,保存图片信息
SysOssVo upload = ossService.upload(file);
res.setImage(upload);
return res;
}
/**
* 实名认证
*
* @param file 人脸图片
* @param req 身份信息认证对象
* @return 是否认证成功
*/
@Override
public Long insertByAuthentication(MultipartFile file, SubConstructionUserAuthenticationReq req) {
// 先进行人脸识别
if (file == null) {
throw new ServiceException("请上传图片", HttpStatus.BAD_REQUEST);
}
String base64;
try {
// 获取文件字节数组
byte[] bytes = file.getBytes();
// Base64 编码
base64 = URLEncoder.encode(Base64.getEncoder().encodeToString(bytes), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new ServiceException("图片转换失败,请重新上传");
}
HumanFaceReq request = new HumanFaceReq();
request.setImage(base64);
baiDuFace.humanFace(request);
// 人脸识别成功,保存人脸数据
SubConstructionUser user = new SubConstructionUser();
BeanUtils.copyProperties(req, user);
SysOssVo upload = ossService.upload(file);
user.setFacePic(upload.getOssId().toString());
// 关联系统用户
Long userId = LoginHelper.getUserId();
// 判断当前系统用户是否已关联
Long count = this.lambdaQuery()
.eq(SubConstructionUser::getSysUserId, userId)
.count();
if (count > 0) {
throw new ServiceException("当前用户已关联施工人员信息");
}
user.setSysUserId(userId);
// 保存施工人员信息
boolean save = this.save(user);
if (!save) {
throw new ServiceException("施工人员信息保存失败");
}
return user.getId();
}
/**
* 人脸识别
*
* @param file 图片文件
* @return 是否匹配成功
*/
@Override
public Boolean faceComparison(MultipartFile file) {
if (file == null) {
throw new ServiceException("请上传图片", HttpStatus.BAD_REQUEST);
}
String reqBase64;
try {
// 获取文件字节数组
byte[] bytes = file.getBytes();
// Base64 编码
reqBase64 = URLEncoder.encode(Base64.getEncoder().encodeToString(bytes), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new ServiceException("图片转换失败,请重新上传");
}
HumanFaceReq request = new HumanFaceReq();
request.setImage(reqBase64);
Long userId = LoginHelper.getUserId();
SubConstructionUser constructionUser = this.getById(userId);
if (constructionUser == null || StringUtils.isBlank(constructionUser.getFacePic())) {
throw new ServiceException("未进行实名认证");
}
String facePic = constructionUser.getFacePic();
SysOssVo sysOssVo = ossService.getById(Long.parseLong(facePic));
if (sysOssVo == null) {
throw new ServiceException("未进行实名认证");
}
// 获取文件输入流
OssClient storage = OssFactory.instance(sysOssVo.getService());
String path = sysOssVo.getUrl();
String faceBase64;
try (InputStream is = storage.getObjectContent(path)) {
byte[] bytes = IoUtil.readBytes(is);
// Base64 编码
faceBase64 = URLEncoder.encode(Base64.getEncoder().encodeToString(bytes), StandardCharsets.UTF_8);
} catch (IOException e) {
// 针对单个文件处理异常,可以选择记录日志或终止处理
throw new OssException("处理文件[" + path + "]失败,错误信息: " + e.getMessage());
}
HumanFaceReq faceReq = new HumanFaceReq();
faceReq.setImage(faceBase64);
List<HumanFaceReq> list = List.of(request, faceReq);
double comparison = baiDuFace.comparison(list);
return comparison > 80;
}
/**
* 根据系统用户id查询施工人员
*
* @param sysUserId 系统用户id
* @return 施工人员
*/
@Override
public SubConstructionUser getBySysUserId(Long sysUserId) {
SubConstructionUser constructionUser = this.lambdaQuery()
.eq(SubConstructionUser::getSysUserId, sysUserId)
.one();
if (constructionUser == null) {
throw new ServiceException("实名认证信息不存在", HttpStatus.NOT_FOUND);
}
return constructionUser;
}
}

View File

@ -0,0 +1,69 @@
package org.dromara.project.controller.app;
import jakarta.annotation.Resource;
import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.dromara.project.domain.dto.attendance.BusAttendanceByDayReq;
import org.dromara.project.domain.dto.attendance.BusAttendanceByMonthReq;
import org.dromara.project.domain.dto.attendance.BusAttendanceMonthByUserIdReq;
import org.dromara.project.domain.dto.attendance.BusAttendancePunchCardByFaceReq;
import org.dromara.project.domain.vo.attendance.BusAttendanceMonthByUserIdVo;
import org.dromara.project.domain.vo.attendance.BusAttendanceVo;
import org.dromara.project.service.IBusAttendanceService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 考勤app
*
* @author lilemy
* @date 2025-07-24 11:53
*/
@Validated
@RestController
@RequestMapping("/app/project/attendance")
public class BusAttendanceAppController {
@Resource
private IBusAttendanceService attendanceService;
@Resource
private ISubConstructionUserService constructionUserService;
/**
* 获取当前登录用户的考勤列表
*
* @param req 查询参数
* @return 考勤列表
*/
@GetMapping("/list/loginUser")
public R<List<BusAttendanceVo>> listLoginUser(BusAttendanceByDayReq req) {
return R.ok(attendanceService.getDayByUserId(LoginHelper.getUserId(), req.getClockDate()));
}
/**
* 获取当前登录用户月份考勤列表
*/
@GetMapping("/list/month/loginUser")
public R<List<BusAttendanceMonthByUserIdVo>> listAttendanceMonthListByUserId(BusAttendanceByMonthReq req) {
Long userId = LoginHelper.getUserId();
SubConstructionUser constructionUser = constructionUserService.getBySysUserId(userId);
BusAttendanceMonthByUserIdReq dto = new BusAttendanceMonthByUserIdReq();
dto.setUserId(constructionUser.getId());
dto.setClockMonth(req.getClockMonth());
return R.ok(attendanceService.listAttendanceMonthListByUserId(dto));
}
/**
* 人脸坐标打卡
*/
@PostMapping("/punch/card/face")
public R<Boolean> punchCardByFace(@RequestPart("file") MultipartFile file, BusAttendancePunchCardByFaceReq req) {
return R.ok(attendanceService.punchCardByFace(file, req));
}
}

View File

@ -0,0 +1,42 @@
package org.dromara.project.controller.app;
import jakarta.annotation.Resource;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.dromara.project.domain.dto.leave.BusLeaveQueryReq;
import org.dromara.project.domain.vo.leave.BusLeaveVo;
import org.dromara.project.service.IBusLeaveService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lilemy
* @date 2025-07-24 14:51
*/
@Validated
@RestController
@RequestMapping("/app/project/leave")
public class BusLeaveAppController {
@Resource
private IBusLeaveService leaveService;
@Resource
private ISubConstructionUserService constructionUserService;
/**
* 查询当前登录用户请假申请列表
*/
@GetMapping("/list/loginUser")
public TableDataInfo<BusLeaveVo> listByLoginUser(BusLeaveQueryReq req, PageQuery pageQuery) {
SubConstructionUser constructionUser = constructionUserService.getBySysUserId(LoginHelper.getUserId());
req.setUserId(constructionUser.getId());
return leaveService.queryPageList(req, pageQuery);
}
}

View File

@ -0,0 +1,33 @@
package org.dromara.project.controller.app;
import jakarta.annotation.Resource;
import org.dromara.common.core.domain.R;
import org.dromara.project.domain.vo.project.BusProjectContractorListVo;
import org.dromara.project.service.IBusProjectService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author lilemy
* @date 2025-07-24 15:14
*/
@Validated
@RestController
@RequestMapping("/app/project/project")
public class BusProjectAppController {
@Resource
private IBusProjectService projectService;
/**
* 查询项目以及项目下的分包公司列表
*/
@GetMapping("/list/project/contractorList")
public R<List<BusProjectContractorListVo>> listProjectContractorList() {
return R.ok(projectService.queryProjectContractorList());
}
}

View File

@ -0,0 +1,41 @@
package org.dromara.project.controller.app;
import jakarta.annotation.Resource;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.dromara.project.domain.dto.reissuecard.BusReissueCardQueryReq;
import org.dromara.project.domain.vo.reissuecard.BusReissueCardVo;
import org.dromara.project.service.IBusReissueCardService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lilemy
* @date 2025-07-24 14:51
*/
@Validated
@RestController
@RequestMapping("/app/project/reissueCard")
public class BusReissueCardAppController {
@Resource
private IBusReissueCardService reissueCardService;
@Resource
private ISubConstructionUserService constructionUserService;
/**
* 查询当前登录用户补卡申请列表
*/
@GetMapping("/list/loginUser")
public TableDataInfo<BusReissueCardVo> listByLoginUser(BusReissueCardQueryReq req, PageQuery pageQuery) {
SubConstructionUser constructionUser = constructionUserService.getBySysUserId(LoginHelper.getUserId());
req.setUserId(constructionUser.getId());
return reissueCardService.queryPageList(req, pageQuery);
}
}

View File

@ -124,6 +124,11 @@ public class BusLeave extends BaseEntity {
*/
private Long teamId;
/**
* 分包公司id
*/
private Long contractorId;
/**
* 备注
*/

View File

@ -0,0 +1,25 @@
package org.dromara.project.domain.dto.attendance;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* @author lilemy
* @date 2025-07-24 12:28
*/
@Data
public class BusAttendanceByDayReq implements Serializable {
@Serial
private static final long serialVersionUID = 1800816047235448533L;
/**
* 打卡日期
*/
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date clockDate;
}

View File

@ -0,0 +1,22 @@
package org.dromara.project.domain.dto.attendance;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author lilemy
* @date 2025-07-24 13:51
*/
@Data
public class BusAttendanceByMonthReq implements Serializable {
@Serial
private static final long serialVersionUID = 4257724767091558549L;
/**
* 打卡月份
*/
private String clockMonth;
}

View File

@ -0,0 +1,28 @@
package org.dromara.project.domain.dto.attendance;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author lilemy
* @date 2025-07-28 18:43
*/
@Data
public class BusAttendancePunchCardByFaceReq implements Serializable {
@Serial
private static final long serialVersionUID = -8906344299387307633L;
/**
* 经度
*/
private String lng;
/**
* 纬度
*/
private String lat;
}

View File

@ -0,0 +1,41 @@
package org.dromara.project.domain.dto.leave;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author lilemy
* @date 2025-07-24 15:28
*/
@Data
public class BusLeaveGangerReviewReq implements Serializable {
@Serial
private static final long serialVersionUID = 6403690469809708710L;
/**
* 主键id
*/
@NotNull(message = "主键不能为空")
private Long id;
/**
* 管理员意见1未读 2同意 3拒绝
*/
@NotNull(message = "管理员意见不能为空")
private String gangerOpinion;
/**
* 管理员说明
*/
private String gangerExplain;
/**
* 备注
*/
private String remark;
}

View File

@ -0,0 +1,24 @@
package org.dromara.project.domain.enums;
import lombok.Getter;
/**
* @author lilemy
* @date 2025-07-24 15:52
*/
@Getter
public enum SubConstructionUserRoleEnum {
NORMAL("普通用户", "1"),
ADMIN("管理员", "2");
private final String text;
private final String value;
SubConstructionUserRoleEnum(String text, String value) {
this.text = text;
this.value = value;
}
}

View File

@ -7,12 +7,15 @@ import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.project.domain.BusAttendance;
import org.dromara.project.domain.dto.attendance.BusAttendanceMonthByUserIdReq;
import org.dromara.project.domain.dto.attendance.BusAttendancePunchCardByFaceReq;
import org.dromara.project.domain.dto.attendance.BusAttendanceQueryReq;
import org.dromara.project.domain.dto.attendance.BusAttendanceQueryTwoWeekReq;
import org.dromara.project.domain.vo.attendance.BusAttendanceClockDateForTwoWeekVo;
import org.dromara.project.domain.vo.attendance.BusAttendanceMonthByUserIdVo;
import org.dromara.project.domain.vo.attendance.BusAttendanceVo;
import org.springframework.web.multipart.MultipartFile;
import java.util.Date;
import java.util.List;
/**
@ -96,4 +99,22 @@ public interface IBusAttendanceService extends IService<BusAttendance> {
*/
Page<BusAttendanceVo> getVoPage(Page<BusAttendance> attendancePage);
/**
* 根据系统用户id和日期查询考勤
*
* @param userId 用户id
* @param clockDate 日期
* @return 考勤
*/
List<BusAttendanceVo> getDayByUserId(Long userId, Date clockDate);
/**
* 人脸打卡
*
* @param file 人脸图片
* @param req 人脸打卡请求
* @return 是否打卡成功
*/
Boolean punchCardByFace(MultipartFile file, BusAttendancePunchCardByFaceReq req);
}

View File

@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.project.domain.BusLeave;
import org.dromara.project.domain.dto.leave.BusLeaveGangerReviewReq;
import org.dromara.project.domain.dto.leave.BusLeaveManagerReviewReq;
import org.dromara.project.domain.dto.leave.BusLeaveQueryReq;
import org.dromara.project.domain.vo.leave.BusLeaveVo;
@ -46,6 +47,14 @@ public interface IBusLeaveService extends IService<BusLeave> {
*/
List<BusLeaveVo> queryList(BusLeaveQueryReq req);
/**
* 班长审核施工人员请假申请
*
* @param req 班长审核施工人员请假申请
* @return 是否审核成功
*/
Boolean gangerReview(BusLeaveGangerReviewReq req);
/**
* 管理员审核施工人员请假申请
*

View File

@ -13,30 +13,41 @@ import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.domain.GeoPoint;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.utils.JSTUtil;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.dromara.project.domain.BusAttendance;
import org.dromara.project.domain.BusProject;
import org.dromara.project.domain.BusProjectPunchrange;
import org.dromara.project.domain.BusProjectTeamMember;
import org.dromara.project.domain.dto.attendance.BusAttendanceMonthByUserIdReq;
import org.dromara.project.domain.dto.attendance.BusAttendancePunchCardByFaceReq;
import org.dromara.project.domain.dto.attendance.BusAttendanceQueryReq;
import org.dromara.project.domain.dto.attendance.BusAttendanceQueryTwoWeekReq;
import org.dromara.project.domain.enums.BusAttendanceClockStatusEnum;
import org.dromara.project.domain.enums.BusAttendanceCommuterEnum;
import org.dromara.project.domain.enums.BusAttendanceStatusEnum;
import org.dromara.project.domain.dto.attendance.BusAttendanceMonthByUserIdReq;
import org.dromara.project.domain.dto.attendance.BusAttendanceQueryReq;
import org.dromara.project.domain.dto.attendance.BusAttendanceQueryTwoWeekReq;
import org.dromara.project.domain.vo.attendance.BusAttendanceClockDateForTwoWeekVo;
import org.dromara.project.domain.vo.attendance.BusAttendanceListByDay;
import org.dromara.project.domain.vo.attendance.BusAttendanceMonthByUserIdVo;
import org.dromara.project.domain.vo.attendance.BusAttendanceVo;
import org.dromara.project.mapper.BusAttendanceMapper;
import org.dromara.project.service.*;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.service.ISysOssService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@ -50,9 +61,15 @@ import java.util.stream.Collectors;
public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, BusAttendance>
implements IBusAttendanceService {
@Resource
private ISysOssService ossService;
@Resource
private IBusProjectService projectService;
@Resource
private IBusProjectPunchrangeService projectPunchrangeService;
@Resource
private IBusProjectTeamMemberService projectTeamMemberService;
@ -404,4 +421,141 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
return attendanceVoPage;
}
/**
* 根据系统用户id和日期查询考勤
*
* @param userId 用户id
* @param clockDate 日期
* @return 考勤
*/
@Override
public List<BusAttendanceVo> getDayByUserId(Long userId, Date clockDate) {
SubConstructionUser constructionUser = constructionUserService.getBySysUserId(userId);
// 当日期未指定时,默认为今天
if (clockDate == null) {
clockDate = DateUtils.parseDateTime(FormatsType.YYYY_MM_DD, DateUtils.getDate());
}
LambdaQueryWrapper<BusAttendance> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(BusAttendance::getUserId, constructionUser.getId())
.eq(BusAttendance::getClockDate, clockDate);
return this.list(queryWrapper).stream().map(this::getVo).toList();
}
/**
* 人脸打卡
*
* @param file 人脸图片
* @param req 人脸打卡请求
* @return 是否打卡成功
*/
@Override
public Boolean punchCardByFace(MultipartFile file, BusAttendancePunchCardByFaceReq req) {
// 获取当前用户
Long userId = LoginHelper.getUserId();
synchronized (userId.toString().intern()) {
// 记录当前打卡时间
LocalTime now = LocalTime.now();
Date nowDate = new Date();
// 获取坐标
String lat = req.getLat();
String lng = req.getLng();
// 校验用户是否合法
SubConstructionUser constructionUser = constructionUserService.getBySysUserId(userId);
Long projectId = constructionUser.getProjectId();
if (projectId == null) {
throw new ServiceException("当前用户未加入项目", HttpStatus.BAD_REQUEST);
}
BusProject project = projectService.getById(projectId);
Long teamId = constructionUser.getTeamId();
if (teamId == null) {
throw new ServiceException("当前用户未加入班组", HttpStatus.BAD_REQUEST);
}
final String status = "1";
if (constructionUser.getStatus().equals(status)) {
throw new ServiceException("当前用户已离职", HttpStatus.BAD_REQUEST);
}
final String noClock = "1";
if (constructionUser.getClock().equals(noClock)) {
throw new ServiceException("当前用户已被禁止打卡", HttpStatus.BAD_REQUEST);
}
// 判断用户是否已经被拉黑
constructionBlacklistService.validUserInBlacklist(constructionUser.getId(), projectId);
// todo 地理位置校验
List<BusProjectPunchrange> punchranges = projectPunchrangeService.lambdaQuery()
.eq(BusProjectPunchrange::getProjectId, projectId)
.list();
if (CollUtil.isEmpty(punchranges)) {
throw new ServiceException("项目未配置考勤范围", HttpStatus.BAD_REQUEST);
}
List<String> punchRangeList = punchranges.stream().map(BusProjectPunchrange::getPunchRange).toList();
List<GeoPoint> matchingRange = JSTUtil.findMatchingRange(lat, lng, punchRangeList);
if (matchingRange == null) {
throw new ServiceException("打卡位置不在范围内", HttpStatus.BAD_REQUEST);
}
// 进行人脸比对
Boolean result = constructionUserService.faceComparison(file);
if (!result) {
throw new ServiceException("人脸识别失败,请重新识别", HttpStatus.BAD_REQUEST);
}
// 判断打卡状态
String punchRange = project.getPunchRange();
if (punchRange == null) {
throw new ServiceException("未设置打卡时间范围", HttpStatus.BAD_REQUEST);
}
String[] time = punchRange.split(",");
// 解析字符串为 LocalTime
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm");
LocalTime startTime = LocalTime.parse(time[0], formatter);
LocalTime endTime = LocalTime.parse(time[1], formatter);
Date date = Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant());
// 判断当前用户打卡状态
List<BusAttendance> attendances = this.lambdaQuery()
.eq(BusAttendance::getUserId, userId)
.eq(BusAttendance::getClockDate, date)
.list();
BusAttendance attendance = new BusAttendance();
if (CollUtil.isEmpty(attendances)) {
// 上班打卡
attendance.setCommuter(BusAttendanceCommuterEnum.CLOCKIN.getValue());
// 上传人脸照
SysOssVo upload = ossService.upload(file);
attendance.setFacePic(upload.getOssId().toString());
// 判断是否为迟到
if (now.isAfter(startTime)) {
attendance.setClockStatus(BusAttendanceClockStatusEnum.LATE.getValue());
} else {
attendance.setClockStatus(BusAttendanceClockStatusEnum.NORMAL.getValue());
}
} else if (attendances.size() == 1 && attendances.getFirst().getCommuter().equals(BusAttendanceCommuterEnum.CLOCKIN.getValue())) {
// 下班打卡
attendance.setCommuter(BusAttendanceCommuterEnum.CLOCKOUT.getValue());
// 判断是否为早退
if (now.isBefore(endTime)) {
attendance.setClockStatus(BusAttendanceClockStatusEnum.LEAVEEARLY.getValue());
} else {
attendance.setClockStatus(BusAttendanceClockStatusEnum.NORMAL.getValue());
}
} else if (attendances.size() == 2) {
throw new ServiceException("当前已完成打卡,请勿重复提交", HttpStatus.BAD_REQUEST);
} else {
throw new ServiceException("打卡异常,请联系管理员", HttpStatus.ERROR);
}
// 填充信息
attendance.setUserId(userId);
attendance.setUserName(constructionUser.getUserName());
attendance.setProjectId(projectId);
attendance.setClockDate(date);
attendance.setClockTime(nowDate);
// 记录打卡坐标
attendance.setLat(lat);
attendance.setLng(lng);
attendance.setPunchRange(matchingRange.toString());
// 上传人脸照
SysOssVo upload = ossService.upload(file);
attendance.setFacePic(upload.getOssId().toString());
return true;
}
}
}

View File

@ -12,19 +12,23 @@ import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.dromara.contractor.service.ISubContractorService;
import org.dromara.project.domain.BusAttendance;
import org.dromara.project.domain.BusLeave;
import org.dromara.project.domain.BusProjectTeam;
import org.dromara.project.domain.enums.BusAttendanceClockStatusEnum;
import org.dromara.project.domain.enums.BusAttendanceCommuterEnum;
import org.dromara.project.domain.enums.BusOpinionStatusEnum;
import org.dromara.project.domain.enums.BusReviewStatusEnum;
import org.dromara.project.domain.BusProjectTeamMember;
import org.dromara.project.domain.dto.leave.BusLeaveGangerReviewReq;
import org.dromara.project.domain.enums.*;
import org.dromara.project.domain.dto.leave.BusLeaveManagerReviewReq;
import org.dromara.project.domain.dto.leave.BusLeaveQueryReq;
import org.dromara.project.domain.vo.leave.BusLeaveVo;
import org.dromara.project.mapper.BusLeaveMapper;
import org.dromara.project.service.IBusAttendanceService;
import org.dromara.project.service.IBusLeaveService;
import org.dromara.project.service.IBusProjectTeamMemberService;
import org.dromara.project.service.IBusProjectTeamService;
import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Lazy;
@ -52,6 +56,15 @@ public class BusLeaveServiceImpl extends ServiceImpl<BusLeaveMapper, BusLeave>
@Resource
private IBusAttendanceService attendanceService;
@Resource
private IBusProjectTeamMemberService projectTeamMemberService;
@Resource
private ISubConstructionUserService constructionUserService;
@Resource
private ISubContractorService contractorService;
/**
* 查询施工人员请假申请
*
@ -88,6 +101,63 @@ public class BusLeaveServiceImpl extends ServiceImpl<BusLeaveMapper, BusLeave>
return baseMapper.selectVoList(lqw);
}
/**
* 班长审核施工人员请假申请
*
* @param req 班长审核施工人员请假申请
* @return 是否审核成功
*/
@Override
public Boolean gangerReview(BusLeaveGangerReviewReq req) {
Long id = req.getId();
String gangerOpinion = req.getGangerOpinion();
// 判断该请假记录是否存在
BusLeave oldLeave = this.getById(id);
if (oldLeave == null) {
throw new ServiceException("施工人员请假申请不存在", HttpStatus.NOT_FOUND);
}
// 如果审核状态相同,则返回
if (Objects.equals(gangerOpinion, oldLeave.getGangerOpinion())) {
throw new ServiceException("请勿重复操作", HttpStatus.BAD_REQUEST);
}
// 如果已经审核过,则返回
if (BusOpinionStatusEnum.PASS.getValue().equals(oldLeave.getManagerOpinion())) {
throw new ServiceException("该请假已审核通过,请勿重复操作", HttpStatus.BAD_REQUEST);
}
// 获取当前用户实名信息
SubConstructionUser constructionUser = constructionUserService.getBySysUserId(LoginHelper.getUserId());
// 判断当前用户是否有权审核
Long teamId = oldLeave.getTeamId();
Long count = projectTeamMemberService.lambdaQuery()
.eq(BusProjectTeamMember::getProjectId, oldLeave.getProjectId())
.eq(BusProjectTeamMember::getMemberId, constructionUser.getId())
.eq(BusProjectTeamMember::getTeamId, teamId)
.eq(BusProjectTeamMember::getPostId, BusProjectTeamMemberPostEnum.FOREMAN.getValue())
.count();
if (count <= 0) {
throw new ServiceException("您无权审核该请假申请", HttpStatus.FORBIDDEN);
}
BusLeave newLeave = new BusLeave();
newLeave.setId(id);
newLeave.setGangerOpinion(gangerOpinion);
newLeave.setGangerExplain(req.getGangerExplain());
newLeave.setGangerTime(new Date());
newLeave.setRemark(req.getRemark());
boolean result = this.updateById(newLeave);
if (!result) {
throw new ServiceException("更新班长审核操作失败", HttpStatus.ERROR);
}
// 向分包管理员发送通知
List<SubConstructionUser> constructionAdminUsers = constructionUserService.lambdaQuery()
.eq(SubConstructionUser::getContractorId, oldLeave.getContractorId())
.eq(SubConstructionUser::getUserRole, SubConstructionUserRoleEnum.ADMIN.getValue())
.list();
if (CollUtil.isNotEmpty(constructionAdminUsers)){
}
return true;
}
/**
* 管理员审核施工人员请假申请
*
@ -114,9 +184,20 @@ public class BusLeaveServiceImpl extends ServiceImpl<BusLeaveMapper, BusLeave>
throw new ServiceException("请等待班组长审核通过后再进行操作", HttpStatus.BAD_REQUEST);
}
// todo 判断当前用户是否为项目管理员
// 判断是否为分包公司管理员
Long contractorId = oldLeave.getContractorId();
SubConstructionUser constructionUser = constructionUserService.lambdaQuery()
.eq(SubConstructionUser::getSysUserId, LoginHelper.getUserId())
.eq(SubConstructionUser::getContractorId, contractorId)
.eq(SubConstructionUser::getUserRole, SubConstructionUserRoleEnum.ADMIN.getValue())
.one();
if (constructionUser == null) {
throw new ServiceException("您无权审核该请假申请", HttpStatus.FORBIDDEN);
}
// 填充默认值,更新数据
BusLeave leave = new BusLeave();
leave.setId(id);
leave.setManagerId(constructionUser.getId());
leave.setManagerOpinion(managerOpinion);
leave.setManagerExplain(req.getManagerExplain());
leave.setManagerTime(new Date());