package cn.iocoder.yudao.module.mes.service.trace.consumer; import cn.iocoder.yudao.module.mes.controller.admin.trace.consumer.vo.MesTraceConsumerScanDistributionReqVO; import cn.iocoder.yudao.module.mes.controller.admin.trace.consumer.vo.MesTraceConsumerScanDistributionRespVO; import cn.iocoder.yudao.module.mes.controller.admin.trace.consumer.vo.MesTraceConsumerScanSummaryRespVO; import cn.iocoder.yudao.module.mes.controller.admin.trace.consumer.vo.MesTraceConsumerScanTrendRespVO; import cn.iocoder.yudao.module.mes.dal.dataobject.trace.consumer.MesTraceConsumerScanEventDO; import cn.iocoder.yudao.module.mes.dal.mysql.trace.consumer.MesTraceConsumerScanStatisticsMapper; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.function.Function; import java.util.stream.Collectors; /** * 消费者扫码访问统计 Service 实现 *

* 采用查明细 + Java 侧聚合:避免统计 SQL 与 MySQL/H2 方言差异,同时不暴露动态列 SQL。 * * @author 超级管理员 */ @Service public class MesTraceConsumerScanStatisticsServiceImpl implements MesTraceConsumerScanStatisticsService { /** * 分组维度白名单:仅允许按固定字段聚合,非法维度直接拒绝 */ private static final Map> GROUP_COLUMNS = Map.of( "sourceType", e -> e.getSourceType(), "success", e -> Boolean.TRUE.equals(e.getSuccess()) ? "1" : "0", "channel", MesTraceConsumerScanEventDO::getChannel, "province", MesTraceConsumerScanEventDO::getProvince, "city", MesTraceConsumerScanEventDO::getCity, "batchId", e -> e.getBatchId() == null ? null : String.valueOf(e.getBatchId())); /** * 空分组键占位符:Collectors.groupingBy 不接受 null key,先统一替换再在展示时映射为"未知" */ private static final String UNKNOWN_KEY = "__unknown__"; @Resource private MesTraceConsumerScanStatisticsMapper mapper; @Override public MesTraceConsumerScanSummaryRespVO getSummary(LocalDateTime startTime, LocalDateTime endTime) { List events = mapper.selectListByTime(startTime, endTime); MesTraceConsumerScanSummaryRespVO vo = new MesTraceConsumerScanSummaryRespVO(); vo.setTotal((long) events.size()); vo.setSuccess(events.stream().filter(e -> Boolean.TRUE.equals(e.getSuccess())).count()); vo.setFailed(events.stream().filter(e -> !Boolean.TRUE.equals(e.getSuccess())).count()); vo.setUniqueBatchCount(events.stream().map(MesTraceConsumerScanEventDO::getBatchId) .filter(java.util.Objects::nonNull).distinct().count()); return vo; } @Override public List getTrend(LocalDateTime startTime, LocalDateTime endTime) { List events = mapper.selectListByTime(startTime, endTime); // 按日期聚合 Map data = new TreeMap<>(); LocalDate min = null; LocalDate max = null; for (MesTraceConsumerScanEventDO event : events) { LocalDate date = event.getEventTime().toLocalDate(); MesTraceConsumerScanTrendRespVO vo = data.computeIfAbsent(date, d -> { MesTraceConsumerScanTrendRespVO v = new MesTraceConsumerScanTrendRespVO(); v.setDate(d.toString()); v.setTotal(0L); v.setSuccess(0L); v.setFailed(0L); return v; }); vo.setTotal(vo.getTotal() + 1); if (Boolean.TRUE.equals(event.getSuccess())) { vo.setSuccess(vo.getSuccess() + 1); } else { vo.setFailed(vo.getFailed() + 1); } if (min == null || date.isBefore(min)) { min = date; } if (max == null || date.isAfter(max)) { max = date; } } // 补齐区间内无数据的日期(时间参数为空时退回已有数据的最小/最大日期区间) LocalDate start = startTime != null ? startTime.toLocalDate() : min; LocalDate end = endTime != null ? (endTime.toLocalTime().equals(LocalTime.MIDNIGHT) ? endTime.toLocalDate().minusDays(1) : endTime.toLocalDate()) : max; if (start != null && end != null && !start.isAfter(end)) { for (LocalDate day = start; !day.isAfter(end); day = day.plusDays(1)) { data.computeIfAbsent(day, d -> { MesTraceConsumerScanTrendRespVO v = new MesTraceConsumerScanTrendRespVO(); v.setDate(d.toString()); v.setTotal(0L); v.setSuccess(0L); v.setFailed(0L); return v; }); } } return new ArrayList<>(data.values()); } @Override public List getDistribution(MesTraceConsumerScanDistributionReqVO reqVO) { Function extractor = GROUP_COLUMNS.get(reqVO.getGroupBy()); if (extractor == null) { throw new IllegalArgumentException("不支持的分组维度"); } List events = mapper.selectListByTime(reqVO.getStartTime(), reqVO.getEndTime()); // 按数量倒序分组;groupingBy 不接受 null key,先统一替换为占位符再聚合 Map counts = events.stream().collect(Collectors.groupingBy(e -> { String key = extractor.apply(e); return key == null ? UNKNOWN_KEY : key; }, Collectors.counting())); return counts.entrySet().stream() .sorted(Comparator.comparing(Map.Entry::getValue).reversed() .thenComparing(Map.Entry.comparingByKey())) .map(entry -> { MesTraceConsumerScanDistributionRespVO vo = new MesTraceConsumerScanDistributionRespVO(); String name = entry.getKey(); vo.setName(UNKNOWN_KEY.equals(name) ? "未知" : displayName(reqVO.getGroupBy(), name)); vo.setCount(entry.getValue()); return vo; }) .collect(Collectors.toCollection(ArrayList::new)); } /** * 布尔分区做可读映射,保证统计口径与前端展示一致 */ private String displayName(String groupBy, String name) { if ("success".equals(groupBy)) { return "1".equals(name) ? "成功" : "失败"; } return name; } }