feat(ticket): 添加工单报表导出功能并优化工单处理逻辑

- 新增 AdminTicketReportVO 类用于工单报表导出- 添加 TicketCloseEvent 事件处理工单关闭逻辑
- 更新 TicketController,增加工单报表导出功能
- 修改 TicketEventListener 和 TicketEventPublisher,支持工单关闭事件
- 更新 TicketInfoVO,增加用户是否为处理人和 CQM 的字段
- 优化 TicketSolution 和 TicketSolutionAudit 的 equals 和 hashCode 方法
- 改进 TicketSolutionServiceImpl 中的措施删除和更新逻辑
- 在 TiketController 中添加工单关闭后的消息推送和事件发布
This commit is contained in:
曹鹏飞 2025-03-29 10:55:38 +08:00
parent c7de80b9cf
commit 1c042b9f0a
11 changed files with 258 additions and 11 deletions

View File

@ -1,6 +1,7 @@
package com.nflg.mobilebroken.admin.controller;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.itextpdf.text.pdf.BaseFont;
import com.nflg.mobilebroken.admin.annotation.ApiMark;
@ -566,10 +567,12 @@ public class TicketController extends ControllerBase {
warrantyStateDesc = warrantyState.getName();
}
String handle = ticket.getHandle();
List<Integer> handleIds= Arrays.stream(handle.split(",")).map(Integer::parseInt).collect(Collectors.toList());
if (StrUtil.isNotBlank(handle)) {
List<AdminUser> adminUsers = adminUserService.listByIds(Arrays.stream(handle.split(",")).map(Integer::parseInt).collect(Collectors.toList()));
List<AdminUser> adminUsers = adminUserService.listByIds(handleIds);
handle = adminUsers.stream().map(AdminUser::getUserName).collect(Collectors.joining(","));
}
List<Integer> cqms=adminUserService.getCQMIds();
TicketInfoVO vo = new TicketInfoVO()
.setId(ticket.getId())
.setNo(ticket.getNo())
@ -602,6 +605,8 @@ public class TicketController extends ControllerBase {
.setHandle(handle)
.setSolution(ticket.getReason())
.setAccidentLevel(ticket.getAccidentLevel())
.setUserIsHandle(handleIds.contains(AdminUserUtil.getUserId()))
.setUserIsCQM(cqms.contains(AdminUserUtil.getUserId()))
.setEvaluate(getTicketEvaluate(ticket.getId()));
return ApiResult.success(vo);
}
@ -773,7 +778,7 @@ public class TicketController extends ControllerBase {
* 驳回工单解决方案
* @param request 请求信息
**/
@GetMapping("rejectSolution")
@PostMapping("rejectSolution")
@ApiMark(moduleName = "工单管理", apiName = "驳回工单解决方案")
public ApiResult<Void> rejectSolution(@Valid @RequestBody SolutionRejectRequest request) {
Ticket ticket = ticketSolutionAuditService.reject(request);
@ -825,4 +830,16 @@ public class TicketController extends ControllerBase {
}
return ApiResult.success();
}
/**
* 导出工单报表
* @param request 请求参数
*/
@PostMapping("exportTicketReport")
@ApiMark(moduleName = "工单管理", apiName = "导出工单报表")
public void exportTicketReport(HttpServletResponse response,@Valid @RequestBody AdminTicketSearchRequest request) throws IOException {
request.setPageSize(Integer.MAX_VALUE);
List<AdminTicketVO> datas = ticketService.exportSearch(request);
EecExcelUtil.export("工单报表", "sheet1", Convert.toList(AdminTicketReportVO.class, datas), response);
}
}

View File

@ -400,6 +400,21 @@ public class TiketController extends ControllerBase {
//推送消息
ssePushService.sendTicketMessageToAdmin(ticket.getId(),message);
ssePushService.sendTicketMessageToApp(ticket.getId(),message);
if (ticketService.close(ticket)){
ticketEventPublisher.publishTicketCloseEvent(ticket);
message = new ChatMessageDTO()
.setId(cn.hutool.core.util.IdUtil.getSnowflakeNextIdStr())
.setFrom("system")
.setTicketState(ticket.getState())
.setSenderId(0)
.setSenderName("服务助手")
.setContent("<b>工单已关闭</b><br/>感谢你的使用,如有问题,请重新提交新的工单。")
.setCreateTime(Instant.now());
ticketChatService.addMessage(request.getTicketId(), message);
//推送消息
ssePushService.sendTicketMessageToAdmin(ticket.getId(),message);
ssePushService.sendTicketMessageToApp(ticket.getId(),message);
}
return ApiResult.success();
}
}

View File

