java使用poi-tl模版+vform自定义表单生成word文件
模版文件

生成文件

java
/**
* 直接下载启动会预约单
* @param id
* @param response
*/
@Override
public void exportAppointment(Long id, HttpServletResponse response) {
TbProjectStart tbProjectStart = tbProjectStartMapper.selectTbProjectStartById(id);
ProjectInitiation initiation = projectInitiationService.findById(tbProjectStart.getProjectInitiationId(), null);
//获取数据
Map<String, Object> data = getMapData(initiation, tbProjectStart);
//模板选择
String url = "classpath:templates/start_yuyue.docx";
String fileName=initiation.getAcceptanceNumber().replace(" ", "")+ "_启动会预约确认单_" + DateUtils.dateTimeNow();
//// // 响应返回word文件
PoiTlWord.generateWordRespose(response,url,data,resourceLoader,fileName + ".docx");
// //地址返回word文件
// String targetDirPath = RuoYiConfig.getUploadPath("/start/download/");
// String fileUrl=PoiTlWord.generateWordToPath(url,data,resourceLoader,fileName + ".docx",targetDirPath);
// System.out.println(fileUrl);
//响应返回 pdf 文件
//WordToPdf.generatePdfRespose(response,url,data,resourceLoader,libreoffice,fileName + ".pdf");
// //地址返回 pdf文件
// String targetDirPath = RuoYiConfig.getUploadPath("/start/download/");
// String fileUrl=WordToPdf.generatePdfToPath(url,data,resourceLoader,libreoffice,fileName + ".pdf",targetDirPath);
// System.out.println(fileUrl);
}
/**
* 获取数据
* @param initiation
* @param tbProjectStart
* @return
*/
private Map<String, Object> getMapData(ProjectInitiation initiation, TbProjectStart tbProjectStart) {
Map<String, Object> data = new HashMap<>();
data.put("projectName", initiation.getProjectName());
//主要研究者+辅助研究着
String masterInvestigator = initiation.getPrincipalInvestigator() +" "+ (initiation.getAuxiliaryInvestigator()!=null? initiation.getAuxiliaryInvestigator():"");
data.put("masterInvestigator", masterInvestigator);
//申办者
data.put("sponsorName", initiation.getSponsorName());
data.put("craNames", initiation.getCraNames());
data.put("crcNames", initiation.getCrcNames());
//签字时间
data.put("signData","");
if(tbProjectStart.getJgApprovalTime()!=null){
data.put("signData",DateUtils.parseDateToStr("yyyy-MM-dd",tbProjectStart.getJgApprovalTime()));
}
//获取自定义表单内容
WfForm wfForm1=formService.queryById(9L);
JSONObject modelJson=JSONObject.parseObject(wfForm1.getContent());
Map<String,JSONObject> formMap=new HashMap<>();
PoiTlWord.getWidgetListMap(formMap,modelJson.getString("widgetList"));
//获取自定义表单数据
TbProjectStartVform tbProjectStartVform = new TbProjectStartVform();
tbProjectStartVform.setStartId(tbProjectStart.getId());
List<TbProjectStartVform> tbProjectStartVforms = tbProjectStartVformMapper.selectList(tbProjectStartVform);
List<TemplateTemp> templateList=new ArrayList<>();
tbProjectStartVforms.forEach(s -> {
if(s.getDataValue()!=null){
TemplateTemp temp = new TemplateTemp();
temp.setName(formMap.get(s.getDataName()).getString("label"));
StringBuilder value= new StringBuilder();
if(s.getDataType().equals("radio")||s.getDataType().equals("checkbox")){
//单选 复选框
JSONArray optionItems=formMap.get(s.getDataName()).getJSONArray("optionItems");
for(int i=0;i<optionItems.size();i++){
JSONObject optionItem=optionItems.getJSONObject(i);
System.out.println(s.getDataValue()+"---"+optionItem.get("value")+"-----"+optionItem.getString("label")+"---"+s.getDataLabel());
value.append(s.getDataValue().equals(optionItem.get("value").toString()) ? " ☒" : " ☐").append(optionItem.getString("label"));
}
}else if(s.getDataType().equals("picture-upload")){
//图片上传的处理
JSONArray jsonArray=JSONArray.parseArray(s.getDataValue());
List<Map<String, PictureRenderData>> list = new ArrayList<>();
for(int i=0;i<jsonArray.size();i++){
JSONObject jsonObject1=jsonArray.getJSONObject(i);
System.out.println(jsonObject1.getString("url"));
list.add(PoiTlWord.createPictureMap(profile,jsonObject1.getString("url"),100,100));
}
temp.setImgs(list);
}else {
value.append(s.getDataValue());
}
temp.setValue(value.toString());
templateList.add(temp);
}
});
data.put("templateList", templateList);
return data;
}
java
public class PoiTlWord {
/**
* 生成 Word 并保存到指定路径,返回文件访问 URL(或完整路径)
* @param url 模板路径(如 classpath:templates/xxx.docx)
* @param data 模板渲染数据
* @param resourceLoader Spring 资源加载器
* @param fileStr 自定义 Word 文件名(不含路径,如 "启动会预约单";若为 null 则自动生成)
* @param targetDirPath Word 保存的目标目录(如 "D:/word/export" 或 "/data/word/export")
* @return 生成的 Word 文件完整访问 URL(如 "file:///D:/word/export/启动会预约单_20251106.docx")
*/
public static String generateWordToPath(String url, Map<String, Object> data,
ResourceLoader resourceLoader, String fileStr,
String targetDirPath) {
// ========== 1. 初始化目标目录和文件名 ==========
// 校验并创建目标目录(不存在则自动创建多级目录)
File targetDir = new File(targetDirPath);
if (!targetDir.exists()) {
boolean mkdirsSuccess = targetDir.mkdirs();
if (!mkdirsSuccess) {
throw new RuntimeException("创建 Word 目标目录失败:" + targetDirPath);
}
}
// 生成最终 Word 文件名(处理后缀和唯一性)
String finalWordName;
if (fileStr == null || fileStr.trim().isEmpty()) {
// 自动生成唯一文件名:UUID + 时间戳 + .docx
String uniqueId = UUID.randomUUID().toString().replace("-", "");
String timestamp = String.valueOf(System.currentTimeMillis());
finalWordName = "Word_" + uniqueId + "_" + timestamp + ".docx";
} else {
// 自定义文件名:补充 .docx 后缀(若未带)
finalWordName = fileStr.endsWith(".docx") ? fileStr : fileStr + ".docx";
}
// 最终 Word 文件完整路径
File finalWordFile = new File(targetDir, finalWordName);
// ========== 2. 加载模板并生成 Word 到目标路径 ==========
Resource resource = resourceLoader.getResource(url);
try (InputStream inputStream = resource.getInputStream();
OutputStream out = new FileOutputStream(finalWordFile);
BufferedOutputStream bos = new BufferedOutputStream(out)) {
// poi-tl 编译模板并渲染数据
Configure config = Configure.builder().build();
XWPFTemplate template = XWPFTemplate.compile(inputStream, config).render(data);
// 写入目标文件
template.write(bos);
bos.flush();
PoitlIOUtils.closeQuietly(template);
System.out.println("Word 生成成功,保存路径:" + finalWordFile.getAbsolutePath());
// ========== 3. 生成并返回文件访问 URL ==========
// 格式1:本地文件 URL(如 file:///D:/word/export/xxx.docx 或 file:///data/word/export/xxx.docx)
// String fileUrl = finalWordFile.toURI().toString();
// 格式2:仅返回绝对路径(若不需要 URL 格式,直接返回 finalWordFile.getAbsolutePath())
return finalWordFile.getAbsolutePath();
// return fileUrl;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("生成 Word 失败:" + e.getMessage(), e);
}
}
/**
* word写入响应中
* @param response
* @param url
* @param data
*/
public static void generateWordRespose(HttpServletResponse response, String url, Map<String, Object> data, ResourceLoader resourceLoader, String fileStr) {
String fileName = URLEncoder.encode(fileStr, StandardCharsets.UTF_8);
// 设置响应头
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
org.springframework.core.io.Resource resource = resourceLoader.getResource(url);
// 获取输入流
try (InputStream inputStream = resource.getInputStream()) {
// 使用输入流加载模板
Configure config = Configure.builder().build();
XWPFTemplate template = XWPFTemplate.compile(inputStream,config)
.render(data);
OutputStream out = response.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(out);
template.write(bos);
bos.flush();
out.flush();
PoitlIOUtils.closeQuietlyMulti(template, bos, out);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 解析自定义表单
* @param map
* @param widgetList
*/
public static void getWidgetListMap(Map<String, JSONObject> map, String widgetList){
JSONArray jsonArray=JSONArray.parseArray(widgetList);
for(int i=0;i<jsonArray.size();i++){
JSONObject jsonObject=jsonArray.getJSONObject(i);
// System.out.println(jsonObject);
if(jsonObject.containsKey("widgetList")){
getWidgetListMap(map,jsonObject.getString("widgetList"));
}else{
String type=jsonObject.getString("type");
JSONObject options=jsonObject.getJSONObject("options");
String id = options.getString("name");
String label=options.getString("label");
JSONArray optionItems=options.getJSONArray("optionItems");
JSONObject temp=new JSONObject();
temp.put("type",type);
temp.put("label",label);
temp.put("optionItems",optionItems);
map.put(id,temp);
}
}
}
public static Map<String, PictureRenderData> createPictureMap(String profile, String pictureName, int width, int height) {
Map<String, PictureRenderData> map = new HashMap<>();
//创建PictureRenderData对象并设置其大小
//Pictures还有其他方法,如Pictures.ofStream()流处理,可根据自己的需求及文档替换
if(pictureName.startsWith("http")){
try {
System.out.println(pictureName);
URL url = new URL(pictureName);
// 发起HTTP请求获取连接(注意:此处不需要try-with-resources,改为手动关闭)
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000); // 连接超时时间
conn.setReadTimeout(5000); // 读取超时时间
// 检查请求是否成功(状态码200表示成功)
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
// 使用远程流创建图片 .size(width, height)
map.put("picture", Pictures.ofStream(conn.getInputStream()).create());
} else {
throw new RuntimeException("远程图片获取失败,状态码:" + conn.getResponseCode());
}
// 手动关闭连接资源
conn.disconnect();
} catch (Exception e) {
e.printStackTrace(); // 捕获网络异常、连接超时等问题
}
}else if(pictureName.startsWith("/dev-api")){
// 2. 拼接完整路径:根路径 + 上传子目录 + 相对路径
// 示例:/opt/gcp-java/file + /profile/upload + /2025/11/06/xxx.png = 完整路径
File fullPathFile = new File(profile + pictureName.replace("/dev-api/profile",""));
String fullPath = fullPathFile.getAbsolutePath();
InputStream inputStream = null;
try {
// 3. 校验文件是否存在、是否为文件
if (!fullPathFile.exists()) {
throw new RuntimeException("图片文件不存在:" + fullPath);
}
if (!fullPathFile.isFile()) {
throw new RuntimeException("路径不是文件:" + fullPath);
}
// 4. 校验文件权限(读权限)
if (!fullPathFile.canRead()) {
throw new RuntimeException("图片文件无读权限:" + fullPath + ",请赋予读权限(chmod +r 文件名)");
}
int targetWidth=600;
// 4. 读取图片原始尺寸(关键:用于比例计算)
inputStream = new FileInputStream(fullPathFile);
BufferedImage originalImage = ImageIO.read(inputStream); // 解析图片获取原始尺寸
int originalWidth = originalImage.getWidth(); // 原始宽度
int originalHeight = originalImage.getHeight(); // 原始高度
// 5. 计算自适应高度(核心公式:目标高度 = 原始高度 * 目标宽度 / 原始宽度)
int targetHeight = (originalHeight * targetWidth) / originalWidth;
System.out.println("图片原始尺寸:" + originalWidth + "x" + originalHeight
+ ",自适应后尺寸:" + targetWidth + "x" + targetHeight);
// 6. 重新读取流(ImageIO.read会消耗流,需重新打开)+ 等比缩放
inputStream.close(); // 关闭已消耗的流
inputStream = new FileInputStream(fullPathFile); // 重新打开流
PictureRenderData renderData = Pictures.ofStream(inputStream)
.size(targetWidth, targetHeight) // 传入100%宽度 + 比例高度
.create();
map.put("picture", renderData);
System.out.println("图片读取并等比缩放成功:" + fullPath);
} catch (Exception e) {
System.err.println("图片读取失败:" + e.getMessage());
e.printStackTrace();
} finally {
// 6. 关闭流资源(避免内存泄漏)
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
return map;
}
}