package jnpf.tmsService.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import jnpf.permission.OrganizeApi; import jnpf.permission.entity.OrganizeEntity; import jnpf.tmsEntity.TmsPersonTaskEntity; import jnpf.tmsEntity.TmsRecordEntity; import jnpf.tmsEntity.enums.TmsEvalMode; import jnpf.tmsEntity.enums.TmsPassFlag; import jnpf.tmsEntity.enums.TmsTaskCategory; import jnpf.tmsEntity.stats.TmsTrainStatsDetailVO; import jnpf.tmsEntity.stats.TmsTrainStatsQuery; import jnpf.tmsEntity.stats.TmsTrainStatsRowVO; import jnpf.tmsMapper.TmsPersonTaskMapper; import jnpf.tmsMapper.TmsRecordMapper; import jnpf.tmsService.TmsTrainStatsService; import jnpf.tmsUtil.TmsExcelExportHelper; import jnpf.util.DateUtil; import jnpf.util.StringUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @Service @RequiredArgsConstructor public class TmsTrainStatsServiceImpl implements TmsTrainStatsService { private final TmsRecordMapper recordMapper; private final TmsPersonTaskMapper personTaskMapper; private final OrganizeApi organizeApi; @Override public List list(TmsTrainStatsQuery query) { return buildRows(query == null ? new TmsTrainStatsQuery() : query); } @Override public List detail(TmsTrainStatsQuery query) { TmsTrainStatsQuery q = query == null ? new TmsTrainStatsQuery() : query; String groupBy = normalizeGroupBy(q.getGroupBy()); String dimCode = StringUtil.isEmpty(q.getDimCode()) ? "-" : q.getDimCode().trim(); List records = loadRecords(q); if (records.isEmpty()) { return Collections.emptyList(); } Set deptFilter = StringUtil.isEmpty(q.getDeptId()) ? null : Collections.singleton(q.getDeptId().trim()); List result = new ArrayList<>(); for (TmsRecordEntity record : records) { if (record == null || StringUtil.isEmpty(record.getTaskId())) { continue; } List persons = loadPersons(record.getTaskId()); List matched = matchPersons(groupBy, dimCode, record, persons, deptFilter); if (matched == null) { continue; } int personCount = 0; int signedCount = 0; int passedCount = 0; for (TmsPersonTaskEntity p : matched) { personCount++; if (p.getSignTime() != null) { signedCount++; } if (TmsPassFlag.isPass(p.getPassFlag())) { passedCount++; } } TmsTrainStatsDetailVO vo = new TmsTrainStatsDetailVO(); vo.setRecordId(record.getId()); vo.setRecordNo(record.getRecordNo()); vo.setSubject(record.getSubject()); vo.setCategory(record.getCategory()); vo.setEvalMode(record.getEvalMode()); vo.setStartTime(record.getStartTime()); vo.setEndTime(record.getEndTime()); vo.setPersonCount(personCount); vo.setSignedCount(signedCount); vo.setPassedCount(passedCount); vo.setSignRate(rateFloor(signedCount, personCount)); vo.setPassRate(rateFloor(passedCount, personCount)); result.add(vo); } return result; } @Override public void exportExcel(TmsTrainStatsQuery query, HttpServletResponse response) { TmsTrainStatsQuery q = query == null ? new TmsTrainStatsQuery() : query; String groupBy = normalizeGroupBy(q.getGroupBy()); List rows = list(q); List headers = Arrays.asList( dimHeaderOf(groupBy), "培训场次", "应到人次", "签到人次", "合格人次", "签到率(%)", "合格率(%)"); List widths = Arrays.asList(140, 80, 80, 80, 80, 80, 80); List> data = new ArrayList<>(); for (TmsTrainStatsRowVO row : rows) { data.add(Arrays.asList( StringUtil.isNotEmpty(row.getDimName()) ? row.getDimName() : row.getDimCode(), str(row.getSessionCount()), str(row.getPersonCount()), str(row.getSignedCount()), str(row.getPassedCount()), str(row.getSignRate()), str(row.getPassRate()) )); } try { TmsExcelExportHelper.writeSpreadsheet(response, "培训效果统计", "统计", headers, widths, data); } catch (IOException e) { throw new RuntimeException("导出失败", e); } } /** 导出首列表头:统计维度(部门/培训分类/考核方式) */ private static String dimHeaderOf(String groupBy) { if ("dept".equalsIgnoreCase(groupBy)) { return "统计维度(部门)"; } if ("evalMode".equalsIgnoreCase(groupBy)) { return "统计维度(考核方式)"; } return "统计维度(培训分类)"; } private List buildRows(TmsTrainStatsQuery query) { String groupBy = normalizeGroupBy(query.getGroupBy()); List records = loadRecords(query); if (records.isEmpty()) { return Collections.emptyList(); } Map aggMap = new LinkedHashMap<>(); Set deptFilter = StringUtil.isEmpty(query.getDeptId()) ? null : Collections.singleton(query.getDeptId().trim()); for (TmsRecordEntity record : records) { if (record == null || StringUtil.isEmpty(record.getTaskId())) { continue; } List persons = loadPersons(record.getTaskId()); if ("dept".equalsIgnoreCase(groupBy)) { Map> byDept = new HashMap<>(); for (TmsPersonTaskEntity p : persons) { if (p == null || "1".equals(p.getGuestFlag())) { continue; } String deptId = StringUtil.isEmpty(p.getDeptId()) ? "-" : p.getDeptId(); if (deptFilter != null && !deptFilter.contains(deptId)) { continue; } byDept.computeIfAbsent(deptId, k -> new ArrayList<>()).add(p); } if (byDept.isEmpty()) { continue; } for (Map.Entry> e : byDept.entrySet()) { Agg agg = aggMap.computeIfAbsent(e.getKey(), k -> new Agg(k)); agg.sessionCount++; accumulate(agg, e.getValue()); } } else { List filtered = filterByDept(persons, deptFilter); if (deptFilter != null && filtered.isEmpty()) { continue; } String key = dimKeyOfRecord(groupBy, record); Agg agg = aggMap.computeIfAbsent(key, k -> new Agg(k)); agg.sessionCount++; accumulate(agg, filtered); } } Map deptNames = resolveDeptNames(groupBy, aggMap.keySet()); List result = new ArrayList<>(); for (Agg agg : aggMap.values()) { TmsTrainStatsRowVO vo = new TmsTrainStatsRowVO(); vo.setDimCode(agg.key); vo.setDimName(resolveDimName(groupBy, agg.key, deptNames)); vo.setSessionCount(agg.sessionCount); vo.setPersonCount(agg.personCount); vo.setSignedCount(agg.signedCount); vo.setPassedCount(agg.passedCount); vo.setSignRate(rateFloor(agg.signedCount, agg.personCount)); vo.setPassRate(rateFloor(agg.passedCount, agg.personCount)); result.add(vo); } return result; } /** * 返回命中当前维度的学员列表;未命中返回 null(该场次不计入明细)。 */ private List matchPersons( String groupBy, String dimCode, TmsRecordEntity record, List persons, Set deptFilter) { if ("dept".equalsIgnoreCase(groupBy)) { List matched = new ArrayList<>(); for (TmsPersonTaskEntity p : persons) { if (p == null || "1".equals(p.getGuestFlag())) { continue; } String deptId = StringUtil.isEmpty(p.getDeptId()) ? "-" : p.getDeptId(); if (deptFilter != null && !deptFilter.contains(deptId)) { continue; } if (!dimCode.equals(deptId)) { continue; } matched.add(p); } return matched.isEmpty() ? null : matched; } if (!dimCode.equals(dimKeyOfRecord(groupBy, record))) { return null; } List filtered = filterByDept(persons, deptFilter); if (deptFilter != null && filtered.isEmpty()) { return null; } return filtered; } private List filterByDept(List persons, Set deptFilter) { List filtered = new ArrayList<>(); for (TmsPersonTaskEntity p : persons) { if (p == null || "1".equals(p.getGuestFlag())) { continue; } if (deptFilter != null && !deptFilter.contains(StringUtil.isEmpty(p.getDeptId()) ? "-" : p.getDeptId())) { continue; } filtered.add(p); } return filtered; } private String dimKeyOfRecord(String groupBy, TmsRecordEntity record) { if ("evalMode".equalsIgnoreCase(groupBy)) { return StringUtil.isEmpty(record.getEvalMode()) ? "-" : record.getEvalMode(); } return StringUtil.isEmpty(record.getCategory()) ? "-" : record.getCategory(); } private List loadRecords(TmsTrainStatsQuery query) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); Date from = parseDate(query.getStartTimeFrom()); Date to = parseDate(query.getStartTimeTo()); if (from != null) { qw.ge(TmsRecordEntity::getStartTime, from); } if (to != null) { qw.le(TmsRecordEntity::getStartTime, to); } if (StringUtil.isNotEmpty(query.getCategory())) { qw.eq(TmsRecordEntity::getCategory, query.getCategory().trim()); } if (StringUtil.isNotEmpty(query.getEvalMode())) { qw.eq(TmsRecordEntity::getEvalMode, query.getEvalMode().trim()); } qw.orderByDesc(TmsRecordEntity::getStartTime); List records = recordMapper.selectList(qw); return records == null ? Collections.emptyList() : records; } private List loadPersons(String taskId) { List persons = personTaskMapper.selectList( new LambdaQueryWrapper() .eq(TmsPersonTaskEntity::getTaskId, taskId)); return persons == null ? Collections.emptyList() : persons; } private Map resolveDeptNames(String groupBy, Set keys) { Map deptNames = new HashMap<>(); if (!"dept".equalsIgnoreCase(groupBy) || keys == null || keys.isEmpty()) { return deptNames; } Set ids = new HashSet<>(keys); ids.remove("-"); for (String id : ids) { try { OrganizeEntity org = organizeApi.getInfoById(id); if (org != null && StringUtil.isNotEmpty(org.getFullName())) { deptNames.put(id, org.getFullName()); } } catch (Exception ignored) { // ignore } } return deptNames; } private String normalizeGroupBy(String groupBy) { return StringUtil.isEmpty(groupBy) ? "category" : groupBy.trim(); } /** 维度展示名:部门取组织名,分类/考核方式取枚举中文,避免前端回显字典编码 */ private String resolveDimName(String groupBy, String key, Map deptNames) { if (StringUtil.isEmpty(key) || "-".equals(key)) { if ("dept".equalsIgnoreCase(groupBy)) { return "未分配部门"; } if ("evalMode".equalsIgnoreCase(groupBy)) { return "未设置考核方式"; } return "未分类"; } if ("dept".equalsIgnoreCase(groupBy)) { return deptNames.getOrDefault(key, key); } if ("evalMode".equalsIgnoreCase(groupBy)) { TmsEvalMode mode = TmsEvalMode.of(key); return mode != null ? mode.getLabel() : key; } TmsTaskCategory category = TmsTaskCategory.of(key); return category != null ? category.getLabel() : key; } private void accumulate(Agg agg, List persons) { for (TmsPersonTaskEntity p : persons) { agg.personCount++; if (p.getSignTime() != null) { agg.signedCount++; } if (TmsPassFlag.isPass(p.getPassFlag())) { agg.passedCount++; } } } private static int rateFloor(int num, int den) { if (den <= 0) { return 0; } return (int) Math.floor(num * 100.0 / den); } private Date parseDate(String text) { if (StringUtil.isEmpty(text)) { return null; } try { return DateUtil.stringToDate(text); } catch (Exception ex) { return null; } } private static String str(Integer v) { return v == null ? "0" : String.valueOf(v); } private static class Agg { private final String key; private int sessionCount; private int personCount; private int signedCount; private int passedCount; private Agg(String key) { this.key = key; } } }