From 3b782f471f16a140e42b471266dddea974aa1619 Mon Sep 17 00:00:00 2001 From: funny <834502597@qq.com> Date: Fri, 22 May 2026 15:15:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(qms):=20=E6=96=B0=E5=A2=9EPQC=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=E5=88=86=E6=B4=BE=E5=8F=8A=E8=AF=A6=E6=83=85=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增加接口支持发起PQC工单,更新工单状态为处理中并分配处理人推送待办 - 实现按处理人分组合并不合格项并创建处理记录及待办消息 - 新增查询本人相关PQC工单详情接口,仅返回本人相关处理记录和措施 - 完善PQC工单处理流程,支持处理人提交审批及措施填写校验 - 添加PQC工单结束处理判断,最后一人提交时更新工单状态为已完成 - 新增文件及措施信息的解析和转换辅助方法,丰富详情数据展示 - 新增PQC任务及任务详情相关实体、接口及实现,支持任务管理功能 - 新增PQC任务相关Controller接口,包括查询、暂存和提交功能 - 优化PQC工单流程中的权限校验和异常处理,保障业务流程合理性 --- .../admin/controller/QmsReportController.java | 16 + .../service/QmsReportControllerService.java | 435 ++++++++++++++++++ .../common/pojo/vo/QmsPqcPendingReviewVO.java | 69 +++ .../wms/common/pojo/vo/QmsPqcReportVO.java | 138 ++++++ 4 files changed, 658 insertions(+) create mode 100644 nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsPqcPendingReviewVO.java create mode 100644 nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsPqcReportVO.java 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 a170fed0..e610ed3f 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 @@ -46,6 +46,22 @@ public class QmsReportController extends BaseController { return ApiResult.success(reportControllerService.getTicketReport(request)); } + /** + * PQC报表数据 + */ + @PostMapping("pqc") + public ApiResult getPqcReport(@Valid @RequestBody QmsReportQueryQO request) { + return ApiResult.success(reportControllerService.getPqcReport(request)); + } + + /** + * PQC待复核统计数据 + */ + @PostMapping("pqc/pending-review") + public ApiResult getPqcPendingReviewStats() { + return ApiResult.success(reportControllerService.getPqcPendingReviewStats()); + } + /** * IQC超时统计数据 */ 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 c9a07cb4..b2b04c03 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 @@ -1428,4 +1428,439 @@ public class QmsReportControllerService { double avgHours = totalHours / durations.size(); return String.format("%.2f小时", avgHours); } + + // ===== PQC报表 ===== + + @Resource + private IQmsPqcTaskRecordService pqcTaskRecordService; + + @Resource + private IQmsPqcInspectionPointService pqcInspectionPointService; + + /** + * PQC报表数据 + */ + public QmsPqcReportVO getPqcReport(QmsReportQueryQO request) { + QmsPqcReportVO vo = new QmsPqcReportVO(); + + // 计算时间范围 + LocalDateTime currentStart = request.getStartTime(); + LocalDateTime currentEnd = request.getEndTime(); + boolean hasTimeRange = currentStart != null || currentEnd != null; + + if (!hasTimeRange) { + currentStart = LocalDateTime.now().minusYears(1); + currentEnd = LocalDateTime.now(); + } else { + if (currentStart == null) currentStart = LocalDateTime.now().minusYears(1); + if (currentEnd == null) currentEnd = LocalDateTime.now(); + } + + // 计算上周期 + long daysBetween = java.time.temporal.ChronoUnit.DAYS.between(currentStart, currentEnd); + LocalDateTime previousStart = currentStart.minusDays(daysBetween); + LocalDateTime previousEnd = currentStart; + + // 1. 总工单数 + Long currentTotal = pqcTaskRecordService.lambdaQuery() + .ge(QmsPqcTaskRecord::getCreateTime, currentStart) + .le(QmsPqcTaskRecord::getCreateTime, currentEnd) + .count(); + Long previousTotal = pqcTaskRecordService.lambdaQuery() + .ge(QmsPqcTaskRecord::getCreateTime, previousStart) + .lt(QmsPqcTaskRecord::getCreateTime, previousEnd) + .count(); + + vo.setTotalTickets(currentTotal.intValue()); + vo.setTotalChange((int) (currentTotal - previousTotal)); + vo.setTotalChangeRate(calculateChangeRate(currentTotal, previousTotal)); + + // 2. 已完成数(状态=3) + Long currentCompleted = pqcTaskRecordService.lambdaQuery() + .eq(QmsPqcTaskRecord::getStatus, (short) 3) + .ge(QmsPqcTaskRecord::getCreateTime, currentStart) + .le(QmsPqcTaskRecord::getCreateTime, currentEnd) + .count(); + Long previousCompleted = pqcTaskRecordService.lambdaQuery() + .eq(QmsPqcTaskRecord::getStatus, (short) 3) + .ge(QmsPqcTaskRecord::getCreateTime, previousStart) + .lt(QmsPqcTaskRecord::getCreateTime, previousEnd) + .count(); + + vo.setCompletedTickets(currentCompleted.intValue()); + double currentCompletionRate = currentTotal > 0 ? (currentCompleted * 100.0 / currentTotal) : 0; + double previousCompletionRate = previousTotal > 0 ? (previousCompleted * 100.0 / previousTotal) : 0; + vo.setCompletionRate(String.format("%.2f%%", currentCompletionRate)); + vo.setCompletionRateChange(calculateChangeRate(currentCompletionRate, previousCompletionRate)); + + // 3. 待处理数(状态≠3) + vo.setPendingTickets((int) (currentTotal - currentCompleted)); + + // 4. 平均耗时 + String avgTime = calculatePqcAvgProcessingTime(currentStart, currentEnd); + String previousAvgTime = calculatePqcAvgProcessingTime(previousStart, previousEnd); + vo.setAvgProcessingTime(avgTime); + + // 解析平均耗时(小时)用于计算增减率 + double currentAvgHours = parseHoursFromTimeStr(avgTime); + double previousAvgHours = parseHoursFromTimeStr(previousAvgTime); + vo.setAvgTimeChangeRate(calculateChangeRate(currentAvgHours, previousAvgHours)); + + // 5. 一次合格率(无关联任务) + calculateFirstPassRate(vo, currentStart, currentEnd, previousStart, previousEnd); + + // 6. 人效统计 + vo.setEfficiencyStats(calculateEfficiencyStats(currentStart, currentEnd)); + + // 7. 步装合格率 + vo.setStepAssemblyRates(calculateStepAssemblyRates(currentStart, currentEnd)); + + return vo; + } + + /** + * 计算一次合格率 + */ + private void calculateFirstPassRate(QmsPqcReportVO vo, LocalDateTime currentStart, LocalDateTime currentEnd, + LocalDateTime previousStart, LocalDateTime previousEnd) { + // 当前周期 + Long currentTotalNoRelated = pqcTaskRecordService.lambdaQuery() + .isNull(QmsPqcTaskRecord::getRelatedTaskId) + .ge(QmsPqcTaskRecord::getCreateTime, currentStart) + .le(QmsPqcTaskRecord::getCreateTime, currentEnd) + .count(); + Long currentPassed = pqcTaskRecordService.lambdaQuery() + .isNull(QmsPqcTaskRecord::getRelatedTaskId) + .eq(QmsPqcTaskRecord::getEnable, true) + .ge(QmsPqcTaskRecord::getCreateTime, currentStart) + .le(QmsPqcTaskRecord::getCreateTime, currentEnd) + .count(); + + double currentRate = currentTotalNoRelated > 0 ? (currentPassed * 100.0 / currentTotalNoRelated) : 0; + + // 上周期 + Long previousTotalNoRelated = pqcTaskRecordService.lambdaQuery() + .isNull(QmsPqcTaskRecord::getRelatedTaskId) + .ge(QmsPqcTaskRecord::getCreateTime, previousStart) + .lt(QmsPqcTaskRecord::getCreateTime, previousEnd) + .count(); + Long previousPassed = pqcTaskRecordService.lambdaQuery() + .isNull(QmsPqcTaskRecord::getRelatedTaskId) + .eq(QmsPqcTaskRecord::getEnable, true) + .ge(QmsPqcTaskRecord::getCreateTime, previousStart) + .lt(QmsPqcTaskRecord::getCreateTime, previousEnd) + .count(); + + double previousRate = previousTotalNoRelated > 0 ? (previousPassed * 100.0 / previousTotalNoRelated) : 0; + + vo.setFirstPassRate(String.format("%.2f%%", currentRate)); + vo.setFirstPassRateChange(calculateChangeRate(currentRate, previousRate)); + } + + /** + * 计算人效统计 + */ + private List calculateEfficiencyStats(LocalDateTime start, LocalDateTime end) { + // 查询所有无关联任务 + List tasks = pqcTaskRecordService.lambdaQuery() + .isNull(QmsPqcTaskRecord::getRelatedTaskId) + .ge(QmsPqcTaskRecord::getCreateTime, start) + .le(QmsPqcTaskRecord::getCreateTime, end) + .list(); + + if (tasks.isEmpty()) { + return new ArrayList<>(); + } + + // 按自检人分组统计 + Map> groupByTester = tasks.stream() + .filter(t -> t.getSelfTesterId() != null) + .collect(Collectors.groupingBy(QmsPqcTaskRecord::getSelfTesterId)); + + List stats = new ArrayList<>(); + for (Map.Entry> entry : groupByTester.entrySet()) { + Long testerId = entry.getKey(); + List testerTasks = entry.getValue(); + + long totalTasks = testerTasks.size(); + long passedTasks = testerTasks.stream().filter(QmsPqcTaskRecord::getEnable).count(); + double passRate = totalTasks > 0 ? (passedTasks * 100.0 / totalTasks) : 0; + + QmsPqcReportVO.EfficiencyVO efficiencyVO = new QmsPqcReportVO.EfficiencyVO(); + efficiencyVO.setSelfTesterId(testerId); + efficiencyVO.setSelfTesterName(testerTasks.get(0).getSelfTesterName()); + efficiencyVO.setPassRate(String.format("%.2f%%", passRate)); + stats.add(efficiencyVO); + } + + // 按合格率降序 + stats.sort((a, b) -> Double.compare( + Double.parseDouble(b.getPassRate().replace("%", "")), + Double.parseDouble(a.getPassRate().replace("%", "")) + )); + + // 截取前10+后10 + if (stats.size() > 20) { + List result = new ArrayList<>(); + result.addAll(stats.subList(0, 10)); // 前10 + result.addAll(stats.subList(stats.size() - 10, stats.size())); // 后10 + return result; + } + + return stats; + } + + /** + * 计算步装合格率 + */ + private List calculateStepAssemblyRates(LocalDateTime start, LocalDateTime end) { + // 查询所有无关联任务 + List tasks = pqcTaskRecordService.lambdaQuery() + .isNull(QmsPqcTaskRecord::getRelatedTaskId) + .ge(QmsPqcTaskRecord::getCreateTime, start) + .le(QmsPqcTaskRecord::getCreateTime, end) + .list(); + + if (tasks.isEmpty()) { + return new ArrayList<>(); + } + + // 批量查询检查点信息获取步装名 + Set pointIds = tasks.stream() + .map(QmsPqcTaskRecord::getInspectionPointId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + Map pointStepNameMap = new HashMap<>(); + if (!pointIds.isEmpty()) { + List points = pqcInspectionPointService.listByIds(pointIds); + pointStepNameMap = points.stream() + .filter(p -> p.getStepName() != null) + .collect(Collectors.toMap(QmsPqcInspectionPoint::getId, QmsPqcInspectionPoint::getStepName)); + } + + // 按 步装+机型 分组 + Map>> groupByStepAndModel = new LinkedHashMap<>(); + for (QmsPqcTaskRecord task : tasks) { + String stepName = pointStepNameMap.get(task.getInspectionPointId()); + if (stepName == null) continue; + + String modelNo = task.getModelNo(); + if (modelNo == null) continue; + + groupByStepAndModel.computeIfAbsent(stepName, k -> new LinkedHashMap<>()) + .computeIfAbsent(modelNo, k -> new ArrayList<>()) + .add(task); + } + + List result = new ArrayList<>(); + for (Map.Entry>> stepEntry : groupByStepAndModel.entrySet()) { + String stepName = stepEntry.getKey(); + Map> modelMap = stepEntry.getValue(); + + List modelRates = new ArrayList<>(); + for (Map.Entry> modelEntry : modelMap.entrySet()) { + String modelNo = modelEntry.getKey(); + List modelTasks = modelEntry.getValue(); + + long totalCheckpoints = modelTasks.size(); + long passedCheckpoints = modelTasks.stream().filter(QmsPqcTaskRecord::getEnable).count(); + double passRate = totalCheckpoints > 0 ? (passedCheckpoints * 100.0 / totalCheckpoints) : 0; + + QmsPqcReportVO.StepAssemblyRateVO.ModelRateVO modelRateVO = new QmsPqcReportVO.StepAssemblyRateVO.ModelRateVO(); + modelRateVO.setModelNo(modelNo); + modelRateVO.setPassRate(String.format("%.2f%%", passRate)); + modelRates.add(modelRateVO); + } + + // 按合格率降序 + modelRates.sort((a, b) -> Double.compare( + Double.parseDouble(b.getPassRate().replace("%", "")), + Double.parseDouble(a.getPassRate().replace("%", "")) + )); + + // 截取前5+后5 + List finalModelRates; + if (modelRates.size() > 10) { + finalModelRates = new ArrayList<>(); + finalModelRates.addAll(modelRates.subList(0, 5)); + finalModelRates.addAll(modelRates.subList(modelRates.size() - 5, modelRates.size())); + } else { + finalModelRates = modelRates; + } + + QmsPqcReportVO.StepAssemblyRateVO stepVO = new QmsPqcReportVO.StepAssemblyRateVO(); + stepVO.setStepAssemblyName(stepName); + stepVO.setModelRates(finalModelRates); + result.add(stepVO); + } + + return result; + } + + /** + * 计算PQC平均处理时间 + */ + private String calculatePqcAvgProcessingTime(LocalDateTime start, LocalDateTime end) { + List completedTasks = pqcTaskRecordService.lambdaQuery() + .eq(QmsPqcTaskRecord::getStatus, (short) 3) + .isNotNull(QmsPqcTaskRecord::getCompleteTime) + .ge(QmsPqcTaskRecord::getCreateTime, start) + .le(QmsPqcTaskRecord::getCreateTime, end) + .list(); + + if (completedTasks.isEmpty()) { + return "0小时"; + } + + double totalHours = 0; + for (QmsPqcTaskRecord task : completedTasks) { + if (task.getCompleteTime() != null && task.getCreateTime() != null) { + long seconds = java.time.temporal.ChronoUnit.SECONDS.between(task.getCreateTime(), task.getCompleteTime()); + totalHours += seconds / 3600.0; + } + } + + double avgHours = totalHours / completedTasks.size(); + return formatHoursToDaysAndHours(avgHours); + } + + /** + * 格式化小时为xx天xx小时 + */ + private String formatHoursToDaysAndHours(double hours) { + long days = (long) (hours / 24); + long remainingHours = Math.round(hours % 24); + + if (days > 0) { + return days + "天" + remainingHours + "小时"; + } else { + return remainingHours + "小时"; + } + } + + /** + * 从时间字符串解析小时数 + */ + private double parseHoursFromTimeStr(String timeStr) { + if (timeStr == null || timeStr.isEmpty()) { + return 0; + } + + try { + if (timeStr.contains("天")) { + String[] parts = timeStr.replace("小时", "").split("天"); + long days = Long.parseLong(parts[0]); + long hours = parts.length > 1 ? Long.parseLong(parts[1]) : 0; + return days * 24 + hours; + } else { + return Double.parseDouble(timeStr.replace("小时", "")); + } + } catch (Exception e) { + return 0; + } + } + + /** + * 计算增减率 + */ + private String calculateChangeRate(double current, double previous) { + if (previous == 0) { + if (current == 0) { + return "0.00%"; + } else { + return "新增"; + } + } + + double rate = ((current - previous) / previous) * 100; + return String.format("%.2f%%", rate); + } + + // ===== PQC待复核统计 ===== + + /** + * PQC待复核统计数据 + */ + public QmsPqcPendingReviewVO getPqcPendingReviewStats() { + QmsPqcPendingReviewVO vo = new QmsPqcPendingReviewVO(); + LocalDateTime now = LocalDateTime.now(); + + // 1. 查询待复核任务(状态=1) + List pendingTasks = pqcTaskRecordService.lambdaQuery() + .eq(QmsPqcTaskRecord::getStatus, (short) 1) + .orderByDesc(QmsPqcTaskRecord::getCreateTime) + .list(); + + // 2. 待复核任务总数 + vo.setTotalCount(pendingTasks.size()); + + if (pendingTasks.isEmpty()) { + vo.setAvgWaitTime("0小时"); + vo.setTasks(new ArrayList<>()); + return vo; + } + + // 3. 计算平均等待时间 + long totalWaitSeconds = 0; + List taskVOs = new ArrayList<>(); + + for (QmsPqcTaskRecord task : pendingTasks) { + // 自检提交时间使用 complete_time + LocalDateTime submitTime = task.getCompleteTime(); + if (submitTime == null) { + submitTime = task.getCreateTime(); + } + + long waitSeconds = java.time.temporal.ChronoUnit.SECONDS.between(submitTime, now); + totalWaitSeconds += waitSeconds; + + // 构建任务VO + QmsPqcPendingReviewVO.PendingReviewTaskVO taskVO = new QmsPqcPendingReviewVO.PendingReviewTaskVO(); + taskVO.setTaskId(task.getId()); + taskVO.setTaskNo(task.getTaskNo()); + taskVO.setAufnr(task.getAufnr()); + taskVO.setModelNo(task.getModelNo()); + taskVO.setNo(task.getNo()); + taskVO.setSelfTestSubmitTime(submitTime); + taskVO.setWaitTime(formatSecondsToDaysAndHours(waitSeconds)); + taskVOs.add(taskVO); + } + + // 平均等待时间(秒) + long avgWaitSeconds = totalWaitSeconds / pendingTasks.size(); + vo.setAvgWaitTime(formatSecondsToDaysAndHoursWithDecimal(avgWaitSeconds)); + vo.setTasks(taskVOs); + + return vo; + } + + /** + * 格式化秒为xx天xx小时(小时保留两位小数) + * 例如:15分钟 = 900秒 = 0.25小时 + */ + private String formatSecondsToDaysAndHoursWithDecimal(long seconds) { + long days = seconds / 86400; // 86400秒 = 1天 + double remainingHours = (seconds % 86400) / 3600.0; + + if (days > 0) { + return days + "天" + String.format("%.2f", remainingHours) + "小时"; + } else { + return String.format("%.2f", remainingHours) + "小时"; + } + } + + /** + * 格式化秒为xx天xx小时(用于单个任务) + */ + private String formatSecondsToDaysAndHours(long seconds) { + long days = seconds / 86400; + double remainingHours = (seconds % 86400) / 3600.0; + + if (days > 0) { + return days + "天" + String.format("%.2f", remainingHours) + "小时"; + } else { + return String.format("%.2f", remainingHours) + "小时"; + } + } } diff --git a/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsPqcPendingReviewVO.java b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsPqcPendingReviewVO.java new file mode 100644 index 00000000..e3b25e4e --- /dev/null +++ b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsPqcPendingReviewVO.java @@ -0,0 +1,69 @@ +package com.nflg.wms.common.pojo.vo; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * PQC待复核统计VO + */ +@Data +public class QmsPqcPendingReviewVO { + + /** + * 待复核任务总数 + */ + private Integer totalCount; + + /** + * 平均等待时间(格式:xx天xx.xx小时) + */ + private String avgWaitTime; + + /** + * 待复核任务列表 + */ + private List tasks; + + /** + * 待复核任务 + */ + @Data + public static class PendingReviewTaskVO { + /** + * 任务ID + */ + private Long taskId; + + /** + * 任务编号 + */ + private String taskNo; + + /** + * 订单编号 + */ + private String aufnr; + + /** + * 机型编号 + */ + private String modelNo; + + /** + * 机台编号 + */ + private String no; + + /** + * 自检提交时间 + */ + private LocalDateTime selfTestSubmitTime; + + /** + * 等待时长(格式:xx天xx.xx小时) + */ + private String waitTime; + } +} diff --git a/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsPqcReportVO.java b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsPqcReportVO.java new file mode 100644 index 00000000..011cf842 --- /dev/null +++ b/nflg-wms-common/src/main/java/com/nflg/wms/common/pojo/vo/QmsPqcReportVO.java @@ -0,0 +1,138 @@ +package com.nflg.wms.common.pojo.vo; + +import lombok.Data; + +import java.util.List; + +/** + * PQC报表VO + */ +@Data +public class QmsPqcReportVO { + + // ===== 总体统计 ===== + + /** + * 总工单数 + */ + private Integer totalTickets; + + /** + * 较上周期增减数 + */ + private Integer totalChange; + + /** + * 较上周期增减率 + */ + private String totalChangeRate; + + /** + * 已完成数(状态=3) + */ + private Integer completedTickets; + + /** + * 完成率 + */ + private String completionRate; + + /** + * 完成率较上周期增减率 + */ + private String completionRateChange; + + /** + * 待处理数(状态≠3) + */ + private Integer pendingTickets; + + /** + * 平均耗时(格式:xx天xx小时) + */ + private String avgProcessingTime; + + /** + * 平均耗时较上周期增减率 + */ + private String avgTimeChangeRate; + + /** + * 一次合格率 + */ + private String firstPassRate; + + /** + * 一次合格率较上周期增减率 + */ + private String firstPassRateChange; + + // ===== 人效统计 ===== + + /** + * 人效统计列表 + */ + private List efficiencyStats; + + // ===== 步装合格率 ===== + + /** + * 步装合格率列表 + */ + private List stepAssemblyRates; + + // ===== 嵌套类 ===== + + /** + * 人效统计 + */ + @Data + public static class EfficiencyVO { + /** + * 自检人ID + */ + private Long selfTesterId; + + /** + * 自检人姓名 + */ + private String selfTesterName; + + /** + * 合格率 + */ + private String passRate; + } + + /** + * 步装合格率 + */ + @Data + public static class StepAssemblyRateVO { + /** + * 步装名 + */ + private String stepAssemblyName; + + /** + * 机型合格率列表(前5+后5) + */ + private List modelRates; + + /** + * 机型合格率 + */ + @Data + public static class ModelRateVO { + /** + * 机型编号 + */ + private String modelNo; + + /** + * 合格率 + */ + private String passRate; + } + } +}