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">医 师:</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());
}
}
}
}