refactor(qms-pdi): 重构PDI检测项及部件管理相关功能

- 将检测项中的部件描述字段改为部件ID,关联部件表数据
- 新增部件管理功能,支持部件的增删改查及排序
- 在PDI检测项新增接口自动处理部件注册及排序
- 批量删除支持区分部件ID和检测项ID,实现联动删除
- 导入检测项时自动识别导入部件,动态创建部件及排序
- 查询检测项时按部件分组返回,提升数据结构清晰度
- 巡检工单及检测结果展示时通过部件ID获取部件名称显示
- 新增API支持根据部件ID查询检测项及现场记录
- QmsPdiReportVO 和 QmsIqcReportVO新增环比统计字段,完善报表数据
- 修改导出模板及分页查询逻辑,统一部件相关字段处理
- 优化图片ID收集及批量查询,提升接口性能和稳定性
This commit is contained in:
funny 2026-05-19 17:51:00 +08:00
parent 5be1791cb4
commit 6822693429
12 changed files with 925 additions and 113 deletions

View File

@ -110,14 +110,14 @@ public class QmsIssueTicketController extends BaseController {
return ApiResult.success();
}
// /**
// * 查询PDI工单详情
// * 返回工单基本信息处理记录及措施列表
// */
// @GetMapping("detail/pdi-ticket")
// public ApiResult<QmsPdiTicketDetailVO> detailPdiTicket(@NotNull(message = "ID不能为空") Long id) {
// return ApiResult.success(issueTicketControllerService.getPdiTicketDetail(id));
// }
/**
* 查询PDI工单详情
* 返回工单基本信息处理记录及措施列表
*/
@GetMapping("detail/pdi-ticket")
public ApiResult<QmsPdiTicketDetailVO> detailPdiTicket(@NotNull(message = "ID不能为空") Long id) {
return ApiResult.success(issueTicketControllerService.getPdiTicketDetail(id));
}
/**
* 查询本人的PDI工单详情

View File

@ -47,10 +47,26 @@ public class QmsReportController extends BaseController {
}
/**
* 超时统计数据
* IQC超时统计数据
*/
@PostMapping("overdue")
public ApiResult<QmsOverdueReportVO> getOverdueReport() {
return ApiResult.success(reportControllerService.getOverdueReport());
@PostMapping("overdue/iqc")
public ApiResult<QmsIqcOverdueReportVO> getIqcOverdueReport() {
return ApiResult.success(reportControllerService.getIqcOverdueReport());
}
/**
* PDI超时统计数据
*/
@PostMapping("overdue/pdi")
public ApiResult<QmsPdiOverdueReportVO> getPdiOverdueReport() {
return ApiResult.success(reportControllerService.getPdiOverdueReport());
}
/**
* 工单超时统计数据
*/
@PostMapping("overdue/ticket")
public ApiResult<QmsTicketOverdueReportVO> getTicketOverdueReport() {
return ApiResult.success(reportControllerService.getTicketOverdueReport());
}
}

View File

@ -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);

View File