@ -0,0 +1,72 @@
package com.nflg.mobilebroken.cfs.event;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.util.StrUtil;
import com.nflg.mobilebroken.common.constant.MessageSubType;
import com.nflg.mobilebroken.common.constant.MessageType;
import com.nflg.mobilebroken.repository.entity.AdminMessage;
import com.nflg.mobilebroken.repository.entity.AdminUser;
import com.nflg.mobilebroken.repository.entity.Ticket;
import com.nflg.mobilebroken.repository.service.*;
import com.nflg.mobilebroken.starter.service.EmailService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class TicketCloseEvent extends ApplicationEvent implements ApplicationContextAware {
private static final DateTimeFormatter FORMATTER= DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN);
private final Ticket ticket;
private IDictionaryItemTranslateService dictionaryItemTranslateService;
private IAppUserService appUserService;
private EmailService emailService;
private ITBaseDeviceTypeService deviceTypeService;
private IAdminUserService adminUserService;
private IAdminMessageService adminMessageService;
public TicketCloseEvent(Object source,Ticket ticket) {
super(source);
this.ticket = ticket;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.dictionaryItemTranslateService = applicationContext.getBean(IDictionaryItemTranslateService.class);
this.appUserService = applicationContext.getBean(IAppUserService.class);
this.emailService = applicationContext.getBean(EmailService.class);
this.adminUserService = applicationContext.getBean(IAdminUserService.class);
this.adminMessageService = applicationContext.getBean(IAdminMessageService.class);
this.deviceTypeService = applicationContext.getBean(ITBaseDeviceTypeService.class);
}
public void send(){
sendUserMessage();
}
private void sendUserMessage(){
//我的待办
List<Integer> userIds= Arrays.stream(ticket.getHandle().split(",")).filter(StrUtil::isNotBlank).map(Integer::parseInt).collect(Collectors.toList());
List<AdminUser> adminUsers=adminUserService.listByIds(userIds);
if (CollectionUtil.isNotEmpty(adminUsers)){
adminUsers.forEach(c -> adminMessageService.add(
new AdminMessage()
.setNo(ticket.getNo())
.setTitle(ticket.getTitle())
.setUserId(c.getId())
.setSourceId(ticket.getId())
.setSource(0)
.setType(MessageType.WorkOrderAssignment.getState())
.setSubType(MessageSubType.TicketClosed.getState())
.setIsRead(false)
.setCreateTime(LocalDateTime.now()))
);
}
}
}

View File

@ -1,9 +1,6 @@
package com.nflg.mobilebroken.cfs.listener;
import com.nflg.mobilebroken.cfs.event.TicketCreateEvent;
import com.nflg.mobilebroken.cfs.event.TicketEvaluateEvent;
import com.nflg.mobilebroken.cfs.event.TicketReplyEvent;
import com.nflg.mobilebroken.cfs.event.TicketRevokeEvent;
import com.nflg.mobilebroken.cfs.event.*;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@ -34,4 +31,10 @@ public class TicketEventListener {
public void handleTicketEvaluateEvent(TicketEvaluateEvent event) {
event.send();
}
@Async
@EventListener
public void handleTicketCloseEvent(TicketCloseEvent event) {
event.send();
}
}

View File

@ -46,4 +46,10 @@ public class TicketEventPublisher {
event.setApplicationContext(applicationContext);
eventPublisher.publishEvent(event);
}
public void publishTicketCloseEvent(Ticket ticket) {
TicketCloseEvent event = new TicketCloseEvent(this, ticket);
event.setApplicationContext(applicationContext);
eventPublisher.publishEvent(event);
}
}

View File

@ -0,0 +1,94 @@
package com.nflg.mobilebroken.common.pojo.vo;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.nflg.mobilebroken.common.constant.TicketState;
import com.nflg.mobilebroken.common.constant.TicketUrgency;
import lombok.Data;
import org.ttzero.excel.annotation.ExcelColumn;
import org.ttzero.excel.annotation.IgnoreExport;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
@Data
public class AdminTicketReportVO {
//工单编号
@ExcelColumn("工单编号")
private String no;
//解决状态
@ExcelColumn("解决状态")
private String stateDesc;
//紧急程度
@IgnoreExport
@JsonIgnore
private Byte urgency;
//紧急程度
@ExcelColumn("紧急程度")
private String urgencyDesc;
//设备编号
@ExcelColumn("设备编号")
private String deviceNo;
//设备类型
@ExcelColumn("设备类型")
private String deviceType;
//使用时长
@ExcelColumn("使用时长(小时)")
private Integer useTime;
//代理商
@ExcelColumn("代理商")
private String agentName;
//客户
@ExcelColumn("客户")
private String customerName;
//提交时间
@ExcelColumn("提交时间")
private LocalDateTime createTime;
//根本原因分析
@IgnoreExport
private String reason;
//根本原因分析
@ExcelColumn("根本原因分析")
private String reason1;
//工单状态
@JsonIgnore
@IgnoreExport
private Byte state;
//处理完成时间
@JsonIgnore
@IgnoreExport
private LocalDateTime completeTime;
//处理时长
@ExcelColumn("处理时长")
private Long processingTime;
public String getUrgencyDesc() {
if (Objects.isNull(urgency)) {
return "";
}
return TicketUrgency.findByValue(urgency).getDescription();
}
public String getReason() {
return StrUtil.subWithLength(reason, 0, 10);
}
public String getReason1() {
return reason;
}
public Long getProcessingTime() {
if (TicketState.Processing.getState().compareTo(state)>=0){
return ChronoUnit.DAYS.between(createTime.toLocalDate(),LocalDateTime.now().toLocalDate())+1;
}
if (TicketState.Closed.getState().compareTo(state)>=0) {
return ChronoUnit.DAYS.between(completeTime, LocalDateTime.now()) + 1;
}
return null;
}
}

