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.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.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.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.*; 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.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; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import java.time.LocalDate; 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; /** *

* 打卡明细 服务实现类 *

* * @author mitao * @since 2026-02-03 */ @Slf4j @Service public class XpClockInServiceImpl extends ServiceImpl implements IXpClockInService { @Resource private XpCheckLocationMapper xpCheckLocationMapper; @Resource private XpShiftMapper xpShiftMapper; @Resource private XpClockInMapper xpClockInMapper; @Resource private XpOperationUserClient xpOperationUserClient; @Resource private IXpSchedulingDetailService xpSchedulingDetailService; @Resource private XpFleetOperationsClient fleetOperationsClient; @Resource private XpOperationUserNotificationClient xpOperationUserNotificationClient; @Resource private XpAliPushMessageClient xpAliPushMessageClient; @Override public Boolean verifyRange(VerifyRangeDTO dto) { XpCheckLocation xpCheckLocation = xpCheckLocationMapper.selectById(dto.getLocationId()); Map distance = GeodesyUtil.getDistance(dto.getUserLon() + "," + dto.getUserLat(), xpCheckLocation.getLocationLon() + "," + xpCheckLocation.getLocationLat()); if (null != distance) { Double distance1 = distance.get("WGS84"); if (BigDecimal.valueOf(distance1).compareTo(xpCheckLocation.getLocationRange()) > 0) { return false; } else { return true; } } else { log.error("调用高德计算距离出错"); return false; } } @Override public R clockIn(ClockDTO dto) { XpCheckLocation xpCheckLocation = xpCheckLocationMapper.selectById(dto.getLocationId()); if (null == xpCheckLocation) { return R.fail("打卡地址不存在"); } if (xpCheckLocation.getStatus() == 0) { return R.fail("打卡地址已禁用"); } Map distance = GeodesyUtil.getDistance(dto.getUserLon() + "," + dto.getUserLat(), xpCheckLocation.getLocationLon() + "," + xpCheckLocation.getLocationLat()); if (null != distance) { Double distance1 = distance.get("WGS84"); if (BigDecimal.valueOf(distance1).compareTo(xpCheckLocation.getLocationRange()) > 0) { return R.fail("不在打卡范围内"); } } else { return R.fail("打卡失败"); } // 查询班次 XpShift xpShift = xpShiftMapper.selectById(dto.getShiftId()); if (null == xpShift) { return R.fail("班次不存在"); } // 开始打卡时间 String startTime = xpShift.getStartTime(); LocalDateTime startLocalDateTime = DateUtils.stringToLocalDateTime(LocalDate.now() + " " + startTime + ":00"); XpClockIn xpClockIn = new XpClockIn(); xpClockIn.setAppUserId(dto.getOperationsUserId()); LocalDateTime now = LocalDateTime.now(); xpClockIn.setClockInTime(now); xpClockIn.setClockInLon(dto.getUserLon()); xpClockIn.setClockInLat(dto.getUserLat()); xpClockIn.setShiftId(dto.getShiftId()); xpClockIn.setClockInAddress(dto.getClockInAddress()); if (xpClockIn.getClockInTime().isAfter(startLocalDateTime)) { xpClockIn.setClockInResult(2); } else { xpClockIn.setClockInResult(1); } this.save(xpClockIn); sendAgreementUpdateNotification(now); return R.ok(xpClockIn.getClockInResult()); } @Override public R clockOut(ClockDTO dto) { XpCheckLocation xpCheckLocation = xpCheckLocationMapper.selectById(dto.getLocationId()); if (null == xpCheckLocation) { return R.fail("打卡地址不存在"); } if (xpCheckLocation.getStatus() == 0) { return R.fail("打卡地址已禁用"); } Map distance = GeodesyUtil.getDistance(dto.getUserLon() + "," + dto.getUserLat(), xpCheckLocation.getLocationLon() + "," + xpCheckLocation.getLocationLat()); if (null != distance) { Double distance1 = distance.get("WGS84"); if (BigDecimal.valueOf(distance1).compareTo(xpCheckLocation.getLocationRange()) > 0) { return R.fail("不在打卡范围内"); } } else { 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("请先上班打卡"); } // 查询班次 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.setClockOutTime(now); xpClockIn.setClockOutLon(dto.getUserLon()); xpClockIn.setClockOutLat(dto.getUserLat()); xpClockIn.setClockOutAddress(dto.getClockOutAddress()); if (xpClockIn.getClockOutTime().isBefore(endLocalDateTime)) { xpClockIn.setClockOutResult(2); } else { xpClockIn.setClockOutResult(1); } xpClockIn.setAppUserId(null); this.updateById(xpClockIn); sendAgreementUpdateNotification(now); return R.ok(xpClockIn.getClockOutResult()); } @Override public Integer checkWorkStatus(Long appUserId) { if (appUserId == null) { log.warn("检查上班状态失败: appUserId 为空"); return 1; } try { // 1. 计算当天时间范围 LocalDateTime startTime = LocalDateTime.of(LocalDate.now(), LocalTime.MIN); // 00:00:00 LocalDateTime endTime = LocalDateTime.of(LocalDate.now(), LocalTime.MAX); // 23:59:59 // 2. 查询当天打卡记录 XpClockIn xpClockIn = baseMapper.selectOne(new LambdaQueryWrapper() .eq(XpClockIn::getAppUserId, appUserId) .between(XpClockIn::getClockInTime, startTime, endTime) .last("limit 1")); // 3. 判断上班状态 // 有上班打卡状态,无下班打卡状态 if (Objects.isNull(xpClockIn)) { return 1; } else if (Objects.nonNull(xpClockIn.getClockInResult()) && Objects.isNull(xpClockIn.getClockOutResult())) { return 2; } else { return 3; } } catch (Exception e) { log.error("检查用户上班状态异常, appUserId={}", appUserId, e); // 异常情况返回 false return 1; } } /** * 获取打卡统计 * * @param query * @return */ @Override public PageInfo getClockInStatistics(ClockInStatisticsVo query) { PageInfo pageInfo = new PageInfo<>(query.getPageNum(), query.getPageSize()); List appUserIds1 = new ArrayList<>(); List appUserIds2 = new ArrayList<>(); List appUserIds3 = new ArrayList<>(); //处理查询条件 if(StringUtils.isNotEmpty(query.getName())){ R> listR = xpOperationUserClient.listByName(query.getName()); if(listR.getCode() == Constants.SUCCESS){ List collect = listR.getData().stream().map(XpOperationsUser::getId).collect(Collectors.toList()); appUserIds1.addAll(collect); } appUserIds1.add(0L); } if(null != query.getRoleId()){ R> roleByRoleId = xpOperationUserClient.getXpOperationsRoleByRoleId(query.getRoleId()); if(roleByRoleId.getCode() == Constants.SUCCESS){ List collect = roleByRoleId.getData().stream().map(XpOperationsRole::getOperationsUserId).collect(Collectors.toList()); appUserIds2.addAll(collect); } 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].split(" ")[0]; endTime = statisticDate.split(" - ")[1].split(" ")[0]; } //查询数据 List clockInStatistics = this.baseMapper.getClockInStatistics(pageInfo, appUserIds3, startTime, endTime); List appUserIdList = clockInStatistics.stream().map(ClockInStatisticsDto::getAppUserId).collect(Collectors.toList()); List ids = appUserIdList.stream().map(Long::parseLong).collect(Collectors.toList()); List data = xpOperationUserClient.listByIdsWithRole(ids).getData(); Map userMap = data.stream().collect(Collectors.toMap(XpOperationsUserWithRoleVO::getId, Function.identity())); //将查询结果组装成map对象,用于循环中提取数据 for (ClockInStatisticsDto clockInStatistic : clockInStatistics) { String appUserId = clockInStatistic.getAppUserId(); //运维账号可以拥有多个角色 if(CollUtil.isNotEmpty(userMap)){ XpOperationsUserWithRoleVO vo = userMap.get(Long.parseLong(appUserId)); if (Objects.nonNull(vo)) { clockInStatistic.setName(vo.getNickName()); String roleName = vo.getRoles().stream().map(SysRole::getRoleName).collect(Collectors.joining(";")); clockInStatistic.setRoleName(roleName); } } } pageInfo.setRecords(clockInStatistics); return pageInfo; } /** * 导出打卡统计 * @param query * @param response */ @Override public void exportClockInStatistics(ClockInStatisticsVo query, HttpServletResponse response) { PageInfo pageInfo = new PageInfo<>(0, 10000); List appUserIds1 = new ArrayList<>(); List appUserIds2 = new ArrayList<>(); List appUserIds3 = new ArrayList<>(); //处理查询条件 if(StringUtils.isNotEmpty(query.getName())){ R> listR = xpOperationUserClient.listByName(query.getName()); if(listR.getCode() == Constants.SUCCESS){ List collect = listR.getData().stream().map(XpOperationsUser::getId).collect(Collectors.toList()); appUserIds1.addAll(collect); } appUserIds1.add(0L); } if(null != query.getRoleId()){ R> roleByRoleId = xpOperationUserClient.getXpOperationsRoleByRoleId(query.getRoleId()); if(roleByRoleId.getCode() == Constants.SUCCESS){ List collect = roleByRoleId.getData().stream().map(XpOperationsRole::getOperationsUserId).collect(Collectors.toList()); appUserIds2.addAll(collect); } 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].split(" ")[0]; endTime = statisticDate.split(" - ")[1].split(" ")[0]; } //查询数据 List clockInStatistics = this.baseMapper.getClockInStatistics(pageInfo, appUserIds3, startTime, endTime); List appUserIdList = clockInStatistics.stream().map(ClockInStatisticsDto::getAppUserId).collect(Collectors.toList()); List ids = appUserIdList.stream().map(Long::parseLong).collect(Collectors.toList()); List data = xpOperationUserClient.listByIdsWithRole(ids).getData(); Map userMap = data.stream().collect(Collectors.toMap(XpOperationsUserWithRoleVO::getId, Function.identity())); for (ClockInStatisticsDto clockInStatistic : clockInStatistics) { String appUserId = clockInStatistic.getAppUserId(); //运维账号可以拥有多个角色 if(CollUtil.isNotEmpty(userMap)){ XpOperationsUserWithRoleVO vo = userMap.get(Long.parseLong(appUserId)); if (Objects.nonNull(vo)) { clockInStatistic.setName(vo.getNickName()); String roleName = vo.getRoles().stream().map(SysRole::getRoleName).collect(Collectors.joining(";")); clockInStatistic.setRoleName(roleName); } } } ExcelUtil excel = new ExcelUtil(ClockInStatisticsDto.class); excel.exportExcel(response, clockInStatistics, "打卡统计"); } /** * 获取打卡统计明细 * @param query * @return */ @Override public GetClockInStatisticsDetailsDto getClockInStatisticsDetails(ClockInStatisticsDetailsVo query) { GetClockInStatisticsDetailsDto dto = new GetClockInStatisticsDetailsDto(); PageInfo pageInfo = new PageInfo<>(query.getPageNum(), query.getPageSize()); String startTime = ""; String endTime = ""; if(StringUtils.isNotEmpty(query.getStatisticDate())){ String statisticDate = query.getStatisticDate(); startTime = statisticDate.split(" - ")[0].split(" ")[0]; endTime = statisticDate.split(" - ")[1].split(" ")[0]; } List clockInStatisticsDetails = this.baseMapper.getClockInStatisticsDetails(pageInfo, query.getAppUserId(), startTime, endTime); List data = xpOperationUserClient.listByIdsWithRole(Arrays.asList(query.getAppUserId())).getData(); XpOperationsUserWithRoleVO xpOperationsUserWithRoleVO = data.get(0); //将查询结果组装成map对象,用于循环中提取数据 for (ClockInStatisticsDetailsDto clockInStatistic : clockInStatisticsDetails) { clockInStatistic.setName(xpOperationsUserWithRoleVO.getNickName()); clockInStatistic.setRoleName(xpOperationsUserWithRoleVO.getRoles().stream().map(SysRole::getRoleName).collect(Collectors.joining(";"))); } pageInfo.setRecords(clockInStatisticsDetails); dto.setList(pageInfo); XpOperationsUser xpOperationsUser = xpOperationUserClient.queryById(query.getAppUserId()).getData(); List longList = fleetOperationsClient.getFleetIdsByUserId(xpOperationsUser.getId()).getData(); List list = xpSchedulingDetailService.list(new QueryWrapper().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; } /** * 导出打卡统计明细 * @param query * @param response */ @Override public void exportClockInStatisticsDetails(ClockInStatisticsDetailsVo query, HttpServletResponse response) { PageInfo pageInfo = new PageInfo<>(0, 10000); String startTime = ""; String endTime = ""; if(StringUtils.isNotEmpty(query.getStatisticDate())){ String statisticDate = query.getStatisticDate(); startTime = statisticDate.split(" - ")[0].split(" ")[0]; endTime = statisticDate.split(" - ")[1].split(" ")[0]; } List clockInStatisticsDetails = this.baseMapper.getClockInStatisticsDetails(pageInfo, query.getAppUserId(), startTime, endTime); List data = xpOperationUserClient.listByIdsWithRole(Arrays.asList(query.getAppUserId())).getData(); XpOperationsUserWithRoleVO xpOperationsUserWithRoleVO = data.get(0); //将查询结果组装成map对象,用于循环中提取数据 for (ClockInStatisticsDetailsDto clockInStatistic : clockInStatisticsDetails) { clockInStatistic.setName(xpOperationsUserWithRoleVO.getNickName()); clockInStatistic.setRoleName(xpOperationsUserWithRoleVO.getRoles().stream().map(SysRole::getRoleName).collect(Collectors.joining(";"))); } ExcelUtil excel = new ExcelUtil(ClockInStatisticsDetailsDto.class); excel.exportExcel(response, clockInStatisticsDetails, "打卡统计明细"); } /** * 发送协议更新通知 * @param clockTime 打卡时间 */ private void sendAgreementUpdateNotification(LocalDateTime clockTime) { // 异步处理,避免阻塞主流程 ThreadPoolExecutor executor = new ThreadPoolExecutor( 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue() ); executor.execute(() -> { try { // 1. 获取所有启用状态的运维用户 R> result = xpOperationUserClient.listAllEnabled(); if (result == null || result.getCode() != 200 || result.getData() == null || result.getData().isEmpty()) { log.warn("没有需要通知的启用状态用户"); return; } List 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 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 users, String message) { // 按设备类型分组 (1=Android, 2=iOS) Map> usersByDeviceType = users.stream() .filter(u -> u.getDeviceType() != null && u.getPhoneNumber() != null) .collect(Collectors.groupingBy(XpOperationsUser::getDeviceType)); // 分别推送 Android 和 iOS for (Map.Entry> entry : usersByDeviceType.entrySet()) { Integer deviceType = entry.getKey(); List deviceUsers = entry.getValue(); // 按 1000 个手机号一批进行推送 List> batches = partition(deviceUsers, 1000); for (List 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 List> partition(List list, int batchSize) { List> 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; } }