zouyu
6 天以前 dc7300e21fe53f74e08eb2fa494a83430e2e54ca
绩效管理:人员考勤功能模块
已添加19个文件
已修改18个文件
1744 ■■■■■ 文件已修改
.gitignore 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/mqtt/MQCallback.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cnas-device/src/main/java/com/ruoyi/device/utils/DataAcquisition.java 144 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/pom.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/controller/StaffAttendanceController.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/dto/StaffAttendanceDTO.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/mapper/StaffAttendanceTrackingRecordMapper.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/pojo/StaffAttendanceTrackingRecord.java 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/StaffAttendanceTrackingRecordService.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/IfsPartPropsRecordServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsReportServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/RawMaterialOrderServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/StaffAttendanceTrackingRecordServiceImpl.java 466 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/task/SyncStaffAttendanceRecordSchedule.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/util/HourDiffCalculator.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/vo/StaffAttendanceVO.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/vo/StaffClockInVO.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/resources/mapper/StaffAttendanceTrackingRecordMapper.xml 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
performance-server/src/main/java/com/ruoyi/performance/dto/PerformanceShiftMapDto.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
performance-server/src/main/java/com/ruoyi/performance/mapper/PerformanceShiftMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
performance-server/src/main/java/com/ruoyi/performance/mapper/ShiftTimeMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
performance-server/src/main/resources/mapper/PerformanceShiftMapper.xml 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin-ztns/src/main/java/com/ruoyi/web/controller/system/SysPostController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin-ztns/src/main/java/com/ruoyi/web/controller/system/UserController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/Custom.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/enums/ClockInState.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/enums/EnterOrExitType.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/enums/SyncStatus.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/api/IfsApiUtils.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/api/icc/IccApiUtil.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/api/icc/config/OauthConfigUtil.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/api/icc/config/PlatformConfig.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/api/icc/model/GetResultPageRequest.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/api/icc/model/GetResultPageResponse.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -21,6 +21,7 @@
### IntelliJ IDEA ###
.idea
.vscode
*.iws
*.iml
*.ipr
cnas-device/src/main/java/com/ruoyi/device/mqtt/MQCallback.java
@@ -82,26 +82,6 @@
                case "/aiot/51eaff10-c6b9-11f0-8b13-c14e8310d70b"://过程试验室-电阻
                    mqCallback.collectBridgeService.dcResistanceDataAnalysis(parse);
                    break;
