diff --git a/ruoyi-sip/pom.xml b/ruoyi-sip/pom.xml index c4ddd953..08f22703 100644 --- a/ruoyi-sip/pom.xml +++ b/ruoyi-sip/pom.xml @@ -53,6 +53,11 @@ javax.mail 1.5.6 + + com.github.librepdf + openpdf + 1.3.23 + diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java index 600fa0c8..029130ff 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java @@ -19,6 +19,22 @@ import com.alibaba.excel.EasyExcel; import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.metadata.CellData; import com.alibaba.excel.metadata.Head; +import com.lowagie.text.Document; +import com.lowagie.text.DocumentException; +import com.lowagie.text.Element; +import com.lowagie.text.Chunk; +import com.lowagie.text.Image; +import com.lowagie.text.PageSize; +import com.lowagie.text.Paragraph; +import com.lowagie.text.Phrase; +import com.lowagie.text.Rectangle; +import com.lowagie.text.pdf.BaseFont; +import com.lowagie.text.pdf.ColumnText; +import com.lowagie.text.pdf.PdfPCell; +import com.lowagie.text.pdf.PdfPageEventHelper; +import com.lowagie.text.pdf.PdfPTable; +import com.lowagie.text.pdf.PdfWriter; +import com.lowagie.text.pdf.draw.LineSeparator; import com.alibaba.excel.write.metadata.WriteSheet; import com.alibaba.excel.write.metadata.WriteTable; import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; @@ -35,7 +51,9 @@ import com.ruoyi.common.utils.DateUtils; import com.ruoyi.common.utils.DictUtils; import com.ruoyi.common.utils.ShiroUtils; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileUploadUtils; import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.utils.spring.SpringUtils; import com.ruoyi.sip.domain.*; import com.ruoyi.sip.dto.ApiDataQueryDto; import com.ruoyi.sip.flowable.domain.Todo; @@ -69,9 +87,11 @@ import org.springframework.stereotype.Service; import com.ruoyi.sip.mapper.ProjectOrderInfoMapper; import com.ruoyi.common.core.text.Convert; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.*; +import java.text.DecimalFormat; import java.text.SimpleDateFormat; import static com.ruoyi.common.utils.ShiroUtils.getSysUser; @@ -119,6 +139,7 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To private static final List N_VIDIA_LIST = Arrays.asList("0504A14F", "0504A14G", "0504A1JX"); private Boolean useMcpExportExcelFormat = Boolean.TRUE; private static final List FILE_INFO_LIST = Arrays.asList( "(请上传商务折扣审批邮件信息).pdf/.jpg/.png","(请上传未盖章合同信息).pdf/.jpg/.png", "(补充附件).zip/.rar/.jpg/.png","(请上传已盖章合同信息).pdf/.jpg/.png"); + private static final String ORDER_CONTENT_FILE_SORT = "5"; @Autowired private TaskService taskService; @@ -1741,6 +1762,7 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To handleApproveOrder(businessKey); // 订单审批通过时推送项目信息到CRM projectInfoService.scheduleOpportunityUpdateByProjectId(dbProjectOrderInfo.getProjectId()); + saveOrderContentPdf(dbProjectOrderInfo.getId()); } @Override @@ -1839,6 +1861,428 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To } } + private void saveOrderContentPdf(Long orderId) { + ProjectOrderInfo orderInfo = selectProjectOrderInfoById(orderId); + if (orderInfo == null) { + throw new ServiceException("订单不存在,无法生成订单内容PDF"); + } + try { + deleteExistingOrderContentPdf(orderId, orderInfo.getVersionCode()); + byte[] pdfBytes = buildOrderContentPdf(orderInfo); + String fileName = buildOrderContentFileName(orderInfo); + String filePath = uploadGeneratedPdf(fileName, pdfBytes); + + ProjectOrderFileLog fileLog = new ProjectOrderFileLog(); + fileLog.setOrderId(orderId); + fileLog.setFileName(fileName); + fileLog.setFilePath(filePath); + fileLog.setFileType(ProjectOrderFileLog.FileTypeEnum.CONTRACT_BAK.getCode()); + fileLog.setFileSort(ORDER_CONTENT_FILE_SORT); + fileLog.setFileVersionCode(orderInfo.getVersionCode()); + fileLog.setUploadTime(DateUtils.getNowDate()); + fileLog.setUploadUser(resolveCurrentUserId()); + fileLog.setUploadUserName(resolveCurrentUserName()); + fileLogService.insertProjectOrderFileLog(fileLog); + } catch (Exception e) { + log.error("生成订单内容PDF失败,订单ID:{}", orderId, e); + throw new ServiceException("生成订单内容PDF失败: " + e.getMessage()); + } + } + + private void deleteExistingOrderContentPdf(Long orderId, String versionCode) { + ProjectOrderFileLog query = new ProjectOrderFileLog(); + query.setOrderId(orderId); + query.setFileType(ProjectOrderFileLog.FileTypeEnum.CONTRACT_BAK.getCode()); + query.setFileVersionCode(versionCode); + query.setFileSort(ORDER_CONTENT_FILE_SORT); + List existList = fileLogService.selectProjectOrderFileLogList(query); + if (CollUtil.isNotEmpty(existList)) { + String ids = existList.stream().map(v -> String.valueOf(v.getId())).collect(Collectors.joining(",")); + fileLogService.deleteProjectOrderFileLogByIds(ids); + } + } + + private String buildOrderContentFileName(ProjectOrderInfo orderInfo) { + String safeProjectName = StringUtils.isEmpty(orderInfo.getProjectName()) ? "订单内容" + : orderInfo.getProjectName().replaceAll("[\\\\/:*?\"<>|]", "_"); + return safeProjectName + "_Rev." + safeText(orderInfo.getVersionCode()) + "_订单内容.pdf"; + } + + private String uploadGeneratedPdf(String fileName, byte[] pdfBytes) throws IOException { + MultipartFile multipartFile = new ByteArrayMultipartFile(fileName, fileName, "application/pdf", pdfBytes); + return FileUploadUtils.upload(multipartFile); + } + + private byte[] buildOrderContentPdf(ProjectOrderInfo orderInfo) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Rectangle pageSize = PageSize.A4.rotate(); + Document document = new Document(pageSize, 22, 22, 20, 20); + BaseFont baseFont = createPdfBaseFont(); + PdfWriter writer = PdfWriter.getInstance(document, outputStream); + writer.setPageEvent(new OrderContentPdfPageEvent(baseFont, buildPdfFooter(orderInfo))); + document.open(); + addPdfHeader(document, baseFont, orderInfo); + + addOrderInfoSection(document, baseFont, orderInfo); + + document.close(); + return outputStream.toByteArray(); + } + + private String buildPdfFooter(ProjectOrderInfo orderInfo) { + return safeEmpty(orderInfo.getProjectCode()) + "-" + + safeEmpty(orderInfo.getOrderCode()) + "-Rev." + + safeEmpty(orderInfo.getVersionCode()); + } + + private BaseFont createPdfBaseFont() throws Exception { + List embeddedFontCandidates = Arrays.asList( + "C:\\Windows\\Fonts\\msyh.ttc,0", + "C:\\Windows\\Fonts\\simsun.ttc,0", + "C:\\Windows\\Fonts\\simhei.ttf" + ); + for (String fontPath : embeddedFontCandidates) { + String actualPath = fontPath.contains(",") ? fontPath.substring(0, fontPath.indexOf(',')) : fontPath; + if (Files.exists(Paths.get(actualPath))) { + return BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); + } + } + return BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); + } + + private void addPdfHeader(Document document, BaseFont baseFont, ProjectOrderInfo orderInfo) throws DocumentException { + PdfPTable headerTable = new PdfPTable(2); + headerTable.setWidthPercentage(100); + headerTable.setWidths(new float[]{4.5f, 1.5f}); + headerTable.setSpacingAfter(4); + headerTable.getDefaultCell().setBorder(Rectangle.NO_BORDER); + + PdfPCell companyCell = createBorderlessCell("紫光汇智信息技术有限公司", baseFont, 13, Element.ALIGN_LEFT, + new java.awt.Color(125, 125, 125), true); + companyCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + companyCell.setPaddingTop(4); + headerTable.addCell(companyCell); + + Image logoImage = loadOrderPdfLogo(); + if (logoImage != null) { + logoImage.scaleToFit(110f, 30f); + PdfPCell logoCell = new PdfPCell(logoImage, false); + logoCell.setBorder(Rectangle.NO_BORDER); + logoCell.setHorizontalAlignment(Element.ALIGN_RIGHT); + logoCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + logoCell.setPadding(0); + headerTable.addCell(logoCell); + } else { + PdfPCell brandCell = createBorderlessCell("紫光汇智 UNIS SENSE", baseFont, 10, Element.ALIGN_RIGHT, + new java.awt.Color(118, 38, 137), true); + brandCell.setVerticalAlignment(Element.ALIGN_MIDDLE); + brandCell.setPaddingTop(4); + headerTable.addCell(brandCell); + } + document.add(headerTable); + + LineSeparator separator = new LineSeparator(); + separator.setLineWidth(0.6f); + separator.setLineColor(new java.awt.Color(200, 200, 200)); + document.add(new Chunk(separator)); + + Paragraph subTitle = new Paragraph(buildPdfSubtitle(orderInfo), + new com.lowagie.text.Font(baseFont, 11, com.lowagie.text.Font.BOLD, new java.awt.Color(85, 85, 85))); + subTitle.setAlignment(Element.ALIGN_CENTER); + subTitle.setSpacingBefore(3); + subTitle.setSpacingAfter(8); + document.add(subTitle); + } + + private Image loadOrderPdfLogo() { + try (InputStream inputStream = SpringUtils.getResource("classpath:static/img/companyLogoHD.jpg").getInputStream()) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[4096]; + int length; + while ((length = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, length); + } + return Image.getInstance(outputStream.toByteArray()); + } catch (Exception e) { + log.warn("读取订单PDF Logo失败", e); + return null; + } + } + + private String buildPdfSubtitle(ProjectOrderInfo orderInfo) { + return safeEmpty(orderInfo.getProjectName()) + "Rev." + safeEmpty(orderInfo.getVersionCode()); + } + + private static class OrderContentPdfPageEvent extends PdfPageEventHelper { + private final BaseFont baseFont; + private final String footerText; + + private OrderContentPdfPageEvent(BaseFont baseFont, String footerText) { + this.baseFont = baseFont; + this.footerText = footerText; + } + + @Override + public void onEndPage(PdfWriter writer, Document document) { + com.lowagie.text.Font footerFont = new com.lowagie.text.Font(baseFont, 9, + com.lowagie.text.Font.NORMAL, new java.awt.Color(120, 120, 120)); + float y = document.bottom() - 10; + ColumnText.showTextAligned(writer.getDirectContent(), Element.ALIGN_LEFT, + new Phrase(footerText, footerFont), document.left(), y, 0); + ColumnText.showTextAligned(writer.getDirectContent(), Element.ALIGN_RIGHT, + new Phrase("第" + writer.getPageNumber() + "页", footerFont), document.right(), y, 0); + } + } + + private void addOrderInfoSection(Document document, BaseFont baseFont, ProjectOrderInfo orderInfo) throws DocumentException { + Paragraph sectionTitle = new Paragraph("订单信息", + new com.lowagie.text.Font(baseFont, 14, com.lowagie.text.Font.BOLD, new java.awt.Color(32, 126, 214))); + sectionTitle.setSpacingAfter(5); + document.add(sectionTitle); + + PdfPTable table = new PdfPTable(6); + table.setWidthPercentage(100); + table.setWidths(new float[]{1.3f, 2.5f, 1.3f, 2.5f, 1.4f, 2.5f}); + table.setSpacingAfter(6); + + addSixColumnRow(table, baseFont, "项目名称", orderInfo.getProjectName(), 3, "版本号", orderInfo.getVersionCode(), 1); + addSixColumnRow(table, baseFont, "项目编号", orderInfo.getProjectCode(), 1, "最终客户", orderInfo.getCustomerName(), 3); + addSixColumnRow(table, baseFont, "BG", + firstNonEmpty(orderInfo.getBgPropertyDesc(), DictUtils.getDictLabel(BG_TYPE_DICT_TYPE, orderInfo.getBgProperty())), 1, + "行业", firstNonEmpty(orderInfo.getIndustryTypeDesc(), + DictUtils.getDictLabel("YYS".equals(orderInfo.getBgProperty()) ? "bg_yys" : "bg_hysy", orderInfo.getIndustryType())), 1, + "代表处", orderInfo.getAgentName(), 1); + addSixColumnRow(table, baseFont, "进货商接口人", orderInfo.getBusinessPerson(), 1, "Email", orderInfo.getBusinessEmail(), 1, + "联系方式", orderInfo.getBusinessPhone(), 1); + addSixColumnRow(table, baseFont, "合同编号", orderInfo.getOrderCode(), 1, "执行单有效截止时间", formatDate(orderInfo.getOrderEndTime()), 3); + addSixColumnRow(table, baseFont, "币种", DictUtils.getDictLabel("currency_type", orderInfo.getCurrencyType()), 1, + "总代出货金额", formatMoney(orderInfo.getShipmentAmount()), 3); + addSixColumnRow(table, baseFont, "要求到货时间", formatDate(orderInfo.getDeliveryTime()), 1, + "公司直发", firstNonEmpty(orderInfo.getCompanyDeliveryDesc(), DictUtils.getDictLabel("company_delivery", orderInfo.getCompanyDelivery())), 3); + addSixColumnRow(table, baseFont, "下单通路", firstNonEmpty(orderInfo.getOrderChannelDesc(), resolveOrderChannel(orderInfo.getOrderChannel())), 1, + "供货商", orderInfo.getSupplier(), 3); + addSixColumnRow(table, baseFont, "汇智责任人", orderInfo.getDutyName(), 1, "Email", orderInfo.getDutyEmail(), 1, + "联系方式", orderInfo.getDutyPhone(), 1); + addSixColumnRow(table, baseFont, "进货商", orderInfo.getPartnerName(), 3, "进货商类型", + firstNonEmpty(orderInfo.getLevelDesc(), DictUtils.getDictLabel("identify_level", orderInfo.getLevel())), 1); + addSixColumnRow(table, baseFont, "进货商联系人", orderInfo.getPartnerUserName(), 1, "Email", orderInfo.getPartnerEmail(), 1, + "联系方式", orderInfo.getPartnerPhone(), 1); + addSingleFieldRow(table, baseFont, "其他特别说明", orderInfo.getRemark()); + + document.add(table); + } + + private void addSixColumnRow(PdfPTable table, BaseFont baseFont, String label1, String value1, int value1Colspan, + String label2, String value2, int value2Colspan) { + table.addCell(createLabelCell(label1, baseFont)); + PdfPCell valueCell1 = createValueCell(value1, baseFont); + valueCell1.setColspan(value1Colspan); + table.addCell(valueCell1); + table.addCell(createLabelCell(label2, baseFont)); + PdfPCell valueCell2 = createValueCell(value2, baseFont); + valueCell2.setColspan(value2Colspan); + table.addCell(valueCell2); + } + + private void addSixColumnRow(PdfPTable table, BaseFont baseFont, String label1, String value1, int value1Colspan, + String label2, String value2, int value2Colspan, + String label3, String value3, int value3Colspan) { + table.addCell(createLabelCell(label1, baseFont)); + PdfPCell valueCell1 = createValueCell(value1, baseFont); + valueCell1.setColspan(value1Colspan); + table.addCell(valueCell1); + table.addCell(createLabelCell(label2, baseFont)); + PdfPCell valueCell2 = createValueCell(value2, baseFont); + valueCell2.setColspan(value2Colspan); + table.addCell(valueCell2); + table.addCell(createLabelCell(label3, baseFont)); + PdfPCell valueCell3 = createValueCell(value3, baseFont); + valueCell3.setColspan(value3Colspan); + table.addCell(valueCell3); + } + + private void addSingleFieldRow(PdfPTable table, BaseFont baseFont, String label, String value) { + table.addCell(createLabelCell(label, baseFont)); + PdfPCell valueCell = createValueCell(value, baseFont); + valueCell.setColspan(5); + table.addCell(valueCell); + } + + private PdfPCell createLabelCell(String value, BaseFont baseFont) { + return createTableCell(value, baseFont, 10, Element.ALIGN_LEFT, new java.awt.Color(248, 248, 248), true, false); + } + + private PdfPCell createValueCell(String value, BaseFont baseFont) { + return createTableCell(value, baseFont, 10, Element.ALIGN_LEFT, java.awt.Color.WHITE, false, true); + } + + private PdfPCell createTableCell(String value, BaseFont baseFont, int fontSize, int alignment, java.awt.Color backgroundColor, + boolean bold, boolean emptyAsBlank) { + com.lowagie.text.Font font = new com.lowagie.text.Font(baseFont, fontSize, + bold ? com.lowagie.text.Font.BOLD : com.lowagie.text.Font.NORMAL); + String displayValue = emptyAsBlank ? safeEmptyValue(value) : safeText(value); + PdfPCell cell = new PdfPCell(new Phrase(displayValue, font)); + cell.setHorizontalAlignment(alignment); + cell.setVerticalAlignment(Element.ALIGN_MIDDLE); + cell.setPadding(4); + cell.setMinimumHeight(26f); + cell.setBackgroundColor(backgroundColor); + cell.setBorderColor(new java.awt.Color(220, 220, 220)); + cell.setBorderWidth(0.7f); + return cell; + } + + private PdfPCell createBorderlessCell(String value, BaseFont baseFont, int fontSize, int alignment, + java.awt.Color fontColor, boolean bold) { + com.lowagie.text.Font font = new com.lowagie.text.Font(baseFont, fontSize, + bold ? com.lowagie.text.Font.BOLD : com.lowagie.text.Font.NORMAL, fontColor); + PdfPCell cell = new PdfPCell(new Phrase(safeText(value), font)); + cell.setHorizontalAlignment(alignment); + cell.setVerticalAlignment(Element.ALIGN_MIDDLE); + cell.setBorder(Rectangle.NO_BORDER); + cell.setPadding(0); + return cell; + } + + private String resolveOrderChannel(String orderChannel) { + if (ProjectOrderInfo.OrderChannelEnum.TOTAL_GENERATION.getCode().equals(orderChannel)) { + return "总代"; + } + if (ProjectOrderInfo.OrderChannelEnum.DIRECT_SIGNING.getCode().equals(orderChannel)) { + return "直签"; + } + return ""; + } + + private String resolvePaymentMethodLabel(ProjectOrderInfo orderInfo) { + if (StringUtils.isEmpty(orderInfo.getPaymentMethod())) { + return ""; + } + if (ProjectOrderInfo.OrderChannelEnum.TOTAL_GENERATION.getCode().equals(orderInfo.getOrderChannel())) { + if ("1-1".equals(orderInfo.getPaymentMethod())) { + return "全款支付,无需预付款"; + } + if ("1-2".equals(orderInfo.getPaymentMethod())) { + return "全款支付,需单独备货生产,预付订单总货款百分比"; + } + } + if (ProjectOrderInfo.OrderChannelEnum.DIRECT_SIGNING.getCode().equals(orderInfo.getOrderChannel())) { + if ("2-1".equals(orderInfo.getPaymentMethod())) { + return "全款支付"; + } + if ("2-2".equals(orderInfo.getPaymentMethod())) { + return "全款支付,需单独备货生产,预付订单总货款百分比"; + } + if ("2-3".equals(orderInfo.getPaymentMethod())) { + return "商业汇票支付,预付订单总货款百分比"; + } + } + return orderInfo.getPaymentMethod(); + } + + private String resolveCurrentUserId() { + try { + Long userId = ShiroUtils.getUserId(); + return userId == null ? "" : String.valueOf(userId); + } catch (Exception e) { + return ""; + } + } + + private String resolveCurrentUserName() { + try { + SysUser sysUser = getSysUser(); + return sysUser == null ? "" : sysUser.getUserName(); + } catch (Exception e) { + return ""; + } + } + + private String formatMoney(BigDecimal value) { + if (value == null) { + return ""; + } + return new DecimalFormat("#,##0.00").format(value); + } + + private String formatDate(Date date) { + return date == null ? "-" : DateUtil.format(date, "yyyy-MM-dd"); + } + + private String safeText(Object value) { + if (value == null) { + return "-"; + } + String text = String.valueOf(value).trim(); + return StringUtils.isEmpty(text) ? "-" : text; + } + + private String firstNonEmpty(String first, String second) { + return StringUtils.isNotEmpty(first) ? first : second; + } + + private String safeEmpty(String value) { + return value == null ? "" : value.trim(); + } + + private String safeEmptyValue(String value) { + return value == null ? "" : value.trim(); + } + + private static class ByteArrayMultipartFile implements MultipartFile { + private final String name; + private final String originalFilename; + private final String contentType; + private final byte[] content; + + private ByteArrayMultipartFile(String name, String originalFilename, String contentType, byte[] content) { + this.name = name; + this.originalFilename = originalFilename; + this.contentType = contentType; + this.content = content == null ? new byte[0] : content; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getOriginalFilename() { + return originalFilename; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public boolean isEmpty() { + return content.length == 0; + } + + @Override + public long getSize() { + return content.length; + } + + @Override + public byte[] getBytes() { + return content; + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(content); + } + + @Override + public void transferTo(File dest) throws IOException { + Files.write(dest.toPath(), content); + } + } + @Override public byte[] exportContractTemplate( ProjectOrderInfo orderInfo) { try { diff --git a/ruoyi-sip/src/main/resources/mapper/sip/ProjectOrderFileLogMapper.xml b/ruoyi-sip/src/main/resources/mapper/sip/ProjectOrderFileLogMapper.xml index e5d628aa..34e400b6 100644 --- a/ruoyi-sip/src/main/resources/mapper/sip/ProjectOrderFileLogMapper.xml +++ b/ruoyi-sip/src/main/resources/mapper/sip/ProjectOrderFileLogMapper.xml @@ -12,6 +12,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + @@ -30,6 +32,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" andt1. upload_time = #{uploadTime} and t1.file_path = #{filePath} and t1.file_type = #{fileType} + and t1.file_sort = #{fileSort} + and t1.file_version_code = #{fileVersionCode} order by t1.id @@ -116,4 +120,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - \ No newline at end of file + diff --git a/ruoyi-sip/src/main/resources/static/img/companyLogoHD.jpg b/ruoyi-sip/src/main/resources/static/img/companyLogoHD.jpg new file mode 100644 index 00000000..fbb17352 Binary files /dev/null and b/ruoyi-sip/src/main/resources/static/img/companyLogoHD.jpg differ