Springboot结合thymeleaf模板生成pdf文件

1. 导入依赖

java 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
    <groupId>com.github.librepdf</groupId>
    <artifactId>openpdf</artifactId>
    <version>1.3.30</version>
</dependency>

<dependency>
    <groupId>org.xhtmlrenderer</groupId>
    <artifactId>flying-saucer-pdf-openpdf</artifactId>
    <version>9.4.0</version>
</dependency>

2. 创建Html模板

在resource/template下创建模板文件,需要注意的是样式必须写在style标签内,不能写在标签内部,否则无效。

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
<head>
    <meta charset="UTF-8"/>
    <title>处方签</title>
    <style>
    @page {
        size: 148mm 210mm;
        margin: 0;
    }
    
    body {
        font-family: "SimSun", sans-serif;
        font-size: 13px;
        line-height: 1.2;
        margin: 0;
        padding: 5mm;
        width: 138mm;
        height: 200mm;
        -fs-pdf-font-embed: embed;
        -fs-pdf-font-encoding: Identity-H;
    }
        
        .prescription-header {
            text-align: center;
            margin-bottom: 8mm;
            font-size: 24px;
            font-family: "KaiTi", sans-serif;
            font-weight: bold;
        }
        .hospital-name {
            margin-bottom: 1mm;
        }

        .rp-header {
            font-size: 26px;
            margin: 2mm 0;
        }
        
        .medicine-table {
            width: 100%;
            border-collapse: collapse;
            margin: 2mm 0;
        }
        
        .medicine-table th,
        .medicine-table td {
            padding: 0.5mm;
            text-align: left;
        }
        
        .medicine-table th {
            font-weight: bold;
        }
        

        .signature-area {
            margin-top: 4mm;
            padding-top: 2mm;
        }
        
        .signature-table {
            width: 100%;
            border-collapse: collapse;
        }
        
        .signature-table td {
            text-align: left;
            vertical-align: top;
            padding: 1mm;
        }
        
        .signature-title {
            margin-bottom: 2mm;
        }

        .signature-image {
            width: 40px;
            height: 20px;
        }
        
        .dynamic-field {
            font-weight: normal;
        }

        .border-bottom{
            border-bottom: 1px solid #000;
            margin-bottom: 1mm;
            padding-bottom: 1mm;
        }
        .margin-top2{
            margin-top: 2mm;
        }

        .margin-left20 {
            margin-left: 20px;
        }

    .margin-left60 {
        margin-left: 60px;
    }
    .thw{
        width: 100px;
    }
    </style>
