From c96fbe6b0aeb413a17c429133ee6d81d138144b9 Mon Sep 17 00:00:00 2001
From: liujie <1793218484@qq.com>
Date: 星期二, 09 六月 2026 10:58:52 +0800
Subject: [PATCH] Merge branch '1.0' of http://120.76.84.145:10101/gitblit/r/java/Robotaxi into 1.0

---
 xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpClockInServiceImpl.java |  488 +++++++++++++++++++++++++++++++++++++++++++++---------
 1 files changed, 405 insertions(+), 83 deletions(-)

diff --git a/xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpClockInServiceImpl.java b/xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpClockInServiceImpl.java
index fed04e9..a5879ea 100644
--- a/xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpClockInServiceImpl.java
+++ b/xp-robotaxi-maintenance-boot/src/main/java/com/xiaopeng/maintenance/service/impl/XpClockInServiceImpl.java
@@ -1,38 +1,53 @@
 package com.xiaopeng.maintenance.service.impl;
 
 import cn.hutool.core.collection.CollUtil;
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.xiaopeng.api.dto.pushMessage.AliPushMessageDTO;
 import com.xiaopeng.api.feignClient.account.XpOperationUserClient;
+import com.xiaopeng.api.feignClient.account.XpOperationUserNotificationClient;
+import com.xiaopeng.api.feignClient.pushMessage.XpAliPushMessageClient;
+import com.xiaopeng.api.feignClient.vehicleConvoy.XpFleetClient;
+import com.xiaopeng.api.feignClient.vehicleConvoy.XpFleetOperationsClient;
 import com.xiaopeng.api.model.account.SysRole;
-import com.xiaopeng.api.model.account.XpOperationsRole;import com.xiaopeng.api.model.account.XpOperationsUser;import com.xiaopeng.api.model.maintenance.XpCheckLocation;
+import com.xiaopeng.api.model.account.XpOperationsRole;import com.xiaopeng.api.model.account.XpOperationsUser;
+import com.xiaopeng.api.model.account.XpOperationsUserNotification;
+import com.xiaopeng.api.model.maintenance.XpCheckLocation;
 import com.xiaopeng.api.model.maintenance.XpClockIn;
+import com.xiaopeng.api.model.maintenance.XpSchedulingDetail;
 import com.xiaopeng.api.model.maintenance.XpShift;
+import com.xiaopeng.api.model.vehicleConvoy.XpFleet;
 import com.xiaopeng.api.vo.account.XpOperationsUserVO;
 import com.xiaopeng.api.vo.account.XpOperationsUserWithRoleVO;import com.xiaopeng.common.core.constant.Constants;
 import com.xiaopeng.common.core.domain.R;
+import com.xiaopeng.common.core.enums.NotificationTypeEnum;
 import com.xiaopeng.common.core.utils.DateUtils;
 import com.xiaopeng.common.core.utils.GeodesyUtil;
 import com.xiaopeng.common.core.utils.StringUtils;
 import com.xiaopeng.common.core.utils.poi.ExcelUtil;
 import com.xiaopeng.common.core.web.page.PageInfo;
-import com.xiaopeng.maintenance.dto.ClockDTO;
-import com.xiaopeng.maintenance.dto.ClockInStatisticsDetailsDto;
-import com.xiaopeng.maintenance.dto.ClockInStatisticsDto;
-import com.xiaopeng.maintenance.dto.VerifyRangeDTO;
+import com.xiaopeng.maintenance.dto.*;
 import com.xiaopeng.maintenance.mapper.XpCheckLocationMapper;
 import com.xiaopeng.maintenance.mapper.XpClockInMapper;
 import com.xiaopeng.maintenance.mapper.XpShiftMapper;
 import com.xiaopeng.maintenance.service.IXpClockInService;
+import com.xiaopeng.maintenance.service.IXpSchedulingDetailService;
 import com.xiaopeng.maintenance.vo.ClockInStatisticsDetailsVo;
 import com.xiaopeng.maintenance.vo.ClockInStatisticsVo;
 import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 
 import lombok.extern.slf4j.Slf4j;
