New file |
| | |
| | | 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<TripOrderVo> 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(10, 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(35); |
| | | 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<TripOrderVo> 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[]{4, 200, 4, 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[]{4, 200, 4, 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<TripOrderVo> 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(5); |
| | | cell.setHorizontalAlignment(Element.ALIGN_CENTER); |
| | | table.addCell(cell); |
| | | } |
| | | SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 格式可根据需求调整 |
| | | // 单元格样式(居中对齐,统一处理) |
| | | Font cellFont = getChineseFont(9, Font.NORMAL); |
| | | 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); |
| | | table.addCell(moneyCell); |
| | | } |
| | | |
| | | document.add(table); |
| | | } |
| | | |
| | | // 辅助方法:创建居中对齐的单元格 |
| | | private void addCenteredCell(PdfPTable table, String text, Font font) { |
| | | PdfPCell cell = new PdfPCell(new Paragraph(text != null ? text : "", font)); |
| | | cell.setPadding(5); |
| | | cell.setHorizontalAlignment(Element.ALIGN_CENTER); |
| | | table.addCell(cell); |
| | | } |
| | | |
| | | private void addTableCell(PdfPTable table, String text, Font font) { |
| | | PdfPCell cell = new PdfPCell(new Paragraph(text != null ? text : "", font)); |
| | | cell.setPadding(5); |
| | | 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); |
| | | } |
| | | } |
| | | } |