</head>
<body>
    <div class="prescription-header">
        <div class="hospital-name" th:text="${prescriptionInfo.hospitalName}">中山大学附属第三医院肇庆医院</div>
        <div class="prescription-title">互联网医院处方笺</div>
    </div>
    
    <div>
        <div class="border-bottom">
            <span>费别:</span><span th:text="${prescriptionInfo.paymentType}">自费</span>
            <span class="margin-left20">医保卡号:</span><span class="txt-label-end" th:text="${prescriptionInfo.medicalInsuranceNo}">XXXXXXXXXX</span>
            <span class="margin-left20">处方编号:</span><span class="txt-label-end" th:text="${prescriptionInfo.prescriptionNo}">XXXXXXXXXXXX</span>
        </div>

        <div>
            <span>姓名:</span><span class="txt-label-end" th:text="${patientInfo.name}">张三三</span>
            <span class="margin-left60">性别:</span><span class="txt-label-end" th:text="${patientInfo.gender}">男</span>
            <span class="margin-left60">年龄:</span><span class="txt-label-end" th:text="${patientInfo.age+ '岁'}">40岁</span>
            <br/>

            <div class="margin-top2">
                <span>门诊号:</span>
                <span class="txt-label-end" th:text="${patientInfo.medicalRecordNo}">XXXXXXXXXXXXX</span>
                <span class="margin-left60">科别:</span>
                <span class="txt-label-end" th:text="${prescriptionInfo.department}">内科互联网医院门诊</span>
            </div>

            <div class="margin-top2">
                <span>临床诊断:</span><span th:text="${prescriptionInfo.clinicalDiagnosis}">发烧,肺炎,肺部感染</span>
            </div>


            <div class="margin-top2">
                <span>开具日期:</span><span th:text="${prescriptionInfo.issueDate}">2026-01-12 19:11:01</span>
            </div>


            <div class="margin-top2 border-bottom">
                <span>住址/电话:</span><span th:text="${patientInfo.address + ' / ' + patientInfo.phone}">广东省广州市天河区凤凰街道1号 / 1300000000</span>
            </div>
        </div>
    </div>
    
    <div class="rp-header">Rp</div>
    
    <table class="medicine-table">
        <thead>
            <tr>
                <th>药品名称</th>
                <th>规格</th>
                <th>单次剂量</th>
                <th>用法</th>
                <th>频次</th>
                <th>总量</th>
                <th>单价(元)</th>
                <th>总价(元)</th>
            </tr>
        </thead>
        <tbody>
            <tr th:each="item : ${medicineItems}">
                <td class="dynamic-field thw" th:text="${item.medicineName}">(基)氯硝西泮片(氯硝安定)(恩华)(精二)(甲)2mgx100片/瓶</td>
                <td class="dynamic-field" th:text="${item.specification}">228mg * 24粒</td>
                <td class="dynamic-field" th:text="${item.dosage}">2粒</td>
                <td class="dynamic-field" th:text="${item.usage}">随餐服用</td>
                <td class="dynamic-field" th:text="${item.frequency}">每天三次</td>
                <td class="dynamic-field" th:text="${item.quantity}">4盒</td>
                <td class="dynamic-field" th:text="${item.unitPrice}">30.96</td>
                <td class="dynamic-field" th:text="${item.totalPrice}">123.84</td>
            </tr>
        </tbody>
    </table>

    
    <div class="signature-area">
        <table class="signature-table">
            <tr>
                <td>
                    <div class="signature-title">
                        <span class="doctor-label">医 &nbsp;&nbsp;师:</span>
                        <img th:if="${medicalStaff.physician.signatureImage}" class="signature-image" th:src="'data:image/png;base64,' + ${medicalStaff.physician.signatureImage}" alt=""/>

                    </div>
                </td>
                <td class="signature-title">
                    <span style="margin-left: 60px;">药品金额(元):</span>
                    <span class="dynamic-field" th:text="${totalAmount}">206.16</span>
                </td>
            </tr>
        </table>

        <table class="signature-table border-bottom">
            <tr>
                <td>
                    <div class="signature-title">
                        审核药师:
                        <img th:if="${medicalStaff.physician.signatureImage}" class="signature-image" th:src="'data:image/png;base64,' + ${medicalStaff.reviewer.signatureImage}" alt=""/>
                    </div>

                </td>
                <td>
                    <div class="signature-title">
                        配药药师:
                        <img th:if="${medicalStaff.physician.signatureImage}" class="signature-image" th:src="'data:image/png;base64,' + ${medicalStaff.dispenser.signatureImage}" alt=""/>
                    </div>
                </td>
                <td>

                    <div class="signature-title">
                        核对、发药药师:
                        <img th:if="${medicalStaff.physician.signatureImage}" class="signature-image" th:src="'data:image/png;base64,' + ${medicalStaff.issuer.signatureImage}" alt=""/>
                    </div>
                </td>
            </tr>
        </table>
    </div>
    <div>
        <span>※注:处方金额以收费时为准!</span>
        <br/>
        <br/>
        <span>药师提示:</span>
        <br/>
        <span> 1.请遵医嘱服药;2.请缴费后到药房窗口取药并点清药品;3.处方当日有效;4.发出药品如无质量问题不予退换 </span>
    </div>
    
</body>
</html>

3. 渲染模板并转为pdf