@@ -43,6 +58,9 @@
 import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -58,6 +76,10 @@
 @Service
 public class XpClockInServiceImpl extends ServiceImpl<XpClockInMapper, XpClockIn> implements IXpClockInService {
 
+    private static final String CLOCK_ATTENDANCE_LOCK_PREFIX = "CLOCK_ATTENDANCE_";
+    private static final long CLOCK_ATTENDANCE_LOCK_WAIT_SECONDS = 1L;
+    private static final long CLOCK_ATTENDANCE_LOCK_LEASE_SECONDS = 10L;
+
     @Resource
     private XpCheckLocationMapper xpCheckLocationMapper;
     @Resource
@@ -67,6 +89,21 @@
 
     @Resource
     private XpOperationUserClient xpOperationUserClient;
+
+    @Resource
+    private IXpSchedulingDetailService xpSchedulingDetailService;
+
+    @Resource
+    private XpFleetClient xpFleetClient;
+
+    @Resource
+    private XpFleetOperationsClient fleetOperationsClient;
+    @Resource
+    private XpOperationUserNotificationClient xpOperationUserNotificationClient;
+    @Resource
+    private XpAliPushMessageClient xpAliPushMessageClient;
+    @Resource
+    private RedissonClient redissonClient;
 
 
 
@@ -92,9 +129,15 @@
     }
 
     @Override
