feat(qms): 新增PQC工单分派及详情查看功能

- 增加接口支持发起PQC工单,更新工单状态为处理中并分配处理人推送待办
- 实现按处理人分组合并不合格项并创建处理记录及待办消息
- 新增查询本人相关PQC工单详情接口,仅返回本人相关处理记录和措施
- 完善PQC工单处理流程,支持处理人提交审批及措施填写校验
- 添加PQC工单结束处理判断,最后一人提交时更新工单状态为已完成
- 新增文件及措施信息的解析和转换辅助方法,丰富详情数据展示
- 新增PQC任务及任务详情相关实体、接口及实现,支持任务管理功能
- 新增PQC任务相关Controller接口,包括查询、暂存和提交功能
- 优化PQC工单流程中的权限校验和异常处理,保障业务流程合理性
This commit is contained in:
funny 2026-05-22 15:15:47 +08:00
parent 4f78cbe83b
commit 3b782f471f
4 changed files with 658 additions and 0 deletions

View File

@ -46,6 +46,22 @@ public class QmsReportController extends BaseController {
return ApiResult.success(reportControllerService.getTicketReport(request));
}
/**
* PQC报表数据
*/
@PostMapping("pqc")
public ApiResult<QmsPqcReportVO> getPqcReport(@Valid @RequestBody QmsReportQueryQO request) {
return ApiResult.success(reportControllerService.getPqcReport(request));
}
/**
* PQC待复核统计数据
*/
@PostMapping("pqc/pending-review")
public ApiResult<QmsPqcPendingReviewVO> getPqcPendingReviewStats() {
return ApiResult.success(reportControllerService.getPqcPendingReviewStats());
}
/**
* IQC超时统计数据
*/

View File

@ -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<QmsPqcReportVO.EfficiencyVO> calculateEfficiencyStats(LocalDateTime start, LocalDateTime end) {
// 查询所有无关联任务
List<QmsPqcTaskRecord> tasks = pqcTaskRecordService.lambdaQuery()
.isNull(QmsPqcTaskRecord::getRelatedTaskId)
.ge(QmsPqcTaskRecord::getCreateTime, start)
.le(QmsPqcTaskRecord::getCreateTime, end)
.list();
if (tasks.isEmpty()) {
return new ArrayList<>();
}
// 按自检人分组统计
Map<Long, List<QmsPqcTaskRecord>> groupByTester = tasks.stream()
.filter(t -> t.getSelfTesterId() != null)
.collect(Collectors.groupingBy(QmsPqcTaskRecord::getSelfTesterId));
List<QmsPqcReportVO.EfficiencyVO> stats = new ArrayList<>();
for (Map.Entry<Long, List<QmsPqcTaskRecord>> entry : groupByTester.entrySet()) {
Long testerId = entry.getKey();
List<QmsPqcTaskRecord> 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<QmsPqcReportVO.EfficiencyVO> 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<QmsPqcReportVO.StepAssemblyRateVO> calculateStepAssemblyRates(LocalDateTime start, LocalDateTime end) {
// 查询所有无关联任务
List<QmsPqcTaskRecord> tasks = pqcTaskRecordService.lambdaQuery()
.isNull(QmsPqcTaskRecord::getRelatedTaskId)
.ge(QmsPqcTaskRecord::getCreateTime, start)
.le(QmsPqcTaskRecord::getCreateTime, end)
.list();
if (tasks.isEmpty()) {
return new ArrayList<>();
}
// 批量查询检查点信息获取步装名
Set<Long> pointIds = tasks.stream()
.map(QmsPqcTaskRecord::getInspectionPointId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, String> pointStepNameMap = new HashMap<>();
if (!pointIds.isEmpty()) {
List<QmsPqcInspectionPoint> points = pqcInspectionPointService.listByIds(pointIds);
pointStepNameMap = points.stream()
.filter(p -> p.getStepName() != null)
.collect(Collectors.toMap(QmsPqcInspectionPoint::getId, QmsPqcInspectionPoint::getStepName));
}
// 步装+机型 分组
Map<String, Map<String, List<QmsPqcTaskRecord>>> 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<QmsPqcReportVO.StepAssemblyRateVO> result = new ArrayList<>();
for (Map.Entry<String, Map<String, List<QmsPqcTaskRecord>>> stepEntry : groupByStepAndModel.entrySet()) {
String stepName = stepEntry.getKey();
Map<String, List<QmsPqcTaskRecord>> modelMap = stepEntry.getValue();
List<QmsPqcReportVO.StepAssemblyRateVO.ModelRateVO> modelRates = new ArrayList<>();
for (Map.Entry<String, List<QmsPqcTaskRecord>> modelEntry : modelMap.entrySet()) {
String modelNo = modelEntry.getKey();
List<QmsPqcTaskRecord> 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<QmsPqcReportVO.StepAssemblyRateVO.ModelRateVO> 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<QmsPqcTaskRecord> 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<QmsPqcTaskRecord> 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<QmsPqcPendingReviewVO.PendingReviewTaskVO> 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) + "小时";
}
}
}

View File

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

View File

@ -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<EfficiencyVO> efficiencyStats;
// ===== 步装合格率 =====
/**
* 步装合格率列表
*/
private List<StepAssemblyRateVO> 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<ModelRateVO> modelRates;
/**
* 机型合格率
*/
@Data
public static class ModelRateVO {
/**
* 机型编号
*/
private String modelNo;
/**
* 合格率
*/
private String passRate;
}
}
}