feat(bi): 新增工单及派工相关业务指标统计接口

- 增加工单处理状态统计功能,返回未完成数、完成数及平均处理时长
- 实现责任人绩效统计,支持按处理数量或平均时长排序分页返回
- 新增出差天数统计,区分国内外出差并返回人员出差详情
- 增加计划达成率统计,按人员汇总完成情况及计划总数
- 实现人员派工统计,支持分页查询并返回进行中状态及未完成数量
- 添加BIDispatchQuery查询参数,支持按部门、用户名及状态筛选
- 扩展GongfuDispatchService支持BIDispatchQuery的业务数据查询
- ApiResult新增分页数据封装success方法支持分页参数返回

feat(common): 新增HEIC图片格式转换PNG工具及支持

- 新增ImageUtil工具类,通过ImageMagick命令行将HEIC格式图片转换为PNG格式
- 文件上传模块(Admin及CFS)支持HEIC图片自动转换为PNG再上传
- 调整文件格式后缀统一为小写,处理HEIC上传时文件类型自动换为.png
- 移除对commons-imaging和imageio-heif依赖,改用外部ImageMagick工具实现转换
- 增加readme.md说明服务器需安装ImageMagick以及HEIC支持相关环境依赖和源码编译步骤

fix(dispatch): 修正派工相关编码及消息通知中派工单编号字段

- 派工单编码统一使用code字段替代原no字段用于消息通知及文件关联
- 去除DispatchAddRequest中deviceNo的@NotBlank注解,添加手动校验规则
- 优化DispatchController,新增机台编号非空校验逻辑
- 修复部分代码重复设置CurrentHandle现象,确保责任人数据准确传递

refactor(common): 细节优化及代码规范调整

- DateTimeUtil新增日期差计算及字符串解析方法
- DeviceVO添加客户名称属性,设备查询接口支持按设备名称模糊搜索
- API请求与返回VO新增及规范化,实现各统计视图对应VO结构
- 优化分页查询基础类PageBaseQuery格式及默认值设置
- 文件上传相关异常处理及流关闭逻辑完善,统一代码风格及格式
- GongfuTicketServiceImpl修正责任人ID设置,确保工单处理流程数据一致性
This commit is contained in:
曹鹏飞 2025-12-17 18:01:15 +08:00
parent 73e4f4d0ee
commit fe9b485b66
28 changed files with 685 additions and 53 deletions

View File

@ -141,17 +141,6 @@
</exclusion>
</exclusions>
</dependency>
<!--处理heic图片文件-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-imaging</artifactId>
<version>1.0.0-alpha6</version>
</dependency>
<dependency>
<groupId>com.github.gotson.nightmonkeys</groupId>
<artifactId>imageio-heif</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>
<build>

View File

