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;
}
}