feat: 导出工单PDF优化

This commit is contained in:
曹鹏飞 2025-02-28 22:31:37 +08:00
parent bf75099c8b
commit 0112f267e3
23 changed files with 174 additions and 56 deletions

View File

@ -81,10 +81,27 @@
<!-- <version>7.2.3</version>-->
<!-- </dependency>-->
<!-- html转pdf包含类似于jsoup依赖的操作html文档的依赖 -->
<!-- <dependency>-->
<!-- <groupId>com.itextpdf</groupId>-->
<!-- <artifactId>html2pdf</artifactId>-->
<!-- <version>4.0.3</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf</artifactId>
<version>9.1.22</version>
</dependency>
<!-- iText (required by Flying Saucer) -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>html2pdf</artifactId>
<version>4.0.3</version>
<artifactId>itextpdf</artifactId>
<version>5.5.13.3</version>
</dependency>
<!-- Additional iText Asian fonts for Chinese support -->
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>

View File

@ -2,9 +2,7 @@ package com.nflg.mobilebroken.admin.controller;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.layout.font.FontProvider;
import com.itextpdf.text.pdf.BaseFont;
import com.nflg.mobilebroken.admin.annotation.ApiMark;
import com.nflg.mobilebroken.admin.publisher.TicketEventPublisher;
import com.nflg.mobilebroken.admin.service.SsePushService;
@ -14,6 +12,7 @@ import com.nflg.mobilebroken.common.exception.NflgException;
import com.nflg.mobilebroken.common.pojo.ApiResult;
import com.nflg.mobilebroken.common.pojo.PageData;
import com.nflg.mobilebroken.common.pojo.dto.ChatMessageDTO;
import com.nflg.mobilebroken.common.pojo.dto.FileInfo;
import com.nflg.mobilebroken.common.pojo.request.*;
import com.nflg.mobilebroken.common.pojo.vo.*;
import com.nflg.mobilebroken.common.util.*;
@ -28,6 +27,7 @@ import org.springframework.web.bind.annotation.*;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
@ -36,6 +36,7 @@ import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
@ -302,24 +303,30 @@ public class TicketController extends ControllerBase {
public void exportPdf(HttpServletResponse response, @Valid @RequestParam @NotBlank(message = "工单编号不能为空") String id) {
Ticket ticket = ticketService.getById(id);
AppUser user = appUserService.getById(ticket.getUserId());
TBaseCustomer company = customerService.getById(Integer.valueOf(user.getCompanyId()));
List<Integer> companyIds= Arrays.stream(user.getCompanyId().split(",")).map(Integer::parseInt).collect(Collectors.toList());
List<TBaseCustomer> companys = customerService.listByIds(companyIds);
DeviceInfoVO device = deviceService.getByDeviceNo(ticket.getDeviceNo());
String handle = ticket.getHandle();
if (StrUtil.isNotBlank(handle)) {
List<AdminUser> adminUsers = adminUserService.listByIds(Arrays.stream(handle.split(",")).map(Integer::parseInt).collect(Collectors.toList()));
handle = adminUsers.stream().map(AdminUser::getUserName).collect(Collectors.joining(","));
}
List<String> images = new ArrayList<>();
List<String> files = new ArrayList<>();
List<FileInfo> images = new ArrayList<>();
List<FileInfo> files = new ArrayList<>();
if (StrUtil.isNotBlank(ticket.getAttachments())) {
StrUtil.split(ticket.getAttachments(), ",").forEach(item -> {
if (item.endsWith(".jpg") || item.endsWith(".png") || item.endsWith(".jpeg")) {
images.add(item);
images.add(new FileInfo(item.substring(item.lastIndexOf("/")+1),item));
} else {
files.add(item);
files.add(new FileInfo(item.substring(item.lastIndexOf("/")+1),item));
}
});
}
if (StrUtil.isNotBlank(ticket.getImages())) {
StrUtil.split(ticket.getImages(), ",").forEach(item -> {
images.add(new FileInfo(item.substring(item.lastIndexOf("/")+1),item));
});
}
TicketPdfVO vo = new TicketPdfVO()
.setNo(ticket.getNo())
.setTitle(ticket.getTitle())
@ -330,10 +337,10 @@ public class TicketController extends ControllerBase {
.setDescription(ticket.getDescription())
.setState(ticket.getState())
.setCreateUserName(user.getName())
.setCreateTime(ticket.getCreateTime())
.setCompanyName(company.getAgencyCompanyName())
.setCreateTime(DateTimeUtil.format(ticket.getCreateTime()))
.setCompanyName(StrUtil.join(",",companys.stream().map(TBaseCustomer::getAgencyCompanyName).collect(Collectors.toList())))
.setUrgency(ticket.getUrgency())
.setUpdateTime(ticket.getUpdateTime())
.setUpdateTime(DateTimeUtil.format(ticket.getUpdateTime()))
.setHandleUserName(handle)
.setSolution(ticket.getSolution())
.setImages(images)
@ -356,18 +363,29 @@ public class TicketController extends ControllerBase {
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=" +encode );
// 生成PDF
try {
ConverterProperties converterProperties = new ConverterProperties();
converterProperties.setCharset("UTF-8");
FontProvider fontProvider = new FontProvider();
fontProvider.addSystemFonts();
converterProperties.setFontProvider(fontProvider);
HtmlConverter.convertToPdf(html, response.getOutputStream(), converterProperties);
// ConverterProperties converterProperties = new ConverterProperties();
// converterProperties.setCharset("UTF-8");
// FontProvider fontProvider = new FontProvider();
// fontProvider.addSystemFonts();
// String fontPath = ResourceUtils.getFile("classpath:static/fonts/msyh.ttc").getPath();
// fontProvider.addFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
// converterProperties.setFontProvider(fontProvider);
// HtmlConverter.convertToPdf(html, response.getOutputStream(), converterProperties);
// ITextRenderer renderer = new ITextRenderer();
// ITextFontResolver fontResolver = renderer.getFontResolver();
// fontResolver.addFont("fonts/simsun.ttc", true);
// renderer.setDocumentFromString(html);
// renderer.layout();
// renderer.createPDF(response.getOutputStream());
ITextRenderer renderer = new ITextRenderer();
BaseFont baseFont = BaseFont.createFont("fonts/simsun.ttc,0", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
renderer.getFontResolver().addFont("fonts/simsun.ttc", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
renderer.setDocumentFromString(html);
renderer.layout();
try (OutputStream outputStream = response.getOutputStream()) {
renderer.createPDF(outputStream);
}
} catch (Exception e) {
log.error("生成pdf出错", e);
throw new NflgException(STATE.BusinessError, "生成pdf出错");

View File

@ -65,6 +65,7 @@ public class TicketAssignedEvent extends ApplicationEvent implements Application
adminUsers.forEach(c -> adminMessageService.add(
new AdminMessage()
.setNo(ticket.getNo())
.setTitle(ticket.getTitle())
.setUserId(c.getId())
.setSourceId(ticket.getId())
.setSource(0)

View File

@ -60,6 +60,7 @@ public class TicketCloseEvent extends ApplicationEvent implements ApplicationCon
adminUsers.forEach(c -> adminMessageService.add(
new AdminMessage()
.setNo(ticket.getNo())
.setTitle(ticket.getTitle())
.setUserId(c.getId())
.setSourceId(ticket.getId())
.setSource(0)

View File

@ -69,6 +69,7 @@ public class TicketCompleteEvent extends ApplicationEvent implements Application
adminMessageService.add(
new AdminMessage()
.setNo(ticket.getNo())
.setTitle(ticket.getTitle())
.setUserId(cqmUser.getId())
.setSourceId(ticket.getId())
.setSource(0)

View File

@ -25,4 +25,6 @@ spring.cloud.nacos.discovery.server-addr=${nacos.server-addr}
spring.cloud.nacos.discovery.namespace=mobilebroken
spring.cloud.nacos.discovery.group=${spring.profiles.active}
spring.cloud.nacos.discovery.metadata.env=${spring.profiles.active}
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.encoding=UTF-8
#spring.thymeleaf.cache=true

View File

@ -4,15 +4,12 @@
<meta charset="UTF-8"/>
<title th:text="${ticket.title}"></title>
<style>
body {
font-size: 12px;
font-family: simsun, Arial, sans-serif;
}
.style1 {
display: flex;
align-items: center;
justify-content: center;
font-family: SimSun, Arial, sans-serif;
font-weight: normal;
font-style: normal;
}
table, th, td {
@ -20,11 +17,25 @@
border-collapse: collapse;
padding: 5px;
}
a{
margin: 5px;
}
.cimg{
margin-left: 5px;
margin-top: 5px;
float: left;
width: 215px;
height: auto;
}
</style>
</head>
<body>
<h2 class="style1"><img alt="" src="https://cabinet-tool.oss-cn-hangzhou.aliyuncs.com/admin/20250207/1/mmwf/logo.png"/>移动破售后问题反馈表
</h2>
<div>
<img style="width: 400px;height: auto;" alt="" src="https://cabinet-tool.oss-cn-hangzhou.aliyuncs.com/admin/20250207/1/mmwf/logo.png"/>
</div>
<h1>移动破售后问题反馈表</h1>
<table style="width: 100%;">
<tr>
<td class="style1">标题</td>
@ -76,13 +87,21 @@
<td colspan="4">文件</td>
</tr>
<tr>
<td colspan="4"><a th:each="url:${ticket.files}" th:href="${url}"></a></td>
<td colspan="4">
<div th:each="file:${ticket.files}">
<a th:href="${file.url}" th:text="${file.name}"></a>
</div>
</td>
</tr>
<tr>
<td colspan="4">图片</td>
</tr>
<tr>
<td colspan="4"><img alt="" th:each="url:${ticket.images}" th:src="${url}"/></td>
<td colspan="4">
<div style="background-color: red;" th:each="file:${ticket.images}">
<img class="cimg" alt="" th:src="${file.url}"/>
</div>
</td>
</tr>
</table>
</body>

View File

@ -183,9 +183,11 @@ public class UserController extends ControllerBase {
AppUserApplyfor applyfor=appUserApplyforService.add(request);
List<AdminUser> adminUsers=adminUserService.getForAccountReview();
if (CollectionUtil.isNotEmpty(adminUsers)){
AppUser createdUser=appUserService.getById(AppUserUtil.getUserId());
adminUsers.forEach(c -> adminMessageService.add(
new AdminMessage()
.setNo(request.getEmail())
.setTitle(createdUser.getName()+"申请新的账号")
.setUserId(c.getId())
.setSourceId(applyfor.getId())
.setSource(1)
@ -210,9 +212,11 @@ public class UserController extends ControllerBase {
AppUser user=appUserService.getById(request.getId());
List<AdminUser> adminUsers=adminUserService.getForAccountReview();
if (CollectionUtil.isNotEmpty(adminUsers)){
AppUser createdUser=appUserService.getById(AppUserUtil.getUserId());
adminUsers.forEach(c -> adminMessageService.add(
new AdminMessage()
.setNo(user.getEmail())
.setTitle(createdUser.getName()+"申请账号启用")
.setUserId(c.getId())
.setSourceId(applyfor.getId())
.setSource(1)
@ -237,9 +241,11 @@ public class UserController extends ControllerBase {
AppUserApplyfor applyfor=appUserApplyforService.applyForExtension(request);
List<AdminUser> adminUsers=adminUserService.getForAccountReview();
if (CollectionUtil.isNotEmpty(adminUsers)){
AppUser createdUser=appUserService.getById(AppUserUtil.getUserId());
adminUsers.forEach(c -> adminMessageService.add(
new AdminMessage()
.setNo(applyfor.getUserEmail())
.setTitle(createdUser.getName()+"请账号延期")
.setUserId(c.getId())
.setSourceId(applyfor.getId())
.setSource(1)

View File

@ -71,6 +71,7 @@ public class TicketCreateEvent extends ApplicationEvent implements ApplicationCo
cqmUsers.forEach(c -> adminMessageService.add(
new AdminMessage()
.setNo(ticket.getNo())
.setTitle(ticket.getTitle())
.setUserId(c.getId())
.setSourceId(ticket.getId())
.setSource(0)

View File

@ -59,6 +59,7 @@ public class TicketReopenEvent extends ApplicationEvent implements ApplicationCo
adminMessageService.add(
new AdminMessage()
.setNo(ticket.getNo())
.setTitle(ticket.getTitle())
.setUserId(cqmUser.getId())
.setSourceId(ticket.getId())
.setSource(0)

View File

@ -71,6 +71,7 @@ public class TicketReplyEvent extends ApplicationEvent implements ApplicationCon
adminUsers.forEach(c -> adminMessageService.add(
new AdminMessage()
.setNo(ticket.getNo())
.setTitle(ticket.getTitle())
.setUserId(c.getId())
.setSourceId(ticket.getId())
.setSource(0)

View File

@ -64,6 +64,7 @@ public class TicketRevokeEvent extends ApplicationEvent implements ApplicationCo
adminMessageService.add(
new AdminMessage()
.setNo(ticket.getNo())
.setTitle(ticket.getTitle())
.setUserId(cqmUser.getId())
.setSourceId(ticket.getId())
.setSource(0)
@ -78,6 +79,7 @@ public class TicketRevokeEvent extends ApplicationEvent implements ApplicationCo
adminUsers.forEach(c -> adminMessageService.add(
new AdminMessage()
.setNo(ticket.getNo())
.setTitle(ticket.getTitle())
.setUserId(c.getId())
.setSourceId(ticket.getId())
.setSource(0)

View File

@ -0,0 +1,13 @@
package com.nflg.mobilebroken.common.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class FileInfo {
private String name;
private String url;
}

View File

@ -1,5 +1,6 @@
package com.nflg.mobilebroken.common.pojo.request;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -24,4 +25,8 @@ public class AdminTicketSearchRequest extends TicketSearchRequest {
//质保状态
private Integer warrantyStatus;
//是否是工单管理者
@JsonIgnore
private boolean ticketManager;
}

View File

@ -16,6 +16,9 @@ public class AdminMessageVO {
//任务编号
private String no;
//标题
private String title;
//来源0工单1代理商账户
private Integer source;
@ -27,28 +30,33 @@ public class AdminMessageVO {
//任务事项描述
private String subTypeDesc;
//任务类别0账号审核1工单处理
private Integer type;
//任务类别描述
private String typeDesc;
//提交人
private String sourceCreateUserName;
//提交时间
private LocalDateTime sourceCreateTime;
//消息时间
private LocalDateTime createTime;
//待处理人
private String userName;
public String getSubTypeDesc() {
return MessageSubType.findByValue(subType).getDescription();
}
public String gettypeDesc() {
//任务类别0账号审核1工单处理
private Integer type;
//任务类别描述
private String typeDesc;
public String getTypeDesc() {
return MessageType.findByValue(type).getDescription();
}
//提交人
private String sourceCreateUserName;
//提交时间
private LocalDateTime sourceCreateTime;
//消息时间
private LocalDateTime createTime;
//待处理人
private String userName;
//是否已读
private boolean isRead;
}

View File

@ -1,10 +1,10 @@
package com.nflg.mobilebroken.common.pojo.vo;
import com.nflg.mobilebroken.common.constant.TicketState;
import com.nflg.mobilebroken.common.pojo.dto.FileInfo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
@ -25,7 +25,7 @@ public class TicketPdfVO {
private String createUserName;
//提交时间
private LocalDateTime createTime;
private String createTime;
//工单状态
private Byte state;
@ -52,15 +52,15 @@ public class TicketPdfVO {
//解决状态描述
private String stateDesc;
//更新时间
private LocalDateTime updateTime;
private String updateTime;
//处理人
private String handleUserName;
//解决方案
private String solution;
//图片
private List<String> images;
private List<FileInfo> images;
//文件
private List<String> files;
private List<FileInfo> files;
public String getUrgencyDesc() {
if (Objects.isNull(urgency)) {

View File

@ -1,6 +1,15 @@
package com.nflg.mobilebroken.common.util;
import cn.hutool.core.date.DatePattern;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateTimeUtil {
private static final DateTimeFormatter FORMATTER=DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN);
public static String format(LocalDateTime dateTime){
return dateTime.format(FORMATTER);
}
}

View File

@ -34,6 +34,11 @@ public class AdminMessage implements Serializable {
*/
private String no;
/**
* 标题
*/
private String title;
/**
* 来源0工单1代理商账户
*/

View File

@ -103,7 +103,10 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
if (request.getType() == 2) {
return baseMapper.searchFromAdminAndFollow(request, AdminUserUtil.getUserId(), new Page<>(request.getPage(), request.getPageSize()));
} else if (request.getType() == 4) {
return baseMapper.searchFromAdmin(request, AdminUserUtil.getUserId(), new Page<>(request.getPage(), request.getPageSize()));
Integer userId=AdminUserUtil.getUserId();
List<Integer> tickerMangagers=adminUserService.getTickerMangagers();
request.setTicketManager(tickerMangagers.stream().anyMatch(uid -> Objects.equals(uid, userId)));
return baseMapper.searchFromAdmin(request, userId, new Page<>(request.getPage(), request.getPageSize()));
}
return new Page<>(request.getPage(), request.getPageSize(), 0);
}
@ -205,7 +208,10 @@ public class TicketServiceImpl extends ServiceImpl<TicketMapper, Ticket> impleme
if (request.getType() == 2) {
return baseMapper.searchAllFromAdminAndFollow(request, AdminUserUtil.getUserId());
} else if (request.getType() == 4) {
return baseMapper.searchAllFromAdmin(request, AdminUserUtil.getUserId());
Integer userId=AdminUserUtil.getUserId();
List<Integer> tickerMangagers=adminUserService.getTickerMangagers();
request.setTicketManager(tickerMangagers.stream().anyMatch(uid -> Objects.equals(uid, userId)));
return baseMapper.searchAllFromAdmin(request, userId);
}
return Collections.emptyList();
}

View File

@ -3,7 +3,7 @@
<mapper namespace="com.nflg.mobilebroken.repository.mapper.AdminMessageMapper">
<select id="search" resultType="com.nflg.mobilebroken.common.pojo.vo.AdminMessageVO">
SELECT m.id,m.no,m.source,m.source_id AS 'sourceId',m.type,m.sub_type AS 'subType',m.create_time AS 'createTime',
SELECT m.id,m.no,m.title,m.source,m.source_id AS 'sourceId',m.type,m.sub_type AS 'subType',m.create_time AS 'createTime',
u.user_name AS 'userName',m.is_read AS 'isRead'
FROM admin_message m
INNER JOIN admin_user u ON m.user_id=u.id
@ -21,7 +21,7 @@
</select>
<select id="getNotReadMessage" resultType="com.nflg.mobilebroken.common.pojo.vo.AdminMessageVO">
SELECT m.id,m.no,m.source,m.source_id AS 'sourceId',m.type,m.sub_type AS 'subType',m.create_time AS 'createTime',
SELECT m.id,m.no,m.title,m.source,m.source_id AS 'sourceId',m.type,m.sub_type AS 'subType',m.create_time AS 'createTime',
u.user_name AS 'userName',m.is_read AS 'isRead'
FROM admin_message m
INNER JOIN admin_user u ON m.user_id=u.id

View File

@ -32,6 +32,9 @@
<sql id="adminSearchWhereCondition">
<where>
t.state!=4
<if test="!request.ticketManager">
AND FIND_IN_SET(#{userId},t.handle)>0
</if>
<if test="request.state!=null">
AND t.state=#{request.state}
</if>

View File

@ -13,7 +13,6 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Objects;
@MappedTypes(LocalDate.class)
public class UTCLocalDateTypeHandler extends BaseTypeHandler<LocalDate> {
@ -55,7 +54,7 @@ public class UTCLocalDateTypeHandler extends BaseTypeHandler<LocalDate> {
String zone = MultilingualUtil.getZone();
return utcTime
.atStartOfDay(ZoneOffset.UTC)
.withZoneSameInstant(ZoneId.of(Objects.isNull(zone) ? "Asia/Shanghai" : zone))
.withZoneSameInstant(ZoneId.of(zone))
.toLocalDate();
}
}