-    public R<String> clockIn(ClockDTO dto) {
+    public R<Integer> clockIn(ClockDTO dto) {
 
         XpCheckLocation xpCheckLocation = xpCheckLocationMapper.selectById(dto.getLocationId());
+        if (null == xpCheckLocation) {
+            return R.fail("打卡地址不存在");
+        }
+        if (xpCheckLocation.getStatus() == 0) {
+            return R.fail("打卡地址已禁用");
+        }
 
         Map<String, Double> distance = GeodesyUtil.getDistance(dto.getUserLon() + "," + dto.getUserLat(), xpCheckLocation.getLocationLon() + "," + xpCheckLocation.getLocationLat());
         if (null != distance) {
@@ -117,7 +160,8 @@
 
         XpClockIn xpClockIn = new XpClockIn();
         xpClockIn.setAppUserId(dto.getOperationsUserId());
-        xpClockIn.setClockInTime(LocalDateTime.now());
+        LocalDateTime now = LocalDateTime.now();
+        xpClockIn.setClockInTime(now);
         xpClockIn.setClockInLon(dto.getUserLon());
         xpClockIn.setClockInLat(dto.getUserLat());
         xpClockIn.setShiftId(dto.getShiftId());
@@ -128,13 +172,20 @@
             xpClockIn.setClockInResult(1);
         }
         this.save(xpClockIn);
-        return R.ok();
+        sendAgreementUpdateNotification(now);
+        return R.ok(xpClockIn.getClockInResult());
     }
 
     @Override
-    public R<String> clockOut(ClockDTO dto) {
+    public R<Integer> clockOut(ClockDTO dto) {
 
         XpCheckLocation xpCheckLocation = xpCheckLocationMapper.selectById(dto.getLocationId());
+        if (null == xpCheckLocation) {
+            return R.fail("打卡地址不存在");
+        }
+        if (xpCheckLocation.getStatus() == 0) {
+            return R.fail("打卡地址已禁用");
+        }
         Map<String, Double> distance = GeodesyUtil.getDistance(dto.getUserLon() + "," + dto.getUserLat(), xpCheckLocation.getLocationLon() + "," + xpCheckLocation.getLocationLat());
         if (null != distance) {
             Double distance1 = distance.get("WGS84");
@@ -145,43 +196,142 @@
             return R.fail("打卡失败");
         }
 
-        // 查询上班打卡信息
-        XpClockIn xpClockIn = xpClockInMapper.selectOne(Wrappers.lambdaQuery(XpClockIn.class)
-                .eq(XpClockIn::getAppUserId, dto.getOperationsUserId())
-                .likeRight(XpClockIn::getClockInTime, LocalDate.now())
-                .last("LIMIT 1"));
-        if (Objects.isNull(xpClockIn)) {
-            return R.fail("请先上班打卡");
-        }
+        LocalDate attendanceDate = LocalDate.now();
+        RLock lock = redissonClient.getLock(buildAttendanceLockKey(dto.getOperationsUserId(), attendanceDate));
+        try {
+            boolean isLocked = lock.tryLock(CLOCK_ATTENDANCE_LOCK_WAIT_SECONDS,
+                    CLOCK_ATTENDANCE_LOCK_LEASE_SECONDS, TimeUnit.SECONDS);
+            if (!isLocked) {
+                return R.fail("系统繁忙,请稍后重试");
+            }
 
-        // 查询班次
-        XpShift xpShift = xpShiftMapper.selectById(xpClockIn.getShiftId());
-        if (null == xpShift) {
-            return R.fail("班次不存在");
-        }
-        // 开始打卡时间
-        String endTime = xpShift.getEndTime();
-        LocalDateTime endLocalDateTime = DateUtils.stringToLocalDateTime(LocalDate.now() + " " + endTime + ":00");
+            LocalDateTime now = LocalDateTime.now();
+            XpClockIn xpClockIn = findTodayClockInRecord(dto.getOperationsUserId(), attendanceDate);
+            boolean isNewRecord = false;
+            if (Objects.isNull(xpClockIn)) {
+                Long shiftId = resolveClockOutShiftId(dto.getOperationsUserId(), dto.getShiftId(), attendanceDate);
+                if (Objects.isNull(shiftId)) {
+                    return R.fail("班次不存在");
+                }
+                xpClockIn = buildMissingClockInRecord(dto.getOperationsUserId(), shiftId, now);
+                isNewRecord = true;
+            }
 
-        xpClockIn.setClockOutTime(LocalDateTime.now());
-        xpClockIn.setClockOutLon(dto.getUserLon());
-        xpClockIn.setClockOutLat(dto.getUserLat());
-        xpClockIn.setClockOutAddress(dto.getClockOutAddress());
-        if (xpClockIn.getClockOutTime().isBefore(endLocalDateTime)) {
-            xpClockIn.setClockInResult(2);
-        } else {
-            xpClockIn.setClockInResult(1);
+            XpShift xpShift = xpShiftMapper.selectById(xpClockIn.getShiftId());
+            if (null == xpShift) {
+                return R.fail("班次不存在");
+            }
+
+            fillClockOutInfo(xpClockIn, dto, xpShift, now, attendanceDate);
+            if (isNewRecord) {
+                this.save(xpClockIn);
+            } else {
+                this.updateById(xpClockIn);
+            }
+            sendAgreementUpdateNotification(now);
+            return R.ok(xpClockIn.getClockOutResult());
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("下班打卡获取考勤锁被中断, appUserId={}", dto.getOperationsUserId(), e);
+            return R.fail("系统繁忙,请稍后重试");
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
         }
-        xpClockIn.setAppUserId(null);
-        this.updateById(xpClockIn);
-        return R.ok();
     }
 
     @Override
-    public Boolean checkWorkStatus(Long appUserId) {
+    public boolean createMissingClockInIfAbsent(Long appUserId, Long shiftId, LocalDate attendanceDate) {
+        if (Objects.isNull(appUserId) || Objects.isNull(shiftId) || Objects.isNull(attendanceDate)) {
+            return false;
+        }
+        RLock lock = redissonClient.getLock(buildAttendanceLockKey(appUserId, attendanceDate));
+        try {
+            boolean isLocked = lock.tryLock(CLOCK_ATTENDANCE_LOCK_WAIT_SECONDS,
+                    CLOCK_ATTENDANCE_LOCK_LEASE_SECONDS, TimeUnit.SECONDS);
+            if (!isLocked) {
+                log.info("补上班缺卡获取锁失败, appUserId={}, attendanceDate={}", appUserId, attendanceDate);
+                return false;
+            }
+            XpClockIn existingClockIn = findTodayClockInRecord(appUserId, attendanceDate);
+            if (Objects.nonNull(existingClockIn)) {
+                return false;
+            }
+            XpClockIn missingClockIn = buildMissingClockInRecord(appUserId, shiftId, LocalDateTime.now());
+            return this.save(missingClockIn);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("补上班缺卡获取考勤锁被中断, appUserId={}, attendanceDate={}", appUserId, attendanceDate, e);
+            return false;
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+    }
+
+    private XpClockIn findTodayClockInRecord(Long appUserId, LocalDate attendanceDate) {
+        LocalDateTime startTime = attendanceDate.atStartOfDay();
+        LocalDateTime endTime = attendanceDate.plusDays(1).atStartOfDay();
+        return xpClockInMapper.selectOne(Wrappers.lambdaQuery(XpClockIn.class)
+                .eq(XpClockIn::getAppUserId, appUserId)
+                .ge(XpClockIn::getClockInTime, startTime)
+                .lt(XpClockIn::getClockInTime, endTime)
+                .orderByDesc(XpClockIn::getClockInTime)
+                .last("LIMIT 1"));
+    }
+
+    private Long resolveClockOutShiftId(Long appUserId, Long fallbackShiftId, LocalDate attendanceDate) {
+        R<XpFleet> fleetResult = xpFleetClient.getFleetByOperationsUserId(appUserId);
+        XpFleet xpFleet = Objects.isNull(fleetResult) ? null : fleetResult.getData();
+        if (Objects.nonNull(xpFleet)) {
+            XpSchedulingDetail schedulingDetail = xpSchedulingDetailService.getOne(Wrappers.lambdaQuery(XpSchedulingDetail.class)
+                    .eq(XpSchedulingDetail::getFleetId, xpFleet.getId())
+                    .eq(XpSchedulingDetail::getPlanTime, attendanceDate)
+                    .last("LIMIT 1"));
+            if (Objects.nonNull(schedulingDetail) && Objects.nonNull(schedulingDetail.getShiftId())) {
+                return schedulingDetail.getShiftId();
+            }
+        }
+        return fallbackShiftId;
+    }
+
+    private XpClockIn buildMissingClockInRecord(Long appUserId, Long shiftId, LocalDateTime now) {
+        XpClockIn xpClockIn = new XpClockIn();
+        xpClockIn.setAppUserId(appUserId);
+        xpClockIn.setClockInTime(now);
+        xpClockIn.setClockInResult(3);
+        xpClockIn.setShiftId(shiftId);
+        return xpClockIn;
+    }
+
+    private void fillClockOutInfo(XpClockIn xpClockIn, ClockDTO dto, XpShift xpShift,
+                                  LocalDateTime now, LocalDate attendanceDate) {
+        LocalDateTime endLocalDateTime = DateUtils.stringToLocalDateTime(attendanceDate + " " + xpShift.getEndTime() + ":00");
+        xpClockIn.setClockOutTime(now);
+        xpClockIn.setClockOutLon(dto.getUserLon());
+        xpClockIn.setClockOutLat(dto.getUserLat());
+        xpClockIn.setClockOutAddress(dto.getClockOutAddress());
+        if (now.isBefore(endLocalDateTime)) {
+            xpClockIn.setClockOutResult(2);
+        } else {
+            xpClockIn.setClockOutResult(1);
+        }
+        xpClockIn.setAppUserId(null);
+        this.updateById(xpClockIn);
+        sendAgreementUpdateNotification(now);
+    }
+
+    private String buildAttendanceLockKey(Long appUserId, LocalDate attendanceDate) {
+        return CLOCK_ATTENDANCE_LOCK_PREFIX + appUserId + "_" + attendanceDate;
+    }
+
+    @Override
+    public Integer checkWorkStatus(Long appUserId) {
         if (appUserId == null) {
             log.warn("检查上班状态失败: appUserId 为空");
-            return false;
+            return 1;
         }
 
         try {
@@ -190,29 +340,23 @@
             LocalDateTime endTime = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);   // 23:59:59
 
             // 2. 查询当天打卡记录
-            List<XpClockIn> records = baseMapper.selectTodayClockInRecords(
-                    appUserId, startTime, endTime
-            );
-
+            XpClockIn xpClockIn = baseMapper.selectOne(new LambdaQueryWrapper<XpClockIn>()
+                    .eq(XpClockIn::getAppUserId, appUserId)
+                    .between(XpClockIn::getClockInTime, startTime, endTime)
+                    .last("limit 1"));
             // 3. 判断上班状态
-            if (records == null || records.isEmpty()) {
-                // 无打卡记录 = 未上班
-                log.debug("用户 {} 今日无打卡记录, workFlag=false", appUserId);
-                return false;
-            } else if (records.size() == 1) {
-                // 1条记录 = 已上班未下班
-                log.debug("用户 {} 今日打卡1次, workFlag=true", appUserId);
-                return true;
+            // 有上班打卡状态,无下班打卡状态
+            if (Objects.isNull(xpClockIn)) {
+                return 1;
+            } else if (Objects.nonNull(xpClockIn.getClockInResult()) && Objects.isNull(xpClockIn.getClockOutResult())) {
+                return 2;
             } else {
-                // 2条及以上记录 = 已下班
-                log.debug("用户 {} 今日打卡{}次, workFlag=false", appUserId, records.size());
-                return false;
+                return 3;
             }
-
         } catch (Exception e) {
             log.error("检查用户上班状态异常, appUserId={}", appUserId, e);
             // 异常情况返回 false
-            return false;
+            return 1;
         }
     }
 
@@ -226,34 +370,46 @@
     @Override
     public PageInfo<ClockInStatisticsDto> getClockInStatistics(ClockInStatisticsVo query) {
         PageInfo<ClockInStatisticsDto> pageInfo = new PageInfo<>(query.getPageNum(), query.getPageSize());
-        List<Long> appUserIds = new ArrayList<>();
+        List<Long> appUserIds1 = new ArrayList<>();
+        List<Long> appUserIds2 = new ArrayList<>();
+        List<Long> appUserIds3 = new ArrayList<>();
         //处理查询条件
         if(StringUtils.isNotEmpty(query.getName())){
             R<List<XpOperationsUser>> listR = xpOperationUserClient.listByName(query.getName());
             if(listR.getCode() == Constants.SUCCESS){
                 List<Long> collect = listR.getData().stream().map(XpOperationsUser::getId).collect(Collectors.toList());
-                appUserIds.addAll(collect);
+                appUserIds1.addAll(collect);
             }
-            appUserIds.add(0L);
+            appUserIds1.add(0L);
         }
         if(null != query.getRoleId()){
             R<List<XpOperationsRole>> roleByRoleId = xpOperationUserClient.getXpOperationsRoleByRoleId(query.getRoleId());
             if(roleByRoleId.getCode() == Constants.SUCCESS){
                 List<Long> collect = roleByRoleId.getData().stream().map(XpOperationsRole::getOperationsUserId).collect(Collectors.toList());
-                appUserIds.addAll(collect);
+                appUserIds2.addAll(collect);
             }
-            appUserIds.add(0L);
+            appUserIds2.add(0L);
+        }
+        if(StringUtils.isNotEmpty(query.getName()) && null != query.getRoleId()){
+            for (Long l : appUserIds1) {
+                if(appUserIds2.contains(l)){
+                    appUserIds3.add(l);
+                }
+            }
+        }else{
+            appUserIds3.addAll(appUserIds1);
+            appUserIds3.addAll(appUserIds2);
         }
 
         String startTime = "";
         String endTime = "";
         if(StringUtils.isNotEmpty(query.getStatisticDate())){
             String statisticDate = query.getStatisticDate();
-            startTime = statisticDate.split(" - ")[0];
-            endTime = statisticDate.split(" - ")[1];
+            startTime = statisticDate.split(" - ")[0].split(" ")[0];
+            endTime = statisticDate.split(" - ")[1].split(" ")[0];
         }
         //查询数据
-        List<ClockInStatisticsDto> clockInStatistics = this.baseMapper.getClockInStatistics(pageInfo, appUserIds, startTime, endTime);
+        List<ClockInStatisticsDto> clockInStatistics = this.baseMapper.getClockInStatistics(pageInfo, appUserIds3, startTime, endTime);
         List<String> appUserIdList = clockInStatistics.stream().map(ClockInStatisticsDto::getAppUserId).collect(Collectors.toList());
         List<Long> ids = appUserIdList.stream().map(Long::parseLong).collect(Collectors.toList());
         List<XpOperationsUserWithRoleVO> data = xpOperationUserClient.listByIdsWithRole(ids).getData();
@@ -270,9 +426,6 @@
                     clockInStatistic.setRoleName(roleName);
                 }
             }
-            Integer shouldClockInDays = clockInStatistic.getShouldClockInDays();
-            Integer clockInDays = clockInStatistic.getClockInDays();
-            clockInStatistic.setNotClockInDays(shouldClockInDays - clockInDays);
         }
         pageInfo.setRecords(clockInStatistics);
         return pageInfo;
@@ -287,33 +440,45 @@
     @Override
     public void exportClockInStatistics(ClockInStatisticsVo query, HttpServletResponse response) {
         PageInfo<ClockInStatisticsDto> pageInfo = new PageInfo<>(0, 10000);
-        List<Long> appUserIds = new ArrayList<>();
+        List<Long> appUserIds1 = new ArrayList<>();
+        List<Long> appUserIds2 = new ArrayList<>();
+        List<Long> appUserIds3 = new ArrayList<>();
         //处理查询条件
         if(StringUtils.isNotEmpty(query.getName())){
             R<List<XpOperationsUser>> listR = xpOperationUserClient.listByName(query.getName());
             if(listR.getCode() == Constants.SUCCESS){
                 List<Long> collect = listR.getData().stream().map(XpOperationsUser::getId).collect(Collectors.toList());
-                appUserIds.addAll(collect);
+                appUserIds1.addAll(collect);
             }
-            appUserIds.add(0L);
+            appUserIds1.add(0L);
         }
         if(null != query.getRoleId()){
             R<List<XpOperationsRole>> roleByRoleId = xpOperationUserClient.getXpOperationsRoleByRoleId(query.getRoleId());
             if(roleByRoleId.getCode() == Constants.SUCCESS){
                 List<Long> collect = roleByRoleId.getData().stream().map(XpOperationsRole::getOperationsUserId).collect(Collectors.toList());
-                appUserIds.addAll(collect);
+                appUserIds2.addAll(collect);
             }
-            appUserIds.add(0L);
+            appUserIds2.add(0L);
+        }
+        if(StringUtils.isNotEmpty(query.getName()) && null != query.getRoleId()){
+            for (Long l : appUserIds1) {
+                if(appUserIds2.contains(l)){
+                    appUserIds3.add(l);
+                }
+            }
+        }else{
+            appUserIds3.addAll(appUserIds1);
+            appUserIds3.addAll(appUserIds2);
         }
         String startTime = "";
         String endTime = "";
         if(StringUtils.isNotEmpty(query.getStatisticDate())){
             String statisticDate = query.getStatisticDate();
-            startTime = statisticDate.split(" - ")[0];
-            endTime = statisticDate.split(" - ")[1];
+            startTime = statisticDate.split(" - ")[0].split(" ")[0];
+            endTime = statisticDate.split(" - ")[1].split(" ")[0];
         }
         //查询数据
-        List<ClockInStatisticsDto> clockInStatistics = this.baseMapper.getClockInStatistics(pageInfo, appUserIds, startTime, endTime);
+        List<ClockInStatisticsDto> clockInStatistics = this.baseMapper.getClockInStatistics(pageInfo, appUserIds3, startTime, endTime);
         List<String> appUserIdList = clockInStatistics.stream().map(ClockInStatisticsDto::getAppUserId).collect(Collectors.toList());
         List<Long> ids = appUserIdList.stream().map(Long::parseLong).collect(Collectors.toList());
         List<XpOperationsUserWithRoleVO> data = xpOperationUserClient.listByIdsWithRole(ids).getData();
@@ -329,9 +494,6 @@
                     clockInStatistic.setRoleName(roleName);
                 }
             }
-            Integer shouldClockInDays = clockInStatistic.getShouldClockInDays();
-            Integer clockInDays = clockInStatistic.getClockInDays();
-            clockInStatistic.setNotClockInDays(shouldClockInDays - clockInDays);
         }
 
         ExcelUtil<ClockInStatisticsDto> excel = new ExcelUtil<ClockInStatisticsDto>(ClockInStatisticsDto.class);
@@ -348,15 +510,16 @@
      * @return
      */
     @Override
-    public PageInfo<ClockInStatisticsDetailsDto> getClockInStatisticsDetails(ClockInStatisticsDetailsVo query) {
+    public GetClockInStatisticsDetailsDto getClockInStatisticsDetails(ClockInStatisticsDetailsVo query) {
+        GetClockInStatisticsDetailsDto dto = new GetClockInStatisticsDetailsDto();
         PageInfo<ClockInStatisticsDetailsDto> pageInfo = new PageInfo<>(query.getPageNum(), query.getPageSize());
 
         String startTime = "";
         String endTime = "";
         if(StringUtils.isNotEmpty(query.getStatisticDate())){
             String statisticDate = query.getStatisticDate();
-            startTime = statisticDate.split(" - ")[0];
-            endTime = statisticDate.split(" - ")[1];
+            startTime = statisticDate.split(" - ")[0].split(" ")[0];
+            endTime = statisticDate.split(" - ")[1].split(" ")[0];
         }
         List<ClockInStatisticsDetailsDto> clockInStatisticsDetails = this.baseMapper.getClockInStatisticsDetails(pageInfo, query.getAppUserId(), startTime, endTime);
         List<XpOperationsUserWithRoleVO> data = xpOperationUserClient.listByIdsWithRole(Arrays.asList(query.getAppUserId())).getData();
@@ -367,7 +530,15 @@
             clockInStatistic.setRoleName(xpOperationsUserWithRoleVO.getRoles().stream().map(SysRole::getRoleName).collect(Collectors.joining(";")));
         }
         pageInfo.setRecords(clockInStatisticsDetails);
-        return pageInfo;
+        dto.setList(pageInfo);
+        XpOperationsUser xpOperationsUser = xpOperationUserClient.queryById(query.getAppUserId()).getData();
+        List<Long> longList = fleetOperationsClient.getFleetIdsByUserId(xpOperationsUser.getId()).getData();
+        List<XpSchedulingDetail> list = xpSchedulingDetailService.list(new QueryWrapper<XpSchedulingDetail>().eq("fleet_id", longList.get(0)).between(StringUtils.isNotEmpty(startTime), "plan_time", startTime, endTime));
+        dto.setShouldClockInCount(list.size() * 2);
+        dto.setNormalClockInCount(this.baseMapper.getClockInStatisticsDetailsCount(1, query.getAppUserId(), startTime, endTime));
+        dto.setLateClockInCount(this.baseMapper.getClockInStatisticsDetailsCount(2, query.getAppUserId(), startTime, endTime));
+        dto.setMissingClockInCount(this.baseMapper.getClockInStatisticsDetailsCount(3, query.getAppUserId(), startTime, endTime));
+        return dto;
     }
 
 
@@ -384,8 +555,8 @@
         String endTime = "";
         if(StringUtils.isNotEmpty(query.getStatisticDate())){
             String statisticDate = query.getStatisticDate();
-            startTime = statisticDate.split(" - ")[0];
-            endTime = statisticDate.split(" - ")[1];
+            startTime = statisticDate.split(" - ")[0].split(" ")[0];
+            endTime = statisticDate.split(" - ")[1].split(" ")[0];
         }
         List<ClockInStatisticsDetailsDto> clockInStatisticsDetails = this.baseMapper.getClockInStatisticsDetails(pageInfo, query.getAppUserId(), startTime, endTime);
         List<XpOperationsUserWithRoleVO> data = xpOperationUserClient.listByIdsWithRole(Arrays.asList(query.getAppUserId())).getData();
@@ -399,4 +570,155 @@
         ExcelUtil<ClockInStatisticsDetailsDto> excel = new ExcelUtil<ClockInStatisticsDetailsDto>(ClockInStatisticsDetailsDto.class);
         excel.exportExcel(response, clockInStatisticsDetails, "打卡统计明细");
     }
