Pārlūkot izejas kodu

mqtt,报表导出

LIVE_YE 1 dienu atpakaļ
vecāks
revīzija
76a0689c8b

+ 5 - 0
ruoyi-admin/pom.xml

@@ -53,6 +53,11 @@
             <groupId>com.ruoyi</groupId>
             <artifactId>ruoyi-generator</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
 
     </dependencies>
 

+ 179 - 103
ruoyi-admin/src/main/java/com/ruoyi/web/controller/kaoqin/KaoQinController.java

@@ -1,13 +1,18 @@
 package com.ruoyi.web.controller.kaoqin;
 
+import com.ruoyi.common.annotation.Log;
 import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.AjaxResult;
 import com.ruoyi.common.core.domain.entity.SysDictData;
 import com.ruoyi.common.core.page.TableDataInfo;
 import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.enums.BusinessType;
 import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.MapToObjectUtils;
 import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.system.domain.*;
+import com.ruoyi.system.domain.vo.BaobiaoVo;
 import com.ruoyi.system.mapper.BusinessTripMapper;
 import com.ruoyi.system.mapper.CardReplacementRecordMapper;
 import com.ruoyi.system.mapper.RecordLeaveMapper;
@@ -19,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletResponse;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.util.*;
@@ -70,7 +76,7 @@ public class KaoQinController extends BaseController {
     public AjaxResult daKa(KaoqinConfig kaoqinConfig) {
         //先去 根据当前人员的部门id查询是否有规则,没有再去查祖籍列表
         KaoqinConfig kaoqinConfigDept = kaoqinConfigService.selectKaoqinConfigByDeptId(kaoqinConfig.getDeptId());
-        if (kaoqinConfigDept == null){
+        if (kaoqinConfigDept == null) {
             //根据部门祖籍列表,查询该部门考勤范围 ,经纬度
             List<KaoqinConfig> kaoqinConfigs = kaoqinConfigService.selectKaoqinConfigList(kaoqinConfig);
             if (kaoqinConfigs != null && kaoqinConfigs.size() > 1) {
@@ -109,7 +115,7 @@ public class KaoQinController extends BaseController {
     @GetMapping("/abnormal")
     public AjaxResult abnormal(KaoqinRecord kaoqinRecord) {
         //传入userId/年 和 月份 设置查询异常的考勤
-        kaoqinRecord.setKaStatus("2");
+        //kaoqinRecord.setKaStatus("2");
         List<KaoqinRecord> kaoQinRecords = kaoqinRecordService.selectKaoqinRecordList(kaoqinRecord);
         //记录该月有多少次异常总数
         Long count = 0L;
@@ -121,8 +127,10 @@ public class KaoQinController extends BaseController {
             //获取异常的值
             String dictValue = sysDictDatum.getDictValue();
             if (!"1".equals(dictValue)) {
+                //获取异常名称
+                String dictLabel = dictToString(sysDictDatum.getDictValue());
                 Long aLong = 0L;
-                map.put("num"+dictValue, aLong);
+                map.put(dictLabel, aLong);
             }
         }
         for (KaoqinRecord kaoQinRecord : kaoQinRecords) {
@@ -131,20 +139,27 @@ public class KaoQinController extends BaseController {
             String kaTypePmIn = kaoQinRecord.getKaTypePmIn();
             String kaTypePmOut = kaoQinRecord.getKaTypePmOut();
             if (!"1".equals(kaTypeAmIn) && StringUtils.isNotBlank(kaTypeAmIn)) {
-                Long aLong = map.get("num"+kaTypeAmIn);
-                map.put("num"+kaTypeAmIn, aLong + 1);
+                //获取异常名称
+                String dictLabel = dictToString(kaTypeAmIn);
+                map.compute(dictLabel, (k, aLong) -> aLong + 1);
                 count = count + 1;
-            }  if (!"1".equals(kaTypeAmOut) && StringUtils.isNotBlank(kaTypeAmOut)) {
-                Long aLong = map.get("num"+kaTypeAmOut);
-                map.put("num"+kaTypeAmOut, aLong + 1);
+            }
+            if (!"1".equals(kaTypeAmOut) && StringUtils.isNotBlank(kaTypeAmOut)) {
+                //获取异常名称
+                String dictLabel = dictToString(kaTypeAmOut);
+                map.compute(dictLabel, (k, aLong) -> aLong + 1);
                 count = count + 1;
-            }  if (!"1".equals(kaTypePmIn) && StringUtils.isNotBlank(kaTypePmIn)) {
-                Long aLong = map.get("num"+kaTypePmIn);
-                map.put("num"+kaTypePmIn, aLong + 1);
+            }
+            if (!"1".equals(kaTypePmIn) && StringUtils.isNotBlank(kaTypePmIn)) {
+                //获取异常名称
+                String dictLabel = dictToString(kaTypePmIn);
+                map.compute(dictLabel, (k, aLong) -> aLong + 1);
                 count = count + 1;
-            }  if (!"1".equals(kaTypePmOut) && StringUtils.isNotBlank(kaTypePmOut)) {
-                Long aLong = map.get("num"+kaTypePmOut);
-                map.put("num"+kaTypePmOut, aLong + 1);
+            }
+            if (!"1".equals(kaTypePmOut) && StringUtils.isNotBlank(kaTypePmOut)) {
+                //获取异常名称
+                String dictLabel = dictToString(kaTypePmOut);
+                map.compute(dictLabel, (k, aLong) -> aLong + 1);
                 count = count + 1;
             }
         }
@@ -164,22 +179,22 @@ public class KaoQinController extends BaseController {
         //查询传入月补卡信息
         CardReplacementRecord cardReplacementRecord = new CardReplacementRecord();
         cardReplacementRecord.setUserId(kaoqinRecord.getUserId());
-        String qDate = kaoqinRecord.getKaYear()+"-"+kaoqinRecord.getKaMonth()+"-01";
+        String qDate = kaoqinRecord.getKaYear() + "-" + kaoqinRecord.getKaMonth() + "-01";
         cardReplacementRecord.setApplicationDate(qDate);
         List<CardReplacementRecord> cardReplacementRecordList = cardReplacementRecordMapper.selectCardReplacementRecordList(cardReplacementRecord);
         Map<String, List<CardReplacementRecord>> cardReplacementCollect = cardReplacementRecordList.stream().collect(Collectors.groupingBy(CardReplacementRecord::getApplicationDate));
         //查询传入月请假信息
         RecordLeave recordLeave = new RecordLeave();
-        String month = kaoqinRecord.getKaYear()+"-"+kaoqinRecord.getKaMonth();
-        recordLeave.setStartTime(DateUtils.convertStringToDate(month+"-"+"01 00:00:00"));
+        String month = kaoqinRecord.getKaYear() + "-" + kaoqinRecord.getKaMonth();
+        recordLeave.setStartTime(DateUtils.convertStringToDate(month + "-" + "01 00:00:00"));
         String fin = DateUtils.getLastDayOfMonth(Integer.parseInt(kaoqinRecord.getKaYear()), Integer.parseInt(kaoqinRecord.getKaMonth()));
-        recordLeave.setEndTime(DateUtils.convertStringToDate(fin+" 23:59:59"));
+        recordLeave.setEndTime(DateUtils.convertStringToDate(fin + " 23:59:59"));
         recordLeave.setAbsenteeId(String.valueOf(kaoqinRecord.getUserId()));
         List<RecordLeave> recordLeaves = recordLeaveMapper.selectRecordLeaveList(recordLeave);
         //查询传入月出差信息
         BusinessTrip businessTrip = new BusinessTrip();
-        businessTrip.setStartTime(DateUtils.convertStringToDate(month+"-"+"01 00:00:00"));
-        businessTrip.setEndTime(DateUtils.convertStringToDate(fin+" 23:59:59"));
+        businessTrip.setStartTime(DateUtils.convertStringToDate(month + "-" + "01 00:00:00"));
+        businessTrip.setEndTime(DateUtils.convertStringToDate(fin + " 23:59:59"));
         businessTrip.setUserId(kaoqinRecord.getUserId());
         List<BusinessTrip> businessTrips = businessTripMapper.selectBusinessTripListLb(businessTrip);
 
@@ -192,14 +207,14 @@ public class KaoQinController extends BaseController {
                 List<BusinessTrip> businessList = new ArrayList<>();
                 //请假信息
                 for (RecordLeave recordLeaf : recordLeaves) {
-                    if(DateUtils.isDateBetween(DateUtils.convertStringToDate(date), recordLeaf.getStartTime(), recordLeaf.getEndTime())) {
+                    if (DateUtils.isDateBetween(DateUtils.convertStringToDate(date), recordLeaf.getStartTime(), recordLeaf.getEndTime())) {
                         recordList.add(recordLeaf);
                     }
 
                 }
                 //出差信息
                 for (BusinessTrip trip : businessTrips) {
-                    if(DateUtils.isDateBetween(DateUtils.convertStringToDate(date), trip.getStartTime(), trip.getEndTime())) {
+                    if (DateUtils.isDateBetween(DateUtils.convertStringToDate(date), trip.getStartTime(), trip.getEndTime())) {
                         businessList.add(trip);
                     }
 
@@ -264,9 +279,19 @@ public class KaoQinController extends BaseController {
      * @return
      */
     @PostMapping("/clockRecord")
-    public AjaxResult clockRecord(@RequestBody KaoqinRecord kaoqinRecord) {
+    public AjaxResult clockRecord(KaoqinRecord kaoqinRecord) {
+
+        List<Map<String, Object>> list = kqRecord(kaoqinRecord);
+        return AjaxResult.success(list);
+    }
+
+    private List<Map<String, Object>> kqRecord(KaoqinRecord kaoqinRecord) {
+        if (StringUtils.isEmpty(kaoqinRecord.getKaMonth())) {
+            kaoqinRecord.setKaYear(DateUtils.getYear());
+            kaoqinRecord.setKaMonth(DateUtils.justMonth());
+        }
         //定义返回值
-        List list = new ArrayList();
+        List<Map<String, Object>> list = new ArrayList();
         //根据部门id查询考勤数据
         List<KaoqinRecord> kaoqinRecords = kaoqinRecordService.selectKaoqinRecordList(kaoqinRecord);
         if (kaoqinRecords != null) {
@@ -276,18 +301,16 @@ public class KaoQinController extends BaseController {
             dictData.setDictType("kaoqin_abnormal");
             //找到所有的异常
             List<SysDictData> sysDictData = dictDataService.selectDictDataList(dictData);
-            Map<String, String> map = new HashMap<>();
+            /*Map<String, String> map = new HashMap<>();
             if (sysDictData != null) {
                 for (SysDictData sysDictDatum : sysDictData) {
                     //获取异常名称
                     String dictLabel = sysDictDatum.getDictLabel();
                     //获取异常的值
                     String dictValue = sysDictDatum.getDictValue();
-                    if (!"1".equals(dictValue)) {
-                        map.put(dictValue, dictLabel);
-                    }
+                    map.put(dictValue, dictValue);
                 }
-            }
+            }*/
 
             for (List<KaoqinRecord> value : kaoQinRecords.values()) {
                 if (value != null) {
@@ -296,32 +319,38 @@ public class KaoQinController extends BaseController {
                     userMap.put("userName", value.get(0).getUserName());
                     userMap.put("deptName", value.get(0).getDeptName());
                     userMap.put("list", value);
-                    for (String dictValue : map.keySet()) {
-                        String dictLabel = map.get(dictValue);
-                        userMap.put(dictLabel, 0);
+
+                    for (SysDictData sysDictDatum : sysDictData) {
+                        //获取异常的值
+                        String dictValue = dictToString(sysDictDatum.getDictValue());
+                        userMap.put(dictValue, 0);
                     }
+
                     for (KaoqinRecord kaoQinRecord : value) {
                         String kaTypeAmIn = kaoQinRecord.getKaTypeAmIn();
                         String kaTypeAmOut = kaoQinRecord.getKaTypeAmOut();
                         String kaTypePmIn = kaoQinRecord.getKaTypePmIn();
                         String kaTypePmOut = kaoQinRecord.getKaTypePmOut();
-                        if (!"1".equals(kaTypeAmIn) && StringUtils.isNotBlank(kaTypeAmIn)) {
+                        if (StringUtils.isNotBlank(kaTypeAmIn)) {
                             //获取异常的名称
-                            String dictLabel = map.get(kaTypeAmIn);
+                            String dictLabel = dictToString(kaTypeAmIn);
                             Integer num = (Integer) userMap.get(dictLabel);
                             userMap.put(dictLabel, num + 1);
-                        }  if (!"1".equals(kaTypeAmOut) && StringUtils.isNotBlank(kaTypeAmOut)) {
-                            String dictLabel = map.get(kaTypeAmOut);
+                        }
+                        if (StringUtils.isNotBlank(kaTypeAmOut)) {
+                            String dictLabel = dictToString(kaTypeAmOut);
                             //获取异常的名称
                             Integer num = (Integer) userMap.get(dictLabel);
                             userMap.put(dictLabel, num + 1);
-                        }  if (!"1".equals(kaTypePmIn) && StringUtils.isNotBlank(kaTypePmIn)) {
-                            String dictLabel = map.get(kaTypePmIn);
+                        }
+                        if (StringUtils.isNotBlank(kaTypePmIn)) {
+                            String dictLabel = dictToString(kaTypePmIn);
                             //获取异常的名称
                             Integer num = (Integer) userMap.get(dictLabel);
                             userMap.put(dictLabel, num + 1);
-                        }  if (!"1".equals(kaTypePmOut) && StringUtils.isNotBlank(kaTypePmOut)) {
-                            String dictLabel = map.get(kaTypePmOut);
+                        }
+                        if (StringUtils.isNotBlank(kaTypePmOut)) {
+                            String dictLabel = dictToString(kaTypePmOut);
                             //获取异常的名称
                             Integer num = (Integer) userMap.get(dictLabel);
                             userMap.put(dictLabel, num + 1);
@@ -331,7 +360,7 @@ public class KaoQinController extends BaseController {
                 }
             }
         }
-        return AjaxResult.success(list);
+        return list;
     }
 
     /**
@@ -342,15 +371,15 @@ public class KaoQinController extends BaseController {
     @PreAuthorize("@ss.hasPermi('kaoqin:statistics:dayAbnormal')")
     @GetMapping("/day/abnormal")
     public AjaxResult dayAbnormal(KaoqinRecord kaoqinRecord) {
-        if(StringUtils.isEmpty(kaoqinRecord.getKaTime())){
+        if (StringUtils.isEmpty(kaoqinRecord.getKaTime())) {
             kaoqinRecord.setKaTime(DateUtils.getDate());
         }
         Map<String, Object> map = new HashMap();
         //查询当天考勤数据
         Map<String, Long> todayMap = day(kaoqinRecord);
         //查询昨天考勤数据
-       //获取昨天日期
-        String  yesterday = DateUtils.getYesterday(kaoqinRecord.getKaTime());
+        //获取昨天日期
+        String yesterday = DateUtils.getYesterday(kaoqinRecord.getKaTime());
         kaoqinRecord.setKaTime(yesterday);
         Map<String, Long> yesterdayMap = day(kaoqinRecord);
         for (String s : todayMap.keySet()) {
@@ -364,23 +393,24 @@ public class KaoQinController extends BaseController {
             BigDecimal yesterdayBd = new BigDecimal(yesterdayMap.get(s));
             BigDecimal fz = todayBd.subtract(yesterdayBd);
             if (fz.compareTo(BigDecimal.ZERO) != 0) {
-                BigDecimal  percentage = BigDecimal.ZERO;
+                BigDecimal percentage = BigDecimal.ZERO;
                 //昨天数据>0
-                if (yesterdayBd.compareTo(BigDecimal.ZERO)>0) {
-                      percentage = fz.multiply(new BigDecimal("100"))
+                if (yesterdayBd.compareTo(BigDecimal.ZERO) > 0) {
+                    percentage = fz.multiply(new BigDecimal("100"))
                             .divide(yesterdayBd, 3, RoundingMode.HALF_UP) // 先计算百分比并保留三位小数以处理可能的精度问题
                             .setScale(1, RoundingMode.HALF_UP);     // 再设置为一位小数
                 }
                 zz = percentage + "%";
                 zt = "1";
-                if(percentage.compareTo(BigDecimal.ZERO) < 0){
+                if (percentage.compareTo(BigDecimal.ZERO) < 0) {
                     zt = "2";
-                    zz = (percentage.multiply(new BigDecimal("-1")))+ "%";;
+                    zz = (percentage.multiply(new BigDecimal("-1"))) + "%";
+                    ;
                 }
             }
             mapZj.put("bfb", zz);
-            mapZj.put("zf",zt);
-            map.put(s,mapZj);
+            mapZj.put("zf", zt);
+            map.put(s, mapZj);
         }
 
 
@@ -395,7 +425,7 @@ public class KaoQinController extends BaseController {
     @PreAuthorize("@ss.hasPermi('kaoqin:statistics:monthCount')")
     @GetMapping("/month/count")
     public AjaxResult monthCount(KaoqinRecord kaoqinRecord) {
-        if(StringUtils.isEmpty(kaoqinRecord.getKaMonth())){
+        if (StringUtils.isEmpty(kaoqinRecord.getKaMonth())) {
             kaoqinRecord.setKaYear(DateUtils.getYear());
             kaoqinRecord.setKaMonth(DateUtils.justMonth());
         }
@@ -404,9 +434,9 @@ public class KaoQinController extends BaseController {
         Map<String, Long> todayMap = day(kaoqinRecord);
         //查询昨天考勤数据
         //获取上个月
-        kaoqinRecord.setKaMonth(String.valueOf(Integer.parseInt(kaoqinRecord.getKaMonth())-1));
-        if("01".equals(kaoqinRecord.getKaMonth())){
-            kaoqinRecord.setKaYear(String.valueOf(Integer.parseInt(kaoqinRecord.getKaYear())-1));
+        kaoqinRecord.setKaMonth(String.valueOf(Integer.parseInt(kaoqinRecord.getKaMonth()) - 1));
+        if ("01".equals(kaoqinRecord.getKaMonth())) {
+            kaoqinRecord.setKaYear(String.valueOf(Integer.parseInt(kaoqinRecord.getKaYear()) - 1));
             kaoqinRecord.setKaMonth("12");
         }
         Map<String, Long> yesterdayMap = day(kaoqinRecord);
@@ -421,23 +451,24 @@ public class KaoQinController extends BaseController {
             BigDecimal yesterdayBd = new BigDecimal(yesterdayMap.get(s));
             BigDecimal fz = todayBd.subtract(yesterdayBd);
             if (fz.compareTo(BigDecimal.ZERO) != 0) {
-                BigDecimal  percentage = BigDecimal.ZERO;
+                BigDecimal percentage = BigDecimal.ZERO;
                 //昨天数据>0
-                if (yesterdayBd.compareTo(BigDecimal.ZERO)>0) {
+                if (yesterdayBd.compareTo(BigDecimal.ZERO) > 0) {
                     percentage = fz.multiply(new BigDecimal("100"))
                             .divide(yesterdayBd, 3, RoundingMode.HALF_UP) // 先计算百分比并保留三位小数以处理可能的精度问题
                             .setScale(1, RoundingMode.HALF_UP);     // 再设置为一位小数
                 }
                 zz = percentage + "%";
                 zt = "1";
-                if(percentage.compareTo(BigDecimal.ZERO) < 0){
+                if (percentage.compareTo(BigDecimal.ZERO) < 0) {
                     zt = "2";
-                    zz = (percentage.multiply(new BigDecimal("-1")))+ "%";;
+                    zz = (percentage.multiply(new BigDecimal("-1"))) + "%";
+                    ;
                 }
             }
             mapZj.put("bfb", zz);
-            mapZj.put("zf",zt);
-            map.put(s,mapZj);
+            mapZj.put("zf", zt);
+            map.put(s, mapZj);
         }
 
 
@@ -457,13 +488,13 @@ public class KaoQinController extends BaseController {
         Map<String, Long> mapCount = new HashMap();
         List<KaoqinRecord> kaoQinRecords = kaoqinRecordService.selectKaoqinRecordList(kaoqinRecord);
         //外勤打卡次数
-        Long wq= 0L;
+        Long wq = 0L;
         //迟到打卡次数
-        Long cd= 0L;
+        Long cd = 0L;
         //早退打卡次数
-        Long zt= 0L;
+        Long zt = 0L;
         //缺卡打卡次数
-        Long qk= 0L;
+        Long qk = 0L;
         //记录该月有多少次异常总数
         Long count = 0L;
         SysDictData dictData = new SysDictData();
@@ -475,7 +506,7 @@ public class KaoQinController extends BaseController {
             String dictValue = sysDictDatum.getDictValue();
             if (!"1".equals(dictValue)) {
                 Long aLong = 0L;
-                map.put("num"+dictValue, aLong);
+                map.put("num" + dictValue, aLong);
             }
         }
         for (KaoqinRecord kaoQinRecord : kaoQinRecords) {
@@ -484,20 +515,23 @@ public class KaoQinController extends BaseController {
             String kaTypePmIn = kaoQinRecord.getKaTypePmIn();
             String kaTypePmOut = kaoQinRecord.getKaTypePmOut();
             if (!"1".equals(kaTypeAmIn) && StringUtils.isNotBlank(kaTypeAmIn)) {
-                Long aLong = map.get("num"+kaTypeAmIn);
-                map.put("num"+kaTypeAmIn, aLong + 1);
+                Long aLong = map.get("num" + kaTypeAmIn);
+                map.put("num" + kaTypeAmIn, aLong + 1);
                 count = count + 1;
-            }  if (!"1".equals(kaTypeAmOut) && StringUtils.isNotBlank(kaTypeAmOut)) {
-                Long aLong = map.get("num"+kaTypeAmOut);
-                map.put("num"+kaTypeAmOut, aLong + 1);
+            }
+            if (!"1".equals(kaTypeAmOut) && StringUtils.isNotBlank(kaTypeAmOut)) {
+                Long aLong = map.get("num" + kaTypeAmOut);
+                map.put("num" + kaTypeAmOut, aLong + 1);
                 count = count + 1;
-            }  if (!"1".equals(kaTypePmIn) && StringUtils.isNotBlank(kaTypePmIn)) {
-                Long aLong = map.get("num"+kaTypePmIn);
-                map.put("num"+kaTypePmIn, aLong + 1);
+            }
+            if (!"1".equals(kaTypePmIn) && StringUtils.isNotBlank(kaTypePmIn)) {
+                Long aLong = map.get("num" + kaTypePmIn);
+                map.put("num" + kaTypePmIn, aLong + 1);
                 count = count + 1;
-            }  if (!"1".equals(kaTypePmOut) && StringUtils.isNotBlank(kaTypePmOut)) {
-                Long aLong = map.get("num"+kaTypePmOut);
-                map.put("num"+kaTypePmOut, aLong + 1);
+            }
+            if (!"1".equals(kaTypePmOut) && StringUtils.isNotBlank(kaTypePmOut)) {
+                Long aLong = map.get("num" + kaTypePmOut);
+                map.put("num" + kaTypePmOut, aLong + 1);
                 count = count + 1;
             }
         }
@@ -506,10 +540,10 @@ public class KaoQinController extends BaseController {
         zt = map.get("num4");
         qk = map.get("num5");
 
-        mapCount.put("wq",wq);
-        mapCount.put("cd",cd);
-        mapCount.put("zt",zt);
-        mapCount.put("qk",qk);
+        mapCount.put("wq", wq);
+        mapCount.put("cd", cd);
+        mapCount.put("zt", zt);
+        mapCount.put("qk", qk);
         for (String s : mapCount.keySet()) {
             Map<String, Object> mapZj = new HashMap();
             mapZj.put("num", mapCount.get(s));
@@ -520,14 +554,13 @@ public class KaoQinController extends BaseController {
                     .divide(countBd, 3, RoundingMode.HALF_UP) // 先计算百分比并保留三位小数以处理可能的精度问题
                     .setScale(1, RoundingMode.HALF_UP);     // 再设置为一位小数
             zz = percentage + "%";
-            mapZj.put("bfb",zz);
+            mapZj.put("bfb", zz);
             mapTj.put(s, mapZj);
         }
         return AjaxResult.success(mapTj);
     }
 
 
-
     public Map<String, Long> day(KaoqinRecord kaoqinRecord) {
 
         Map<String, Long> map = new HashMap();
@@ -537,15 +570,15 @@ public class KaoQinController extends BaseController {
         KaoqinConfig kaoqinConfigDept = kaoqinConfigService.selectKaoqinConfigBynew();
         List<KaoqinRecord> kaoQinRecords = kaoqinRecordService.selectKaoqinRecordList(kaoqinRecord);
         //正常打卡次数
-        Long zc= 0L;
+        Long zc = 0L;
         //外勤打卡次数
-        Long wq= 0L;
+        Long wq = 0L;
         //迟到打卡次数
-        Long cd= 0L;
+        Long cd = 0L;
         //早退打卡次数
-        Long zt= 0L;
+        Long zt = 0L;
         //缺卡打卡次数
-        Long qk= 0L;
+        Long qk = 0L;
         //记录该月打卡总人数
         Long count = Long.valueOf(kaoQinRecords.size());
         SysDictData dictData = new SysDictData();
@@ -569,7 +602,7 @@ public class KaoQinController extends BaseController {
                 todayMap.put(kaTypeAmIn, aLong + 1);
             }
             //如果只配置了两次打卡,中间两次打卡不计入统计
-            if("4".equals(kaoqinConfigDept.getKaNum())){
+            if ("4".equals(kaoqinConfigDept.getKaNum())) {
                 if (StringUtils.isNotBlank(kaTypeAmOut)) {
                     Long aLong = todayMap.get(kaTypeAmOut);
                     todayMap.put(kaTypeAmOut, aLong + 1);
@@ -590,15 +623,31 @@ public class KaoQinController extends BaseController {
         zt = todayMap.get("4");
         qk = todayMap.get("5");
 
-        map.put("zs",count);
-        map.put("zc",zc);
-        map.put("wq",wq);
-        map.put("cd",cd);
-        map.put("zt",zt);
-        map.put("qk",qk);
+        map.put("zs", count);
+        map.put("zc", zc);
+        map.put("wq", wq);
+        map.put("cd", cd);
+        map.put("zt", zt);
+        map.put("qk", qk);
         return map;
     }
 
+    public String dictToString(String value) {
+        String str = "";
+        if ("1".equals(value)) {
+            str = "ydk";
+        } else if ("2".equals(value)) {
+            str = "cd";
+        } else if ("3".equals(value)) {
+            str = "wq";
+        } else if ("4".equals(value)) {
+            str = "zt";
+        } else if ("5".equals(value)) {
+            str = "qk";
+        }
+        return str;
+    }
+
     /**
      * 本周补卡统计
      */
@@ -615,13 +664,13 @@ public class KaoQinController extends BaseController {
         List<Integer> nums = new ArrayList<>();
         for (int i = 0; i < weekDates.size(); i++) {
             int num = 0;
-            if (cardMap.get(weekDates.get(i))!=null && !cardMap.get(weekDates.get(i)).isEmpty()) {
+            if (cardMap.get(weekDates.get(i)) != null && !cardMap.get(weekDates.get(i)).isEmpty()) {
                 num = cardMap.get(weekDates.get(i)).size();
             }
             nums.add(num);
         }
-        map.put("x",weekName);
-        map.put("y",nums);
+        map.put("x", weekName);
+        map.put("y", nums);
         return AjaxResult.success(map);
     }
 
@@ -641,14 +690,41 @@ public class KaoQinController extends BaseController {
         List<Integer> nums = new ArrayList<>();
         for (int i = 0; i < weekDates.size(); i++) {
             int num = 0;
-            RecordLeave recordLeave = recordLeaves.get(i);
-            if(DateUtils.isDateBetween(DateUtils.convertStringToDate(weekDates.get(i)), recordLeave.getStartTime(), recordLeave.getEndTime())) {
-                num++;
+            if (recordLeaves != null && !recordLeaves.isEmpty()) {
+                for (RecordLeave recordLeaf : recordLeaves) {
+                    if (DateUtils.isDateBetween(DateUtils.convertStringToDate(weekDates.get(i)), recordLeaf.getStartTime(), recordLeaf.getEndTime())) {
+                        num++;
+                    }
+                }
             }
+
             nums.add(num);
         }
-        map.put("x",weekName);
-        map.put("y",nums);
+        map.put("x", weekName);
+        map.put("y", nums);
         return AjaxResult.success(map);
     }
+
+
+    /**
+     * 导出人员打卡情况报表
+     */
+    @PreAuthorize("@ss.hasPermi('kaoqin:statistics:export')")
+    @Log(title = "导出人员打卡情况报表", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, KaoqinRecord kaoqinRecord) {
+        List<Map<String, Object>> maps = kqRecord(kaoqinRecord);
+        List<BaobiaoVo> baobiaoVoList = new ArrayList<>();
+        try {
+            for (Map<String, Object> map : maps) {
+
+                BaobiaoVo baobiaoVo = MapToObjectUtils.populate(map, BaobiaoVo.class);
+                baobiaoVoList.add(baobiaoVo);
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        ExcelUtil<BaobiaoVo> util = new ExcelUtil<BaobiaoVo>(BaobiaoVo.class);
+        util.exportExcel(response, baobiaoVoList, "考勤统计数据");
+    }
 }

+ 227 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/mqtt/MqttController.java

@@ -0,0 +1,227 @@
+package com.ruoyi.web.controller.mqtt;
+
+import com.ruoyi.common.model.MqttMessage;
+import com.ruoyi.mqtt.service.MqttService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * MQTT控制器
+ *
+ * <p>提供MQTT相关操作的REST API接口,包括消息发布、主题订阅和连接管理等,
+ * 方便通过HTTP请求操作MQTT客户端。</p>
+ *
+ * @author andy
+ * @version 1.0.0
+ * @since 2025-06-17
+ */
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/mqtt")
+public class MqttController {
+
+    private final MqttService mqttService;
+
+    /**
+     * 获取MQTT连接状态
+     *
+     * @return 连接状态信息
+     */
+    @GetMapping("/status")
+    public ResponseEntity<Map<String, Object>> getStatus() {
+        Map<String, Object> status = new HashMap<>();
+        status.put("connected", mqttService.isConnected());
+        status.put("timestamp", System.currentTimeMillis());
+
+        return ResponseEntity.ok(status);
+    }
+
+    /**
+     * 重新连接MQTT服务器
+     *
+     * @return 重连结果
+     */
+    @RequestMapping("/reconnect")
+    public ResponseEntity<Map<String, Object>> reconnect() {
+        Map<String, Object> result = new HashMap<>();
+
+        try {
+            CompletableFuture<Void> future = mqttService.reconnect();
+            future.get(10, TimeUnit.SECONDS);
+
+            result.put("success", true);
+            result.put("message", "已成功重新连接到MQTT服务器");
+            return ResponseEntity.ok(result);
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            log.error("重连MQTT服务器失败: {}", e.getMessage(), e);
+
+            result.put("success", false);
+            result.put("message", "重连MQTT服务器失败: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
+        }
+    }
+
+    /**
+     * 发布MQTT消息
+     *
+     * @param message MQTT消息对象
+     * @return 发布结果
+     */
+    @PostMapping("/publish")
+    public ResponseEntity<Map<String, Object>> publishMessage(@RequestBody MqttMessage message) {
+        Map<String, Object> result = new HashMap<>();
+
+        if (message.getTopic() == null || message.getTopic().isEmpty()) {
+            result.put("success", false);
+            result.put("message", "消息主题不能为空");
+            return ResponseEntity.badRequest().body(result);
+        }
+
+        if (message.getPayload() == null) {
+            result.put("success", false);
+            result.put("message", "消息内容不能为空");
+            return ResponseEntity.badRequest().body(result);
+        }
+
+        try {
+            CompletableFuture<Void> future = mqttService.publish(message);
+            future.get(10, TimeUnit.SECONDS);
+
+            result.put("success", true);
+            result.put("message", "消息发布成功");
+            result.put("topic", message.getTopic());
+            return ResponseEntity.ok(result);
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            log.error("发布消息失败: {}", e.getMessage(), e);
+
+            result.put("success", false);
+            result.put("message", "发布消息失败: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
+        }
+    }
+
+    /**
+     * 简化版发布消息接口
+     *
+     * @param topic 消息主题
+     * @param payload 消息内容
+     * @return 发布结果
+     */
+    @RequestMapping("/publish/simple")
+    public ResponseEntity<Map<String, Object>> publishSimpleMessage(
+            @RequestParam String topic,
+            @RequestParam String payload) {
+
+        MqttMessage message = new MqttMessage(topic, payload);
+        return publishMessage(message);
+    }
+
+    /**
+     * 订阅主题
+     *
+     * @param topic 要订阅的主题
+     * @return 订阅结果
+     */
+    @RequestMapping("/subscribe")
+    public ResponseEntity<Map<String, Object>> subscribeTopic(@RequestParam String topic) {
+        Map<String, Object> result = new HashMap<>();
+
+        if (topic == null || topic.isEmpty()) {
+            result.put("success", false);
+            result.put("message", "订阅主题不能为空");
+            return ResponseEntity.badRequest().body(result);
+        }
+
+        try {
+            String subscribedTopic = mqttService.subscribe(topic);
+
+            result.put("success", true);
+            result.put("message", "主题订阅成功");
+            result.put("topic", subscribedTopic);
+            return ResponseEntity.ok(result);
+        } catch (MqttException e) {
+            log.error("订阅主题失败: {}", e.getMessage(), e);
+
+            result.put("success", false);
+            result.put("message", "订阅主题失败: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
+        }
+    }
+
+    /**
+     * 批量订阅主题
+     *
+     * @param topics 要订阅的主题列表
+     * @return 订阅结果
+     */
+    @PostMapping("/subscribe/batch")
+    public ResponseEntity<Map<String, Object>> subscribeTopics(@RequestBody List<String> topics) {
+        Map<String, Object> result = new HashMap<>();
+
+        if (topics == null || topics.isEmpty()) {
+            result.put("success", false);
+            result.put("message", "订阅主题列表不能为空");
+            return ResponseEntity.badRequest().body(result);
+        }
+
+        try {
+            List<String> subscribedTopics = mqttService.subscribe(topics);
+
+            result.put("success", true);
+            result.put("message", "批量主题订阅成功");
+            result.put("topics", subscribedTopics);
+            return ResponseEntity.ok(result);
+        } catch (MqttException e) {
+            log.error("批量订阅主题失败: {}", e.getMessage(), e);
+
+            result.put("success", false);
+            result.put("message", "批量订阅主题失败: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
+        }
+    }
+
+    /**
+     * 取消订阅主题
+     *
+     * @param topic 要取消订阅的主题
+     * @return 取消订阅结果
+     */
+    @PostMapping("/unsubscribe")
+    public ResponseEntity<Map<String, Object>> unsubscribeTopic(@RequestParam String topic) {
+        Map<String, Object> result = new HashMap<>();
+
+        if (topic == null || topic.isEmpty()) {
+            result.put("success", false);
+            result.put("message", "取消订阅的主题不能为空");
+            return ResponseEntity.badRequest().body(result);
+        }
+
+        try {
+            mqttService.unsubscribe(topic);
+
+            result.put("success", true);
+            result.put("message", "主题取消订阅成功");
+            result.put("topic", topic);
+            return ResponseEntity.ok(result);
+        } catch (MqttException e) {
+            log.error("取消订阅主题失败: {}", e.getMessage(), e);
+
+            result.put("success", false);
+            result.put("message", "取消订阅主题失败: " + e.getMessage());
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
+        }
+    }
+}

+ 39 - 0
ruoyi-common/pom.xml

@@ -15,6 +15,12 @@
         common通用工具
     </description>
 
+    <properties>
+        <paho.mqtt.version>1.2.5</paho.mqtt.version>
+        <moquette.version>0.16</moquette.version>
+        <lombok.version>1.18.30</lombok.version>
+    </properties>
+
     <dependencies>
 
         <!--阿里短信服务-->
@@ -151,6 +157,39 @@
             <version>2.5.1</version>
         </dependency>
 
+        <dependency>
+            <groupId>commons-beanutils</groupId>
+            <artifactId>commons-beanutils</artifactId>
+            <version>1.9.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- === MQTT 相关依赖 === -->
+        <!-- Moquette MQTT broker -->
+        <dependency>
+            <groupId>io.moquette</groupId>
+            <artifactId>moquette-broker</artifactId>
+            <version>${moquette.version}</version>
+        </dependency>
+
+        <!-- Eclipse Paho MQTT Client (MQTT 3.x) -->
+        <dependency>
+            <groupId>org.eclipse.paho</groupId>
+            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
+            <version>${paho.mqtt.version}</version>
+        </dependency>
+
+        <!-- Eclipse Paho MQTT Client (MQTT 5.0) -->
+        <dependency>
+            <groupId>org.eclipse.paho</groupId>
+            <artifactId>org.eclipse.paho.mqttv5.client</artifactId>
+            <version>${paho.mqtt.version}</version>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 84 - 0
ruoyi-common/src/main/java/com/ruoyi/common/config/MqttCallbackHandler.java

@@ -0,0 +1,84 @@
+package com.ruoyi.common.config;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.MqttCallback;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * MQTT回调处理器
+ *
+ * <p>处理MQTT客户端的各种回调事件,包括连接丢失、消息到达和消息发送完成。
+ * 通过Spring事件机制将消息分发到其他组件进行处理。</p>
+ *
+ * @author andy
+ * @version 1.0.0
+ * @since 2025-06-17
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class MqttCallbackHandler implements MqttCallback {
+
+    private final ApplicationEventPublisher eventPublisher;
+
+    /**
+     * 连接丢失回调
+     * 当与MQTT服务器的连接意外断开时调用此方法
+     *
+     * @param cause 连接丢失原因
+     */
+    @Override
+    public void connectionLost(Throwable cause) {
+        log.warn("MQTT连接丢失: {}", cause.getMessage());
+        // 连接丢失后的处理逻辑
+        // 可以根据需要添加重连机制,通常由MqttConnectOptions.setAutomaticReconnect处理
+    }
+
+    /**
+     * 消息到达回调
+     * 当从订阅的主题接收到新消息时调用此方法
+     *
+     * @param topic 消息的主题
+     * @param message 接收到的消息
+     */
+    @Override
+    public void messageArrived(String topic, MqttMessage message) throws Exception {
+        String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
+        log.info("收到MQTT消息: 主题={}, 消息内容={}, QoS={}", topic, payload, message.getQos());
+
+        // 创建事件对象
+        com.ruoyi.common.model.MqttMessage mqttMessage = new com.ruoyi.common.model.MqttMessage();
+        mqttMessage.setTopic(topic);
+        mqttMessage.setPayload(payload);
+        mqttMessage.setQos(message.getQos());
+        mqttMessage.setRetained(message.isRetained());
+
+        // 发布事件,通知其他组件处理此消息
+        eventPublisher.publishEvent(mqttMessage);
+    }
+
+    /**
+     * 消息发送完成回调
+     * 当消息成功发送到服务器后调用此方法
+     *
+     * @param token 消息传递令牌
+     */
+    @Override
+    public void deliveryComplete(IMqttDeliveryToken token) {
+        try {
+            MqttMessage message = token.getMessage();
+            if (message != null) {
+                String payload = new String(message.getPayload(), StandardCharsets.UTF_8);
+                log.debug("MQTT消息发送完成: {}", payload);
+            }
+        } catch (Exception e) {
+            log.error("处理消息发送完成回调时出错: {}", e.getMessage(), e);
+        }
+    }
+}

+ 106 - 0
ruoyi-common/src/main/java/com/ruoyi/common/config/MqttConfig.java

@@ -0,0 +1,106 @@
+package com.ruoyi.common.config;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.UUID;
+
+/**
+ * MQTT客户端配置类
+ *
+ * <p>配置MQTT客户端的连接和回调处理,包括自动重连、证书配置、回调处理等,
+ * 创建MQTT连接并初始化相关组件。</p>
+ *
+ * @author andy
+ * @version 1.0.0
+ * @since 2025-06-17
+ */
+@Slf4j
+@Configuration
+@RequiredArgsConstructor
+public class MqttConfig {
+
+    private final MqttProperties mqttProperties;
+    private final MqttCallbackHandler mqttCallbackHandler;
+
+    /**
+     * 创建MQTT连接选项
+     *
+     * @return MQTT连接选项
+     */
+    @Bean
+    public MqttConnectOptions mqttConnectOptions() {
+        MqttConnectOptions options = new MqttConnectOptions();
+
+        // 设置是否清除会话
+        options.setCleanSession(mqttProperties.isCleanSession());
+
+        // 设置连接超时
+        options.setConnectionTimeout(mqttProperties.getTimeout());
+
+        // 设置心跳间隔
+        options.setKeepAliveInterval(mqttProperties.getKeepAlive());
+
+        // 设置自动重连
+        options.setAutomaticReconnect(mqttProperties.isAutoReconnect());
+
+        // 设置用户名和密码
+        if (mqttProperties.getUsername() != null && !mqttProperties.getUsername().isEmpty()) {
+            options.setUserName(mqttProperties.getUsername());
+
+            if (mqttProperties.getPassword() != null && !mqttProperties.getPassword().isEmpty()) {
+                options.setPassword(mqttProperties.getPassword().toCharArray());
+            }
+        }
+
+        log.info("MQTT连接选项已配置,服务器地址: {}", mqttProperties.getServerUri());
+        return options;
+    }
+
+    /**
+     * 创建MQTT客户端
+     *
+     * @return MQTT客户端
+     * @throws MqttException MQTT异常
+     */
+    @Bean
+    public MqttAsyncClient mqttClient() throws MqttException {
+        // 检查服务器URI
+        if (mqttProperties.getServerUri() == null || mqttProperties.getServerUri().isEmpty()) {
+            throw new IllegalArgumentException("MQTT服务器URI不能为空");
+        }
+
+        // 生成客户端ID
+        String clientId = mqttProperties.getClientId();
+        if (clientId == null || clientId.isEmpty()) {
+            clientId = "spring-mqtt-" + UUID.randomUUID().toString().substring(0, 8);
+            log.info("未配置客户端ID,已自动生成: {}", clientId);
+        }
+
+        // 创建客户端实例
+        MqttAsyncClient client = new MqttAsyncClient(
+                mqttProperties.getServerUri(),
+                clientId,
+                new MemoryPersistence());
+
+        // 设置回调
+        client.setCallback(mqttCallbackHandler);
+
+        // 初始化连接
+        try {
+            client.connect(mqttConnectOptions()).waitForCompletion(mqttProperties.getCommandTimeout() * 1000L);
+            log.info("MQTT客户端已连接到服务器: {}", mqttProperties.getServerUri());
+        } catch (MqttException e) {
+            log.error("MQTT客户端连接失败: {}", e.getMessage(), e);
+            throw e;
+        }
+
+        return client;
+    }
+}

+ 76 - 0
ruoyi-common/src/main/java/com/ruoyi/common/config/MqttProperties.java

@@ -0,0 +1,76 @@
+package com.ruoyi.common.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * MQTT连接属性配置类
+ *
+ * <p>用于从application.yml或application.properties文件中加载MQTT相关的配置项,
+ * 支持自定义MQTT服务器地址、客户端ID、用户名密码等各种连接参数。</p>
+ *
+ * @author andy
+ * @version 1.0.0
+ * @since 2025-06-17
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "mqtt")
+public class MqttProperties {
+
+    /**
+     * 服务器地址URI,例如: tcp://localhost:1883
+     */
+    private String serverUri;
+
+    /**
+     * 客户端ID
+     */
+    private String clientId;
+
+    /**
+     * 用户名
+     */
+    private String username;
+
+    /**
+     * 密码
+     */
+    private String password;
+
+    /**
+     * 超时时间,单位秒
+     */
+    private int timeout = 30;
+
+    /**
+     * 保活时间,单位秒
+     */
+    private int keepAlive = 60;
+
+    /**
+     * 是否自动重连
+     */
+    private boolean autoReconnect = true;
+
+    /**
+     * 是否清除会话
+     */
+    private boolean cleanSession = true;
+
+    /**
+     * 默认QoS级别
+     */
+    private int defaultQos = 1;
+
+    /**
+     * 默认主题
+     */
+    private String defaultTopic = "test/topic";
+
+    /**
+     * 消息发布/订阅超时,单位秒
+     */
+    private int commandTimeout = 10;
+}

+ 83 - 0
ruoyi-common/src/main/java/com/ruoyi/common/model/MqttMessage.java

@@ -0,0 +1,83 @@
+package com.ruoyi.common.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * MQTT消息模型类
+ *
+ * <p>封装MQTT消息的各种属性,包括主题、内容、QoS等,
+ * 可用于消息发布和接收场景。</p>
+ *
+ * @author andy
+ * @version 1.0.0
+ * @since 2025-06-17
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class MqttMessage {
+
+    /**
+     * 消息主题
+     */
+    private String topic;
+
+    /**
+     * 消息内容
+     */
+    private String payload;
+
+    /**
+     * 服务质量
+     * <p>0 - 最多发送一次,不保证送达</p>
+     * <p>1 - 至少发送一次,确保送达但可能重复</p>
+     * <p>2 - 确保仅送达一次</p>
+     */
+    private int qos = 1;
+
+    /**
+     * 是否为保留消息
+     */
+    private boolean retained = false;
+
+    /**
+     * 消息发送/接收时间
+     */
+    private LocalDateTime timestamp = LocalDateTime.now();
+
+    /**
+     * 消息自定义属性
+     */
+    private Map<String, Object> properties;
+
+    /**
+     * 带主题和内容的构造函数
+     *
+     * @param topic 消息主题
+     * @param payload 消息内容
+     */
+    public MqttMessage(String topic, String payload) {
+        this.topic = topic;
+        this.payload = payload;
+    }
+
+    /**
+     * 带主题、内容、QoS和保留标志的构造函数
+     *
+     * @param topic 消息主题
+     * @param payload 消息内容
+     * @param qos 服务质量
+     * @param retained 是否保留
+     */
+    public MqttMessage(String topic, String payload, int qos, boolean retained) {
+        this.topic = topic;
+        this.payload = payload;
+        this.qos = qos;
+        this.retained = retained;
+    }
+}

+ 14 - 0
ruoyi-common/src/main/java/com/ruoyi/common/utils/MapToObjectUtils.java

@@ -0,0 +1,14 @@
+package com.ruoyi.common.utils;
+
+import org.apache.commons.beanutils.BeanUtils;
+
+import java.util.Map;
+
+public class MapToObjectUtils {
+
+    public static <T> T populate(Map<String, ?> map, Class<T> clazz) throws Exception {
+        T obj = clazz.getDeclaredConstructor().newInstance();
+        BeanUtils.populate(obj, map); // 自动匹配Map键和对象属性名
+        return obj;
+    }
+}

+ 5 - 0
ruoyi-system/pom.xml

@@ -22,6 +22,11 @@
             <groupId>com.ruoyi</groupId>
             <artifactId>ruoyi-common</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
 
     </dependencies>
 

+ 77 - 0
ruoyi-system/src/main/java/com/ruoyi/mqtt/service/MqttMessageListener.java

@@ -0,0 +1,77 @@
+package com.ruoyi.mqtt.service;
+
+import com.ruoyi.common.model.MqttMessage;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * MQTT消息监听器
+ *
+ * <p>通过Spring事件机制接收MQTT消息,可以添加自定义的业务逻辑来处理接收到的消息。
+ * 可以创建多个不同的监听器来处理不同主题或类型的消息。</p>
+ *
+ * @author andy
+ * @version 1.0.0
+ * @since 2025-06-17
+ */
+@Slf4j
+@Component
+public class MqttMessageListener {
+
+
+
+
+    /**
+     * 处理接收到的MQTT消息
+     *
+     * @param message MQTT消息对象
+     */
+    @EventListener
+    public void handleMqttMessage(MqttMessage message) {
+        log.info("收到MQTT消息事件: 主题={}, 消息内容={}", message.getTopic(), message.getPayload());
+        // TODO: 添加自定义的业务逻辑来处理消息
+        /**
+         * 返回值为
+         {
+         "userId":"人员id",
+         "time":"时间",
+
+         "photoUrl":"算法截图URL",
+         }
+         */
+
+
+        // 例如:解析JSON消息内容,更新数据库,触发其他操作等
+
+/*        // 可以根据主题进行不同的处理
+        if (message.getTopic().startsWith("device/")) {
+            handleDeviceMessage(message);
+        } else if (message.getTopic().startsWith("alert/")) {
+            handleAlertMessage(message);
+        }*/
+    }
+
+
+
+    /**
+     * 处理设备相关消息
+     *
+     * @param message MQTT消息
+     */
+    private void handleDeviceMessage(MqttMessage message) {
+        // 设备消息处理逻辑
+        log.debug("处理设备消息: {}", message.getPayload());
+    }
+
+    /**
+     * 处理告警相关消息
+     *
+     * @param message MQTT消息
+     */
+    private void handleAlertMessage(MqttMessage message) {
+        // 告警消息处理逻辑
+        log.debug("处理告警消息: {}", message.getPayload());
+    }
+}

+ 276 - 0
ruoyi-system/src/main/java/com/ruoyi/mqtt/service/MqttService.java

@@ -0,0 +1,276 @@
+package com.ruoyi.mqtt.service;
+
+
+import com.ruoyi.common.config.MqttProperties;
+import com.ruoyi.common.model.MqttMessage;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.*;
+import org.eclipse.paho.client.mqttv3.IMqttActionListener;
+import org.eclipse.paho.client.mqttv3.IMqttToken;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * MQTT服务类
+ *
+ * <p>提供MQTT消息发布和订阅功能的核心服务,封装了底层MQTT客户端操作,
+ * 提供简单易用的API供应用程序使用。</p>
+ *
+ * @author andy
+ * @version 1.0.0
+ * @since 2025-06-17
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class MqttService {
+
+    private final MqttAsyncClient mqttClient;
+    private final MqttProperties mqttProperties;
+
+    /**
+     * 初始化方法
+     * 在服务启动时订阅默认主题
+     */
+    @PostConstruct
+    public void init() {
+        // 如果配置了默认主题,则自动订阅
+        if (mqttProperties.getDefaultTopic() != null && !mqttProperties.getDefaultTopic().isEmpty()) {
+            try {
+                List<String> topics = Arrays.asList(mqttProperties.getDefaultTopic().split(","));
+                subscribe(topics);
+                log.info("已自动订阅默认主题: {}", mqttProperties.getDefaultTopic());
+            } catch (MqttException e) {
+                log.error("自动订阅默认主题失败: {}", e.getMessage(), e);
+            }
+        }
+    }
+
+    /**
+     * 销毁方法
+     * 在服务关闭时断开MQTT连接
+     */
+    @PreDestroy
+    public void destroy() {
+        try {
+            if (mqttClient != null && mqttClient.isConnected()) {
+                mqttClient.disconnect().waitForCompletion(mqttProperties.getCommandTimeout() * 1000L);
+                log.info("MQTT客户端已断开连接");
+            }
+        } catch (MqttException e) {
+            log.error("断开MQTT连接时出错: {}", e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 检查客户端连接状态
+     *
+     * @return 是否已连接
+     */
+    public boolean isConnected() {
+        return mqttClient != null && mqttClient.isConnected();
+    }
+
+    /**
+     * 重新连接到MQTT服务器
+     *
+     * @return 重连操作的CompletableFuture
+     */
+    public CompletableFuture<Void> reconnect() {
+        CompletableFuture<Void> future = new CompletableFuture<>();
+
+        try {
+            if (mqttClient == null) {
+                future.completeExceptionally(new IllegalStateException("MQTT客户端未初始化"));
+                return future;
+            }
+
+            if (mqttClient.isConnected()) {
+                log.info("MQTT客户端已经连接,无需重连");
+                future.complete(null);
+                return future;
+            }
+
+            mqttClient.connect(new MqttConnectOptions())
+                    .setActionCallback(new IMqttActionListener() {
+                        @Override
+                        public void onSuccess(IMqttToken token) {
+                            log.info("MQTT客户端重连成功");
+                            future.complete(null);
+                        }
+
+                        @Override
+                        public void onFailure(IMqttToken token, Throwable exception) {
+                            log.error("MQTT客户端重连失败: {}", exception.getMessage(), exception);
+                            future.completeExceptionally(exception);
+                        }
+                    });
+        } catch (MqttException e) {
+            log.error("MQTT重连过程中出错: {}", e.getMessage(), e);
+            future.completeExceptionally(e);
+        }
+
+        return future;
+    }
+
+    /**
+     * 发布消息
+     *
+     * @param message MQTT消息对象
+     * @return 发布操作的CompletableFuture
+     */
+    public CompletableFuture<Void> publish(MqttMessage message) {
+        CompletableFuture<Void> future = new CompletableFuture<>();
+
+        try {
+            if (!isConnected()) {
+                throw new MqttException(MqttException.REASON_CODE_CLIENT_NOT_CONNECTED);
+            }
+
+            // 创建Paho消息对象
+            org.eclipse.paho.client.mqttv3.MqttMessage mqttMessage =
+                    new org.eclipse.paho.client.mqttv3.MqttMessage(message.getPayload().getBytes(StandardCharsets.UTF_8));
+            mqttMessage.setQos(message.getQos());
+            mqttMessage.setRetained(message.isRetained());
+
+            // 发布消息
+            mqttClient.publish(message.getTopic(), mqttMessage)
+                    .setActionCallback(new IMqttActionListener() {
+                        @Override
+                        public void onSuccess(IMqttToken token) {
+                            log.debug("消息发布成功: 主题={}, 内容={}", message.getTopic(), message.getPayload());
+                            future.complete(null);
+                        }
+
+                        @Override
+                        public void onFailure(IMqttToken token, Throwable exception) {
+                            log.error("消息发布失败: {}", exception.getMessage(), exception);
+                            future.completeExceptionally(exception);
+                        }
+                    });
+        } catch (Exception e) {
+            log.error("发布消息时出错: {}", e.getMessage(), e);
+            future.completeExceptionally(e);
+        }
+
+        return future;
+    }
+
+    /**
+     * 使用单独参数发布消息
+     *
+     * @param topic 消息主题
+     * @param payload 消息内容
+     * @return 发布操作的CompletableFuture
+     */
+    public CompletableFuture<Void> publish(String topic, String payload) {
+        MqttMessage message = new MqttMessage(topic, payload, mqttProperties.getDefaultQos(), false);
+        return publish(message);
+    }
+
+    /**
+     * 使用完整参数发布消息
+     *
+     * @param topic 消息主题
+     * @param payload 消息内容
+     * @param qos 服务质量
+     * @param retained 是否保留
+     * @return 发布操作的CompletableFuture
+     */
+    public CompletableFuture<Void> publish(String topic, String payload, int qos, boolean retained) {
+        MqttMessage message = new MqttMessage(topic, payload, qos, retained);
+        return publish(message);
+    }
+
+    /**
+     * 订阅主题
+     *
+     * @param topic 要订阅的主题
+     * @return 订阅成功的主题
+     * @throws MqttException MQTT异常
+     */
+    public String subscribe(String topic) throws MqttException {
+        return subscribe(topic, mqttProperties.getDefaultQos());
+    }
+
+    /**
+     * 使用指定QoS订阅主题
+     *
+     * @param topic 要订阅的主题
+     * @param qos 服务质量
+     * @return 订阅成功的主题
+     * @throws MqttException MQTT异常
+     */
+    public String subscribe(String topic, int qos) throws MqttException {
+        if (!isConnected()) {
+            throw new MqttException(MqttException.REASON_CODE_CLIENT_NOT_CONNECTED);
+        }
+
+        mqttClient.subscribe(topic, qos).waitForCompletion(mqttProperties.getCommandTimeout() * 1000L);
+        log.info("已订阅主题: {}, QoS: {}", topic, qos);
+        return topic;
+    }
+
+    /**
+     * 批量订阅主题
+     *
+     * @param topics 要订阅的主题列表
+     * @return 订阅成功的主题列表
+     * @throws MqttException MQTT异常
+     */
+    public List<String> subscribe(List<String> topics) throws MqttException {
+        if (!isConnected()) {
+            throw new MqttException(MqttException.REASON_CODE_CLIENT_NOT_CONNECTED);
+        }
+
+        String[] topicArray = topics.toArray(new String[0]);
+        int[] qosArray = new int[topics.size()];
+
+        // 默认使用相同的QoS级别
+        Arrays.fill(qosArray, mqttProperties.getDefaultQos());
+
+        mqttClient.subscribe(topicArray, qosArray).waitForCompletion(mqttProperties.getCommandTimeout() * 1000L);
+        log.info("已批量订阅主题: {}", topics);
+        return topics;
+    }
+
+    /**
+     * 取消订阅主题
+     *
+     * @param topic 要取消订阅的主题
+     * @throws MqttException MQTT异常
+     */
+    public void unsubscribe(String topic) throws MqttException {
+        if (!isConnected()) {
+            throw new MqttException(MqttException.REASON_CODE_CLIENT_NOT_CONNECTED);
+        }
+
+        mqttClient.unsubscribe(topic).waitForCompletion(mqttProperties.getCommandTimeout() * 1000L);
+        log.info("已取消订阅主题: {}", topic);
+    }
+
+    /**
+     * 批量取消订阅主题
+     *
+     * @param topics 要取消订阅的主题列表
+     * @throws MqttException MQTT异常
+     */
+    public void unsubscribe(List<String> topics) throws MqttException {
+        if (!isConnected()) {
+            throw new MqttException(MqttException.REASON_CODE_CLIENT_NOT_CONNECTED);
+        }
+
+        String[] topicArray = topics.toArray(new String[0]);
+        mqttClient.unsubscribe(topicArray).waitForCompletion(mqttProperties.getCommandTimeout() * 1000L);
+        log.info("已批量取消订阅主题: {}", topics);
+    }
+}

+ 49 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/BaobiaoVo.java

@@ -0,0 +1,49 @@
+package com.ruoyi.system.domain.vo;
+
+import com.ruoyi.common.annotation.Excel;
+
+public class BaobiaoVo {
+
+    /**
+     * 姓名
+     */
+    @Excel(name = "姓名", type = Excel.Type.EXPORT)
+    private String name;
+
+    /**
+     * 部门名称
+     */
+    @Excel(name = "部门名称", type = Excel.Type.EXPORT)
+    private String deptName;
+
+    /**
+     * 已打卡
+     */
+    @Excel(name = "已打卡", type = Excel.Type.EXPORT)
+    private boolean ydk;
+
+    /**
+     * 迟到
+     */
+    @Excel(name = "迟到", type = Excel.Type.EXPORT)
+    private String cd;
+
+    /**
+     * 早退
+     */
+    @Excel(name = "早退", type = Excel.Type.EXPORT)
+    private String zt;
+
+    /**
+     * 缺卡
+     */
+    @Excel(name = "缺卡", type = Excel.Type.EXPORT)
+    private String qk;
+
+    /**
+     * 外勤
+     */
+    @Excel(name = "外勤", type = Excel.Type.EXPORT)
+    private String wq;
+
+}

+ 2 - 2
ruoyi-system/src/main/resources/mapper/system/BusinessTripMapper.xml

@@ -85,10 +85,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isPass != null  and isPass != ''"> and t.is_pass = #{isPass}</if>
             <if test="submitTime != null "> and t.submit_time = #{submitTime}</if>
             <if test="startTime != null and startTime != ''">
-                AND start_time &gt;= #{startTime}
+                AND date_format(start_time,'%y%m%d %H:%i:%s') &gt;=  date_format(#{startTime},'%y%m%d %H:%i:%s')
             </if>
             <if test="endTime != null and endTime != ''">
-                AND end_time &lt;= #{endTime}
+                AND date_format(end_time,'%y%m%d %H:%i:%s') &lt;=  date_format(#{endTime},'%y%m%d %H:%i:%s')
             </if>
         </where>
         order by t.is_pass, t.submit_time desc

+ 2 - 2
ruoyi-system/src/main/resources/mapper/system/RecordLeaveMapper.xml

@@ -44,10 +44,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 </foreach>
             </if>
             <if test="startTime != null and startTime != ''">
-                AND start_time &gt;= #{startTime}
+                AND date_format(start_time,'%y%m%d %H:%i:%s') &gt;=  date_format(#{startTime},'%y%m%d %H:%i:%s')
             </if>
             <if test="endTime != null and endTime != ''">
-                AND end_time &lt;= #{endTime}
+                AND date_format(end_time,'%y%m%d %H:%i:%s') &lt;=  date_format(#{endTime},'%y%m%d %H:%i:%s')
             </if>
         </where>
         order by is_pass,submit_time desc