View File

@ -112,4 +112,14 @@ public class TicketInfoVO {
* 事故等级0一般1较严重2严重
*/
private Byte accidentLevel;
/**
* 当前用户是否处理人
*/
private Boolean userIsHandle;
/**
* 当前用户是否为CQM
*/
private Boolean userIsCQM;
}

View File

@ -9,6 +9,7 @@ import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* <p>
@ -78,4 +79,23 @@ public class TicketSolution implements Serializable {
* 创建时间
*/
private LocalDateTime createTime;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TicketSolution that = (TicketSolution) o;
return Objects.equals(id, that.id) &&
Objects.equals(ticketId, that.ticketId) &&
Objects.equals(dictionaryItemId, that.dictionaryItemId) &&
Objects.equals(description, that.description) &&
Objects.equals(superintendent, that.superintendent) &&
Objects.equals(scheduleDate, that.scheduleDate) &&
Objects.equals(confirmedDate, that.confirmedDate);
}
@Override
public int hashCode() {
return Objects.hash(id, ticketId, dictionaryItemId, description, superintendent, scheduleDate, confirmedDate);
}
}

View File

@ -1,5 +1,7 @@
package com.nflg.mobilebroken.repository.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
@ -24,6 +26,7 @@ public class TicketSolutionAudit implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**

View File

@ -84,6 +84,11 @@ public class TicketSolutionAuditServiceImpl extends ServiceImpl<TicketSolutionAu
.setCreateTime(LocalDateTime.now());
if (Objects.isNull(detp.getId())){
forAdd.add(audit);
VUtils.trueThrowBusinessError(lambdaQuery()
.eq(TicketSolutionAudit::getTicketId,request.getTicketId())
.eq(TicketSolutionAudit::getDeptName,detp.getDeptName())
.exists())
.throwMessage(detp.getDeptName()+"部门已设置了审核人员");
}else {
audit.setId(detp.getId());
TicketSolutionAudit entity=getById(detp.getId());

View File

@ -153,7 +153,7 @@ public class TicketSolutionServiceImpl extends ServiceImpl<TicketSolutionMapper,
VUtils.trueThrowBusinessError(solutions.stream().anyMatch(s->!Objects.equals(s.getCreateUserId(), userId)))
.throwMessage("不能删除他人创建的措施");
solutions1.removeIf(s->Objects.equals(s.getCreateUserId(), userId));
List<TicketSolution> ts=forUpdate.stream().filter(s->!Objects.equals(s.getCreateUserId(), userId)).collect(Collectors.toList());
List<TicketSolution> ts=forUpdate.stream().filter(s->!Objects.equals(s.getCreateUserId(), userId)).sorted(Comparator.comparing(TicketSolution::getId)).collect(Collectors.toList());
VUtils.trueThrowBusinessError(!ts.equals(solutions1)).throwMessage("不能修改他人创建的措施");
}
ticketService.lambdaUpdate()
@ -161,10 +161,12 @@ public class TicketSolutionServiceImpl extends ServiceImpl<TicketSolutionMapper,
.set(Ticket::getAccidentLevel, request.getAccidentLevel())
.eq(Ticket::getId, request.getTicketId())
.update();
baseMapper.delete(new LambdaQueryWrapper<TicketSolution>()
.eq(TicketSolution::getTicketId,request.getTicketId())
.notIn(TicketSolution::getId, idForReserve)
);
if(CollectionUtil.isNotEmpty(idForReserve)) {
baseMapper.delete(new LambdaQueryWrapper<TicketSolution>()
.eq(TicketSolution::getTicketId, request.getTicketId())
.notIn(TicketSolution::getId, idForReserve)
);
}
if (CollectionUtil.isNotEmpty(forAdd)){
saveBatch(forAdd);
}