package com.stylefeng.guns.modular.system.pdf; import com.itextpdf.text.*; import com.itextpdf.text.pdf.BaseFont; import com.itextpdf.text.pdf.PdfPCell; import com.itextpdf.text.pdf.PdfPTable; import com.itextpdf.text.pdf.PdfWriter; import com.stylefeng.guns.modular.system.model.vo.TripOrderVo; import com.stylefeng.guns.modular.system.warpper.OrderWarpper; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; import org.springframework.web.context.ServletContextAware; import java.io.*; import java.net.URL; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.UUID; @Component public class TripSheetGenerator { @Value("${trip.sheet.filePath}") private String pdfDir; // 内置中文备用字体 private static final String FALLBACK_FONT = "STSong-Light"; private static final String FALLBACK_ENCODING = "UniGB-UCS2-H"; // 完整日期时间格式 private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public String generatePdf(List orders) throws DocumentException, IOException { if (orders == null || orders.isEmpty()) { throw new IllegalArgumentException("订单列表不能为空"); } String fileName = "贵人家园行程单_" + UUID.randomUUID() + ".pdf"; String filePath = pdfDir + fileName; File file = new File(filePath); FileUtils.forceMkdirParent(file); Document document = new Document(PageSize.A4); PdfWriter.getInstance(document, new FileOutputStream(file)); document.open(); // 新布局:Logo+标题 → 主标题 → 信息行 → 表格 addLogoAndTitle(document); addMainTitle(document); addTripInfo(document, orders); addOrderTable(document, orders); document.close(); return filePath; } private void addLogoAndTitle(Document document) throws DocumentException { try { document.setMargins(0, 50, 50, 50); // 左=0,右=50,上=50,下=50(单位:pt) Resource logoResource = new ClassPathResource("static/img/logo.png"); if (logoResource.exists()) { Image logo = Image.getInstance(logoResource.getURL()); logo.scaleToFit(80, 40); // 保持logo原有尺寸(宽100pt,高40pt) // 1. 调整表格列宽:左列刚好容纳logo,右列仅够放下文字(缩小间距) // 列宽比例:左列100pt(logo宽度),右列80pt(文字宽度,足够放下“贵人家园”) PdfPTable layoutTable = new PdfPTable(new float[]{81f, 100f}); // 左列(Logo),右列100f(文字) layoutTable.setWidthPercentage(25); layoutTable.setSpacingAfter(15f); // 与下方主标题的间距不变 layoutTable.setHorizontalAlignment(Element.ALIGN_LEFT); // 整体左对齐,避免居中导致的空隙 // 2. 左单元格(logo):消除内边距,紧贴右侧 PdfPCell logoCell = new PdfPCell(logo); logoCell.setBorder(Rectangle.NO_BORDER); // 无边框 logoCell.setPadding(0); // 去除内边距(关键:默认padding会导致空隙) logoCell.setVerticalAlignment(Element.ALIGN_MIDDLE); // 垂直居中对齐 logoCell.setHorizontalAlignment(Element.ALIGN_RIGHT); //logo居中 layoutTable.addCell(logoCell); // 3. 右单元格(文字):消除内边距,紧贴左侧 Font titleFont = getChineseFont(18, Font.BOLD); Paragraph titlePara = new Paragraph("贵人家园", titleFont); titlePara.setAlignment(Element.ALIGN_LEFT); // 文字左对齐,贴近logo titlePara.setSpacingBefore(0); titlePara.setSpacingAfter(0); PdfPCell titleCell = new PdfPCell(titlePara); titleCell.setBorder(Rectangle.NO_BORDER); // 无边框 titleCell.setPadding(0); // 去除内边距(关键) titleCell.setVerticalAlignment(Element.ALIGN_MIDDLE); // 垂直居中对齐 titleCell.setHorizontalAlignment(Element.ALIGN_LEFT); // 文字靠左,贴近logo layoutTable.addCell(titleCell); document.add(layoutTable); // 关键3:恢复页面默认边距(避免影响后续内容) document.setMargins(50, 50, 50, 50); } else { // 降级处理(无logo时) Font titleFont = getChineseFont(18, Font.BOLD); Paragraph fallbackTitle = new Paragraph("贵人家园", titleFont); fallbackTitle.setAlignment(Element.ALIGN_LEFT); fallbackTitle.setSpacingAfter(15f); document.add(fallbackTitle); } } catch (Exception e) { System.err.println("Logo加载失败: " + e.getMessage()); // 降级处理 Font titleFont = getChineseFont(18, Font.BOLD); Paragraph fallbackTitle = new Paragraph("贵人家园", titleFont); fallbackTitle.setAlignment(Element.ALIGN_LEFT); fallbackTitle.setSpacingAfter(15f); document.add(fallbackTitle); } } // 新增主标题方法(原 addTitle 逻辑调整) private void addMainTitle(Document document) throws DocumentException { Font titleFont = getChineseFont(18, Font.BOLD); Paragraph mainTitle = new Paragraph("贵人家园—打车—行程单", titleFont); mainTitle.setAlignment(Element.ALIGN_CENTER); mainTitle.setSpacingBefore(5f); mainTitle.setSpacingAfter(15f); document.add(mainTitle); } // 修改信息行构建逻辑(以第一行为例) private void addTripInfo(Document document, List orders) throws DocumentException { if (orders.isEmpty()) return; TripOrderVo first = orders.get(0); TripOrderVo last = orders.size() > 1 ? orders.get(orders.size() - 1) : first; Font infoFont = getChineseFont(10, Font.NORMAL); // 申请时间现在的时间 String applyTime = DATE_TIME_FORMATTER.format(LocalDateTime.now()); // 首先定义SimpleDateFormat(可以是类的静态成员) SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String tripTimeStart = first.getBoardingTime() != null ? DATE_FORMATTER.format(first.getBoardingTime()) : "N/A"; String tripTimeEnd = last.getBoardingTime() != null ? DATE_FORMATTER.format(last.getBoardingTime()) : "N/A"; String tripTime = tripTimeStart + " 至 " + tripTimeEnd; // 总金额计算(修复:先定义 totalText 并拼接内容) double totalAmount = orders.stream() .mapToDouble(o -> o.getPayMoney() != null ? o.getPayMoney() : 0) .sum(); String totalPrefix = "共计" + orders.size() + "单行程,合计"; String totalMoney = String.format("%.2f元", totalAmount); String totalText = totalPrefix + totalMoney; // 正确定义 totalText // ========== 第一行:申请时间 + 行程时间(均带分隔条) ========== // 关键:通过表格列宽控制分隔条宽度(8pt) PdfPTable line1Table = new PdfPTable(4); line1Table.setWidthPercentage(100); line1Table.setSpacingAfter(5f); // 列宽比例:分隔条列固定为8pt,内容列按比例分配 line1Table.setWidths(new float[]{1, 300, 1, 374}); // 列1:申请时间前的蓝色分隔条(宽度由表格列宽控制) line1Table.addCell(createBlueSeparatorCell()); // 列2:申请时间内容 Paragraph applyPara = new Paragraph(); applyPara.add(new Chunk("申请时间:", infoFont)); applyPara.add(new Chunk(applyTime, infoFont)); line1Table.addCell(createContentCell(applyPara)); // 列3:行程时间前的蓝色分隔条(与列1宽度一致) line1Table.addCell(createBlueSeparatorCell()); // 列4:行程时间内容 Paragraph tripPara = new Paragraph(); tripPara.add(new Chunk("行程时间:", infoFont)); tripPara.add(new Chunk(tripTime, infoFont)); line1Table.addCell(createContentCell(tripPara)); document.add(line1Table); // ========== 第二行:手机号 + 共计订单(均带分隔条) ========== PdfPTable line2Table = new PdfPTable(4); line2Table.setWidthPercentage(100); line2Table.setSpacingAfter(15f); // 与第一行保持完全相同的列宽比例(确保分隔条对齐) line2Table.setWidths(new float[]{1, 300, 1, 374}); // 列1:手机号前的蓝色分隔条 line2Table.addCell(createBlueSeparatorCell()); // 列2:手机号内容 Paragraph phonePara = new Paragraph(); phonePara.add(new Chunk("行程人手机号:", infoFont)); phonePara.add(new Chunk(first.getPassengersPhone() != null ? first.getPassengersPhone() : "N/A", infoFont)); line2Table.addCell(createContentCell(phonePara)); // 列3:共计订单前的蓝色分隔条 line2Table.addCell(createBlueSeparatorCell()); // 列4:共计订单内容 Paragraph totalPara = new Paragraph(); totalPara.add(new Chunk(totalPrefix, infoFont)); Font totalFont = getChineseFont(10, Font.BOLD); totalFont.setColor(BaseColor.RED); totalPara.add(new Chunk(totalMoney, totalFont)); line2Table.addCell(createContentCell(totalPara)); document.add(line2Table); } /** * 蓝色分隔条单元格(宽度由所在表格的列宽控制,无需单独设置) */ private PdfPCell createBlueSeparatorCell() { PdfPCell separatorCell = new PdfPCell(); // 关键:不设置宽度,完全由表格的 setWidths() 控制 separatorCell.setBackgroundColor(BaseColor.BLUE); // 蓝色背景 separatorCell.setBorder(Rectangle.NO_BORDER); // 无边框 separatorCell.setMinimumHeight(8); // 匹配文字行高 separatorCell.setPadding(0); // 去除内边距,确保分隔条紧凑 return separatorCell; } /** * 内容单元格样式 */ private PdfPCell createContentCell(Paragraph content) { PdfPCell cell = new PdfPCell(content); cell.setBorder(Rectangle.NO_BORDER); cell.setPaddingLeft(5); // 内容与分隔条的间距 cell.setHorizontalAlignment(Element.ALIGN_LEFT); return cell; } private void addOrderTable(Document document, List orders) throws DocumentException { // 调整列宽:第1列(序号)加宽至1.5f float[] columnWidths = {1.5f, 2f, 2f, 3f, 2f, 3f, 3f, 2f}; PdfPTable table = new PdfPTable(columnWidths); table.setWidthPercentage(100); table.setSpacingBefore(10f); // 与上方信息行的间距 // 表头样式(蓝色背景,白色文字,居中) Font headerFont = getChineseFont(10, Font.BOLD); headerFont.setColor(BaseColor.WHITE); BaseColor headerBg = new BaseColor(59, 130, 246); String[] headers = {"序号","服务商","车型","上车时间","城市","起点","终点","金额"}; for (String header : headers) { PdfPCell cell = new PdfPCell(new Paragraph(header, headerFont)); cell.setBackgroundColor(headerBg); cell.setPadding(8); cell.setHorizontalAlignment(Element.ALIGN_CENTER); cell.setBorder(Rectangle.NO_BORDER); // 无边框 table.addCell(cell); } SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 格式可根据需求调整 // 单元格样式(居中对齐,统一处理) Font cellFont = getChineseFont(9, Font.NORMAL); cellFont.setColor(new BaseColor(80, 80, 80)); for (int i = 0; i < orders.size(); i++) { TripOrderVo order = orders.get(i); // 序号列:强制居中,内容为 i+1 addCenteredCell(table, String.valueOf(i + 1), cellFont); addCenteredCell(table, order.getCompanyName(), cellFont); addCenteredCell(table, order.getServerCarModel(), cellFont); addCenteredCell(table, order.getBoardingTime() != null ? DATE_FORMATTER.format(order.getBoardingTime()) // Date类型用SimpleDateFormat的format方法 : "N/A", cellFont); addCenteredCell(table, order.getCity(), cellFont); addCenteredCell(table, order.getStartAddress(), cellFont); addCenteredCell(table, order.getEndAddress(), cellFont); // 金额列:右对齐 PdfPCell moneyCell = new PdfPCell( new Paragraph(String.format("%.2f元", order.getPayMoney() != null ? order.getPayMoney() : 0), cellFont) ); moneyCell.setHorizontalAlignment(Element.ALIGN_RIGHT); moneyCell.setPadding(5); moneyCell.setBorder(Rectangle.NO_BORDER); // 无边框 moneyCell.setBorderWidthBottom(0.5f); moneyCell.setBorderColorBottom(new BaseColor(190, 190, 190)); table.addCell(moneyCell); } document.add(table); } // 辅助方法:创建居中对齐的单元格 private void addCenteredCell(PdfPTable table, String text, Font font) { Paragraph elements = new Paragraph(text != null ? text : "", font); PdfPCell cell = new PdfPCell(elements); cell.setPadding(5); cell.setHorizontalAlignment(Element.ALIGN_CENTER); cell.setBorder(Rectangle.NO_BORDER); // 无边框 cell.setBorderWidthBottom(0.5f); cell.setBorderColorBottom(new BaseColor(190, 190, 190)); table.addCell(cell); } /** * 获取中文字体,优先自定义字体,fallback到 CJK 内置 */ private Font getChineseFont(float size, int style) { // 自定义字体 try { Resource res = new ClassPathResource("static/fonts/AlibabaPuHuiTi-3-105-Heavy.ttf"); if (res.exists()) { BaseFont bf = BaseFont.createFont( res.getURL().toString(), BaseFont.IDENTITY_H, BaseFont.EMBEDDED); return new Font(bf, size, style); } } catch (Exception e) { System.err.println("加载自定义字体失败: " + e.getMessage()); } // 内置 CJK 字体 try { BaseFont bf = BaseFont.createFont( FALLBACK_FONT, FALLBACK_ENCODING, BaseFont.EMBEDDED); return new Font(bf, size, style); } catch (Exception e) { System.err.println("加载备用字体失败: " + e.getMessage()); return new Font(Font.FontFamily.HELVETICA, size, style); } } }