@ -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<String, Object> result = new HashMap<>();
// 当前周期平均时间
String currentAvgTime = "0H0Min0S";
String currentAvgTime = "0天0小时";
long currentAvgSeconds = 0;
List<QmsPdiTaskRecord> 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<QmsIssueTicket> 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<QmsIssueTicket> tickets) {
List<QmsIssueTicket> 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<QmsTicketReportVO.StatusDistribution> buildStatusDistributions(
int total, int pendingTransfer, int processing, int completed) {
List<QmsTicketReportVO.StatusDistribution> 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<QmsTicketReportVO.StatusDistribution> buildEmptyStatusDistributions() {
return buildStatusDistributions(1, 0, 0, 0);
}
/**
* 构建完成状态分布
*/
private List<QmsTicketReportVO.CompletionStatus> buildCompletionStatusList(
List<QmsIssueTicket> allTickets, int completedCount) {
if (completedCount == 0) {
return Collections.emptyList();
}
// 已完成工单的审批结果统计
Map<Short, Long> approvalCountMap = allTickets.stream()
.filter(t -> t.getStatus() != null && t.getStatus() == 2)
.filter(t -> t.getApprovalStatus() != null)
.collect(Collectors.groupingBy(QmsIssueTicket::getApprovalStatus, Collectors.counting()));
// 审批结果名称映射
Map<Short, String> 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<QmsTicketReportVO.CompletionStatus> result = new ArrayList<>();
for (Map.Entry<Short, String> 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<QmsIssueTicket> allTickets) {
// 收集所有工单ID
Set<Long> 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<QmsIssueTicketProcess> processes = issueTicketProcessService.lambdaQuery()
.in(QmsIssueTicketProcess::getIssueTicketId, ticketIds)
.list();
// 工单状态映射ticketId status
Map<Long, Short> ticketStatusMap = allTickets.stream()
.collect(Collectors.toMap(QmsIssueTicket::getId, t -> t.getStatus() != null ? t.getStatus() : 0));
// 按处理人分组统计
Map<Long, List<QmsIssueTicketProcess>> handlerGroup = processes.stream()
.filter(p -> p.getHandlerUserId() != null)
.collect(Collectors.groupingBy(QmsIssueTicketProcess::getHandlerUserId));
List<QmsTicketReportVO.UserCompletionRate> userRates = new ArrayList<>();
for (Map.Entry<Long, List<QmsIssueTicketProcess>> entry : handlerGroup.entrySet()) {
Long userId = entry.getKey();
List<QmsIssueTicketProcess> 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<QmsTicketReportVO.UserCompletionRate> 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<QmsTicketReportVO.UserCompletionRate> 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<QmsOverdueReportVO.OverdueTaskItem> 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<QmsOverdueReportVO.OverdueTaskItem> 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<QmsOverdueReportVO.OverdueTaskItem> ticketOverdueTasks = buildTicketOverdueTasks(now);
vo.setTicketOverdueTasks(ticketOverdueTasks);
vo.setTotalOverdueCount(ticketOverdueTasks.size());
vo.setAvgOverdueTime(calculateAvgOverdueTimeFromTasks(ticketOverdueTasks));
vo.setSevereOverdueCount(countTicketSevereOverdue(ticketOverdueTasks));
return vo;
}
// ======================== 超时统计辅助方法 ========================
/**
* 构建工单超时任务列表
*/
private List<QmsOverdueReportVO.OverdueTaskItem> buildTicketOverdueTasks(LocalDateTime now) {
// 查询未完成且超时的工单
List<QmsIssueTicket> overdueTickets = issueTicketService.lambdaQuery()
.ne(QmsIssueTicket::getStatus, 2)
.in(QmsIssueTicket::getOverdue, 1, 2)
.list();
if (overdueTickets.isEmpty()) {
return Collections.emptyList();
}
// 批量查询处理记录用于PDI和巡检工单
Set<Long> ticketIds = overdueTickets.stream()
.map(QmsIssueTicket::getId)
.collect(Collectors.toSet());
List<QmsIssueTicketProcess> processes = issueTicketProcessService.lambdaQuery()
.in(QmsIssueTicketProcess::getIssueTicketId, ticketIds)
.list();
Map<Long, List<QmsIssueTicketProcess>> processMap = processes.stream()
.collect(Collectors.groupingBy(QmsIssueTicketProcess::getIssueTicketId));
// 组装VO
List<QmsOverdueReportVO.OverdueTaskItem> 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<QmsIssueTicketProcess> ticketProcesses = processMap.getOrDefault(ticket.getId(), Collections.emptyList());
List<String> handlerNames = ticketProcesses.stream()
.map(QmsIssueTicketProcess::getHandlerUserName)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
List<Long> 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<QmsOverdueReportVO.OverdueTaskItem> buildIqcOverdueTasks(LocalDateTime now) {
List<QmsIncomingInspectionTask> overdueTasks = incomingInspectionTaskService.lambdaQuery()
.eq(QmsIncomingInspectionTask::getIsOverdue, true)
.ne(QmsIncomingInspectionTask::getInspectionStatus, 2)
.list();
if (overdueTasks.isEmpty()) {
return Collections.emptyList();
}
List<QmsOverdueReportVO.OverdueTaskItem> 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<String> handlerNames = new ArrayList<>();
List<Long> 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<QmsOverdueReportVO.OverdueTaskItem> buildPdiOverdueTasks(LocalDateTime now) {
List<QmsPdiTaskRecord> overdueTasks = pdiTaskRecordService.lambdaQuery()
.eq(QmsPdiTaskRecord::getOverdue, true)
.ne(QmsPdiTaskRecord::getInspectionEnable, 2)
.list();
if (overdueTasks.isEmpty()) {
return Collections.emptyList();
}
// 批量查询检测规则获取inspector
Set<Long> ruleIds = overdueTasks.stream()
.map(QmsPdiTaskRecord::getDetectionRulesId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, QmsPdiDetectionRules> rulesMap = pdiDetectionRulesService.listByIds(ruleIds).stream()
.collect(Collectors.toMap(QmsPdiDetectionRules::getId, r -> r, (a, b) -> a));
// 收集所有需要查询的用户ID
Set<Long> 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<Long, User> userMap = new HashMap<>();
if (!userIds.isEmpty()) {
userMap = userService.listByIds(userIds).stream()
.collect(Collectors.toMap(User::getId, u -> u, (a, b) -> a));
}
Map<Long, User> finalUserMap = userMap;
List<QmsOverdueReportVO.OverdueTaskItem> 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<String> handlerNames = new ArrayList<>();
List<Long> 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<QmsOverdueReportVO.OverdueTaskItem> 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<QmsOverdueReportVO.OverdueTaskItem> tasks) {
if (tasks.isEmpty()) return 0;
// 需要从原始工单数据中筛选overdue=2的
// 这里简化处理通过taskNo重新查询
Set<String> 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<QmsOverdueReportVO.OverdueTaskItem> tasks) {
if (tasks.isEmpty()) {
return "0.00小时";
}
List<String> 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);
}
}

View File

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

View File

@ -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<QmsOverdueReportVO.OverdueTaskItem> iqcOverdueTasks;
}

View File

@ -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<OverdueTaskItem> iqcOverdueTasks;
// ===== PDI超时统计 =====
/**
* PDI超时任务总数
*/
private Integer pdiTotalOverdueCount;
/**
* PDI平均超时时间格式X.XX小时
*/
private String pdiAvgOverdueTime;
/**
* PDI严重超时数>=24小时
*/
private Integer pdiSevereOverdueCount;
/**
* PDI超时任务列表
*/
private List<OverdueTaskItem> 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<String> handlerNames;
/**
* 处理人ID列表
*/
/** 处理人ID列表 */
private List<Long> 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;
}
}

View File

@ -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<QmsOverdueReportVO.OverdueTaskItem> pdiOverdueTasks;
}

View File

@ -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<QmsOverdueReportVO.OverdueTaskItem> ticketOverdueTasks;
}

