Pu Zhibing
2025-08-01 926e065fb0b4424d0d51023c234a92433bac61c8
UserQYTTravel/guns-admin/src/main/java/com/stylefeng/guns/modular/system/pdf/TripSheetGenerator.java
New file
@@ -0,0 +1,337 @@
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);
        }
    }
}