@ -15,6 +15,7 @@ import com.nflg.mobilebroken.common.pojo.vo.FileUploadVO;
import com.nflg.mobilebroken.common.pojo.vo.FileVO;
import com.nflg.mobilebroken.common.util.AdminUserUtil;
import com.nflg.mobilebroken.common.util.DateTimeUtil;
import com.nflg.mobilebroken.common.util.ImageUtil;
import com.nflg.mobilebroken.common.util.VUtils;
import com.nflg.mobilebroken.repository.entity.FileUploadRecord;
import com.nflg.mobilebroken.repository.service.IFileUploadRecordService;
@ -22,7 +23,6 @@ import com.nflg.mobilebroken.starter.service.FileUploadService;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.imaging.Imaging;
import org.apache.commons.io.FilenameUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@ -32,13 +32,14 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.awt.image.BufferedImage;
import java.io.*;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
@ -89,7 +90,7 @@ public class FileController extends ControllerBase {
InputStream is;
if (fileType.equals(".heic")) {
is = convertHeic(file);
fileType = ".jpg";
fileType = ".png";
} else {
is = file.getInputStream();
}
@ -103,11 +104,7 @@ public class FileController extends ControllerBase {
}
private InputStream convertHeic(MultipartFile file) throws IOException {
BufferedImage image = Imaging.getBufferedImage(file.getBytes());
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", outputStream);
byte[] imageBytes = outputStream.toByteArray();
return new ByteArrayInputStream(imageBytes);
return new ByteArrayInputStream(ImageUtil.heicToPng(file.getBytes()));
}
private String buildFilePath(String fileType) {
@ -136,7 +133,7 @@ public class FileController extends ControllerBase {
InputStream is;
if (fileType.equals(".heic")) {
is = convertHeic(file);
fileType = ".jpg";
fileType = ".png";
} else {
is = file.getInputStream();
}
@ -169,7 +166,7 @@ public class FileController extends ControllerBase {
}
private String getFileType(String fileName) {
return "." + FilenameUtils.getExtension(fileName);
return "." + FilenameUtils.getExtension(fileName).toLowerCase();
}
private FileUploadRecord buildFileUploadRecord(Byte source, Long sourceId, String fileName, String fileType, String url) {

View File

@ -8,6 +8,7 @@ import com.nflg.mobilebroken.common.exception.NflgException;
import com.nflg.mobilebroken.common.pojo.ApiResult;
import com.nflg.mobilebroken.common.pojo.vo.FileUploadVO;
import com.nflg.mobilebroken.common.util.AppUserUtil;
import com.nflg.mobilebroken.common.util.ImageUtil;
import com.nflg.mobilebroken.common.util.SaTokenAppUtil;
import com.nflg.mobilebroken.repository.entity.FileUploadRecord;
import com.nflg.mobilebroken.repository.service.IFileUploadRecordService;
@ -23,6 +24,9 @@ import javax.annotation.Resource;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
@ -59,7 +63,14 @@ public class FileController extends ControllerBase {
try {
String fileName = file.getOriginalFilename();
String fileType = getFileType(fileName);
String url = fileUploadService.upload(buildFilePath(fileType), file);
InputStream is;
if (fileType.equals(".heic")) {
is = convertHeic(file);
fileType = ".png";
} else {
is = file.getInputStream();
}
String url = fileUploadService.upload(buildFilePath(fileType), is);
FileUploadRecord record = buildFileUploadRecord(source, sourceId, fileName, fileType, url);
fileUploadRecordService.save(record);
return ApiResult.success(new FileUploadVO(record.getId(), fileName, url, file.getSize()));
@ -68,6 +79,10 @@ public class FileController extends ControllerBase {
}
}
private InputStream convertHeic(MultipartFile file) throws IOException {
return new ByteArrayInputStream(ImageUtil.heicToPng(file.getBytes()));
}
private String buildFilePath(String fileType) {
if (SaTokenAppUtil.isLogin()) {
return StrUtil.format("cfs/{}/{}/{}/{}{}", LocalDateTime.now().format(FORMATTER)
@ -94,7 +109,14 @@ public class FileController extends ControllerBase {
for (MultipartFile file : files) {
String fileName = file.getOriginalFilename();
String fileType = getFileType(fileName);
String url = fileUploadService.upload(buildFilePath(fileType), file);
InputStream is;
if (fileType.equals(".heic")) {
is = convertHeic(file);
fileType = ".png";
} else {
is = file.getInputStream();
}
String url = fileUploadService.upload(buildFilePath(fileType), is);
FileUploadRecord record = buildFileUploadRecord(source, sourceId, fileName, fileType, url);
fileUploadRecordService.save(record);
list.add(new FileUploadVO(record.getId(), fileName, url, file.getSize()));
@ -123,7 +145,7 @@ public class FileController extends ControllerBase {
}
private String getFileType(String fileName) {
return "." + FilenameUtils.getExtension(fileName);
return "." + FilenameUtils.getExtension(fileName).toLowerCase();
}
private FileUploadRecord buildFileUploadRecord(Byte source, Long sourceId, String fileName, String fileType, String url) {

View File

@ -53,7 +53,6 @@ public class ApiResult<T> implements Serializable {
return vo;
}
public static <T> ApiResult<T> error(int state,String msg,T value) {
ApiResult<T> vo = new ApiResult<>();
vo.result = value;
@ -101,4 +100,17 @@ public class ApiResult<T> implements Serializable {
public static <T> ApiResult<T> error(int msgCode, String msg) {
return error(msgCode, msg, msg);
}
public static <T> ApiResult<PageData<T>> success(Collection<T> datas, int pageIndex, int pageSize) {
ApiResult<PageData<T>> vo = new ApiResult<>();
PageData<T> pageData = new PageData<>();
pageData.setPage(pageIndex);
pageData.setPageSize(pageSize);
pageData.setTotal(datas.size());
pageData.setItems(datas);
vo.setCode(STATE.Success.getState());
vo.setType(STATE.Success.getType());
vo.setResult(pageData);
return vo;
}
}

View File

@ -0,0 +1,27 @@
package com.nflg.mobilebroken.common.pojo.query;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import java.util.List;
@Data
public class BIDispatchQuery extends BIBaseQuery {
/**
* 部门id
*/
private Long deptId;
/**
* 用户名
*/
private String userName;
@JsonIgnore
private List<Integer> states = List.of(2);
private Integer page = 1;
private Integer pageSize = 20;
}

View File

@ -0,0 +1,16 @@
package com.nflg.mobilebroken.common.pojo.query;
import lombok.Data;
@Data
public class HandlePerformanceQuery extends BIBaseQuery {
/**
* 排序方式0处理数量1平均处理时长
*/
private Integer sortType = 0;
private Integer page = 1;
private Integer pageSize = 20;
}

View File

@ -5,7 +5,7 @@ import lombok.Data;
@Data
public class PageBaseQuery {
private Integer page=1;
private Integer page = 1;
private Integer pageSize=20;
private Integer pageSize = 20;
}

View File

@ -30,7 +30,6 @@ public class DispatchAddRequest {
/**
* 设备编号
*/
@NotBlank
private String deviceNo;
/**

View File

@ -26,4 +26,7 @@ public class SearchDeviceRequest extends PageRequest {
//客户名称
private String customerName;
//设备名称
private String deviceName;
}

View File

@ -0,0 +1,38 @@
package com.nflg.mobilebroken.common.pojo.vo;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class DaysOnBusinessTripVO {
/**
* 用户id
*/
private Long userId;
/**
* 处理人
*/
private String userName;
/**
* 国内出差天数
*/
private int daysForInternal;
/**
* 国外出差天数
*/
private int daysForForeign;
/**
* 总出差天数
*/
private int totalDays;
public int getTotalDays() {
return daysForInternal + daysForForeign;
}
}

View File

@ -26,4 +26,7 @@ public class DeviceVO {
// 销售日期
private LocalDate shipmentDate;
// 客户名称
private String customerName;
}

View File

@ -0,0 +1,74 @@
package com.nflg.mobilebroken.common.pojo.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
@Accessors(chain = true)
public class HandlePerformanceVO {
/**
* 处理人
*/
@Getter
@Setter
private String userName;
/**
* 工单数量
*/
@Getter
@Setter
private Integer total = 0;
/**
* 工单完成率
*/
private String completionRate;
public String getCompletionRate() {
if (total == 0) {
return "0";
}
double rate = (double) completes * 100 / total;
return String.format("%.2f", rate).replaceAll("\\.?0+$", "");
}
/**
* 工单处理平均时长单位小时
*/
private String averageProcessingTime;
public String getAverageProcessingTime() {
if (completes == 0) {
return "0";
}
double rate = (double) processingTime / completes / 60.0;
return String.format("%.2f", rate).replaceAll("\\.?0+$", "");
}
/**
* 用户id
*/
@Getter
@Setter
@JsonIgnore
private Integer userId;
/**
* 已完成工单数量
*/
@Getter
@Setter
@JsonIgnore
private int completes = 0;
/**
* 工单处理时长单位分钟
*/
@Getter
@Setter
@JsonIgnore
private long processingTime = 0;
}

View File

@ -0,0 +1,44 @@
package com.nflg.mobilebroken.common.pojo.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class PlanAchievementRateVO {
/**
* 用户id
*/
private Long userId;
/**
* 处理人
*/
private String userName;
/**
* 计划完成率
*/
private int planAchievementRate;
public int getPlanAchievementRate() {
if (total == 0) {
return 0;
}
return completes * 100 / total;
}
/**
* 总工单数量
*/
@JsonIgnore
private int total;
/**
* 已完成工单数量
*/
@JsonIgnore
private int completes;
}

View File

@ -0,0 +1,33 @@
package com.nflg.mobilebroken.common.pojo.vo;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class StateStatisticsVO {
/**
* 未完成工单数量
*/
private int unaccomplished;
/**
* 已完成工单数量
*/
private int completes;
/**
* 总工单数量
*/
private int total;
public int getTotal() {
return unaccomplished + completes;
}
/**
* 工单处理平均时长单位小时
*/
private Double averageProcessingTime;
}

View File

@ -0,0 +1,39 @@
package com.nflg.mobilebroken.common.pojo.vo;
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class UserStatisticsVO {
/**
* 用户id
*/
private Long userId;
/**
* 处理人
*/
private String userName;
/**
* 产品线
*/
private String productLine;
/**
* 当前是否在派工中
*/
private boolean inProgress;
/**
* 最近一次派工时间
*/
private String recentDispatchTime;
/**
* 未完成工单数量
*/
private int unfinishedNum;
}

View File

@ -7,35 +7,36 @@ import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
public class DateTimeUtil {
private static final DateTimeFormatter FORMATTER=DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN);
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN);
public static String format(LocalDateTime dateTime){
if (Objects.isNull(dateTime)){
public static String format(LocalDateTime dateTime) {
if (Objects.isNull(dateTime)) {
return "";
}
return dateTime.format(FORMATTER);
}
public static String format(LocalDateTime dateTime,String pattern){
if (Objects.isNull(dateTime)){
public static String format(LocalDateTime dateTime, String pattern) {
if (Objects.isNull(dateTime)) {
return "";
}
return dateTime.format(DateTimeFormatter.ofPattern(pattern));
}
public static String format(LocalDate date,String pattern){
if (Objects.isNull(date)){
public static String format(LocalDate date, String pattern) {
if (Objects.isNull(date)) {
return "";
}
return date.format(DateTimeFormatter.ofPattern(pattern));
}
public static LocalDate asSystemDate(LocalDate date){
if (Objects.isNull(date)){
public static LocalDate asSystemDate(LocalDate date) {
if (Objects.isNull(date)) {
return null;
}
return date.atStartOfDay(ZoneOffset.UTC)
@ -43,12 +44,20 @@ public class DateTimeUtil {
.toLocalDate();
}
public static LocalDateTime asSystemDateTime(LocalDateTime datetime){
if (Objects.isNull(datetime)){
public static LocalDateTime asSystemDateTime(LocalDateTime datetime) {
if (Objects.isNull(datetime)) {
return null;
}
return datetime.atZone(ZoneOffset.UTC)
.withZoneSameInstant(ZoneId.systemDefault())
.toLocalDateTime();
}
public static LocalDate parse(String dateStr) {
return LocalDate.parse(dateStr, DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN));
}
public static int between(LocalDate date1, LocalDate date2) {
return (int) ChronoUnit.DAYS.between(date1, date2) + 1;
}
}

View File

@ -0,0 +1,59 @@
package com.nflg.mobilebroken.common.util;
import java.io.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class ImageUtil {
public static byte[] heicToPng(byte[] heicBytes) throws IOException {
ProcessBuilder pb = new ProcessBuilder("magick", "-", "png:-");
Process process = pb.start();
try (OutputStream stdin = process.getOutputStream();
InputStream stdout = process.getInputStream();
InputStream stderr = process.getErrorStream()) {
// 4. 在单独的线程中读取标准错误防止缓冲区填满导致死锁
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> stderrFuture = executor.submit(() -> readStreamAsString(stderr));
executor.shutdown(); // 不再接受新任务
// 5. 将HEIC字节数组写入到进程的标准输入
stdin.write(heicBytes);
stdin.flush(); // 确保数据被写入
stdin.close(); // 关闭输入流表示输入结束
// 6. 从进程的标准输出读取转换后的PNG数据
ByteArrayOutputStream pngBuffer = new ByteArrayOutputStream();
byte[] buffer = new byte[8192]; // 8KB 缓冲区
int bytesRead;
while ((bytesRead = stdout.read(buffer)) != -1) {
pngBuffer.write(buffer, 0, bytesRead);
}
// 7. 等待进程执行完成
int exitCode = process.waitFor();
// 8. 检查退出码如果不为0说明转换失败
if (exitCode != 0) {
// 获取错误信息
String errorOutput = stderrFuture.get(5, TimeUnit.SECONDS);
throw new IOException("ImageMagick conversion failed with exit code " + exitCode + ".\nError: " + errorOutput);
}
return pngBuffer.toByteArray();
} catch (Exception e) {
// 确保进程被销毁
process.destroyForcibly();
throw new IOException("HEIC转换失败", e);
}
}
private static String readStreamAsString(InputStream inputStream) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line).append(System.lineSeparator());
}
return stringBuilder.toString();
}
}
}

View File

@ -1,11 +1,21 @@
package com.nflg.mobilebroken.gongfu.controller;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import com.nflg.mobilebroken.common.constant.TicketState;
import com.nflg.mobilebroken.common.pojo.ApiResult;
import com.nflg.mobilebroken.common.pojo.PageData;
import com.nflg.mobilebroken.common.pojo.query.BIBaseQuery;
import com.nflg.mobilebroken.common.pojo.query.BIDispatchQuery;
import com.nflg.mobilebroken.common.pojo.query.EquipmentFailureRankingSearchQuery;
import com.nflg.mobilebroken.common.pojo.vo.EquipmentFailureRankingVO;
import com.nflg.mobilebroken.common.pojo.query.HandlePerformanceQuery;
import com.nflg.mobilebroken.common.pojo.vo.*;
import com.nflg.mobilebroken.common.util.DateTimeUtil;
import com.nflg.mobilebroken.repository.entity.AdminUser;
import com.nflg.mobilebroken.repository.entity.GongfuDispatch;
import com.nflg.mobilebroken.repository.entity.GongfuTicket;
import com.nflg.mobilebroken.repository.service.IAdminUserService;
import com.nflg.mobilebroken.repository.service.IGongfuDispatchService;
import com.nflg.mobilebroken.repository.service.IGongfuTicketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
@ -15,7 +25,11 @@ import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
@ -29,6 +43,12 @@ public class BiController extends ControllerBase {
@Resource
private IGongfuTicketService ticketService;
@Resource
private IAdminUserService adminUserService;
@Resource
private IGongfuDispatchService dispatchService;
/**
* 设备故障排名
*/
@ -37,16 +57,184 @@ public class BiController extends ControllerBase {
return ApiResult.success(ticketService.getEquipmentFailureRanking(qo));
}
public ApiResult getStateStatistics(@Valid @RequestBody BIBaseQuery qo) {
List<GongfuTicket> datas = ticketService.lambdaQuery()
/**
* 工单处理状态
*/
@PostMapping("ticket/getStateStatistics")
public ApiResult<StateStatisticsVO> getStateStatistics(@Valid @RequestBody BIBaseQuery qo) {
long unaccomplished = ticketService.lambdaQuery()
.ne(GongfuTicket::getState, TicketState.Revoked.getState())
.lt(GongfuTicket::getState, TicketState.ProcessingCompleted.getState())
.ge(GongfuTicket::getCreateTime, qo.getStartDate())
.lt(GongfuTicket::getCreateTime, qo.getEndDate())
.count();
List<GongfuTicket> completes = ticketService.lambdaQuery()
.select(GongfuTicket::getCreateTime, GongfuTicket::getSolveTime)
.ne(GongfuTicket::getState, TicketState.Revoked.getState())
.in(GongfuTicket::getState, List.of(TicketState.ProcessingCompleted.getState(), TicketState.Closed.getState()))
.ge(GongfuTicket::getCreateTime, qo.getStartDate())
.lt(GongfuTicket::getCreateTime, qo.getEndDate())
.list();
List<GongfuTicket> completes = datas.stream()
.filter(it -> it.getState() >= TicketState.ProcessingCompleted.getState())
.collect(Collectors.toList());
StateStatisticsVO vo = new StateStatisticsVO()
.setUnaccomplished((int) unaccomplished)
.setCompletes(completes.size())
.setAverageProcessingTime(completes.stream()
.map(t -> LocalDateTimeUtil.between(t.getCreateTime(), t.getSolveTime(), ChronoUnit.MINUTES) / 60.0)
.mapToDouble(Double::doubleValue)
.average()
.orElse(0)
);
return ApiResult.success(vo);
}
return ApiResult.success();
/**
* 责任人绩效
*/
@PostMapping("ticket/getHandlePerformance")
public ApiResult<PageData<HandlePerformanceVO>> getHandlePerformance(@Valid @RequestBody HandlePerformanceQuery qo) {
List<GongfuTicket> tickets = ticketService.lambdaQuery()
.select(GongfuTicket::getCurrentHandle, GongfuTicket::getState, GongfuTicket::getCreateTime, GongfuTicket::getSolveTime)
.notIn(GongfuTicket::getState, List.of(TicketState.Revoked.getState(), TicketState.PendingProcessing.getState()))
.ge(GongfuTicket::getCreateTime, qo.getStartDate())
.lt(GongfuTicket::getCreateTime, qo.getEndDate())
.list();
if (CollectionUtil.isEmpty(tickets)) {
return ApiResult.success(new PageData<>());
}
List<AdminUser> users = adminUserService.lambdaQuery()
.in(AdminUser::getId, tickets.stream().map(GongfuTicket::getCurrentHandle).collect(Collectors.toSet()))
.list();
List<HandlePerformanceVO> vos = new ArrayList<>();
tickets.forEach(ticket -> {
HandlePerformanceVO vo = vos.stream()
.filter(v -> Objects.equals(v.getUserId(), ticket.getCurrentHandle()))
.findFirst()
.orElseGet(() -> {
HandlePerformanceVO v = new HandlePerformanceVO()
.setUserId(ticket.getCurrentHandle())
.setUserName(users.stream()
.filter(user -> Objects.equals(user.getId(), ticket.getCurrentHandle()))
.findFirst()
.orElse(new AdminUser().setUserName("无效用户"))
.getUserName()
);
vos.add(v);
return v;
});
vo.setTotal(vo.getTotal() + 1);
if (Objects.equals(TicketState.ProcessingCompleted.getState(), ticket.getState())
|| Objects.equals(TicketState.Closed.getState(), ticket.getState())) {
vo.setCompletes(vo.getCompletes() + 1);
vo.setProcessingTime(vo.getProcessingTime() + LocalDateTimeUtil.between(ticket.getCreateTime(), ticket.getSolveTime(), ChronoUnit.MINUTES));
}
});
if (qo.getSortType() == 0) {
vos.sort(Comparator.comparingInt(HandlePerformanceVO::getTotal).reversed());
} else if (qo.getSortType() == 1) {
vos.sort(Comparator.comparing(HandlePerformanceVO::getAverageProcessingTime));
}
return ApiResult.success(vos, qo.getPage(), qo.getPageSize());
}
/**
* 出差天数
*/
@PostMapping("dispatch/getDaysOnBusinessTrip")
public ApiResult<List<DaysOnBusinessTripVO>> getDaysOnBusinessTrip(@Valid @RequestBody BIDispatchQuery qo) {
List<GongfuDispatch> dispatches = dispatchService.getForBI(qo);
return ApiResult.success(dispatches.stream()
.collect(Collectors.groupingBy(GongfuDispatch::getHandlerUserId))
.values()
.stream()
.map(ds -> new DaysOnBusinessTripVO()
.setUserId(ds.get(0).getHandlerUserId())
.setUserName(ds.get(0).getHandlerUserName())
.setDaysForInternal(ds.stream()
.filter(d -> Objects.equals(d.getCategory(), 0))
.map(d -> DateTimeUtil.between(DateTimeUtil.parse(d.getPlanStartDate()), DateTimeUtil.parse(d.getPlanEndDate())))
.mapToInt(Integer::intValue)
.sum()
)
.setDaysForForeign(ds.stream()
.filter(d -> Objects.equals(d.getCategory(), 1))
.map(d -> DateTimeUtil.between(DateTimeUtil.parse(d.getPlanStartDate()), DateTimeUtil.parse(d.getPlanEndDate())))
.mapToInt(Integer::intValue)
.sum()
))
.sorted(Comparator.comparingInt(DaysOnBusinessTripVO::getTotalDays).reversed())
.collect(Collectors.toList())
);
}
/**
* 计划达成率
*/
@PostMapping("dispatch/getPlanAchievementRate")
public ApiResult<List<PlanAchievementRateVO>> getPlanAchievementRate(@Valid @RequestBody BIDispatchQuery qo) {
qo.setStates(List.of(1, 2));
List<GongfuDispatch> dispatches = dispatchService.getForBI(qo);
return ApiResult.success(dispatches.stream()
.collect(Collectors.groupingBy(GongfuDispatch::getHandlerUserId))
.values()
.stream()
.map(ds -> new PlanAchievementRateVO()
.setUserId(ds.get(0).getHandlerUserId())
.setUserName(ds.get(0).getHandlerUserName())
.setTotal(ds.size())
.setCompletes(Math.toIntExact(ds.stream()
.filter(d -> Objects.equals(d.getState(), 2))
.count()
)
)
)
.sorted(Comparator.comparingInt(PlanAchievementRateVO::getPlanAchievementRate).reversed())
.collect(Collectors.toList())
);
}
/**
* 人员派工统计
*/
@PostMapping("dispatch/getUserStatistics")
public ApiResult<PageData<UserStatisticsVO>> getUserStatistics(@Valid @RequestBody BIDispatchQuery qo) {
qo.setStates(List.of(1));
List<GongfuDispatch> dispatches = dispatchService.getForBI(qo);
if (CollectionUtil.isEmpty(dispatches)) {
return ApiResult.success(new PageData<>());
}
List<AdminUser> users = adminUserService.lambdaQuery()
.in(AdminUser::getId, dispatches.stream().map(GongfuDispatch::getHandlerUserId).collect(Collectors.toSet()))
.list();
List<UserStatisticsVO> vos = dispatches.stream()
.collect(Collectors.groupingBy(GongfuDispatch::getHandlerUserId))
.values()
.stream()
.map(ds -> new UserStatisticsVO()
.setUserId(ds.get(0).getHandlerUserId())
.setUserName(ds.get(0).getHandlerUserName())
.setProductLine(
users.stream()
.filter(user -> Objects.equals(user.getId(), Math.toIntExact(ds.get(0).getHandlerUserId())))
.findFirst()
.orElse(new AdminUser().setUserName("无效用户"))
.getProductLine()
)
.setInProgress(ds.stream().anyMatch(d -> Objects.equals(d.getState(), 1)))
.setRecentDispatchTime(
ds.stream()
.filter(d -> Objects.equals(d.getState(), 1))
.max(Comparator.comparing(GongfuDispatch::getPlanStartDate))
.map(GongfuDispatch::getPlanStartDate)
.orElse("")
)
.setUnfinishedNum(
Math.toIntExact(ds.stream()
.filter(d -> Objects.equals(d.getState(), 1))
.count())
)
)
.sorted(Comparator.comparingInt(UserStatisticsVO::getUnfinishedNum).reversed())
.collect(Collectors.toList());
return ApiResult.success(vos, qo.getPage(), qo.getPageSize());
}
}

View File

@ -2,6 +2,7 @@ package com.nflg.mobilebroken.gongfu.controller;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.nflg.mobilebroken.common.constant.MessageSubType;
import com.nflg.mobilebroken.common.constant.MessageType;
@ -67,6 +68,8 @@ public class DispatchController extends ControllerBase {
*/
@PostMapping("/add")
public ApiResult<Void> add(@Valid @RequestBody DispatchAddRequest request) {
VUtils.trueThrowBusinessError(!Objects.equals(request.getType(), 4) && StrUtil.isBlank(request.getDeviceNo()))
.throwMessage("机台编号需必填");
GongfuDispatch dispatch = new GongfuDispatch()
.setTitle(request.getTitle())
.setType(request.getType())
@ -180,7 +183,7 @@ public class DispatchController extends ControllerBase {
.map(it -> new GongfuFile()
.setType(0)
.setSourceId(dispatch.getId())
.setNo(dispatch.getNo())
.setNo(dispatch.getCode())
.setFileName(it.getFileName())
.setFileUrl(it.getUrl())
.setFileSuffix(FilenameUtils.getExtension(it.getFileName()))

View File

@ -1474,6 +1474,7 @@ public class TicketController extends ControllerBase {
images = fileUploadRecordService.lambdaQuery()
.eq(FileUploadRecord::getSource, (byte) 0)
.eq(FileUploadRecord::getSourceId, id)
.in(FileUploadRecord::getFileType, List.of(".jpg", ".png", ".heic", ".jpeg", ".webp"))
.list();
if (CollectionUtil.isEmpty(images)) {
return ApiResult.success(Collections.emptyList());

View File

@ -41,7 +41,7 @@ public class DispatchApplyforEvent extends ApplicationEvent implements Applicati
GongfuDispatch dispatch = dispatchService.getById(applyfor.getTicketId());
adminMessageService.add(
new AdminMessage()
.setNo(dispatch.getNo())
.setNo(dispatch.getCode())
.setTitle(dispatch.getTitle())
.setUserId(dispatch.getCreateById())
.setSourceId(applyfor.getId())

View File

@ -3,10 +3,13 @@ package com.nflg.mobilebroken.repository.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nflg.mobilebroken.common.pojo.query.BIDispatchQuery;
import com.nflg.mobilebroken.common.pojo.request.DispatchSearchRequest;
import com.nflg.mobilebroken.common.pojo.vo.DispatchVO;
import com.nflg.mobilebroken.repository.entity.GongfuDispatch;
import java.util.List;
/**
* <p>
* 派工单 Mapper 接口
@ -19,4 +22,6 @@ public interface GongfuDispatchMapper extends BaseMapper<GongfuDispatch> {
IPage<DispatchVO> search(DispatchSearchRequest request, Page<?> objectPage);
DispatchVO getInfo(Long id);
List<GongfuDispatch> getForBI(BIDispatchQuery qo);
}

View File

@ -2,10 +2,13 @@ package com.nflg.mobilebroken.repository.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.nflg.mobilebroken.common.pojo.query.BIDispatchQuery;
import com.nflg.mobilebroken.common.pojo.request.DispatchSearchRequest;
import com.nflg.mobilebroken.common.pojo.vo.DispatchVO;
import com.nflg.mobilebroken.repository.entity.GongfuDispatch;
import java.util.List;
/**
* <p>
* 派工单 服务类
@ -20,4 +23,6 @@ public interface IGongfuDispatchService extends IService<GongfuDispatch> {
DispatchVO getInfo(Long id);
String getMaxCode();
List<GongfuDispatch> getForBI(BIDispatchQuery qo);
}

View File

@ -3,6 +3,7 @@ package com.nflg.mobilebroken.repository.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nflg.mobilebroken.common.pojo.query.BIDispatchQuery;
import com.nflg.mobilebroken.common.pojo.request.DispatchSearchRequest;
import com.nflg.mobilebroken.common.pojo.vo.DispatchVO;
import com.nflg.mobilebroken.repository.entity.GongfuDispatch;
@ -10,6 +11,7 @@ import com.nflg.mobilebroken.repository.mapper.GongfuDispatchMapper;
import com.nflg.mobilebroken.repository.service.IGongfuDispatchService;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
/**
@ -41,4 +43,9 @@ public class GongfuDispatchServiceImpl extends ServiceImpl<GongfuDispatchMapper,
return dispatch.getCode();
}
}
@Override
public List<GongfuDispatch> getForBI(BIDispatchQuery qo) {
return baseMapper.getForBI(qo);
}
}

View File

@ -95,6 +95,7 @@ public class GongfuTicketServiceImpl extends ServiceImpl<GongfuTicketMapper, Gon
ticket.setCurrentHandle(request.getHandlerUserId());
ticket.setHandle(String.valueOf(request.getHandlerUserId()));
ticket.setHandleName(request.getHandlerUserName());
ticket.setCurrentHandle(request.getHandlerUserId());
ticket.setUrgency(request.getUrgency());
ticket.setState(TicketState.Processing.getState());
}
@ -222,7 +223,7 @@ public class GongfuTicketServiceImpl extends ServiceImpl<GongfuTicketMapper, Gon
ticket.setHandle(StrUtil.join(",", request.getUserIds()));
ticket.setHandleName(StrUtil.join(",", adminUserService.getSimples(request.getUserIds()).stream().map(AdminUserSimpleVO::getUserName).collect(Collectors.toList())));
// ticket.setCqm(AdminUserUtil.getUserId());
ticket.setCurrentHandle(AdminUserUtil.getUserId());
ticket.setCurrentHandle(request.getUserIds().get(0));
ticket.setUpdateTime(LocalDateTime.now());
updateById(ticket);
return ticket;

View File

@ -60,8 +60,8 @@
</update>
<select id="searchDevice" resultType="com.nflg.mobilebroken.common.pojo.vo.DeviceVO">
SELECT d.device_no AS 'deviceNo',d.device_name AS 'deviceName',d.model_no AS 'modelNo'
,d.device_type AS 'deviceType',d.shipment_date AS 'shipmentDate',IFNULL(dit2.value,di2.value) AS 'warrantyState'
SELECT d.device_no,d.device_name,d.model_no,d.device_type,d.shipment_date,d.customer_name
,IFNULL(dit2.value,di2.value) AS 'warrantyState'
FROM v_all_device d
INNER JOIN t_base_customer c ON d.agent_code=c.agency_company_code
INNER JOIN dictionary_item di ON di.id=d.device_state
@ -92,6 +92,9 @@
<if test="request.key!=null and request.key!=''">
and (d.device_no LIKE concat('%', #{request.key}, '%') or d.model_no LIKE concat('%', #{request.key}, '%'))
</if>
<if test="request.deviceName!=null and request.deviceName!=''">
and d.device_name LIKE concat('%', #{request.deviceName}, '%')
</if>
</select>
<!--定时任务-质保未开始-->
<update id="taskWarrantyStateNotStarted">

View File

@ -53,4 +53,26 @@
left join v_dispatch_applyfor af on da.id=af.ticket_id
where da.id = #{id}
</select>
<select id="getForBI" resultType="com.nflg.mobilebroken.repository.entity.GongfuDispatch">
SELECT gd.*
FROM gongfu_dispatch gd
LEFT JOIN admin_user au ON gd.handler_user_id = au.id
WHERE gd.handler_user_type=1 and gd.state in
<foreach collection="states" close=")" open="(" item="state" separator=",">
#{state}
</foreach>
<if test="deptId!=null">
and au.department_id=#{deptId}
</if>
<if test="startDate!=null">
and gd.plan_start_date >= #{startDate}
</if>
<if test="endDate!=null">
and gd.plan_end_date &lt;= #{endDate}
</if>
<if test="userName!=null and userName!=''">
and au.user_name LIKE CONCAT('%',#{userName},'%')
</if>
</select>
</mapper>

33
readme.md Normal file
View File

@ -0,0 +1,33 @@
## 服务器需要安装的软件
- ImageMagick
> 用于将苹果的HEIC图片格式转换为PNG图片格式必须源码编译否则不支持HEIC格式
```bash
yum install -y epel-release
rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-7.rpm
yum install -y yum-utils
yum-config-manager --enable remi
wget https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm
wget https://download1.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-7.noarch.rpm
rpm -Uvh rpmfusion-free-release-7.noarch.rpm rpmfusion-nonfree-release-7.noarch.rpm
yum clean all
yum install -y libde265 libx265
yum install -y libheif-devel
yum install -y libtool-ltdl-devel
# 源码编译安装ImageMagick
yum groupinstall -y "Development Tools"
yum install -y libjpeg-turbo-devel libpng-devel freetype-devel libtiff-devel giflib-devel
yum install -y libheif-devel libde265-devel x265-devel
wget https://github.com/ImageMagick/ImageMagick/archive/refs/tags/7.1.2-11.tar.gz
tar xf 7.1.2-11.tar.gz
cd ImageMagick-7.1.2-11/
./configure --with-heif=yes --with-modules --enable-hdri
make -j$(nproc)
make install
which magick
ln -s /usr/local/bin/magick /usr/bin/magick
magick -version
magick -list format | grep HEIC
```