java 复制代码
package com.chinaunicom.medical.ihm.pub.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.chinaunicom.medical.ihm.baseservice.api.dict.models.Base64FileVO;
import com.chinaunicom.medical.ihm.baseservice.service.FileService;
import com.chinaunicom.medical.ihm.core.utils.DesensitizedUtils;
import com.chinaunicom.medical.ihm.obs.healthcare.repository.model.Prescription;
import com.chinaunicom.medical.ihm.pub.mapper.PubPrescriptionMapper;
import com.chinaunicom.medical.ihm.pub.model.oo.*;
import com.chinaunicom.medical.ihm.pub.utils.MetaObjectUtil;
import com.lowagie.text.DocumentException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestParam;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.xhtmlrenderer.pdf.ITextRenderer;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class PrescriptionPdfService {

    private final TemplateEngine templateEngine;
    private final PubPrescriptionMapper pubPrescriptionMapper;
    private final FileService fileService;


    public String generatePrescriptionPdf(@RequestParam Long prescriptionId) {
        try {
            log.info("开始生成处方签PDF ,处方编号:{}", prescriptionId);

            PrescriptionDTO prescriptionDTO = getPrintData(prescriptionId);
            byte[] pdfBytes = generatePrescriptionPdf(prescriptionDTO);

            String pdfFileId = fileService.upload(pdfBytes, prescriptionId + ".pdf", MediaType.APPLICATION_PDF_VALUE, SpringUtil.getApplicationName(), "prescription-pdf", null);
            log.info("处方签PDF生成成功,处方id:{} 文件id:{},文件大小:{}字节", prescriptionId, pdfFileId, pdfBytes.length);

            //设置文件id
            pubPrescriptionMapper.update(new Prescription(), Wrappers.lambdaUpdate(Prescription.class).set(Prescription::getPdfFileId, pdfFileId).eq(Prescription::getId, prescriptionId));
            return pdfFileId;
        } catch (Exception e) {
            log.error("生成处方签PDF失败", e);
        }
        return "";
    }


    public PrescriptionDTO getPrintData(Long prescriptionId){
        //查询处方
        PrescriptionSlipDTO prescriptionSlipDTO = pubPrescriptionMapper.getPrescriptionSlipInfoById(prescriptionId);
        Validator.validateTrue(Objects.nonNull(prescriptionSlipDTO), "处方不存在");
        //查询药品
        List<PrescriptionSlipDrugDTO> prescriptionSlipDrugInfoList = pubPrescriptionMapper.getPrescriptionSlipDrugInfoById(prescriptionId);

        //查询频次编码
        List<FrequencyDict> frequencyDict = MetaObjectUtil.listByCode("Frequency_dict", FrequencyDict.class);
        Map<String, String> freqMap = CollUtil.emptyIfNull(frequencyDict).stream().collect(Collectors.toMap(FrequencyDict::getFrequencyCode, FrequencyDict::getFrequencyName, (oldValue, newValue) -> newValue));

        //查询用法编码
        List<DrugUsageDict> drugUsageDict = MetaObjectUtil.listByCode("Drug_usage_code", DrugUsageDict.class);
        Map<String, String> drugUsageMap = CollUtil.emptyIfNull(drugUsageDict).stream().collect(Collectors.toMap(DrugUsageDict::getDrugUsageCode, DrugUsageDict::getDrugUsageName, (oldValue, newValue) -> newValue));

        PrescriptionDTO prescriptionDTO = new PrescriptionDTO();

        //设置患者信息
        PrescriptionDTO.PatientInfo patientInfo = new PrescriptionDTO.PatientInfo();
        {
            patientInfo.setName(StrUtil.emptyIfNull(prescriptionSlipDTO.getPatientName()));
            if(Objects.nonNull(prescriptionSlipDTO.getPatientGender())){
                patientInfo.setGender(StrUtil.emptyIfNull(prescriptionSlipDTO.getPatientGender().getDesc()));
            }
            patientInfo.setAge(prescriptionSlipDTO.getPatientAge());
            patientInfo.setMedicalRecordNo(prescriptionSlipDTO.getTreatmentNo());
            patientInfo.setAddress(prescriptionSlipDTO.getPatientAddress());
            if(StrUtil.isNotBlank(prescriptionSlipDTO.getPatientPhone())){
                patientInfo.setPhone(DesensitizedUtils.decrypt(prescriptionSlipDTO.getPatientPhone()));
            }
        }

        //设置处方信息
        PrescriptionDTO.PrescriptionInfo prescriptionInfo = new PrescriptionDTO.PrescriptionInfo();
        {
            //医院名称
            prescriptionInfo.setHospitalName(prescriptionSlipDTO.getHospitalName());
            //费别
            if(prescriptionSlipDTO.getTreatmentType() != null){
                prescriptionInfo.setPaymentType(StrUtil.emptyIfNull(prescriptionSlipDTO.getTreatmentType().getDesc()));
            }else {
                prescriptionInfo.setPaymentType("");
            }
            //医保卡号
            prescriptionInfo.setMedicalInsuranceNo("");
            //处方编号
            prescriptionInfo.setPrescriptionNo(prescriptionSlipDTO.getPrescriptionNo());
            //科室名称
            prescriptionInfo.setDepartment(prescriptionSlipDTO.getDepartmentName());
            //临床诊断
            prescriptionInfo.setClinicalDiagnosis(prescriptionSlipDTO.getDiagnoseName());
            //开方日期
            prescriptionInfo.setIssueDate(prescriptionSlipDTO.getCreateTime());
        }

        //设置药品信息
        List<PrescriptionDTO.MedicineItem> medicineItems = prescriptionSlipDrugInfoList.stream().map(drug -> {
            PrescriptionDTO.MedicineItem info = new PrescriptionDTO.MedicineItem();
            info.setMedicineName(StrUtil.emptyIfNull(drug.getDrugName()));
            info.setSpecification(StrUtil.emptyIfNull(drug.getDrugSpec()));
            //单次剂量
            info.setDosage(StrUtil.join("",drug.getSingleDose(),drug.getDosageUnitCode()));
            //用法
            info.setUsage(StrUtil.emptyIfNull(drugUsageMap.get(drug.getDrugUsageCode())));
            //频次
            info.setFrequency(StrUtil.emptyIfNull(freqMap.getOrDefault(drug.getFrequencyCode(),drug.getFrequencyCode())));
            info.setQuantity(drug.getAmount());
            info.setUnitPrice(ObjectUtil.defaultIfNull(drug.getPrice(), BigDecimal.ZERO));
            info.setTotalPrice(ObjectUtil.defaultIfNull(drug.getTotalPrice(),BigDecimal.ZERO));
            return info;
        }).toList();

        //设置患者信息
        prescriptionDTO.setPatientInfo(patientInfo);
        //设置处方信息
        prescriptionDTO.setPrescriptionInfo(prescriptionInfo);
        //总金额
        prescriptionDTO.setTotalAmount(prescriptionSlipDTO.getTotalAmount());
        prescriptionDTO.setMedicineItems(medicineItems);

        PrescriptionDTO.MedicalStaff medicalStaff = new PrescriptionDTO.MedicalStaff();

        //医生签名
        PrescriptionDTO.StaffInfo physician = new PrescriptionDTO.StaffInfo();
        medicalStaff.setPhysician(physician);
        try {
            Base64FileVO doctorFileBase64 = fileService.getFileBase64(prescriptionSlipDTO.getDoctorFileId(),null,null);
            if(Objects.nonNull(doctorFileBase64)){
                physician.setSignatureImage(doctorFileBase64.getBase64Str());
            }
        } catch (Exception e) {
        }


        //审核药师签名
        PrescriptionDTO.StaffInfo reviewer = new PrescriptionDTO.StaffInfo();
        medicalStaff.setReviewer(reviewer);
        try {
            if(StrUtil.isNotBlank(prescriptionSlipDTO.getAuditFileId())){
                Base64FileVO reViewFileBase64 = fileService.getFileBase64(prescriptionSlipDTO.getAuditFileId(),null,null);
                if(Objects.nonNull(reViewFileBase64)){
                    reviewer.setSignatureImage(reViewFileBase64.getBase64Str());
                }
            }
        } catch (Exception e) {
        }

        PrescriptionDTO.StaffInfo dispenser = new PrescriptionDTO.StaffInfo();
        dispenser.setSignatureImage("");
        medicalStaff.setDispenser(dispenser);

        PrescriptionDTO.StaffInfo issuer = new PrescriptionDTO.StaffInfo();
        issuer.setSignatureImage("");
        medicalStaff.setIssuer(issuer);

        prescriptionDTO.setMedicalStaff(medicalStaff);
        return prescriptionDTO;
    }


    /**
     * 生成处方签PDF
     * @param prescriptionDTO 处方数据
     * @return PDF字节数组
     */
    public byte[] generatePrescriptionPdf(PrescriptionDTO prescriptionDTO) {
        try {
            // 渲染HTML模板
            String htmlContent = renderHtmlTemplate(prescriptionDTO);
            // 将HTML转换为PDF
            return convertHtmlToPdf(htmlContent);
        } catch (Exception e) {
            log.error("生成处方签PDF失败", e);
            throw new RuntimeException("生成处方签PDF失败: " + e.getMessage(), e);
        }
    }

    /**
     * 渲染HTML模板
     * @param prescriptionDTO 处方数据
     * @return HTML内容
     */
    private String renderHtmlTemplate(PrescriptionDTO prescriptionDTO) {
        Context context = new Context();
        
        // 设置模板变量
        context.setVariable("patientInfo", prescriptionDTO.getPatientInfo());
        context.setVariable("prescriptionInfo", prescriptionDTO.getPrescriptionInfo());
        context.setVariable("medicineItems", prescriptionDTO.getMedicineItems());
        context.setVariable("medicalStaff", prescriptionDTO.getMedicalStaff());
        context.setVariable("totalAmount", prescriptionDTO.getTotalAmount());
        
        // 渲染模板
        return templateEngine.process("prescription-template", context);
    }

    /**
     * 将HTML转换为PDF
     * @param htmlContent HTML内容
     * @return PDF字节数组
     */
    private byte[] convertHtmlToPdf(String htmlContent) throws DocumentException, IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        
        try {
            ITextRenderer renderer = new ITextRenderer();
            // 设置字体支持
            setFonts(renderer);
            
            // 设置页面大小
            renderer.setDocumentFromString(htmlContent);
            renderer.layout();
            
            // 生成PDF
            renderer.createPDF(outputStream);
            
        } finally {
            if (outputStream != null) {
                outputStream.close();
            }
        }
        
        return outputStream.toByteArray();
    }
    
    /**
     * 设置中文字体支持
     * @param renderer PDF渲染器
     */
    private void setFonts(ITextRenderer renderer) {

        List<String> fontNames = Arrays.asList("simsun.ttc", "simkai.ttf");

        for (String fontName : fontNames) {
            try {
            // 获取字体文件路径
            java.net.URL fontUrl = getClass().getClassLoader().getResource("font/"+fontName);
            if (fontUrl != null) {
                //注册字体
                log.info("加载字体: {}", fontName);
                renderer.getFontResolver().addFont(fontUrl.getPath(), com.lowagie.text.pdf.BaseFont.IDENTITY_H, com.lowagie.text.pdf.BaseFont.NOT_EMBEDDED);
            }
            } catch (Exception e) {
                log.error("加载字体失败: {}", e.getMessage());
            }
        }
    }

}
相关推荐
IT_陈寒2 小时前
SpringBoot 3.2实战:5个性能优化技巧让你的应用提速50%
前端·人工智能·后端
上进小菜猪2 小时前
基于 YOLOv8 的农作物叶片病害、叶片病斑精准识别项目 [目标检测完整源码]
后端
老毛肚2 小时前
Spring源码探究2.0
java·后端·spring
程序员鱼皮2 小时前
你的 IP 归属地,是咋被挖出来的?
前端·后端·计算机·程序员·互联网·编程经验
南风微微吹3 小时前
2025年12月英语四级真题及答案解析完整版(第一、二、三套全PDF)
pdf·英语四级真题
Coder_Boy_3 小时前
基于SpringAI的在线考试系统软件系统验收案例
人工智能·spring boot·软件工程·devops
fisher_sky3 小时前
流媒体服务mediamtx和FFMpeg工具链联合实验
后端
qq_12498707533 小时前
基于SSM框架的智能密室逃脱信息管理系统(源码+论文+部署+安装)
java·大数据·人工智能·spring boot·后端·毕业设计·计算机毕业设计
掉鱼的猫3 小时前
从 Chat 到 Agent:Solon AI 带你进入“行动派”大模型时代
后端