From 682269342935fe7fad3e351412bc241c7f159d6c Mon Sep 17 00:00:00 2001 From: funny <834502597@qq.com> Date: Tue, 19 May 2026 17:51:00 +0800 Subject: [PATCH] =?UTF-8?q?refactor(qms-pdi):=20=E9=87=8D=E6=9E=84PDI?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E9=A1=B9=E5=8F=8A=E9=83=A8=E4=BB=B6=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将检测项中的部件描述字段改为部件ID,关联部件表数据 - 新增部件管理功能,支持部件的增删改查及排序 - 在PDI检测项新增接口自动处理部件注册及排序 - 批量删除支持区分部件ID和检测项ID,实现联动删除 - 导入检测项时自动识别导入部件,动态创建部件及排序 - 查询检测项时按部件分组返回,提升数据结构清晰度 - 巡检工单及检测结果展示时通过部件ID获取部件名称显示 - 新增API支持根据部件ID查询检测项及现场记录 - QmsPdiReportVO 和 QmsIqcReportVO新增环比统计字段,完善报表数据 - 修改导出模板及分页查询逻辑,统一部件相关字段处理 - 优化图片ID收集及批量查询,提升接口性能和稳定性 --- .../controller/QmsIssueTicketController.java | 16 +- .../admin/controller/QmsReportController.java | 24 +- .../QmsPdiStatusItemControllerService.java | 9 + .../service/QmsReportControllerService.java | 638 +++++++++++++++++- .../wms/common/pojo/qo/QmsReportQueryQO.java | 52 +- .../common/pojo/vo/QmsIqcOverdueReportVO.java | 32 + .../common/pojo/vo/QmsOverdueReportVO.java | 83 ++- .../common/pojo/vo/QmsPdiOverdueReportVO.java | 32 + .../pojo/vo/QmsTicketOverdueReportVO.java | 32 + .../wms/common/pojo/vo/QmsTicketReportVO.java | 93 ++- .../wms/repository/entity/QmsIssueTicket.java | 5 + .../main/resources/mapper/QmsReportMapper.xml | 22 +- 12 files changed, 925 insertions(+), 113 deletions(-) create mode 100644 nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsIqcOverdueReportVO.java create mode 100644 nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsPdiOverdueReportVO.java create mode 100644 nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsTicketOverdueReportVO.java diff --git a/nflg-qms-admin/src/main/java/com/nflg/qms/admin/controller/QmsIssueTicketController.java b/nflg-qms-admin/src/main/java/com/nflg/qms/admin/controller/QmsIssueTicketController.java index 2b954bb6..3349db93 100644 --- a/nflg-qms-admin/src/main/java/com/nflg/qms/admin/controller/QmsIssueTicketController.java +++ b/nflg-qms-admin/src/main/java/com/nflg/qms/admin/controller/QmsIssueTicketController.java @@ -110,14 +110,14 @@ public class QmsIssueTicketController extends BaseController { return ApiResult.success(); } -// /** -// * 查询PDI工单详情 -// * 返回工单基本信息、处理记录及措施列表 -// */ -// @GetMapping("detail/pdi-ticket") -// public ApiResult detailPdiTicket(@NotNull(message = "ID不能为空") Long id) { -// return ApiResult.success(issueTicketControllerService.getPdiTicketDetail(id)); -// } + /** + * 查询PDI工单详情 + * 返回工单基本信息、处理记录及措施列表 + */ + @GetMapping("detail/pdi-ticket") + public ApiResult detailPdiTicket(@NotNull(message = "ID不能为空") Long id) { + return ApiResult.success(issueTicketControllerService.getPdiTicketDetail(id)); + } /** * 查询本人的PDI工单详情 diff --git a/nflg-qms-admin/src/main/java/com/nflg/qms/admin/controller/QmsReportController.java b/nflg-qms-admin/src/main/java/com/nflg/qms/admin/controller/QmsReportController.java index 6819ed10..a170fed0 100644 --- a/nflg-qms-admin/src/main/java/com/nflg/qms/admin/controller/QmsReportController.java +++ b/nflg-qms-admin/src/main/java/com/nflg/qms/admin/controller/QmsReportController.java @@ -47,10 +47,26 @@ public class QmsReportController extends BaseController { } /** - * 超时统计数据 + * IQC超时统计数据 */ - @PostMapping("overdue") - public ApiResult getOverdueReport() { - return ApiResult.success(reportControllerService.getOverdueReport()); + @PostMapping("overdue/iqc") + public ApiResult getIqcOverdueReport() { + return ApiResult.success(reportControllerService.getIqcOverdueReport()); + } + + /** + * PDI超时统计数据 + */ + @PostMapping("overdue/pdi") + public ApiResult getPdiOverdueReport() { + return ApiResult.success(reportControllerService.getPdiOverdueReport()); + } + + /** + * 工单超时统计数据 + */ + @PostMapping("overdue/ticket") + public ApiResult getTicketOverdueReport() { + return ApiResult.success(reportControllerService.getTicketOverdueReport()); } } diff --git a/nflg-qms-admin/src/main/java/com/nflg/qms/admin/service/QmsPdiStatusItemControllerService.java b/nflg-qms-admin/src/main/java/com/nflg/qms/admin/service/QmsPdiStatusItemControllerService.java index 850ad243..491a0838 100644 --- a/nflg-qms-admin/src/main/java/com/nflg/qms/admin/service/QmsPdiStatusItemControllerService.java +++ b/nflg-qms-admin/src/main/java/com/nflg/qms/admin/service/QmsPdiStatusItemControllerService.java @@ -358,6 +358,15 @@ public class QmsPdiStatusItemControllerService { componentsId = null; } + // 处理检查核实内容:如果以英文字母结束,追加换行符 + String content = dto.getInspectionContent(); + if (StrUtil.isNotBlank(content)) { + char lastChar = content.charAt(content.length() - 1); + if ((lastChar >= 'a' && lastChar <= 'z') || (lastChar >= 'A' && lastChar <= 'Z')) { + dto.setInspectionContent(content + "\n"); + } + } + // 插入检测项 int itemSort = componentItemCountMap.getOrDefault(componentsId, 0) + 1; componentItemCountMap.put(componentsId, itemSort); diff --git a/nflg-qms-admin/src/main/java/com/nflg/qms/admin/service/QmsReportControllerService.java b/nflg-qms-admin/src/main/java/com/nflg/qms/admin/service/QmsReportControllerService.java index fa533231..75210c35 100644 --- a/nflg-qms-admin/src/main/java/com/nflg/qms/admin/service/QmsReportControllerService.java +++ b/nflg-qms-admin/src/main/java/com/nflg/qms/admin/service/QmsReportControllerService.java @@ -1,5 +1,6 @@ package com.nflg.qms.admin.service; +import cn.hutool.core.util.StrUtil; import com.nflg.wms.common.pojo.qo.QmsReportQueryQO; import com.nflg.wms.common.pojo.vo.*; import com.nflg.wms.repository.entity.*; @@ -50,6 +51,12 @@ public class QmsReportControllerService { @Resource private IQmsPdiDetectionRulesService pdiDetectionRulesService; + @Resource + private IQmsIssueTicketService issueTicketService; + + @Resource + private IQmsIssueTicketProcessService issueTicketProcessService; + /** * IQC报表数据 */ @@ -61,13 +68,13 @@ public class QmsReportControllerService { LocalDateTime currentEnd = request.getEndTime(); boolean hasTimeRange = currentStart != null || currentEnd != null; - // 如果都没传,查询全部 + // 如果都没传,使用合理的时间范围(最近1年) if (!hasTimeRange) { - currentStart = LocalDateTime.MIN; - currentEnd = LocalDateTime.MAX; + currentStart = LocalDateTime.now().minusYears(1); + currentEnd = LocalDateTime.now(); } else { - if (currentStart == null) currentStart = LocalDateTime.MIN; - if (currentEnd == null) currentEnd = LocalDateTime.MAX; + if (currentStart == null) currentStart = LocalDateTime.now().minusYears(1); + if (currentEnd == null) currentEnd = LocalDateTime.now(); } // 计算上周期 @@ -460,13 +467,13 @@ public class QmsReportControllerService { LocalDateTime currentEnd = request.getEndTime(); boolean hasTimeRange = currentStart != null || currentEnd != null; - // 如果都没传,查询全部 + // 如果都没传,使用合理的时间范围(最近1年) if (!hasTimeRange) { - currentStart = LocalDateTime.MIN; - currentEnd = LocalDateTime.MAX; + currentStart = LocalDateTime.now().minusYears(1); + currentEnd = LocalDateTime.now(); } else { - if (currentStart == null) currentStart = LocalDateTime.MIN; - if (currentEnd == null) currentEnd = LocalDateTime.MAX; + if (currentStart == null) currentStart = LocalDateTime.now().minusYears(1); + if (currentEnd == null) currentEnd = LocalDateTime.now(); } // 计算上周期 @@ -606,7 +613,7 @@ public class QmsReportControllerService { Map result = new HashMap<>(); // 当前周期平均时间 - String currentAvgTime = "0H0Min0S"; + String currentAvgTime = "0天0小时"; long currentAvgSeconds = 0; List currentCompletedTasks = pdiTaskRecordService.lambdaQuery() @@ -628,7 +635,7 @@ public class QmsReportControllerService { } // 上周期平均时间 - String previousAvgTime = "0H0Min0S"; + String previousAvgTime = "0天0小时"; long previousAvgSeconds = 0; if (hasTimeRange) { @@ -812,28 +819,613 @@ public class QmsReportControllerService { } /** - * 格式化时长:秒 -> xxHxxMinxxS + * 格式化时长:秒 -> xx天xx小时 */ private String formatDurationPDI(long seconds) { - long hours = seconds / 3600; - long minutes = (seconds % 3600) / 60; - long secs = seconds % 60; - return hours + "H" + minutes + "Min" + secs + "S"; + long days = seconds / 86400; + double hours = (seconds % 86400) / 3600.0; + return days + "天" + String.format("%.2f", hours) + "小时"; } /** * 工单报表数据 */ public QmsTicketReportVO getTicketReport(QmsReportQueryQO request) { - // TODO: 实现逻辑 - return new QmsTicketReportVO(); + QmsTicketReportVO vo = new QmsTicketReportVO(); + + // 计算时间范围 + LocalDateTime start = request.getStartTime() != null ? request.getStartTime() : LocalDateTime.now().minusYears(1); + LocalDateTime end = request.getEndTime() != null ? request.getEndTime() : LocalDateTime.now(); + + // 1. 查询周期内所有工单 + List allTickets = issueTicketService.lambdaQuery() + .ge(QmsIssueTicket::getCreateTime, start) + .le(QmsIssueTicket::getCreateTime, end) + .list(); + + int total = allTickets.size(); + vo.setTotalTickets(total); + + if (total == 0) { + // 无数据时返回空结构 + vo.setCompletedCount(0); + vo.setUnfinishedCount(0); + vo.setPendingTransferCount(0); + vo.setProcessingCount(0); + vo.setCompletionRate(0.0); + vo.setAvgProcessingTime("0天0小时"); + vo.setStatusDistributions(buildEmptyStatusDistributions()); + vo.setCompletionStatusList(Collections.emptyList()); + vo.setIssueTypeCounts(Collections.emptyList()); + vo.setTop10Users(Collections.emptyList()); + vo.setBottom10Users(Collections.emptyList()); + vo.setOverallAvgCompletionRate(0.0); + return vo; + } + + // 2. 各状态统计 + int completedCount = (int) allTickets.stream().filter(t -> t.getStatus() != null && t.getStatus() == 2).count(); + int pendingTransferCount = (int) allTickets.stream().filter(t -> t.getStatus() != null && t.getStatus() == 0).count(); + int processingCount = (int) allTickets.stream().filter(t -> t.getStatus() != null && t.getStatus() == 1).count(); + int unfinishedCount = total - completedCount; + + vo.setCompletedCount(completedCount); + vo.setUnfinishedCount(unfinishedCount); + vo.setPendingTransferCount(pendingTransferCount); + vo.setProcessingCount(processingCount); + + // 3. 总体完成率 + vo.setCompletionRate(round2(completedCount * 100.0 / total)); + + // 4. 平均处理时长(已完成工单的 completeTime - createTime) + vo.setAvgProcessingTime(calculateTicketAvgProcessingTime(allTickets)); + + // 5. 三种状态占比 + vo.setStatusDistributions(buildStatusDistributions(total, pendingTransferCount, processingCount, completedCount)); + + // 6. 完成状态分布(已完成工单中各审批结果占比) + vo.setCompletionStatusList(buildCompletionStatusList(allTickets, completedCount)); + + // 7. 问题类别占比(暂空置) + vo.setIssueTypeCounts(Collections.emptyList()); + + // 8. 用户完成率(按处理人) + buildUserCompletionRates(vo, allTickets); + + return vo; + } + + // ======================== 工单报表辅助方法 ======================== + + /** + * 计算平均处理时长 + */ + private String calculateTicketAvgProcessingTime(List tickets) { + List completedTickets = tickets.stream() + .filter(t -> t.getStatus() != null && t.getStatus() == 2) + .filter(t -> t.getCompleteTime() != null && t.getCreateTime() != null) + .collect(Collectors.toList()); + + if (completedTickets.isEmpty()) { + return "0天0.00小时"; + } + + long totalSeconds = completedTickets.stream() + .mapToLong(t -> Duration.between(t.getCreateTime(), t.getCompleteTime()).getSeconds()) + .sum(); + long avgSeconds = totalSeconds / completedTickets.size(); + + long days = avgSeconds / 86400; + double hours = (avgSeconds % 86400) / 3600.0; + + return days + "天" + String.format("%.2f", hours) + "小时"; } /** - * 超时统计数据 + * 构建三种状态占比 */ - public QmsOverdueReportVO getOverdueReport() { - // TODO: 实现逻辑 - return new QmsOverdueReportVO(); + private List buildStatusDistributions( + int total, int pendingTransfer, int processing, int completed) { + List list = new ArrayList<>(); + + QmsTicketReportVO.StatusDistribution d0 = new QmsTicketReportVO.StatusDistribution(); + d0.setStatus((short) 0); + d0.setStatusName("待流转"); + d0.setPercentage(round2(pendingTransfer * 100.0 / total)); + list.add(d0); + + QmsTicketReportVO.StatusDistribution d1 = new QmsTicketReportVO.StatusDistribution(); + d1.setStatus((short) 1); + d1.setStatusName("处理中"); + d1.setPercentage(round2(processing * 100.0 / total)); + list.add(d1); + + QmsTicketReportVO.StatusDistribution d2 = new QmsTicketReportVO.StatusDistribution(); + d2.setStatus((short) 2); + d2.setStatusName("已完成"); + d2.setPercentage(round2(completed * 100.0 / total)); + list.add(d2); + + return list; + } + + /** + * 构建空状态分布 + */ + private List buildEmptyStatusDistributions() { + return buildStatusDistributions(1, 0, 0, 0); + } + + /** + * 构建完成状态分布 + */ + private List buildCompletionStatusList( + List allTickets, int completedCount) { + if (completedCount == 0) { + return Collections.emptyList(); + } + + // 已完成工单的审批结果统计 + Map approvalCountMap = allTickets.stream() + .filter(t -> t.getStatus() != null && t.getStatus() == 2) + .filter(t -> t.getApprovalStatus() != null) + .collect(Collectors.groupingBy(QmsIssueTicket::getApprovalStatus, Collectors.counting())); + + // 审批结果名称映射 + Map approvalNames = new LinkedHashMap<>(); + approvalNames.put((short) 0, "通过"); + approvalNames.put((short) 1, "驳回"); + approvalNames.put((short) 2, "退货"); + approvalNames.put((short) 3, "报废"); + approvalNames.put((short) 4, "维修"); + approvalNames.put((short) 5, "挑选使用"); + approvalNames.put((short) 6, "让渡使用"); + + List result = new ArrayList<>(); + for (Map.Entry entry : approvalNames.entrySet()) { + Long count = approvalCountMap.getOrDefault(entry.getKey(), 0L); + if (count > 0) { + QmsTicketReportVO.CompletionStatus cs = new QmsTicketReportVO.CompletionStatus(); + cs.setApprovalStatus(entry.getKey()); + cs.setStatusName(entry.getValue()); + cs.setCount(count.intValue()); + cs.setPercentage(round2(count * 100.0 / completedCount)); + result.add(cs); + } + } + return result; + } + + /** + * 按处理人统计用户完成率 + */ + private void buildUserCompletionRates(QmsTicketReportVO vo, List allTickets) { + // 收集所有工单ID + Set ticketIds = allTickets.stream() + .map(QmsIssueTicket::getId) + .collect(Collectors.toSet()); + + if (ticketIds.isEmpty()) { + vo.setTop10Users(Collections.emptyList()); + vo.setBottom10Users(Collections.emptyList()); + vo.setOverallAvgCompletionRate(0.0); + return; + } + + // 查询所有处理记录 + List processes = issueTicketProcessService.lambdaQuery() + .in(QmsIssueTicketProcess::getIssueTicketId, ticketIds) + .list(); + + // 工单状态映射(ticketId → status) + Map ticketStatusMap = allTickets.stream() + .collect(Collectors.toMap(QmsIssueTicket::getId, t -> t.getStatus() != null ? t.getStatus() : 0)); + + // 按处理人分组统计 + Map> handlerGroup = processes.stream() + .filter(p -> p.getHandlerUserId() != null) + .collect(Collectors.groupingBy(QmsIssueTicketProcess::getHandlerUserId)); + + List userRates = new ArrayList<>(); + for (Map.Entry> entry : handlerGroup.entrySet()) { + Long userId = entry.getKey(); + List userProcesses = entry.getValue(); + int totalAssigned = userProcesses.size(); + int totalCompleted = (int) userProcesses.stream() + .filter(p -> { + Short status = ticketStatusMap.get(p.getIssueTicketId()); + return status != null && status == 2; + }) + .count(); + + String userName = userProcesses.get(0).getHandlerUserName(); + + QmsTicketReportVO.UserCompletionRate rate = new QmsTicketReportVO.UserCompletionRate(); + rate.setUserId(userId); + rate.setUserName(userName); + rate.setTotalAssigned(totalAssigned); + rate.setTotalCompleted(totalCompleted); + rate.setCompletionRate(round2(totalCompleted * 100.0 / totalAssigned)); + userRates.add(rate); + } + + // 按完成率降序排列 + userRates.sort((a, b) -> Double.compare(b.getCompletionRate(), a.getCompletionRate())); + + // 整体平均完成率 + if (!userRates.isEmpty()) { + double avgRate = userRates.stream() + .mapToDouble(QmsTicketReportVO.UserCompletionRate::getCompletionRate) + .average() + .orElse(0.0); + vo.setOverallAvgCompletionRate(round2(avgRate)); + } else { + vo.setOverallAvgCompletionRate(0.0); + } + + // 用户总数 < 20 → 全部返回 + if (userRates.size() < 20) { + vo.setTop10Users(new ArrayList<>(userRates)); + List reversed = new ArrayList<>(userRates); + reversed.sort((a, b) -> Double.compare(a.getCompletionRate(), b.getCompletionRate())); + vo.setBottom10Users(reversed); + } else { + vo.setTop10Users(new ArrayList<>(userRates.subList(0, 10))); + List reversed = new ArrayList<>(userRates); + reversed.sort((a, b) -> Double.compare(a.getCompletionRate(), b.getCompletionRate())); + vo.setBottom10Users(new ArrayList<>(reversed.subList(0, 10))); + } + } + + /** + * 保留两位小数 + */ + private double round2(double value) { + return Math.round(value * 100.0) / 100.0; + } + + /** + * IQC超时统计数据 + */ + public QmsIqcOverdueReportVO getIqcOverdueReport() { + QmsIqcOverdueReportVO vo = new QmsIqcOverdueReportVO(); + LocalDateTime now = LocalDateTime.now(); + + List iqcOverdueTasks = buildIqcOverdueTasks(now); + vo.setIqcOverdueTasks(iqcOverdueTasks); + vo.setTotalOverdueCount(iqcOverdueTasks.size()); + vo.setAvgOverdueTime(calculateAvgOverdueTimeFromTasks(iqcOverdueTasks)); + vo.setSevereOverdueCount(countSevereOverdue(iqcOverdueTasks)); + + return vo; + } + + /** + * PDI超时统计数据 + */ + public QmsPdiOverdueReportVO getPdiOverdueReport() { + QmsPdiOverdueReportVO vo = new QmsPdiOverdueReportVO(); + LocalDateTime now = LocalDateTime.now(); + + List pdiOverdueTasks = buildPdiOverdueTasks(now); + vo.setPdiOverdueTasks(pdiOverdueTasks); + vo.setTotalOverdueCount(pdiOverdueTasks.size()); + vo.setAvgOverdueTime(calculateAvgOverdueTimeFromTasks(pdiOverdueTasks)); + vo.setSevereOverdueCount(countSevereOverdue(pdiOverdueTasks)); + + return vo; + } + + /** + * 工单超时统计数据 + */ + public QmsTicketOverdueReportVO getTicketOverdueReport() { + QmsTicketOverdueReportVO vo = new QmsTicketOverdueReportVO(); + LocalDateTime now = LocalDateTime.now(); + + List ticketOverdueTasks = buildTicketOverdueTasks(now); + vo.setTicketOverdueTasks(ticketOverdueTasks); + vo.setTotalOverdueCount(ticketOverdueTasks.size()); + vo.setAvgOverdueTime(calculateAvgOverdueTimeFromTasks(ticketOverdueTasks)); + vo.setSevereOverdueCount(countTicketSevereOverdue(ticketOverdueTasks)); + + return vo; + } + + // ======================== 超时统计辅助方法 ======================== + + /** + * 构建工单超时任务列表 + */ + private List buildTicketOverdueTasks(LocalDateTime now) { + // 查询未完成且超时的工单 + List overdueTickets = issueTicketService.lambdaQuery() + .ne(QmsIssueTicket::getStatus, 2) + .in(QmsIssueTicket::getOverdue, 1, 2) + .list(); + + if (overdueTickets.isEmpty()) { + return Collections.emptyList(); + } + + // 批量查询处理记录(用于PDI和巡检工单) + Set ticketIds = overdueTickets.stream() + .map(QmsIssueTicket::getId) + .collect(Collectors.toSet()); + + List processes = issueTicketProcessService.lambdaQuery() + .in(QmsIssueTicketProcess::getIssueTicketId, ticketIds) + .list(); + + Map> processMap = processes.stream() + .collect(Collectors.groupingBy(QmsIssueTicketProcess::getIssueTicketId)); + + // 组装VO + List result = new ArrayList<>(); + for (QmsIssueTicket ticket : overdueTickets) { + QmsOverdueReportVO.OverdueTaskItem item = new QmsOverdueReportVO.OverdueTaskItem(); + item.setTaskId(ticket.getId()); + item.setTaskNo(ticket.getTicketNo()); + item.setCreateDate(ticket.getCreateTime() != null ? ticket.getCreateTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) : ""); + + // 根据事故类型计算要求完成时间:严重(2)=3天,较严重(1)=7天,一般(0)=14天 + LocalDateTime requiredFinishTime = null; + if (ticket.getCreateTime() != null) { + int days = 14; // 默认一般 + if (ticket.getIncidentType() != null) { + if (ticket.getIncidentType() == 2) days = 3; + else if (ticket.getIncidentType() == 1) days = 7; + } + requiredFinishTime = ticket.getCreateTime().plusDays(days); + } + item.setRequiredCompletionTime(requiredFinishTime != null ? requiredFinishTime.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) : ""); + item.setOverdueDuration(calculateOverdueDuration(requiredFinishTime, now)); + + // 根据sourceType确定类型和处理人 + if (ticket.getSourceType() != null && ticket.getSourceType() == 0) { + // IQC工单:处理人 = approval_user + item.setType("IQC工单"); + if (ticket.getApprovalUserId() != null) { + item.setHandlerNames(Collections.singletonList(ticket.getApprovalUserName())); + item.setHandlerIds(Collections.singletonList(ticket.getApprovalUserId())); + } else { + item.setHandlerNames(Collections.emptyList()); + item.setHandlerIds(Collections.emptyList()); + } + } else { + // PDI工单(1) 或 巡检工单(2):处理人 = process.handler + item.setType(ticket.getSourceType() != null && ticket.getSourceType() == 1 ? "PDI工单" : "巡检工单"); + List ticketProcesses = processMap.getOrDefault(ticket.getId(), Collections.emptyList()); + List handlerNames = ticketProcesses.stream() + .map(QmsIssueTicketProcess::getHandlerUserName) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + List handlerIds = ticketProcesses.stream() + .map(QmsIssueTicketProcess::getHandlerUserId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + item.setHandlerNames(handlerNames); + item.setHandlerIds(handlerIds); + } + + result.add(item); + } + + return result; + } + + /** + * 构建IQC任务超时列表 + */ + private List buildIqcOverdueTasks(LocalDateTime now) { + List overdueTasks = incomingInspectionTaskService.lambdaQuery() + .eq(QmsIncomingInspectionTask::getIsOverdue, true) + .ne(QmsIncomingInspectionTask::getInspectionStatus, 2) + .list(); + + if (overdueTasks.isEmpty()) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + for (QmsIncomingInspectionTask task : overdueTasks) { + QmsOverdueReportVO.OverdueTaskItem item = new QmsOverdueReportVO.OverdueTaskItem(); + item.setTaskId(task.getId()); + item.setTaskNo(task.getTaskNo()); + item.setType("IQC"); + item.setCreateDate(task.getSubmitTime() != null ? task.getSubmitTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) : ""); + item.setRequiredCompletionTime(task.getRequiredFinishTime() != null ? task.getRequiredFinishTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) : ""); + item.setOverdueDuration(calculateOverdueDuration(task.getRequiredFinishTime(), now)); + + // 处理人:inspector + agent + List handlerNames = new ArrayList<>(); + List handlerIds = new ArrayList<>(); + if (StrUtil.isNotBlank(task.getInspectorName())) { + handlerNames.add(task.getInspectorName()); + handlerIds.add(task.getInspectorId()); + } + if (StrUtil.isNotBlank(task.getAgentName())) { + handlerNames.add(task.getAgentName()); + handlerIds.add(task.getAgentId()); + } + item.setHandlerNames(handlerNames); + item.setHandlerIds(handlerIds); + + result.add(item); + } + + return result; + } + + /** + * 构建PDI任务超时列表 + */ + private List buildPdiOverdueTasks(LocalDateTime now) { + List overdueTasks = pdiTaskRecordService.lambdaQuery() + .eq(QmsPdiTaskRecord::getOverdue, true) + .ne(QmsPdiTaskRecord::getInspectionEnable, 2) + .list(); + + if (overdueTasks.isEmpty()) { + return Collections.emptyList(); + } + + // 批量查询检测规则获取inspector + Set ruleIds = overdueTasks.stream() + .map(QmsPdiTaskRecord::getDetectionRulesId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + Map rulesMap = pdiDetectionRulesService.listByIds(ruleIds).stream() + .collect(Collectors.toMap(QmsPdiDetectionRules::getId, r -> r, (a, b) -> a)); + + // 收集所有需要查询的用户ID + Set userIds = new HashSet<>(); + rulesMap.values().forEach(rule -> { + if (rule.getInspectorId() != null) { + userIds.add(rule.getInspectorId()); + } + }); + overdueTasks.forEach(task -> { + if (task.getAssistantId() != null) { + userIds.add(task.getAssistantId()); + } + }); + + // 批量查询用户信息 + Map userMap = new HashMap<>(); + if (!userIds.isEmpty()) { + userMap = userService.listByIds(userIds).stream() + .collect(Collectors.toMap(User::getId, u -> u, (a, b) -> a)); + } + + Map finalUserMap = userMap; + List result = new ArrayList<>(); + for (QmsPdiTaskRecord task : overdueTasks) { + QmsOverdueReportVO.OverdueTaskItem item = new QmsOverdueReportVO.OverdueTaskItem(); + item.setTaskId(task.getId()); + item.setTaskNo(task.getTaskNo()); + item.setType("PDI"); + item.setCreateDate(task.getSubmissionTime() != null ? task.getSubmissionTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) : ""); + item.setRequiredCompletionTime(task.getRequiredCompletionTime() != null ? task.getRequiredCompletionTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) : ""); + item.setOverdueDuration(calculateOverdueDuration(task.getRequiredCompletionTime(), now)); + + // 处理人:rules.inspector + task.assistant + List handlerNames = new ArrayList<>(); + List handlerIds = new ArrayList<>(); + + QmsPdiDetectionRules rule = rulesMap.get(task.getDetectionRulesId()); + if (rule != null && rule.getInspectorId() != null) { + User inspector = finalUserMap.get(rule.getInspectorId()); + if (inspector != null) { + handlerNames.add(inspector.getUserName()); + } + handlerIds.add(rule.getInspectorId()); + } + if (task.getAssistantId() != null) { + User assistant = finalUserMap.get(task.getAssistantId()); + if (assistant != null) { + handlerNames.add(assistant.getUserName()); + } + handlerIds.add(task.getAssistantId()); + } + + item.setHandlerNames(handlerNames); + item.setHandlerIds(handlerIds); + + result.add(item); + } + + return result; + } + + /** + * 计算超时时长 + */ + private String calculateOverdueDuration(LocalDateTime startTime, LocalDateTime endTime) { + if (startTime == null) { + return "0.00小时"; + } + long totalMinutes = Duration.between(startTime, endTime).toMinutes(); + if (totalMinutes < 0) totalMinutes = 0; + double hours = totalMinutes / 60.0; + return String.format("%.2f小时", hours); + } + + /** + * 判断是否严重超时(>=24小时) + */ + private boolean isSevereOverdue(String overdueDuration) { + try { + double hours = Double.parseDouble(overdueDuration.replace("小时", "")); + return hours >= 24; + } catch (Exception e) { + return false; + } + } + + /** + * 统计严重超时数量(>=24小时) + */ + private int countSevereOverdue(List tasks) { + if (tasks.isEmpty()) return 0; + + return (int) tasks.stream() + .filter(t -> { + String duration = t.getOverdueDuration(); + return duration != null && isSevereOverdue(duration); + }) + .count(); + } + + /** + * 统计工单严重超时数量(overdue=2) + */ + private int countTicketSevereOverdue(List tasks) { + if (tasks.isEmpty()) return 0; + + // 需要从原始工单数据中筛选overdue=2的 + // 这里简化处理:通过taskNo重新查询 + Set taskNos = tasks.stream() + .map(QmsOverdueReportVO.OverdueTaskItem::getTaskNo) + .collect(Collectors.toSet()); + + return (int) issueTicketService.lambdaQuery() + .in(QmsIssueTicket::getTicketNo, taskNos) + .eq(QmsIssueTicket::getOverdue, 2) + .count() + .intValue(); + } + + /** + * 计算平均超时时长(单个类别) + */ + private String calculateAvgOverdueTimeFromTasks(List tasks) { + if (tasks.isEmpty()) { + return "0.00小时"; + } + + List durations = tasks.stream() + .map(QmsOverdueReportVO.OverdueTaskItem::getOverdueDuration) + .filter(Objects::nonNull) + .toList(); + + if (durations.isEmpty()) { + return "0.00小时"; + } + + double totalHours = 0; + for (String duration : durations) { + try { + totalHours += Double.parseDouble(duration.replace("小时", "")); + } catch (Exception e) { + // 忽略解析错误 + } + } + + double avgHours = totalHours / durations.size(); + return String.format("%.2f小时", avgHours); } } diff --git a/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/qo/QmsReportQueryQO.java b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/qo/QmsReportQueryQO.java index 8efe16b0..4745ee5e 100644 --- a/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/qo/QmsReportQueryQO.java +++ b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/qo/QmsReportQueryQO.java @@ -3,6 +3,9 @@ package com.nflg.wms.common.pojo.qo; import lombok.Data; import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; /** * 报表查询请求QO @@ -11,12 +14,53 @@ import java.time.LocalDateTime; public class QmsReportQueryQO { /** - * 开始时间(可选) + * 时间范围(前端传入,支持多种格式) */ - private LocalDateTime startTime; + private List dateRange; /** - * 结束时间(可选) + * 获取开始时间 */ - private LocalDateTime endTime; + public LocalDateTime getStartTime() { + if (dateRange != null && dateRange.size() >= 1) { + return parseDateTime(dateRange.get(0)); + } + return null; + } + + /** + * 获取结束时间 + */ + public LocalDateTime getEndTime() { + if (dateRange != null && dateRange.size() >= 2) { + return parseDateTime(dateRange.get(1)); + } + return null; + } + + /** + * 解析时间字符串,兼容多种格式 + */ + private LocalDateTime parseDateTime(String text) { + if (text == null || text.isBlank()) return null; + try { + // ISO格式带时区:2026-05-02T16:00:00.000Z + if (text.endsWith("Z") || text.contains("+")) { + return OffsetDateTime.parse(text).toLocalDateTime(); + } + // yyyy/MM/dd + if (text.contains("/")) { + return LocalDateTime.parse(text + " 00:00:00", + DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")); + } + // yyyy-MM-dd + if (text.length() == 10) { + return LocalDateTime.parse(text + "T00:00:00"); + } + // yyyy-MM-ddTHH:mm:ss + return LocalDateTime.parse(text); + } catch (Exception e) { + return null; + } + } } diff --git a/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsIqcOverdueReportVO.java b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsIqcOverdueReportVO.java new file mode 100644 index 00000000..abd25009 --- /dev/null +++ b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsIqcOverdueReportVO.java @@ -0,0 +1,32 @@ +package com.nflg.wms.common.pojo.vo; + +import lombok.Data; + +import java.util.List; + +/** + * IQC超时统计报表VO + */ +@Data +public class QmsIqcOverdueReportVO { + + /** + * IQC超时任务总数 + */ + private Integer totalOverdueCount; + + /** + * IQC平均超时时间(格式:X.XX小时) + */ + private String avgOverdueTime; + + /** + * IQC严重超时数(>=24小时) + */ + private Integer severeOverdueCount; + + /** + * IQC超时任务列表 + */ + private List iqcOverdueTasks; +} diff --git a/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsOverdueReportVO.java b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsOverdueReportVO.java index c499ca03..e41bd0b3 100644 --- a/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsOverdueReportVO.java +++ b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsOverdueReportVO.java @@ -10,31 +10,67 @@ import java.util.List; @Data public class QmsOverdueReportVO { - /** - * 超时任务总数 - */ - private Integer totalOverdueCount; + // ===== IQC超时统计 ===== /** - * 平均超时时间(格式:xx小时xx分钟xx秒) + * IQC超时任务总数 */ - private String avgOverdueTime; + private Integer iqcTotalOverdueCount; /** - * 严重超时数(阈值待定) + * IQC平均超时时间(格式:X.XX小时) */ - private Integer severeOverdueCount; + private String iqcAvgOverdueTime; + + /** + * IQC严重超时数(>=24小时) + */ + private Integer iqcSevereOverdueCount; /** * IQC超时任务列表 */ private List iqcOverdueTasks; + // ===== PDI超时统计 ===== + + /** + * PDI超时任务总数 + */ + private Integer pdiTotalOverdueCount; + + /** + * PDI平均超时时间(格式:X.XX小时) + */ + private String pdiAvgOverdueTime; + + /** + * PDI严重超时数(>=24小时) + */ + private Integer pdiSevereOverdueCount; + /** * PDI超时任务列表 */ private List pdiOverdueTasks; + // ===== 工单超时统计 ===== + + /** + * 工单超时任务总数 + */ + private Integer ticketTotalOverdueCount; + + /** + * 工单平均超时时间(格式:X.XX小时) + */ + private String ticketAvgOverdueTime; + + /** + * 工单严重超时数(overdue=2) + */ + private Integer ticketSevereOverdueCount; + /** * 工单超时任务列表 */ @@ -45,39 +81,28 @@ public class QmsOverdueReportVO { */ @Data public static class OverdueTaskItem { - /** - * 工单编号 - */ + /** 任务/工单ID */ + private Long taskId; + + /** 工单编号 */ private String taskNo; - /** - * 类型(IQC/PDI/工单类型) - */ + /** 类型(IQC/PDI/IQC工单/PDI工单/巡检工单) */ private String type; - /** - * 处理人姓名列表 - */ + /** 处理人姓名列表 */ private List handlerNames; - /** - * 处理人ID列表 - */ + /** 处理人ID列表 */ private List handlerIds; - /** - * 创建日期 - */ + /** 创建日期(格式:yyyy-MM-dd HH:mm) */ private String createDate; - /** - * 要求完成时间 - */ + /** 要求完成时间(格式:yyyy-MM-dd HH:mm) */ private String requiredCompletionTime; - /** - * 超时时长(格式:xx小时xx分钟xx秒) - */ + /** 超时时长(格式:X.XX小时) */ private String overdueDuration; } } diff --git a/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsPdiOverdueReportVO.java b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsPdiOverdueReportVO.java new file mode 100644 index 00000000..1e448dcc --- /dev/null +++ b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsPdiOverdueReportVO.java @@ -0,0 +1,32 @@ +package com.nflg.wms.common.pojo.vo; + +import lombok.Data; + +import java.util.List; + +/** + * PDI超时统计报表VO + */ +@Data +public class QmsPdiOverdueReportVO { + + /** + * PDI超时任务总数 + */ + private Integer totalOverdueCount; + + /** + * PDI平均超时时间(格式:X.XX小时) + */ + private String avgOverdueTime; + + /** + * PDI严重超时数(>=24小时) + */ + private Integer severeOverdueCount; + + /** + * PDI超时任务列表 + */ + private List pdiOverdueTasks; +} diff --git a/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsTicketOverdueReportVO.java b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsTicketOverdueReportVO.java new file mode 100644 index 00000000..45597e42 --- /dev/null +++ b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsTicketOverdueReportVO.java @@ -0,0 +1,32 @@ +package com.nflg.wms.common.pojo.vo; + +import lombok.Data; + +import java.util.List; + +/** + * 工单超时统计报表VO + */ +@Data +public class QmsTicketOverdueReportVO { + + /** + * 工单超时任务总数 + */ + private Integer totalOverdueCount; + + /** + * 工单平均超时时间(格式:X.XX小时) + */ + private String avgOverdueTime; + + /** + * 工单严重超时数(overdue=2) + */ + private Integer severeOverdueCount; + + /** + * 工单超时任务列表 + */ + private List ticketOverdueTasks; +} diff --git a/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsTicketReportVO.java b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsTicketReportVO.java index d61a12a5..e745fb99 100644 --- a/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsTicketReportVO.java +++ b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsTicketReportVO.java @@ -16,15 +16,25 @@ public class QmsTicketReportVO { private Integer totalTickets; /** - * 已完成数 + * 已完成数(status=2) */ private Integer completedCount; /** - * 未完成数 + * 未完成数(status≠2) */ private Integer unfinishedCount; + /** + * 待流转数(status=0) + */ + private Integer pendingTransferCount; + + /** + * 处理中数(status=1) + */ + private Integer processingCount; + /** * 总体完成率(百分比) */ @@ -36,38 +46,62 @@ public class QmsTicketReportVO { private String avgProcessingTime; /** - * 各状态占比列表 + * 各状态占比列表(待流转、处理中、已完成) */ private List statusDistributions; /** - * 问题类别数量列表 + * 完成状态分布(已完成工单中各审批结果占比) + */ + private List completionStatusList; + + /** + * 问题类别数量列表(暂空置) */ private List issueTypeCounts; /** - * 用户完成率列表 + * 用户完成率前10 */ - private List userCompletionRates; + private List top10Users; + + /** + * 用户完成率后10 + */ + private List bottom10Users; + + /** + * 整体平均完成率 + */ + private Double overallAvgCompletionRate; + + // ===== 内部类 ===== /** * 状态分布 */ @Data public static class StatusDistribution { - /** - * 状态(0=待流转,1=处理中,2=已完成) - */ + /** 状态(0=待流转,1=处理中,2=已完成) */ private Short status; - - /** - * 状态名称 - */ + /** 状态名称 */ private String statusName; + /** 占比(百分比) */ + private Double percentage; + } - /** - * 占比(百分比) - */ + /** + * 完成状态(已完成工单的审批结果分布) + */ + @Data + public static class CompletionStatus { + /** 审批结果(0=通过,1=驳回,2=退货,3=报废,4=维修,5=挑选使用,6=让渡使用) */ + private Short approvalStatus; + /** 状态名称 */ + private String statusName; + /** 数量 */ + private Integer count; + /** 占比(百分比) */ private Double percentage; } @@ -76,14 +110,9 @@ public class QmsTicketReportVO { */ @Data public static class IssueTypeCount { - /** - * 问题类别名称 - */ + /** 问题类别名称 */ private String issueTypeName; - - /** - * 数量 - */ + /** 数量 */ private Integer count; } @@ -92,19 +121,15 @@ public class QmsTicketReportVO { */ @Data public static class UserCompletionRate { - /** - * 用户姓名 - */ + /** 用户姓名 */ private String userName; - - /** - * 用户ID - */ + /** 用户ID */ private Long userId; - - /** - * 完成率(百分比) - */ + /** 完成率(百分比) */ private Double completionRate; + /** 分配工单数 */ + private Integer totalAssigned; + /** 完成工单数 */ + private Integer totalCompleted; } } diff --git a/nflg-wms-repository/src/main/java/com/nflg/wms/repository/entity/QmsIssueTicket.java b/nflg-wms-repository/src/main/java/com/nflg/wms/repository/entity/QmsIssueTicket.java index 5e63dd03..ea9185b5 100644 --- a/nflg-wms-repository/src/main/java/com/nflg/wms/repository/entity/QmsIssueTicket.java +++ b/nflg-wms-repository/src/main/java/com/nflg/wms/repository/entity/QmsIssueTicket.java @@ -109,6 +109,11 @@ public class QmsIssueTicket implements Serializable { */ private Short status; + /** + * 超时状态:0=未超时,1=已超时,2=严重超时 + */ + private Integer overdue; + /** * 审批状态:0=通过,1=驳回,2=退货,3=报废,4=维修,5=挑选使用,6=让渡使用 */ diff --git a/nflg-wms-repository/src/main/resources/mapper/QmsReportMapper.xml b/nflg-wms-repository/src/main/resources/mapper/QmsReportMapper.xml index 70f4b855..1e00f577 100644 --- a/nflg-wms-repository/src/main/resources/mapper/QmsReportMapper.xml +++ b/nflg-wms-repository/src/main/resources/mapper/QmsReportMapper.xml @@ -5,8 +5,8 @@