+
+
+    /**
+     * 发送协议更新通知
+     * @param clockTime 打卡时间
+     */
+    private void sendAgreementUpdateNotification(LocalDateTime clockTime) {
+        // 异步处理,避免阻塞主流程
+        ThreadPoolExecutor executor = new ThreadPoolExecutor(
+                1, 1, 0L, TimeUnit.MILLISECONDS,
+                new LinkedBlockingQueue<Runnable>()
+        );
+
+        executor.execute(() -> {
+            try {
+                // 1. 获取所有启用状态的运维用户
+                R<List<XpOperationsUser>> result = xpOperationUserClient.listAllEnabled();
+                if (result == null || result.getCode() != 200 || result.getData() == null || result.getData().isEmpty()) {
+                    log.warn("没有需要通知的启用状态用户");
+                    return;
+                }
+
+                List<XpOperationsUser> enabledUsers = result.getData();
+                log.info("打卡成功通知,共需通知 {} 位用户", enabledUsers.size());
+
+                // 2. 构建通知内容
+                String format = clockTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+                String message = String.format("你已于%s打卡成功!", format);
+                String bodyJson = buildAgreementPushBodyJson(message);
+
+                // 3. 构建并保存通知记录
+                List<XpOperationsUserNotification> notifications = enabledUsers.stream()
+                        .map(user -> buildAgreementNotification(message, bodyJson, user))
+                        .collect(Collectors.toList());
+
+                // 批量保存通知记录
+                xpOperationUserNotificationClient.addNotificationBatch(notifications);
+                log.info("协议更新通知记录保存成功,共 {} 条", notifications.size());
+
+                // 4. 按设备类型分组并批量推送
+                pushMessageByDeviceType(enabledUsers, message);
+
+            } catch (Exception e) {
+                log.error("发送协议更新通知失败", e);
+            }
+        });
+
+        executor.shutdown();
+    }
+
+    /**
+     * 构建推送 body JSON
+     * @param message 消息内容
+     * @return JSON 字符串
+     */
+    private String buildAgreementPushBodyJson(String message) {
+        JSONObject body = new JSONObject();
+        body.put("content", message);
+        body.put("strategy", NotificationTypeEnum.CLOCK_IN_SUCCESS.getCode()); // 11
+        body.put("type", 0);
+        body.put("id", 0);
+        return body.toJSONString();
+    }
+
+    /**
+     * 构建通知实体
+     * @param message 消息内容
+     * @param bodyJson 推送body内容
+     * @param user 运维用户
+     * @return 通知实体
+     */
+    private XpOperationsUserNotification buildAgreementNotification(String message, String bodyJson, XpOperationsUser user) {
+        XpOperationsUserNotification notification = new XpOperationsUserNotification();
+        notification.setNotificationType(NotificationTypeEnum.CLOCK_IN_SUCCESS.getCode()); // 11
+        notification.setNotificationContent(message);
+        notification.setOperationsUserId(user.getId());
+        notification.setDataId(0L);
+        notification.setReadStatus(0); // 未读
+        notification.setNotificationExt(bodyJson);
+        return notification;
+    }
+
+    /**
+     * 按设备类型分组推送消息
+     * @param users 用户列表
+     * @param message 消息内容
+     */
+    private void pushMessageByDeviceType(List<XpOperationsUser> users, String message) {
+        // 按设备类型分组 (1=Android, 2=iOS)
+        Map<Integer, List<XpOperationsUser>> usersByDeviceType = users.stream()
+                .filter(u -> u.getDeviceType() != null && u.getPhoneNumber() != null)
+                .collect(Collectors.groupingBy(XpOperationsUser::getDeviceType));
+
+        // 分别推送 Android 和 iOS
+        for (Map.Entry<Integer, List<XpOperationsUser>> entry : usersByDeviceType.entrySet()) {
+            Integer deviceType = entry.getKey();
+            List<XpOperationsUser> deviceUsers = entry.getValue();
+
+            // 按 1000 个手机号一批进行推送
+            List<List<XpOperationsUser>> batches = partition(deviceUsers, 1000);
+
+            for (List<XpOperationsUser> batch : batches) {
+                String accounts = batch.stream()
+                        .map(XpOperationsUser::getPhoneNumber)
+                        .collect(Collectors.joining(","));
+
+                AliPushMessageDTO pushDTO = buildPushMessageDTO(deviceType, accounts, message);
+
+                try {
+                    xpAliPushMessageClient.pushMessage(pushDTO);
+                    log.info("打卡成功推送成功,设备类型={},用户数={}",
+                            deviceType == 1 ? "Android" : "iOS", batch.size());
+                } catch (Exception e) {
+                    log.error("打卡成功推送失败,设备类型={}", deviceType, e);
+                }
+            }
+        }
+    }
+
+
+    /**
+     * 将列表按指定大小分批
+     * @param list 原始列表
+     * @param batchSize 批次大小
+     * @return 分批后的列表
+     */
+    private <T> List<List<T>> partition(List<T> list, int batchSize) {
+        List<List<T>> batches = new ArrayList<>();
+        for (int i = 0; i < list.size(); i += batchSize) {
+            batches.add(list.subList(i, Math.min(i + batchSize, list.size())));
+        }
+        return batches;
+    }
+
+    /**
+     * 构建推送消息 DTO
+     * @param deviceType 设备类型
+     * @param accounts 手机号(逗号分隔)
+     * @param message 消息内容
+     * @return 推送 DTO
+     */
+    private AliPushMessageDTO buildPushMessageDTO(Integer deviceType, String accounts, String message) {
+        AliPushMessageDTO dto = new AliPushMessageDTO();
+        dto.setDeviceType(deviceType);
+        dto.setAccount(accounts);
+        dto.setTitle("打卡成功通知");
+        dto.setBody(buildAgreementPushBodyJson(message));
+        dto.setPushType("MESSAGE");
+        return dto;
+    }
+
 }

--
Gitblit v1.7.1