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);
|
}
|
}
|
}
|