View File

@ -16,15 +16,25 @@ public class QmsTicketReportVO {
private Integer totalTickets;
/**
* 已完成数
* 已完成数status=2
*/
private Integer completedCount;
/**
* 未完成数
* 未完成数status2
*/
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<StatusDistribution> statusDistributions;
/**
* 问题类别数量列表
* 完成状态分布已完成工单中各审批结果占比
*/
private List<CompletionStatus> completionStatusList;
/**
* 问题类别数量列表暂空置
*/
private List<IssueTypeCount> issueTypeCounts;
/**
* 用户完成率列表
* 用户完成率前10
*/
private List<UserCompletionRate> userCompletionRates;
private List<UserCompletionRate> top10Users;
/**
* 用户完成率后10
*/
private List<UserCompletionRate> 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;
}
}

View File

@ -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=让渡使用
*/

View File

@ -5,8 +5,8 @@
<!-- 查询检测项分布(按检测项名称统计数量) -->
<select id="getInspectionItemDistribution" resultType="map">
SELECT
sic.name AS itemName,
COUNT(*) AS itemCount
sic.name AS "itemName",
COUNT(*) AS "itemCount"
FROM qms_incoming_inspection_task t
INNER JOIN qms_incoming_inspection_task_record r ON r.task_id = t.id
INNER JOIN qms_incoming_inspection_task_record_item ri ON ri.record_id = r.id
@ -14,34 +14,34 @@
WHERE t.submit_time &gt;= #{startTime}
AND t.submit_time &lt;= #{endTime}
GROUP BY sic.name
ORDER BY itemCount DESC
ORDER BY COUNT(*) DESC
</select>
<!-- 查询物料类别不合格率(按不合格数量排序) -->
<select id="getMaterialCategoryFailRate" resultType="map">
SELECT
mc.name AS categoryName,
SUM(CASE WHEN t.inspection_result = false THEN 1 ELSE 0 END) AS failCount
mc.category_name AS "categoryName",
SUM(CASE WHEN t.inspection_result = false THEN 1 ELSE 0 END) AS "failCount"
FROM qms_incoming_inspection_task t
INNER JOIN qms_qc_material m ON m.id = t.material_id
INNER JOIN qms_qc_material_category mc ON mc.id = m.category_id
INNER JOIN qms_qc_material_category mc ON mc.category_code = m.material_category_code
WHERE t.submit_time &gt;= #{startTime}
AND t.submit_time &lt;= #{endTime}
GROUP BY mc.name
ORDER BY failCount DESC
GROUP BY mc.category_name
ORDER BY SUM(CASE WHEN t.inspection_result = false THEN 1 ELSE 0 END) DESC
</select>
<!-- 查询PDI主要缺陷按机型编号统计数量 -->
<select id="getPdiMainDefects" resultType="map">
SELECT
dr.machine_no AS machineNo,
COUNT(*) AS defectCount
dr.machine_no AS "machineNo",
COUNT(*) AS "defectCount"
FROM qms_pdi_task_record t
INNER JOIN qms_pdi_detection_rules dr ON dr.id = t.detection_rules_id
WHERE t.submission_time &gt;= #{startTime}
AND t.submission_time &lt;= #{endTime}
GROUP BY dr.machine_no
ORDER BY defectCount DESC
ORDER BY COUNT(*) DESC
</select>
<!-- 查询部件名称 -->