//                case "/ztt/v3/2455220/publish":
//                    //耐丝:直流电阻数据解析NS-ER02001
//                    mqCallback.collectBridgeService.dcResistanceDataAnalysis(parse,"NS-ER02001");
//                    break;
////                case "/aiot/8fac9fa0-c517-11f0-954c-255ce11213f1":
////                    //耐丝:直流电阻数据解析NS-ER02002
////                    mqCallback.collectBridgeService.dcResistanceDataAnalysis(parse,"NS-ER02002");
////                    break;
//                case "/ztt/v3/2455221/publish":
//                    //耐丝:伸长率数据解析NS-FM05003
//                    mqCallback.collectBridgeService.elongationDataAnalysis(parse,"NS-FM05003");
////                    break;
////                case "/aiot/38568140-c697-11f0-8b13-c14e8310d70b":
////                    //耐丝:伸长率数据解析NS-FM05002
////                    mqCallback.collectBridgeService.elongationDataAnalysis(parse,"NS-FM05002");
//                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
cnas-device/src/main/java/com/ruoyi/device/service/impl/DeviceServiceImpl.java
@@ -197,9 +197,6 @@
    @Override
    public Result<?> dataAcquisition(HttpServletRequest request, DeviceCollectionDto dto) {
        // æŸ¥è¯¢æ£€éªŒé¡¹
        List<Integer> itemIds = dto.getItemIds();
        if (CollectionUtils.isEmpty(itemIds)) {
cnas-device/src/main/java/com/ruoyi/device/utils/DataAcquisition.java
@@ -2,11 +2,13 @@
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.fasterxml.jackson.core.type.TypeReference;
import com.ruoyi.common.core.domain.Result;
import com.ruoyi.common.utils.RedisUtil;
import com.ruoyi.device.constant.DCResistanceMqttConstants;
@@ -130,16 +132,17 @@
                    break;
                case ".xls":
                case ".xlsx":
                case ".csv":
                    map = analysisList(data, userMap, device, entrustCode, sampleCode);
                    break;
                case ".serialPort":
                    map = analysisSerialPortList(data, userMap, device, entrustCode, sampleCode);
                    break;
                case ".pngInExcel":
                    map = analysisPngInExcel(data, userMap, device, entrustCode, sampleCode);
                    break;
                case ".txt":
                    map = analysisTxt(data, userMap, device, entrustCode, sampleCode);
                    break;
                case ".csv":
                    map = analysisList(data, userMap, device, entrustCode, sampleCode);
                    break;
                case ".mdb":
                    // åˆ¤æ–­æ˜¯å¦æ˜¯æ‹‰åŠ›æœºJCZX-ZB-FF01014
@@ -150,10 +153,8 @@
                    }
                    break;
                case ".db":
//                    map = analysisDb1(data, userMap, device);
                    map = analysisDb(data, userMap, device);
                    break;
                case ".mysql":
                case ".sqlserver":
//                    map = analysisDb1(data, userMap, device);
                    map = analysisDb(data, userMap, device);
                    break;
@@ -499,7 +500,7 @@
     * @return
     */
    public static Object calculationFormula(List<Object> list, DataConfig dataConfig, String insProductItem, Device device) {
        if (list.size() == 0) {
        if (list.isEmpty()) {
            Map<String, Object> hashMap = new HashMap<>();
            hashMap.put("equipName", device.getDeviceName());
            hashMap.put("equipValue", device.getManagementNumber());
@@ -666,6 +667,25 @@
        return map;
    }
    /**
     * å¤„理串口数据
     *
     * @param data       é‡‡é›†åˆ°çš„æ–‡ä»¶å­—符串
     * @param dataConfig ç”¨æˆ·é…ç½®å¥½çš„x,y轴定位数据与参照物
     * @return
     */
    public static Map<String, Object> analysisSerialPortList(String data, Map<String, List<DataConfig>> dataConfig,
                                                   Device device, String entrustCode, String sampleCode) {
        Map<String, Object> map = new HashMap<>();
        dataConfig.forEach((k, v) -> {
            List<Object> list = JSONObject.parseObject(data,new TypeReference<List<Object>>(){}.getType());
            // è¿›è¡Œå…¬å¼è®¡ç®—
            Object resultValue = calculationFormula(list, v.get(0), k, device);
            map.put(k, resultValue);
        });
        return map;
    }
    private static List<Object> analyzeDataEntrustCodAndSampleCode(String data, List<DataConfig> v, String k, String splitIdentifier,
                                                                   Device device, String entrustCodeValue, String sampleCodeValue) {
        entrustCodeValue = entrustCodeValue.replaceAll(" ", "");
@@ -751,117 +771,7 @@
    public static String getRefer(String refer) {
        return ObjectUtils.isNotEmpty(refer) ? refer.replaceAll(" ", "") : "";
    }
    /**
     * å§”托编号与样品编号都为空执行
     *
     * @param data  é‡‡é›†åˆ°çš„æ–‡ä»¶å­—符串
     * @param v     ç”¨æˆ·é…ç½®å¥½çš„x,y轴定位数据与参照物
     * @param k     æ£€éªŒé¡¹åç§°
     * @param split åˆ†å‰²ç¬¦
     * @return æå–的数据列表
     */
    public static List<Object> analyzeData1(String data, List<DataConfig> v, String k, String split) {
        List<Object> list = new ArrayList<>();
        // é¢„处理数据:移除多余空格并保留关键分隔符
        String processedData = data.replaceAll("\\s+", " ").trim();
        for (int configIndex = 0; configIndex < v.size(); configIndex++) {
            // å–两个用户配置的参照物
            String referx = getRefer(v.get(configIndex).getReferx());
            String refery = getRefer(v.get(configIndex).getRefery());
            if (ObjectUtils.isEmpty(refery) && ObjectUtils.isEmpty(referx)) {
                System.out.println("参照物为空,跳过当前配置,k: " + k);
                continue;
            }
            // æœ€ç»ˆç»“æžœ
            List<Object> result = new ArrayList<>();
            // é€šè¿‡\n将字符串分割为行
            String[] aColumnY = processedData.replaceAll(" ", "").split("\n");
            Integer end = null;
            // é‡‡é›†æ•°æ®ï¼šYè½´
            for (int i = 0; i < aColumnY.length; i++) {
                String line = aColumnY[i].trim();
                if (line.isEmpty()) continue; // è·³è¿‡ç©ºè¡Œ
                // å¦‚æžœY参照不为空与X参照为空则执行,同时该行包含Y参照
                if (ObjectUtils.isNotEmpty(refery) && ObjectUtils.isEmpty(referx) && line.contains(refery)) {
                    try {
                        // å–Y坐标值
                        int y = getXOrY(v.get(configIndex).getY(), k, "Y");
                        String[] aLineX = line.split(split);
                        for (int j = 0; j < aLineX.length; j++) {
                            if (aLineX[j].contains(refery)) {
                                if (i + y >= aColumnY.length) {
                                    System.err.println(k + ":Y轴定位超出数据范围,当前数据行数: " + aColumnY.length + ",尝试访问行: " + (i + y));
                                    continue;
                                }
                                String[] split1 = aColumnY[i + y].split(split);
                                if (j >= split1.length) {
                                    System.err.println(k + ":X轴定位超出数据范围,当前行元素个数: " + split1.length + ",尝试访问位置: " + j);
                                    continue;
                                }
                                result.add(split1[j]);
                            }
                        }
                    } catch (Exception e) {
                        System.err.println(k + ":在处理仅Y参照逻辑时出现异常: " + e.getMessage());
                    }
                }
                // å¦‚æžœY参照不为空与X参照不为空则执行,此处Y定区域
                else if (ObjectUtils.isNotEmpty(refery) && ObjectUtils.isNotEmpty(referx)) {
                    try {
                        // å–x的值,防止报错
                        int x = getXOrY(v.get(configIndex).getX(), k, "X");
                        // å–Y坐标值
                        int y = getXOrY(v.get(configIndex).getY(), k, "Y");
                        // ç¼“å­˜Y的结束值
                        if (ObjectUtils.isEmpty(end) && line.contains(refery)) {
                            end = i + y;
                        }
                        // åˆ¤æ–­æ˜¯å¦åœ¨å‚照物为起点,Y坐标值为最终范围
                        if (ObjectUtils.isNotEmpty(end) && i <= end) {
                            String[] aLineX = line.split(split);
                            for (int j = 0; j < aLineX.length; j++) {
                                if (aLineX[j].contains(referx)) {
                                    if (j + x >= aLineX.length) {
                                        System.err.println(k + ":X轴定位超出数据范围,当前行元素个数: " + aLineX.length + ",尝试访问位置: " + (j + x));
                                        continue;
                                    }
                                    result.add(aLineX[j + x]);
                                    break;
                                }
                            }
                        }
                    } catch (Exception e) {
                        System.err.println(k + ":在处理XY参照逻辑时出现异常: " + e.getMessage());
                    }
                }
                // å¦‚æžœX参照不为空同时该行包含X参照,则执行下面的代码
                else if (line.contains(referx) && ObjectUtils.isEmpty(refery)) {
                    try {
                        String[] aLineX = line.split(split);
                        // å–x的值,防止报错
                        int x = getXOrY(v.get(configIndex).getX(), k, "X");
                        for (int j = 0; j < aLineX.length; j++) {
                            if (aLineX[j].contains(referx)) {
                                if (j + x >= aLineX.length) {
                                    System.err.println(k + ":X轴定位超出数据范围,当前行元素个数: " + aLineX.length + ",尝试访问位置: " + (j + x));
                                    continue;
                                }
                                result.add(aLineX[j + x]);
                            }
                        }
                    } catch (Exception e) {
                        System.err.println(k + ":在处理仅X参照逻辑时出现异常: " + e.getMessage());
                    }
                }
            }
            // é˜²æ­¢è®¡ç®—公式的时候出现:[null] è¿™ç§æ•°æ®
            if (ObjectUtils.isNotEmpty(result)) {
                list.addAll(result);
            }
        }
        return list;
    }
    /**
     * å§”托编号与样品编号都为空执行
     *
inspect-server/pom.xml
@@ -67,8 +67,6 @@
            <artifactId>itextpdf</artifactId>
            <version>5.0.6</version>
        </dependency>
    </dependencies>
    <properties>
inspect-server/src/main/java/com/ruoyi/inspect/controller/StaffAttendanceController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,81 @@
package com.ruoyi.inspect.controller;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.domain.Result;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.inspect.dto.StaffAttendanceDTO;
import com.ruoyi.inspect.pojo.StaffAttendanceTrackingRecord;
import com.ruoyi.inspect.service.StaffAttendanceTrackingRecordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.Arrays;
/**
 * ç»©æ•ˆç®¡ç†-人员考勤接口
 */
@Api(value = "绩效管理-人员考勤接口")
@RestController
@RequestMapping("/staff/attendance")
public class StaffAttendanceController {
    @Autowired
    private StaffAttendanceTrackingRecordService trackingRecordService;
    @ApiOperation("查询人员打卡记录")
    @GetMapping("/getClockInRecord")
    public Result getClockInRecord(StaffAttendanceDTO staffAttendanceDTO){
        return Result.success(trackingRecordService.getClockInRecord(staffAttendanceDTO));
    }
    @ApiOperation("分页查询考勤记录")
    @GetMapping("/pageAttendanceRecord")
    public Result pageAttendanceRecord(Page<StaffAttendanceTrackingRecord> page, StaffAttendanceDTO staffAttendanceDTO){
        return Result.success(trackingRecordService.pageAttendanceRecord(page,staffAttendanceDTO));
    }
    @ApiOperation("校验选择的考勤时间是否存在同一人员的考勤记录,没有则返回人员当天的班次信息")
    @GetMapping("/checkDutyDate")
    public Result checkDutyDate(StaffAttendanceDTO staffAttendanceDTO){
        return Result.success(trackingRecordService.checkDutyDate(staffAttendanceDTO));
    }
    @ApiOperation("保存或更新考勤记录")
    @PostMapping("/saveOrUpdateStaffAttendanceTrackingRecord")
    public Result saveStaffAttendanceTrackingRecord(@RequestBody StaffAttendanceDTO staffAttendanceDTO){
        return Result.success(trackingRecordService.saveOrUpdateRecord(staffAttendanceDTO));
    }
    @ApiOperation("删除考勤记录")
    @DeleteMapping("/removeStaffAttendanceTrackingRecord")
    public Result removeStaffAttendanceTrackingRecord(@RequestBody StaffAttendanceDTO staffAttendanceDTO){
        return Result.success(trackingRecordService.removeByIds(Arrays.asList(staffAttendanceDTO.getWorkDataId(),staffAttendanceDTO.getOffWorkDataId())));
    }
    @ApiOperation("同步考勤记录")
    @GetMapping("/syncAttendanceRecord")
    public Result syncAttendanceRecord(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startDate,@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endDate){
        return Result.success(trackingRecordService.syncAttendanceRecord(startDate,endDate));
    }
    @ApiOperation("修改进出记录状态")
    @PostMapping("/changeEnableReport")
    public Result changeEnableReport(@RequestBody StaffAttendanceDTO staffAttendanceDTO){
        if(ObjectUtils.isEmpty(staffAttendanceDTO.getId())){
            throw new RuntimeException("传参异常,主键ID不能为空!");
        }
        StaffAttendanceTrackingRecord trackingRecord = new StaffAttendanceTrackingRecord();
        trackingRecord.setId(staffAttendanceDTO.getId());
        trackingRecord.setEnableReport(staffAttendanceDTO.getEnableReport());
        return Result.success(trackingRecordService.updateById(trackingRecord));
    }
}
inspect-server/src/main/java/com/ruoyi/inspect/dto/StaffAttendanceDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
package com.ruoyi.inspect.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.inspect.pojo.StaffAttendanceTrackingRecord;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.NonNull;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
@Data
public class StaffAttendanceDTO extends StaffAttendanceTrackingRecord {
    /**
     * å¼€å§‹æ—¶é—´
     */
    @ApiModelProperty("开始时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startDate;
    /**
     * ç»“束时间
     */
    @ApiModelProperty("结束时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endDate;
    /**
     * å‘˜å·¥åç§°/工号
     */
    @ApiModelProperty("员工名称/工号")
    private String keyword;
    /**
     * è€ƒå‹¤æ—¶é—´
     */
    @ApiModelProperty("考勤时间")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate swingDate;
    /**
     * ç­æ¬¡id
     */
    @ApiModelProperty("班次id")
    private String shiftId;
    /**
     * æ˜¯å¦çº³å…¥è€ƒå‹¤ç»Ÿè®¡
     */
    @ApiModelProperty("是否纳入考勤统计")
    private Boolean enableReport;
    /**
     * ä¸Šç­æ—¶é—´
     */
    @JsonFormat(pattern = "HH:mm")
    @DateTimeFormat(pattern = "HH:mm")
    @ApiModelProperty("上班时间")
    private LocalTime workDateTime;
    /**
     * ä¸‹ç­æ—¶é—´
     */
    @JsonFormat(pattern = "HH:mm")
    @DateTimeFormat(pattern = "HH:mm")
    @ApiModelProperty("下班时间")
    private LocalTime offWorkDateTime;
    /**
     * ä¸Šç­è®°å½•id
     */
    @ApiModelProperty("上班记录id")
    private Long workDataId;
    /**
     * ä¸‹ç­è®°å½•id
     */
    @ApiModelProperty("下班记录id")
    private Long offWorkDataId;
}
inspect-server/src/main/java/com/ruoyi/inspect/mapper/StaffAttendanceTrackingRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package com.ruoyi.inspect.mapper;
import com.ruoyi.inspect.pojo.StaffAttendanceTrackingRecord;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
/**
* @author 27233
* @description é’ˆå¯¹è¡¨ã€staff_attendance_tracking_record(人员考勤-考勤记录)】的数据库操作Mapper
* @createDate 2026-03-20 14:42:09
* @Entity com.ruoyi.inspect.pojo.StaffAttendanceTrackingRecord
*/
public interface StaffAttendanceTrackingRecordMapper extends BaseMapper<StaffAttendanceTrackingRecord> {
    public List<Long> selectIccIdList();
}
inspect-server/src/main/java/com/ruoyi/inspect/pojo/StaffAttendanceTrackingRecord.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,205 @@
package com.ruoyi.inspect.pojo;
import com.baomidou.mybatisplus.annotation.*;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
/**
 * äººå‘˜è€ƒå‹¤-考勤记录
 * @TableName staff_attendance_tracking_record
 */
@TableName(value ="staff_attendance_tracking_record")
@Data
public class StaffAttendanceTrackingRecord implements Serializable {
    /**
     * ä¸»é”®id
     */
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * icc开放平台主键id
     */
    @JsonSerialize(using = ToStringSerializer.class)
    private Long iccId;
    /**
     * åˆ·å¡æ—¶é—´
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime swingTime;
    /**
     * å‘˜å·¥id
     */
    @JsonSerialize(using = ToStringSerializer.class)
    private Long personId;
    /**
     * å‘˜å·¥ç¼–号
     */
    private String personCode;
    /**
     * å‘˜å·¥å§“名
     */
    private String personName;
    /**
     * éƒ¨é—¨åç§°
     */
    private String deptName;
    /**
     * è€ƒå‹¤ç»“æžœ:0-异常,1-正常
     */
    private Integer result;
    /**
     * å¡å·
     */
    private String cardNumber;
    /**
     * å¡çŠ¶æ€ï¼Œ-1-空白卡,0-正常卡,1-挂失卡,2-注销卡
     */
    private Integer cardStatus;
    /**
     * ç±»åž‹ï¼Œ0-IC卡, 1-有源RFID, 2-CPU卡
     */
    private Integer cardType;
    /**
     * é€šé“编码
     */
    private String channelCode;
    /**
     * é€šé“名称
     */
    private String channelName;
    /**
     * è®¾å¤‡ç¼–码
     */
    private String deviceCode;
    /**
     * è®¾å¤‡åç§°
     */
    private String deviceName;
    /**
     * è¿›å‡ºé—¨ç±»åž‹ï¼Œ1-进门, 2出门, 3-进/出门
     */
    private Integer enterOrExit;
    /**
     * 1-内部人员, 2-访客:内部人员是在人员管理中添加的人员,访客是在访客子系统中录入的访客
     */
    private Integer imageType;
    /**
     * å¼€é—¨ç»“果,0-失败,1-成功
     */
    private Integer openResult;
    /**
     * å¼€é—¨ç±»åž‹ï¼Œè¯¦è§ å¼€é—¨ç±»åž‹ å­—å…¸
     */
    private Integer openType;
    /**
     * è¯ä»¶å·ç ï¼Œä¼šè¿›è¡Œè„±æ•å¤„理
     */
    private String paperNumber;
    /**
     * æŠ“图,相对路径,完整访问路径参考OSS方式组装
     */
    private String recordImageUrl;
    /**
     * æŠ“图,绝对路径,兼容历史版本,不推荐使用
     */
    private String recordImage;
    /**
     * å¼€é—¨å¤±è´¥åŽŸå› 
     */
    private String remark;
    /**
     * å£ç½©çŠ¶æ€(3-带口罩,2—没带口罩,1-未识别)
     */
    private Integer maskState;
    /**
     * æ˜¯å¦è¶…温
     */
    private Integer overTemp;
    /**
     * ä½“温
     */
    private Double curTemp;
    /**
     * æ˜¯å¦åŒæ­¥(0:同步 1:手动新增)
     */
    private Integer isSync;
    /**
     * æ˜¯å¦çº³å…¥è€ƒå‹¤ç»Ÿè®¡
     */
    private Boolean enableReport;
    /**
     * åˆ›å»ºäºº
     */
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    /**
     * åˆ›å»ºæ—¶é—´
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * æ›´æ–°äºº
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    /**
     * æ›´æ–°æ—¶é—´
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
    public StaffAttendanceTrackingRecord(Long id,LocalDateTime swingTime, String personCode, String personName, String deptName, Integer result, Integer enterOrExit, Integer isSync) {
        this.id = id;
        this.swingTime = swingTime;
        this.personCode = personCode;
        this.personName = personName;
        this.deptName = deptName;
        this.result = result;
        this.enterOrExit = enterOrExit;
        this.isSync = isSync;
    }
    public StaffAttendanceTrackingRecord() {
    }
}
inspect-server/src/main/java/com/ruoyi/inspect/service/StaffAttendanceTrackingRecordService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.inspect.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.inspect.dto.StaffAttendanceDTO;
import com.ruoyi.inspect.pojo.StaffAttendanceTrackingRecord;
import com.ruoyi.inspect.vo.StaffAttendanceVO;
import com.ruoyi.inspect.vo.StaffClockInVO;
import com.ruoyi.performance.dto.PerformanceShiftMapDto;
import java.time.LocalDateTime;
import java.util.List;
/**
* @author 27233
* @description é’ˆå¯¹è¡¨ã€staff_attendance_tracking_record(人员考勤-考勤记录)】的数据库操作Service
* @createDate 2026-03-09 17:42:25
*/
public interface StaffAttendanceTrackingRecordService extends IService<StaffAttendanceTrackingRecord> {
    boolean syncAttendanceRecord(LocalDateTime startDate, LocalDateTime endDate);
    IPage<StaffAttendanceVO> pageAttendanceRecord(Page<StaffAttendanceTrackingRecord> page, StaffAttendanceDTO staffAttendanceDTO);
    List<StaffAttendanceTrackingRecord> getClockInRecord(StaffAttendanceDTO staffAttendanceDTO);
    PerformanceShiftMapDto checkDutyDate(StaffAttendanceDTO staffAttendanceDTO);
    boolean saveOrUpdateRecord(StaffAttendanceDTO staffAttendanceDTO);
}
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/IfsPartPropsRecordServiceImpl.java
@@ -18,7 +18,6 @@
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
@@ -811,7 +811,7 @@
                )
                .ne(InsProduct::getIsBinding, 1));
        insProducts.addAll(insProductMapper.selectFiberInsProduct(InsSampleIds, laboratory));
        if (insProducts.size() > 0) {
        if (!insProducts.isEmpty()) {
            String str = "";
            int count = 0;
            for (InsProduct product : insProducts) {
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsReportServiceImpl.java
@@ -860,6 +860,7 @@
                // ä¼ä¸šå¾®ä¿¡é€šçŸ¥
                String message = "";
                message += "检测结果提交通知";
                message += "\nIFS域: " + one.getContract();
                message += "\n批次号: " + one.getUpdateBatchNo();
                message += "\n零件号: " + one.getPartNo();
                message += "\n零件描述: " + one.getPartDesc();
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/RawMaterialOrderServiceImpl.java
@@ -242,6 +242,7 @@
        if(!validateValue){
            throw new ErrorException("报检失败,非法的销售订单分类枚举");
        }
        validateUpdateBatchNo(ifsInventoryQuantity);
        //批次号字母转大写
        ifsInventoryQuantity.setUpdateBatchNo(ifsInventoryQuantity.getUpdateBatchNo().toUpperCase(Locale.ROOT));
        ifsInventoryQuantityMapper.update(null, Wrappers.<IfsInventoryQuantity>lambdaUpdate()
@@ -266,6 +267,26 @@
            WxCpUtils.informWebHook(wechatProperty.getExaminingUrl(), message);
        });
        return 1;
    }
    /**
     * æ ¡éªŒå¤–购订单报检的批次号是否重复
     * @param ifsInventoryQuantity
     */
    public void validateUpdateBatchNo(IfsInventoryQuantity ifsInventoryQuantity){
        if(StringUtils.equals(ifsInventoryQuantity.getOrderType(),OrderType.WG.getValue())){
            //查询历史记录
            Long count = ifsInventoryQuantityMapper.selectCount(Wrappers.<IfsInventoryQuantity>lambdaQuery()
                    .eq(IfsInventoryQuantity::getContract,ifsInventoryQuantity.getContract())
                    .eq(IfsInventoryQuantity::getPartNo,ifsInventoryQuantity.getPartNo())
                    .eq(IfsInventoryQuantity::getUpdateBatchNo,ifsInventoryQuantity.getUpdateBatchNo())
                    .eq(IfsInventoryQuantity::getOrderType,OrderType.WG.getValue())
                    .ne(IfsInventoryQuantity::getState,0)
            );
            if(count>0){
                throw new RuntimeException("报检失败,零件"+ifsInventoryQuantity.getPartNo()+"所报检的批次号【"+ifsInventoryQuantity.getUpdateBatchNo()+"】已存在!");
            }
        }
    }
    /**
@@ -526,6 +547,7 @@
        if(!OrderType.validateValue(ifsInventoryQuantity.getOrderType())){
            throw new ErrorException("新增报检信息失败,非法的销售订单分类枚举");
        }
        validateUpdateBatchNo(ifsInventoryQuantity);
        ifsInventoryQuantityMapper.insert(ifsInventoryQuantity);
    }
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/StaffAttendanceTrackingRecordServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,466 @@
package com.ruoyi.inspect.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.core.domain.Result;
import com.ruoyi.common.enums.ClockInState;
import com.ruoyi.common.enums.EnterOrExitType;
import com.ruoyi.common.enums.SyncStatus;
import com.ruoyi.common.utils.api.icc.IccApiUtil;
import com.ruoyi.common.utils.api.icc.model.GetResultPageRequest;
import com.ruoyi.common.utils.api.icc.model.GetResultPageResponse;
import com.ruoyi.inspect.dto.StaffAttendanceDTO;
import com.ruoyi.inspect.mapper.StaffAttendanceTrackingRecordMapper;
import com.ruoyi.inspect.pojo.StaffAttendanceTrackingRecord;
import com.ruoyi.inspect.service.StaffAttendanceTrackingRecordService;
import com.ruoyi.inspect.util.HourDiffCalculator;
import com.ruoyi.inspect.vo.StaffAttendanceVO;
import com.ruoyi.inspect.vo.StaffClockInVO;
import com.ruoyi.performance.dto.PerformanceShiftMapDto;
import com.ruoyi.performance.mapper.PerformanceShiftMapper;
import com.ruoyi.performance.mapper.ShiftTimeMapper;
import com.ruoyi.performance.pojo.ShiftTime;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
 * @author 27233
 * @description é’ˆå¯¹è¡¨ã€staff_attendance_tracking_record(人员考勤-考勤记录)】的数据库操作Service实现
 * @createDate 2026-03-09 17:42:25
 */
@Slf4j
@Service
public class StaffAttendanceTrackingRecordServiceImpl
        extends ServiceImpl<StaffAttendanceTrackingRecordMapper, StaffAttendanceTrackingRecord>
        implements StaffAttendanceTrackingRecordService {
    @Autowired
    private IccApiUtil iccApiUtil;
    @Autowired
    private PerformanceShiftMapper performanceShiftMapper;
    @Autowired
    private ShiftTimeMapper shiftTimeMapper;
    private DateTimeFormatter yyyMMdd = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private DateTimeFormatter HHmm = DateTimeFormatter.ofPattern("HH:mm");
    private DateTimeFormatter yyyMMddHHmmss = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    /** è´¨é‡éƒ¨id */
    private final static String deptIds = "6";
    // å›ºå®šå‰ç¼€
    private static final String PREFIX = "ZT-";
    // æ•°å­—部分固定长度
    private static final int DIGIT_LENGTH = 6;
    /**
     * åŒæ­¥çš„门禁设备列表
     * channel_name device_code
     * 10.100.22.2_门禁通道_1 1001538
     * 10.100.22.3_门禁通道_1 1001539
     * 10.100.22.4_门禁通道_1 1001540
     * 10.100.22.5_门禁通道_1 1001541
     * 10.100.22.12_门禁通道_1 1001626
     * 10.100.22.13_门禁通道_1 1001627
     * 10.100.22.14_门禁通道_1 1001628
     * 10.100.22.15_门禁通道_1 1001629
     */
    private final static List<String> syncDeviceCode = Arrays.asList("1001538", "1001539", "1001540", "1001541",
            "1001626", "1001627", "1001628", "1001629");
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean syncAttendanceRecord(LocalDateTime startDate, LocalDateTime endDate) {
        if (ObjectUtil.isAllEmpty(startDate, endDate)) {
            throw new RuntimeException("同步日期不能为空");
        }
        // æŸ¥è¯¢å·²åŒæ­¥çš„记录
        List<Long> trackingRecordIccIdList = baseMapper.selectIccIdList();
        try {
            // æŸ¥è¯¢icc开放平台的考勤记录
            GetResultPageRequest getResultPageRequest = new GetResultPageRequest();
            getResultPageRequest.setPageNum(1);
            getResultPageRequest.setPageSize(9999);
            getResultPageRequest.setDeptIds(deptIds);
            getResultPageRequest.setStartSwingTime(startDate.format(yyyMMddHHmmss));
            getResultPageRequest.setEndSwingTime(endDate.format(yyyMMddHHmmss));
            GetResultPageResponse trackingRecordResponse = iccApiUtil.getAttendanceResultPage(getResultPageRequest);
            if (trackingRecordResponse.isSuccess()) {
                if (ObjectUtil.isNotNull(trackingRecordResponse.getData())
                        && !trackingRecordResponse.getData().getPageData().isEmpty()) {
                    List<StaffAttendanceTrackingRecord> recordList = trackingRecordResponse.getData().getPageData()
                            .stream()
                            .filter(f -> !trackingRecordIccIdList.contains(f.getId())
                                    && syncDeviceCode.contains(f.getDeviceCode()))
                            .map(result -> {
                                StaffAttendanceTrackingRecord trackingRecord = new StaffAttendanceTrackingRecord();
                                BeanUtil.copyProperties(result, trackingRecord);
                                trackingRecord.setIccId(result.getId());
                                trackingRecord.setPersonCode(restorePersonCode(result.getPersonCode()));
                                trackingRecord.setId(null);
                                return trackingRecord;
                            }).collect(Collectors.toList());
                    if (!recordList.isEmpty())
                        this.saveBatch(recordList);
                    log.info("同步ICC考勤记录条数->,{}", recordList.size());
                }
            } else {
                log.error("同步ICC开放平台考勤记录错误,{}", trackingRecordResponse.getErrMsg());
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return true;
    }
    @Override
    public IPage<StaffAttendanceVO> pageAttendanceRecord(Page<StaffAttendanceTrackingRecord> page,
            StaffAttendanceDTO staffAttendanceDTO) {
        // æŸ¥è¯¢æ‰“卡记录
        System.out.println(staffAttendanceDTO.getStartDate());
        System.out.println(staffAttendanceDTO.getEndDate());
        System.out.println(ObjectUtils.allNotNull(staffAttendanceDTO.getStartDate(),staffAttendanceDTO.getEndDate()));
        Wrapper<StaffAttendanceTrackingRecord> queryWrapper = Wrappers.<StaffAttendanceTrackingRecord>lambdaQuery()
                .eq(StaffAttendanceTrackingRecord::getEnableReport, Boolean.TRUE)
                .between(ObjectUtils.allNotNull(staffAttendanceDTO.getStartDate(),staffAttendanceDTO.getEndDate()),
                        StaffAttendanceTrackingRecord::getSwingTime, staffAttendanceDTO.getStartDate(),ObjectUtils.isNotEmpty(staffAttendanceDTO.getEndDate())?staffAttendanceDTO.getEndDate().plusDays(1L):null)
                .and(StringUtils.isNotEmpty(staffAttendanceDTO.getKeyword()),
                        i -> i.like(StaffAttendanceTrackingRecord::getPersonCode, staffAttendanceDTO.getKeyword())
                                .or()
                                .like(StaffAttendanceTrackingRecord::getPersonName, staffAttendanceDTO.getKeyword()));
        List<StaffAttendanceTrackingRecord> recordList = baseMapper.selectList(queryWrapper);
        // æŸ¥è¯¢ç­æ¬¡
        List<PerformanceShiftMapDto> performanceShifts = performanceShiftMapper.selectListByWorkTime(
                staffAttendanceDTO.getStartDate(), staffAttendanceDTO.getEndDate(), staffAttendanceDTO.getKeyword());
        // ç»„装数据
        List<StaffAttendanceVO> resultList = new ArrayList<>();
        for (int i = 0; i < performanceShifts.size(); i++) {
            PerformanceShiftMapDto p = performanceShifts.get(i);
            StaffAttendanceVO vo = new StaffAttendanceVO();
            // èŽ·å–å¯¹åº”ç­æ¬¡å°æ—¶æ•°
            if (ObjectUtil.isAllNotEmpty(p.getStartTime(), p.getEndTime()) && !recordList.isEmpty()) {
                double hourDiff = HourDiffCalculator.getHourDiff(p.getStartTime(), p.getEndTime());
                /*
                 * ä¸Šç­æ—¶é—´å–值:
                 * æ­£å¸¸ï¼šå½“前班次开始前最后一次进门时间
                 * å¼‚常(迟到):无当前班次开始前进门记录,取当前班次开始后第一次进门时间
                 * ä¸‹ç­æ—¶é—´å–值:
                 * æ­£å¸¸ï¼šå½“前班次结束后第一次出门时间
                 * å¼‚常(早退):无当前班次结束到下一班次开始前的出门记录并且最后一次出门在当前班次时间范围内,取当前班次最后一次出门时间
                 */
                // å½“前班次开始天0点时间
                LocalDateTime startDateTime = LocalDateTime.of(p.getWorkTime().toLocalDate(), LocalTime.MIN);
                // å½“前班次结束天24点时间
                LocalDateTime endDateTime = LocalDateTime.of(p.getWorkTime().toLocalDate(), LocalTime.MAX);
                // å½“前班次开始时间
                LocalTime currentShiftStartTime = LocalTime.parse(p.getStartTime(), HHmm);
                LocalDateTime currentShiftStartDateTime = LocalDateTime.of(p.getWorkTime().toLocalDate(),
                        currentShiftStartTime);
                // å½“前班次结束时间
                LocalTime currentShiftEndTime = LocalTime.parse(p.getEndTime(), HHmm);
                LocalDateTime currentShiftEndDateTime = LocalDateTime.of(endDateTime.toLocalDate(),
                        currentShiftEndTime);
                // ä¸‹ä¸€ç­æ¬¡å¼€å§‹æ—¶é—´
                LocalDateTime nextShiftStartDateTime = getShiftStartDateTime(i + 1, performanceShifts,
                        startDateTime.plusDays(1L));
                if (Double.compare(hourDiff, 0) == -1) {
                    // å¦‚果小时差为负数,表示跨天,结束时间需加一
                    endDateTime = endDateTime.plusDays(1L);
                }
                // è¿‡æ»¤å‡ºå½“前人员当前班次的进/出记录
                LocalDateTime workDateTime = null;
                LocalDateTime offWorkDateTime = null;
                List<StaffAttendanceTrackingRecord> enterRecords = filterAttendanceRecord(p.getPersonCode(),
                        EnterOrExitType.ENTER.getValue(), startDateTime, endDateTime, recordList);
                if (!enterRecords.isEmpty()) {
                    // ä¸Šç­æ—¶é—´å’ŒçŠ¶æ€
                    StaffAttendanceTrackingRecord enterRecord = enterRecords.stream()
                            .filter(s -> !s.getSwingTime().isAfter(currentShiftStartDateTime))
                            .max(Comparator.comparing(StaffAttendanceTrackingRecord::getSwingTime))
                            .orElse(new StaffAttendanceTrackingRecord());
                    if (BeanUtil.isEmpty(enterRecord)) {
                        enterRecord = enterRecords.stream()
                                .filter(s -> (s.getSwingTime().isAfter(currentShiftStartDateTime)
                                        && s.getSwingTime().isBefore(currentShiftEndDateTime)))
                                .min(Comparator.comparing(StaffAttendanceTrackingRecord::getSwingTime))
                                .orElse(new StaffAttendanceTrackingRecord());
                        workDateTime = enterRecord.getSwingTime();
                        vo.setWorkClockInState(ClockInState.ABNORMAL.getValue());
                        vo.setWorkDataId(enterRecord.getId());
                    } else {
                        workDateTime = enterRecord.getSwingTime();
                        vo.setWorkClockInState(ClockInState.NORMAL.getValue());
                        vo.setWorkDataId(enterRecord.getId());
                    }
                }
                List<StaffAttendanceTrackingRecord> exitRecords = filterAttendanceRecord(p.getPersonCode(),
                        EnterOrExitType.EXIT.getValue(), startDateTime, endDateTime, recordList);
                if (!exitRecords.isEmpty()) {
                    // ä¸‹ç­æ—¶é—´å’ŒçŠ¶æ€
                    StaffAttendanceTrackingRecord exitRecord = exitRecords.stream()
                            .filter(s -> !s.getSwingTime().isBefore(currentShiftEndDateTime)
                                    && s.getSwingTime().isBefore(nextShiftStartDateTime))
                            .min(Comparator.comparing(StaffAttendanceTrackingRecord::getSwingTime))
                            .orElse(new StaffAttendanceTrackingRecord());
                    if (BeanUtil.isEmpty(exitRecord)) {
                        exitRecord = exitRecords.stream()
                                .filter(s -> (s.getSwingTime().isAfter(currentShiftStartDateTime)
                                        && s.getSwingTime().isBefore(currentShiftEndDateTime)))
                                .max(Comparator.comparing(StaffAttendanceTrackingRecord::getSwingTime))
                                .orElse(new StaffAttendanceTrackingRecord());
                        offWorkDateTime = exitRecord.getSwingTime();
                        vo.setOffClockInState(ClockInState.ABNORMAL.getValue());
                        vo.setOffWorkDataId(exitRecord.getId());
                    } else {
                        offWorkDateTime = exitRecord.getSwingTime();
                        vo.setOffClockInState(ClockInState.NORMAL.getValue());
                        vo.setOffWorkDataId(exitRecord.getId());
                    }
                }
                if (ObjectUtils.allNotNull(workDateTime, offWorkDateTime)) {
                    vo.setActualWorkHours(HourDiffCalculator.getHourDiff(workDateTime.toLocalTime().format(HHmm),
                            offWorkDateTime.toLocalTime().format(HHmm)));
                }
                // èµ‹å€¼
                vo.setShiftId(p.getShift());
                vo.setPersonCode(p.getPersonCode());
                vo.setPersonName(p.getUserName());
                vo.setPlannedWorkHours(hourDiff);
                vo.setSwingDate(startDateTime);
                vo.setWorkDateTime(workDateTime);
                vo.setOffWorkDateTime(offWorkDateTime);
                vo.setDeptName(recordList.get(0).getDeptName());
                vo.setIsSync(recordList.get(0).getIsSync());
                vo.setCreateUser(recordList.get(0).getCreateUser());
                vo.setCreateTime(recordList.get(0).getCreateTime());
                vo.setUpdateUser(recordList.get(0).getUpdateUser());
                vo.setUpdateTime(recordList.get(0).getUpdateTime());
                vo.setResult(ClockInState.ABNORMAL.getValue());
                if(ObjectUtils.allNotNull(vo.getWorkClockInState(),vo.getOffClockInState())){
                    vo.setResult(Integer.min(vo.getWorkClockInState(),vo.getOffClockInState()));
                }
                // è®¡ç®—缺勤时长
                if (ObjectUtils.allNotNull(vo.getActualWorkHours(), vo.getPlannedWorkHours())) {
                    double absenceWorkHours = BigDecimal.valueOf(vo.getPlannedWorkHours())
                            .subtract(BigDecimal.valueOf(vo.getActualWorkHours())).setScale(2, RoundingMode.HALF_EVEN)
                            .doubleValue();
                    if (Double.compare(absenceWorkHours, 0) > 0) {
                        vo.setAbsenceWorkHours(absenceWorkHours);
                    }
                }
                if (!enterRecords.isEmpty() || !exitRecords.isEmpty())
                    resultList.add(vo);
            }
        }
        return limitPages(page, resultList);
    }
    @Override
    public List<StaffAttendanceTrackingRecord> getClockInRecord(StaffAttendanceDTO staffAttendanceDTO) {
        ShiftTime shiftTime = shiftTimeMapper.selectOne(Wrappers.<ShiftTime>lambdaQuery().eq(ShiftTime::getShift, staffAttendanceDTO.getShiftId()));
        if(ObjectUtils.isEmpty(shiftTime)){
            throw new RuntimeException("未查询到当前班次的时间配置");
        }
        LocalDateTime startDateTime = LocalDateTime.of(staffAttendanceDTO.getSwingDate(),LocalTime.MIN);
        LocalDateTime endDateTime = LocalDateTime.of(staffAttendanceDTO.getSwingDate(),LocalTime.MAX);
        //判断当前班次是否要跨天
        double hourDiff = HourDiffCalculator.getHourDiff(shiftTime.getStartTime(), shiftTime.getEndTime());
        if(Double.compare(hourDiff,0)<0){
            endDateTime = endDateTime.plusDays(1L);
        }
        return baseMapper.selectList(Wrappers.<StaffAttendanceTrackingRecord>lambdaQuery()
                .eq(StaffAttendanceTrackingRecord::getPersonCode,staffAttendanceDTO.getPersonCode())
                .between(ObjectUtil.isAllNotEmpty(startDateTime, endDateTime),StaffAttendanceTrackingRecord::getSwingTime, startDateTime,endDateTime)
                .orderByAsc(StaffAttendanceTrackingRecord::getSwingTime)
        );
    }
    @Override
    public PerformanceShiftMapDto checkDutyDate(StaffAttendanceDTO staffAttendanceDTO) {
        if(ObjectUtils.isEmpty(staffAttendanceDTO.getSwingDate())){
            throw new RuntimeException("考勤日期不能为空");
        }
        LocalDateTime startDateTime = LocalDateTime.of(staffAttendanceDTO.getSwingDate(),LocalTime.MIN);
        LocalDateTime endDateTime = LocalDateTime.of(staffAttendanceDTO.getSwingDate(),LocalTime.MAX);
        Long count = baseMapper.selectCount(Wrappers.<StaffAttendanceTrackingRecord>lambdaQuery()
                .eq(StaffAttendanceTrackingRecord::getPersonCode, staffAttendanceDTO.getPersonCode())
                .between(StaffAttendanceTrackingRecord::getSwingTime, startDateTime, endDateTime));
        if(count>0){
            throw new RuntimeException("所选日期已存在考勤记录!");
        }
        //查询人员当天班次配置
        List<PerformanceShiftMapDto> shiftMapDtos = performanceShiftMapper.selectListByWorkTime(startDateTime, endDateTime, staffAttendanceDTO.getPersonCode());
        if(shiftMapDtos.isEmpty()){
            throw new RuntimeException("未找到人员所选考勤时间的班次配置,请先配置班次");
        }
        return shiftMapDtos.get(0);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean saveOrUpdateRecord(StaffAttendanceDTO staffAttendanceDTO) {
        if(ObjectUtils.isEmpty(staffAttendanceDTO)){
            throw new RuntimeException("传参不能为空");
        }
        LocalDateTime workDateTime = LocalDateTime.of(staffAttendanceDTO.getSwingDate(),staffAttendanceDTO.getWorkDateTime());
        LocalDateTime offWorkDateTime = LocalDateTime.of(staffAttendanceDTO.getSwingDate(),staffAttendanceDTO.getOffWorkDateTime());
        //校验上下班时间是否跨天
        double hourDiff = HourDiffCalculator.getHourDiff(staffAttendanceDTO.getWorkDateTime().format(HHmm), staffAttendanceDTO.getOffWorkDateTime().format(HHmm));
        if(Double.compare(hourDiff,0)<0){
            offWorkDateTime = offWorkDateTime.plusDays(1L);
        }
        //组装上/下班考勤记录
        StaffAttendanceTrackingRecord workRecord = new StaffAttendanceTrackingRecord(
                staffAttendanceDTO.getWorkDataId(),
                workDateTime,
                staffAttendanceDTO.getPersonCode(),
                staffAttendanceDTO.getPersonName(),
                staffAttendanceDTO.getDeptName(),
                staffAttendanceDTO.getResult(),
                EnterOrExitType.ENTER.getValue(),
                SyncStatus.INERT.getValue());
        StaffAttendanceTrackingRecord offWorkRecord = new StaffAttendanceTrackingRecord(
                staffAttendanceDTO.getOffWorkDataId(),
                offWorkDateTime,
                staffAttendanceDTO.getPersonCode(),
                staffAttendanceDTO.getPersonName(),
                staffAttendanceDTO.getDeptName(),
                staffAttendanceDTO.getResult(),
                EnterOrExitType.EXIT.getValue(),
                SyncStatus.INERT.getValue());
        List<StaffAttendanceTrackingRecord> records = Arrays.asList(workRecord, offWorkRecord);
        return this.saveOrUpdateBatch(records);
    }
    /**
     * è‡ªå®šä¹‰åˆ†é¡µæ–¹æ³•
     *
     * @param page       åˆ†é¡µå¯¹è±¡
     * @param resultList æ•°æ®åˆ—表
     * @return
     */
    private IPage<StaffAttendanceVO> limitPages(Page<StaffAttendanceTrackingRecord> page,
            List<StaffAttendanceVO> resultList) {
        IPage<StaffAttendanceVO> resultPage = new Page<>();
        long current = page.getCurrent();
        long size = page.getSize();
        if (current < 1)
            current = 1;
        long total = resultList.size();
        long pages = getPages(size,total);
        int startIndex = Math.toIntExact((current - 1) * size >= total ? (pages - 1) * size : (current - 1) * size);
        int endIndex = Math.toIntExact(Math.min(current * size, total));
        List<StaffAttendanceVO> records = resultList.subList(startIndex, endIndex);
        resultPage.setRecords(records);
        resultPage.setTotal(total);
        resultPage.setSize(size);
        resultPage.setCurrent(current);
        if(current>=pages)resultPage.setCurrent(pages);
        return resultPage;
    }
    /**
     * å½“前分页总页数
     */
    private long getPages(long size,long total) {
        if (size == 0) {
            return 0L;
        }
        long pages = total / size;
        if (total % size != 0) {
            pages++;
        }
        return pages;
    }
    /**
     * èŽ·å–æŒ‡å®šä¸‹æ ‡çš„ç­æ¬¡å¼€å§‹æ—¶é—´
     *
     * @param index
     * @param dtoList
     * @return
     */
    private LocalDateTime getShiftStartDateTime(int index, List<PerformanceShiftMapDto> dtoList,
            LocalDateTime nextShiftTime) {
        if (dtoList.isEmpty() || index >= dtoList.size()) {
            return LocalDateTime.of(nextShiftTime.toLocalDate(), LocalTime.MAX);
        }
        LocalTime localTime = ObjectUtil.isNull(dtoList.get(index).getStartTime()) ? LocalTime.MAX
                : LocalTime.parse(dtoList.get(index).getStartTime(), HHmm);
        return LocalDateTime.of(nextShiftTime.toLocalDate(), localTime);
    }
    /**
     * è¿‡æ»¤æŒ‡å®šæ—¶é—´èŒƒå›´çš„进出记录
     *
     * @param personCode    äººå‘˜ç¼–号
     * @param enterOrExit   è¿›é—¨/出门
     * @param startDateTime å¼€å§‹æ—¶é—´
     * @param endDateTime   ç»“束时间
     * @param recordList    è¿›å‡ºè®°å½•列表
     * @return
     */
    public List<StaffAttendanceTrackingRecord> filterAttendanceRecord(String personCode, Integer enterOrExit,
            LocalDateTime startDateTime, LocalDateTime endDateTime, List<StaffAttendanceTrackingRecord> recordList) {
        if (recordList.isEmpty()) {
            return Collections.emptyList();
        }
        return recordList.stream()
                .filter(s -> ObjectUtil.equal(s.getEnterOrExit(), enterOrExit))
                .filter(s -> (s.getSwingTime().isAfter(startDateTime) && s.getSwingTime().isBefore(endDateTime))
                        && StringUtils.equals(s.getPersonCode(), personCode))
                .collect(Collectors.toList());
    }
    /**
     * å°†çº¯æ•°å­—复原为标准员工编号
     *
     * @param number ä¼ å…¥çš„æ•°å­—(员工编号去除前缀和前置0后的数字,支持int/long/String类型)
     * @return æ ‡å‡†æ ¼å¼å‘˜å·¥ç¼–号(如输入123 â†’ ZT-000123)
     * @throws IllegalArgumentException ä¼ å…¥éžæ•°å­—/负数时抛出异常
     */
    public static String restorePersonCode(Object number) {
        // 1. ç©ºå€¼æ ¡éªŒ
        if (number == null) {
            throw new IllegalArgumentException("传入数字不能为空");
        }
        // 2. ç»Ÿä¸€è½¬æ¢ä¸ºå­—符串并去除首尾空格
        String numStr = number.toString().trim();
        // 3. æ ¡éªŒæ˜¯å¦ä¸ºçº¯æ•°å­—(排除负数、非数字字符)
        if (!numStr.matches("\\d+")) {
            throw new IllegalArgumentException("传入的不是有效正整数:" + numStr);
        }
        // 4. è¡¥å‰ç½®0到指定长度(6位),超出则保留原数字
        String paddedNum = String.format("%0" + DIGIT_LENGTH + "d", Long.parseLong(numStr));
        // 5. æ‹¼æŽ¥å‰ç¼€è¿”回
        return PREFIX + paddedNum;
    }
}
inspect-server/src/main/java/com/ruoyi/inspect/task/SyncStaffAttendanceRecordSchedule.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package com.ruoyi.inspect.task;
import com.ruoyi.inspect.service.StaffAttendanceTrackingRecordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
/**
 * åŒæ­¥æ‹‰å–icc开放平台考勤记录定时任务
 */
@Slf4j
@Component
public class SyncStaffAttendanceRecordSchedule {
    @Autowired
    private StaffAttendanceTrackingRecordService trackingRecordService;
    @Scheduled(cron = "0 0 1 * * ?")
    public void sync() {
        log.info("--------同步考勤记录定时任务开始--------");
        LocalDate yesterday = LocalDate.now(ZoneId.of("Asia/Shanghai")).minusDays(1L);
        LocalDateTime startTime = LocalDateTime.of(yesterday, LocalTime.MIN);
        LocalDateTime endTime = LocalDateTime.of(yesterday, LocalTime.MAX);
        trackingRecordService.syncAttendanceRecord(startTime,endTime);
        log.info("--------同步考勤记录定时任务结束--------");
    }
}
inspect-server/src/main/java/com/ruoyi/inspect/util/HourDiffCalculator.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package com.ruoyi.inspect.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Objects;
import java.util.regex.Pattern;
/**
 * æ—¶åˆ†æ—¶é—´å·®è®¡ç®—工具类
 * æ”¯æŒæ ¼å¼ï¼šHH:mm(24小时制),如 08:30、23:59、00:00
 */
public class HourDiffCalculator {
    // æ ¡éªŒHH:mm格式的正则(24小时制,小时00-23,分钟00-59)
    private static final Pattern TIME_PATTERN = Pattern.compile("^([01]?[0-9]|2[0-3]):[0-5][0-9]$");
    /**
     * è®¡ç®—两个时分字符串之间的相隔小时数
     * @param startTime å¼€å§‹æ—¶é—´ï¼ˆHH:mm,如 "08:30")
     * @param endTime ç»“束时间(HH:mm,如 "17:45")
     * @return ç›¸éš”小时数(保留2位小数,如 9.25 å°æ—¶ï¼‰
     * @throws IllegalArgumentException æ—¶é—´æ ¼å¼é”™è¯¯æ—¶æŠ›å‡ºå¼‚常
     */
    public static double getHourDiff(String startTime, String endTime) {
        // 1. æ ¡éªŒæ—¶é—´æ ¼å¼
        validateTimeFormat(startTime);
        validateTimeFormat(endTime);
        // 2. è§£æžæ—¶åˆ†å­—符串为总分钟数
        int startMinutes = convertTimeToMinutes(startTime);
        int endMinutes = convertTimeToMinutes(endTime);
        // 3. è®¡ç®—分钟差
        int minuteDiff = endMinutes - startMinutes;
        // 4. è½¬æ¢ä¸ºå°æ—¶æ•°ï¼ˆä¿ç•™2位小数,避免精度丢失)
        BigDecimal hourDiff = new BigDecimal(minuteDiff)
                .divide(new BigDecimal(60), 2, RoundingMode.HALF_UP);
        return hourDiff.doubleValue();
    }
    /**
     * æ ¡éªŒæ—¶é—´æ ¼å¼æ˜¯å¦ä¸ºHH:mm
     */
    private static void validateTimeFormat(String time) {
        if (time == null || !TIME_PATTERN.matcher(time).matches()) {
            throw new IllegalArgumentException("时间格式错误,需为HH:mm(24小时制):" + time);
        }
    }
    /**
     * å°†HH:mm字符串转换为总分钟数(如 08:30 â†’ 510 åˆ†é’Ÿï¼‰
     */
    private static int convertTimeToMinutes(String time) {
        String[] parts = time.split(":");
        int hour = Integer.parseInt(parts[0]);
        int minute = Integer.parseInt(parts[1]);
        return hour * 60 + minute;
    }
}
inspect-server/src/main/java/com/ruoyi/inspect/vo/StaffAttendanceVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
package com.ruoyi.inspect.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class StaffAttendanceVO {
    /**
     * ä¸Šç­æ•°æ®ä¸»é”®id
     */
    private Long workDataId;
    /**
     * ä¸‹ç­æ•°æ®ä¸»é”®id
     */
    private Long offWorkDataId;
    /**
     * äººå‘˜ç¼–号
     */
    private String personCode;
    /**
     * äººå‘˜åç§°
     */
    private String personName;
    /**
     *部门名称
     */
    private String deptName;
    /**
     *班次id
     */
    private String shiftId;
    /**
     *考勤结果
     */
    private Integer result;
    /**
     *考勤时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDateTime swingDate;
    /**
     *上班时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime workDateTime;
    /**
     *下班时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime offWorkDateTime;
    /**
     *上班打卡状态(0:正常,1:异常)
     */
    private Integer workClockInState;
    /**
     *下班打卡状态(0:正常,1:异常)
     */
    private Integer offClockInState;
    /**
     * åº”勤时长
     */
    private Double plannedWorkHours;
    /**
     * å®žé™…æ—¶é•¿
     */
    private Double actualWorkHours;
    /**
     * ç¼ºå‹¤æ—¶é•¿
     */
    private Double absenceWorkHours;
    /**
     * æ˜¯å¦ä¸ºåŒæ­¥æ•°æ®
     */
    private Integer isSync;
    private Integer createUser;
    private LocalDateTime createTime;
    private Integer updateUser;
    private LocalDateTime updateTime;
}
inspect-server/src/main/java/com/ruoyi/inspect/vo/StaffClockInVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.inspect.vo;
import com.ruoyi.inspect.pojo.StaffAttendanceTrackingRecord;
import lombok.Data;
/**
 * äººå‘˜è€ƒå‹¤ï¼šäººå‘˜æ‰“卡记录响应对象
 */
@Data
public class StaffClockInVO extends StaffAttendanceTrackingRecord {
}
inspect-server/src/main/resources/mapper/StaffAttendanceTrackingRecordMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.inspect.mapper.StaffAttendanceTrackingRecordMapper">
    <resultMap id="BaseResultMap" type="com.ruoyi.inspect.pojo.StaffAttendanceTrackingRecord">
            <id property="id" column="id" jdbcType="BIGINT"/>
            <result property="iccId" column="icc_id" jdbcType="BIGINT"/>
            <result property="swingTime" column="swing_time" jdbcType="TIMESTAMP"/>
            <result property="personId" column="person_id" jdbcType="BIGINT"/>
            <result property="personCode" column="person_code" jdbcType="VARCHAR"/>
            <result property="personName" column="person_name" jdbcType="VARCHAR"/>
            <result property="deptName" column="dept_name" jdbcType="VARCHAR"/>
            <result property="result" column="result" jdbcType="INTEGER"/>
            <result property="cardNumber" column="card_number" jdbcType="VARCHAR"/>
            <result property="cardStatus" column="card_status" jdbcType="INTEGER"/>
            <result property="cardType" column="card_type" jdbcType="INTEGER"/>
            <result property="channelCode" column="channel_code" jdbcType="VARCHAR"/>
            <result property="channelName" column="channel_name" jdbcType="VARCHAR"/>
            <result property="deviceCode" column="device_code" jdbcType="VARCHAR"/>
            <result property="deviceName" column="device_name" jdbcType="VARCHAR"/>
            <result property="enterOrExit" column="enter_or_exit" jdbcType="INTEGER"/>
            <result property="imageType" column="image_type" jdbcType="INTEGER"/>
            <result property="openResult" column="open_result" jdbcType="INTEGER"/>
            <result property="openType" column="open_type" jdbcType="INTEGER"/>
            <result property="paperNumber" column="paper_number" jdbcType="VARCHAR"/>
            <result property="recordImageUrl" column="record_image_url" jdbcType="VARCHAR"/>
            <result property="recordImage" column="record_image" jdbcType="VARCHAR"/>
            <result property="remark" column="remark" jdbcType="VARCHAR"/>
            <result property="maskState" column="mask_state" jdbcType="INTEGER"/>
            <result property="overTemp" column="over_temp" jdbcType="TINYINT"/>
            <result property="curTemp" column="cur_temp" jdbcType="DOUBLE"/>
            <result property="isSync" column="is_sync" jdbcType="INTEGER"/>
            <result property="enableReport" column="enable_report" jdbcType="BOOLEAN"/>
            <result property="createUser" column="create_user" jdbcType="INTEGER"/>
            <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
            <result property="updateUser" column="update_user" jdbcType="INTEGER"/>
            <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
    </resultMap>
    <sql id="Base_Column_List">
        id,icc_id,swing_time,
        person_id,person_code,person_name,
        dept_name,result,card_number,
        card_status,card_type,channel_code,
        channel_name,device_code,device_name,
        enter_or_exit,image_type,open_result,
        open_type,paper_number,record_image_url,
        record_image,remark,mask_state,
        over_temp,cur_temp,is_sync,enable_report,
        create_user,create_time,update_user,
        update_time
    </sql>
    <select id="selectIccIdList" resultType="java.lang.Long">
        select icc_id from staff_attendance_tracking_record where icc_id!='' and icc_id is not null
    </select>
</mapper>
performance-server/src/main/java/com/ruoyi/performance/dto/PerformanceShiftMapDto.java
@@ -12,6 +12,9 @@
@Data
public class PerformanceShiftMapDto extends PerformanceShift {
    @ApiModelProperty("用户编号")
    private String personCode;
    @ApiModelProperty("用户名称")
    private String userName;
@@ -35,4 +38,11 @@
    @ApiModelProperty("日期表头列表")
    private List<Map<Object, Object>> headerList = new ArrayList<>();
    @ApiModelProperty("班次开始时间")
    private String startTime;
    @ApiModelProperty("班次结束时间")
    private String endTime;
}
performance-server/src/main/java/com/ruoyi/performance/mapper/PerformanceShiftMapper.java
@@ -5,9 +5,11 @@
import com.ruoyi.framework.mybatis_config.MyBaseMapper;
import com.ruoyi.performance.dto.PerformanceShiftMapDto;
import com.ruoyi.performance.pojo.PerformanceShift;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@@ -19,6 +21,7 @@
 * @author æ±Ÿè‹éµ·é›ç½‘络科技有限公司
 * @since 2024-05-08 09:12:04
 */
@Mapper
public interface PerformanceShiftMapper extends MyBaseMapper<PerformanceShift> {
    List<PerformanceShiftMapDto> performanceShift(
@@ -39,4 +42,7 @@
    List<PerformanceShiftMapDto> performanceShiftList(@Param("time") String time, @Param("userName") String userName, @Param("laboratory") String laboratory);
    String seldepLimsId(@Param("depLimsId") int depLimsId);
    List<PerformanceShiftMapDto> selectListByWorkTime(@Param("startTime") LocalDateTime startTime,@Param("endTime") LocalDateTime endTime,@Param("keyword") String keyword);
}
performance-server/src/main/java/com/ruoyi/performance/mapper/ShiftTimeMapper.java
@@ -2,6 +2,7 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.performance.pojo.ShiftTime;
import org.apache.ibatis.annotations.Mapper;
/**
 * <p>
@@ -11,6 +12,7 @@
 * @author æ±Ÿè‹éµ·é›ç½‘络科技有限公司
 * @since 2024-07-24 11:22:17
 */
@Mapper
public interface ShiftTimeMapper extends BaseMapper<ShiftTime> {
}
performance-server/src/main/resources/mapper/PerformanceShiftMapper.xml
@@ -145,4 +145,27 @@
        from department_lims
        where id = #{depLimsId}
    </select>
    <select id="selectListByWorkTime" resultType="com.ruoyi.performance.dto.PerformanceShiftMapDto">
        SELECT
            ps.id,
            ps.shift,
            ps.work_time,
            ps.user_id,
            u.name AS user_name,
            u.account AS person_code,
            st.start_time,
            st.end_time
        FROM performance_shift ps
        LEFT JOIN user u ON ps.user_id = u.id
        LEFT JOIN shift_time st ON ps.shift = st.shift
        <where>
            <if test="startTime!=null and endTime!=null">
                AND ps.work_time BETWEEN #{startTime} AND #{endTime}
            </if>
            <if test="keyword!='' and keyword!=null">
                AND (u.account like concat('%',#{keyword},'%') OR u.name like concat('%',#{keyword},'%'))
            </if>
        </where>
        ORDER BY ps.work_time
    </select>
</mapper>
pom.xml
@@ -51,7 +51,6 @@
    <!-- ä¾èµ–声明 -->
    <dependencyManagement>
        <dependencies>
            <!-- è¦†ç›–SpringFramework的依赖配置-->
            <dependency>
                <groupId>org.springframework</groupId>
@@ -314,7 +313,12 @@
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- å¯¹æŽ¥ICC平台-->
        <dependency>
            <groupId>com.dahuatech.icc</groupId>
            <artifactId>java-sdk-oauth</artifactId>
            <version>1.0.13.10</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
ruoyi-admin-ztns/src/main/java/com/ruoyi/web/controller/system/SysPostController.java
@@ -120,8 +120,8 @@
    /**
     * èŽ·å–å²—ä½é€‰æ‹©æ¡†åˆ—è¡¨
     */
    @GetMapping("/optionselect")
    public AjaxResult optionselect()
    @GetMapping("/optionSelect")
    public AjaxResult optionSelect()
    {
        List<SysPost> posts = postService.selectPostAll();
        return success(posts);
ruoyi-admin-ztns/src/main/java/com/ruoyi/web/controller/system/UserController.java
@@ -1,6 +1,7 @@
package com.ruoyi.web.controller.system;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.domain.Result;
import com.ruoyi.common.core.domain.entity.User;
@@ -70,6 +71,11 @@
        return Result.success(userService.delUserDepardLimsId(id));
    }
    @ApiOperation(value = "查询所有用户列表")
    @GetMapping("/selectAllUser")
    public Result selectAllUser(){
        return Result.success(userService.list(Wrappers.<User>lambdaQuery().eq(User::getStatus,0)));
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/Custom.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -20,6 +22,7 @@
    @ApiModelProperty(value = "主键")
    @TableId(type = IdType.AUTO)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;
    @ApiModelProperty(value = "客户名称")
ruoyi-common/src/main/java/com/ruoyi/common/enums/ClockInState.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package com.ruoyi.common.enums;
import lombok.Getter;
/**
 * ICC对接:打卡状态枚举
 */
@Getter
public enum ClockInState {
    NORMAL("正常",1),
    ABNORMAL("不正常",0);
    private String desc;
    private Integer value;
    ClockInState(String desc, Integer value) {
        this.desc = desc;
        this.value = value;
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/enums/EnterOrExitType.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package com.ruoyi.common.enums;
import lombok.Getter;
/**
 * ICC平台对接:进出门类型枚举
 */
@Getter
public enum EnterOrExitType {
    ENTER("进门",1),
    EXIT("出门",2),
    ENTER_OR_EXIT("进/出门",3);
    private String desc;
    private Integer value;
    EnterOrExitType(String desc, Integer value) {
        this.desc = desc;
        this.value = value;
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/enums/SyncStatus.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.common.enums;
import lombok.Getter;
@Getter
public enum SyncStatus {
    SYNC("同步",0),
    INERT("手动新增",1);
    private String desc;
    private Integer value;
    SyncStatus(String desc, Integer value) {
        this.desc = desc;
        this.value = value;
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/utils/api/IfsApiUtils.java
@@ -17,6 +17,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
 * @Author zhuo
@@ -47,9 +48,10 @@
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                Map map = objectMapper.readValue(body, Map.class);
                maps.addAll(objectMapper.readValue(JSONUtil.toJsonStr(map.get("LIST_INFO")), new TypeReference<List<Map<String, Object>>>() {
                }));
                log.info("获取采购订单-->>" + maps);
                if(Objects.nonNull(map.get("SuccessFlag")) && map.get("SuccessFlag").equals("1")){
                    maps.addAll(objectMapper.readValue(JSONUtil.toJsonStr(map.get("LIST_INFO")), new TypeReference<List<Map<String, Object>>>() {}));
                    log.info("同步的采购订单数量-->>,{}" , maps.size());
                }
            } catch (JsonProcessingException e) {
                e.printStackTrace();
                throw new RuntimeException(e);
@@ -108,7 +110,7 @@
     * @return
     */
    public Result importPartLotAttr(String contract,String inAttr) {
        return getResult(contract,"IMPORT_PART_LOT_ATTR_STD", inAttr, "POST", "库存物料批次属性新增修改接口-->");
        return getResult(contract,"IMPORT_PART_LOT_ATTR_STD", inAttr, "GET", "库存物料批次属性新增修改接口-->");
    }
    /**
ruoyi-common/src/main/java/com/ruoyi/common/utils/api/icc/IccApiUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package com.ruoyi.common.utils.api.icc;
import com.dahuatech.hutool.http.Method;
import com.dahuatech.icc.exception.ClientException;
import com.dahuatech.icc.oauth.model.v202010.OauthConfigUserPwdInfo;
import com.dahuatech.icc.oauth.utils.HttpUtils;
import com.ruoyi.common.utils.api.icc.config.OauthConfigUtil;
import com.ruoyi.common.utils.api.icc.model.GetResultPageRequest;
import com.ruoyi.common.utils.api.icc.model.GetResultPageResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
 * å¯¹æŽ¥ICC开放平台工具
 */
@Slf4j
@Component
public class IccApiUtil {
    /**
     * åˆ†é¡µèŽ·å–é—¨ç¦è¿›å‡ºç»“æžœ
     * @param getResultPageRequest
     * @return
     * @throws ClientException
     */
    public GetResultPageResponse getAttendanceResultPage(GetResultPageRequest getResultPageRequest) {
        OauthConfigUserPwdInfo config = OauthConfigUtil.getOauthConfig();
        GetResultPageResponse response=null;
        try {
            response = HttpUtils.executeJson("/evo-apigw/evo-accesscontrol/1.0.0/card/accessControl/swingCardRecord/bycondition/combined", getResultPageRequest,null, Method.POST , config, GetResultPageResponse.class);
        } catch (ClientException e) {
            log.error(e.getErrMsg(), e);
        }
        if(!response.isSuccess()) {
            log.info("分页获取门禁进出失败:{}",response.getErrMsg());
        }
        return response;
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/utils/api/icc/config/OauthConfigUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.common.utils.api.icc.config;
import com.dahuatech.icc.oauth.model.v202010.OauthConfigUserPwdInfo;
public class OauthConfigUtil {
    /**
     * èŽ·å–Oauth配置信息
     *
     * @return Oauth配置信息
     */
    public static OauthConfigUserPwdInfo getOauthConfig() {
        PlatformConfig platformConfig = new PlatformConfig();//读取配置
        OauthConfigUserPwdInfo oauthConfigUserPwdInfo = new OauthConfigUserPwdInfo(
                platformConfig.getHost(),
                platformConfig.getClientId(),
                platformConfig.getClientSecret(),
                platformConfig.getUsername(),
                platformConfig.getPassword(),
                false,
                platformConfig.getHttpsPort(),
                platformConfig.getHttpPort()
        );
        oauthConfigUserPwdInfo.getHttpConfigInfo().setReadTimeout(platformConfig.getReadTimeout());
        oauthConfigUserPwdInfo.getHttpConfigInfo().setConnectionTimeout(platformConfig.getConnectionTimeout());
        return oauthConfigUserPwdInfo;
    }
}
ruoyi-common/src/main/java/com/ruoyi/common/utils/api/icc/config/PlatformConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.common.utils.api.icc.config;
import lombok.Getter;
/**
 * å¹³å°ä¿¡æ¯é…ç½®
 */
@Getter
public class PlatformConfig {
    private final String clientId = "NS-LIMS";//凭证ID自定义,配合申请凭证流程申请
    private final String clientSecret = "3ca893c4-bfda-4d33-bd73-1eb5f3a68df1";//凭证密钥,参考此地址申请https://open-icc.dahuatech.com/iccdoc/enterprisebase/5.0.15/wiki/common/quickstart.html#%E7%94%B3%E8%AF%B7OpenAPI%E7%94%A8%E6%88%B7
    private final String username = "NS-LIMS";//平台登录用户名
    private final String password = "zttZTT123!";//平台登录密码
    private final String host = "10.100.23.241";//平台IP,联调环境IP
    private final String httpsPort = "443";//https默认端口是443,联调环境443映射外网端口为4077
    private final String httpPort = "83";//http默认端口是83,但不开启,,联调环境83映射外网端口为4078;需运维中心开启http调试模式后才支持;isEnableHttpTest=true时有效,
    private final Long connectionTimeout = -1L;//连接超时
    private final Long readTimeout = -1L;//读取超时
}
ruoyi-common/src/main/java/com/ruoyi/common/utils/api/icc/model/GetResultPageRequest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
package com.ruoyi.common.utils.api.icc.model;
import lombok.Data;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
 * program:java-sdk-demo
 *
 * @Author: 355079
 * @Date:2024-04-29 13:48
 * @Description: åˆ†é¡µèŽ·å–è€ƒå‹¤ç»“æžœè¯·æ±‚å‚æ•°
 */
@Data
public class GetResultPageRequest {
    /** ç¬¬å‡ é¡µ */
    @NotNull
    private Integer pageNum;
    /** æ¯é¡µæ¡æ•° */
    @NotNull
    private Integer pageSize;
    /** å¼€å§‹æ—¶é—´(yyyy-MM-dd) */
    @NotNull
    private String startSwingTime;
    /** ç»“束时间(yyyy-MM-dd) */
    @NotNull
    private String endSwingTime;
    /** å…¥åº“开始时间,格式:yyyy-MM-dd HH:mm:ss,V1.1.4新增 */
    private String startCreateTime;
    /** å…¥åº“结束时间,格式:yyyy-MM-dd HH:mm:ss,V1.1.4新增 æŽ¨èå–当前时间减5分钟或更长时间,因为存在客户服务器时间比icc服务器时间快,查询时有新数据进入导致页码与总数不准,数据错位 */
    private String endCreateTime;
    /** å¼€é—¨ç±»åž‹, è¯¦è§ å¼€é—¨ç±»åž‹ å­—å…¸ */
    private Integer openType;
    /** å¼€é—¨ç±»åž‹å¤šé€‰, è¯¦è§ å¼€é—¨ç±»åž‹ å­—典,V1.1.4新增 */
    private List<Integer> openTypes;
    /** å¡ç‰‡ç±»åž‹, ä¸ä¼ -全部, 0-IC卡, 1-有源RFID, 2-CPU卡 */
    private String category;
    /** äººå‘˜åç§°ï¼Œä»…允许汉字字母数字 -_.@ */
    @Max(value = 64,message = "人员名称最大长度限制64")
    private String personName;
    /** äººå‘˜ç¼–号,人员编号仅支持字母或数字 */
    @Max(value = 64,message = "人员编号最大长度限制64")
    private String personCode;
    /** é€šé“编码列表(可通过设备管理接口,获取unitType为7的channels) */
    @Max(value = 500,message = "通道编码列表最大长度限制500")
    private List<String> channelCodes;
    /** éƒ¨é—¨ID, éƒ¨é—¨é—´", "分隔 */
    private String deptIds;
    /** å¡å·ï¼Œå­—母或数字 */
    @Max(value = 64,message = "卡号最大长度限制64")
    private String cardNumber;
    /** äº‹ä»¶ç±»åž‹, ä¸ä¼ -全部, 1-进门, 2出门, 3-进/出门 */
    private Integer enterOrExit;
    /** å¼€é—¨ç»“æžœ, ä¸ä¼ -全部, 1-成功, 0-失败 */
    private Integer openResult;
    /** æ˜¯å¦è¶…温 */
    private Boolean overTemp;
    /** ä½“温低限 */
    private Double curTempStart;
    /** ä½“温高限 */
    private Double curTempEnd;
    /** å£ç½©çŠ¶æ€ 3-带口罩,2—没带口罩,1-未识别 */
    private Integer maskState;
    /** è®¿å®¢ç­›é€‰ï¼Œ1 - åªæŸ¥è¯¢è®¿å®¢è®°å½• 2 - åªæŸ¥è¯¢éžè®¿å®¢è®°å½• */
    private Integer visitorFilter;
}
ruoyi-common/src/main/java/com/ruoyi/common/utils/api/icc/model/GetResultPageResponse.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,89 @@
package com.ruoyi.common.utils.api.icc.model;
import com.dahuatech.icc.oauth.http.IccResponse;
import lombok.Data;
import java.util.List;
/**
 * program:java-sdk-demo
 *
 * @Author: 355079
 * @Date:2024-04-29 14:21
 * @Description: åˆ†é¡µèŽ·å–è€ƒå‹¤ç»“æžœè¯·æ±‚å‚æ•°
 */
@Data
public class GetResultPageResponse extends IccResponse {
    private Data data;
    @lombok.Data
    public static class Data{
        /** å½“前页 */
        private Integer currentPage;
        /** æ¯é¡µæ¡æ•° */
        private Integer pageSize;
        /** æ€»é¡µæ•° */
        private Integer totalPage;
        /** æ€»æ¡æ•° */
        private Integer totalRows;
        /**是否启用云数据库,false-不启用,true-启用*/
        private Boolean enableCloudDB;
        /** åˆ†é¡µæ•°æ® */
        private List<PageData> pageData;
        @lombok.Data
        public static class PageData{
            /**主键id*/
            private Long id;
            /** åˆ·å¡æ—¶é—´ï¼Œæ ¼å¼ï¼šyyyy-MM-dd HH:mm:ss */
            private String swingTime;
            /** äººå‘˜ID */
            private Long personId;
            /** äººå‘˜ç¼–号 ï¼Œè‹¥ä¸ºè®¿å®¢é€šè¡Œè®°å½•,则是访客预约记录id */
            private String personCode;
            /** äººå‘˜å§“名 */
            private String personName;
            /** éƒ¨é—¨åç§° ,为空时代表记录为访客通行记录,非空代表内部人员通行记录 */
            private String deptName;
            /** å¡å· */
            private String cardNumber;
            /** å¡çŠ¶æ€ï¼Œ-1-空白卡,0-正常卡,1-挂失卡,2-注销卡 */
            private Integer cardStatus;
            /** å¡ç±»åž‹ï¼Œ0-IC卡, 1-有源RFID, 2-CPU卡 */
            private Integer cardType;
            /** é€šé“编码 */
            private String channelCode;
            /** é€šé“名称 */
            private String channelName;
            /** è®¾å¤‡ç¼–码 */
            private String deviceCode;
            /** è®¾å¤‡åç§° */
            private String deviceName;
            /** è¿›å‡ºé—¨ç±»åž‹ï¼Œ1-进门, 2出门, 3-进/出门 */
            private Integer enterOrExit;
            /** 1-内部人员, 2-访客:内部人员是在人员管理中添加的人员,访客是在访客子系统中录入的访客 */
            private Integer imageType;
            /** å¼€é—¨ç»“果,0-失败,1-成功 */
            private Integer openResult;
            /** å¼€é—¨ç±»åž‹ï¼Œè¯¦è§ å¼€é—¨ç±»åž‹ å­—å…¸ */
            private Integer openType;
            /** è¯ä»¶å·ç ï¼Œä¼šè¿›è¡Œè„±æ•å¤„理 */
            private String paperNumber;
            /** æŠ“图,相对路径,完整访问路径参考OSS方式组装 */
            private String recordImageUrl;
            /** æŠ“图,绝对路径,兼容历史版本,不推荐使用 */
            private String recordImage;
            /** å¼€é—¨å¤±è´¥åŽŸå›  */
            private String remark;
            /** å…¥åº“时间,格式:yyyy-MM-dd HH:mm:ss */
            private String createTime;
            /** å£ç½©çŠ¶æ€(3-带口罩,2—没带口罩,1-未识别) */
            private Integer maskState;
            /** æ˜¯å¦è¶…温 */
            private Boolean overTemp;
            /** ä½“温 */
            private Double curTemp;
        }
    }
}