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 实现
|
* <p>
|
* 采用查明细 + Java 侧聚合:避免统计 SQL 与 MySQL/H2 方言差异,同时不暴露动态列 SQL。
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
public class MesTraceConsumerScanStatisticsServiceImpl implements MesTraceConsumerScanStatisticsService {
|
|
/**
|
* 分组维度白名单:仅允许按固定字段聚合,非法维度直接拒绝
|
*/
|
private static final Map<String, Function<MesTraceConsumerScanEventDO, String>> 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<MesTraceConsumerScanEventDO> 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<MesTraceConsumerScanTrendRespVO> getTrend(LocalDateTime startTime, LocalDateTime endTime) {
|
List<MesTraceConsumerScanEventDO> events = mapper.selectListByTime(startTime, endTime);
|
// 按日期聚合
|
Map<LocalDate, MesTraceConsumerScanTrendRespVO> 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<MesTraceConsumerScanDistributionRespVO> getDistribution(MesTraceConsumerScanDistributionReqVO reqVO) {
|
Function<MesTraceConsumerScanEventDO, String> extractor = GROUP_COLUMNS.get(reqVO.getGroupBy());
|
if (extractor == null) {
|
throw new IllegalArgumentException("不支持的分组维度");
|
}
|
List<MesTraceConsumerScanEventDO> events = mapper.selectListByTime(reqVO.getStartTime(), reqVO.getEndTime());
|
// 按数量倒序分组;groupingBy 不接受 null key,先统一替换为占位符再聚合
|
Map<String, Long> 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<String, Long>::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;
|
}
|
|
}
|