Archived
refactor: export API returns JSON with download URL, add FileCacheService
This commit is contained in:
@@ -5,39 +5,92 @@ import jakarta.validation.Valid;
|
|||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
|
||||||
|
|
||||||
|
import com.gmp.mock.dto.ApiResponse;
|
||||||
import com.gmp.mock.dto.DocxExportRequest;
|
import com.gmp.mock.dto.DocxExportRequest;
|
||||||
|
import com.gmp.mock.dto.ExportResultData;
|
||||||
import com.gmp.mock.service.DocxService;
|
import com.gmp.mock.service.DocxService;
|
||||||
import com.gmp.mock.service.DocxService.ExportResult;
|
import com.gmp.mock.service.DocxService.CachedResult;
|
||||||
|
import com.gmp.mock.service.FileCacheService;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/docx")
|
@RequestMapping("/api/docx")
|
||||||
public class DocxController {
|
public class DocxController {
|
||||||
|
|
||||||
private final DocxService docxService;
|
private final DocxService docxService;
|
||||||
|
private final FileCacheService fileCache;
|
||||||
|
|
||||||
public DocxController(DocxService docxService) {
|
public DocxController(DocxService docxService, FileCacheService fileCache) {
|
||||||
this.docxService = docxService;
|
this.docxService = docxService;
|
||||||
|
this.fileCache = fileCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/docx/export
|
||||||
|
*
|
||||||
|
* 导出文档,成功时返回 JSON:
|
||||||
|
* {
|
||||||
|
* "code": 0,
|
||||||
|
* "msg": "success",
|
||||||
|
* "data": {
|
||||||
|
* "url": "http://localhost:6666/api/docx/download?id=xxxx",
|
||||||
|
* "fileName": "annualReport.docx"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
@PostMapping("/export")
|
@PostMapping("/export")
|
||||||
public ResponseEntity<StreamingResponseBody> exportDocx(@Valid @RequestBody DocxExportRequest request) {
|
public ResponseEntity<ApiResponse<ExportResultData>> exportDocx(
|
||||||
|
@Valid @RequestBody DocxExportRequest request,
|
||||||
|
HttpServletRequest httpRequest
|
||||||
|
) {
|
||||||
|
CachedResult result = docxService.export(request.getExportId(), request.getBusinessData());
|
||||||
|
|
||||||
ExportResult result = docxService.export(request.getExportId());
|
// 构造下载 URL
|
||||||
|
String baseUrl = getBaseUrl(httpRequest);
|
||||||
|
String downloadUrl = baseUrl + "/api/docx/download?id=" + result.key();
|
||||||
|
|
||||||
StreamingResponseBody stream = outputStream ->
|
ExportResultData data = new ExportResultData(downloadUrl, result.fileName());
|
||||||
result.writer().write(outputStream);
|
return ResponseEntity.ok(ApiResponse.ok(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/docx/download?id={key}
|
||||||
|
*
|
||||||
|
* 根据 key 返回缓存的文档文件。
|
||||||
|
*/
|
||||||
|
@GetMapping("/download")
|
||||||
|
public ResponseEntity<?> downloadDocx(@RequestParam("id") String key) {
|
||||||
|
byte[] fileBytes = fileCache.retrieve(key);
|
||||||
|
if (fileBytes == null) {
|
||||||
|
return ResponseEntity.status(404)
|
||||||
|
.body(ApiResponse.error(404, "文件不存在或已过期: " + key));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从缓存文件名可以推断,但我们不需要 fileName,因为下载时由客户端决定。
|
||||||
|
// 这里固定一个通用的 Content-Disposition
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.contentType(MediaType.parseMediaType(
|
.contentType(MediaType.parseMediaType(
|
||||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"))
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"))
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
"attachment; filename=\"" + result.fileName() + "\"")
|
"attachment; filename=\"document.docx\"")
|
||||||
.body(stream);
|
.body(fileBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getBaseUrl(HttpServletRequest request) {
|
||||||
|
String scheme = request.getScheme();
|
||||||
|
String host = request.getServerName();
|
||||||
|
int port = request.getServerPort();
|
||||||
|
if ((scheme.equals("http") && port == 80) || (scheme.equals("https") && port == 443)) {
|
||||||
|
return scheme + "://" + host;
|
||||||
|
}
|
||||||
|
return scheme + "://" + host + ":" + port;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.gmp.mock.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一 API 响应格式
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class ApiResponse<T> {
|
||||||
|
|
||||||
|
private int code;
|
||||||
|
private String msg;
|
||||||
|
private T data;
|
||||||
|
|
||||||
|
private ApiResponse(int code, String msg, T data) {
|
||||||
|
this.code = code;
|
||||||
|
this.msg = msg;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> ok(T data) {
|
||||||
|
return new ApiResponse<>(0, "success", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> ok(T data, String msg) {
|
||||||
|
return new ApiResponse<>(0, msg, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> error(int code, String msg) {
|
||||||
|
return new ApiResponse<>(code, msg, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() { return code; }
|
||||||
|
public String getMsg() { return msg; }
|
||||||
|
public T getData() { return data; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.gmp.mock.dto;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export 成功后返回的数据
|
||||||
|
*/
|
||||||
|
public class ExportResultData {
|
||||||
|
private String url;
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
public ExportResultData(String url, String fileName) {
|
||||||
|
this.url = url;
|
||||||
|
this.fileName = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() { return url; }
|
||||||
|
public String getFileName() { return fileName; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.gmp.mock.handler;
|
package com.gmp.mock.handler;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -8,23 +7,24 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
|
|||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
import com.gmp.mock.dto.ApiResponse;
|
||||||
import com.gmp.mock.service.TemplateNotFoundException;
|
import com.gmp.mock.service.TemplateNotFoundException;
|
||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
|
public ResponseEntity<ApiResponse<Void>> handleValidation(MethodArgumentNotValidException ex) {
|
||||||
String message = ex.getBindingResult().getFieldErrors().stream()
|
String message = ex.getBindingResult().getFieldErrors().stream()
|
||||||
.map(e -> e.getDefaultMessage())
|
.map(e -> e.getDefaultMessage())
|
||||||
.collect(Collectors.joining("; "));
|
.collect(Collectors.joining("; "));
|
||||||
return ResponseEntity.badRequest()
|
return ResponseEntity.badRequest()
|
||||||
.body(Map.of("code", 400, "msg", message));
|
.body(ApiResponse.error(400, message));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(TemplateNotFoundException.class)
|
@ExceptionHandler(TemplateNotFoundException.class)
|
||||||
public ResponseEntity<Map<String, Object>> handleTemplateNotFound(TemplateNotFoundException ex) {
|
public ResponseEntity<ApiResponse<Void>> handleTemplateNotFound(TemplateNotFoundException ex) {
|
||||||
return ResponseEntity.status(404)
|
return ResponseEntity.status(404)
|
||||||
.body(Map.of("code", 404, "msg", ex.getMessage()));
|
.body(ApiResponse.error(404, ex.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
package com.gmp.mock.service;
|
package com.gmp.mock.service;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.deepoove.poi.XWPFTemplate;
|
import com.deepoove.poi.XWPFTemplate;
|
||||||
import com.deepoove.poi.xwpf.NiceXWPFDocument;
|
|
||||||
import com.gmp.mock.template.TemplateProvider;
|
import com.gmp.mock.template.TemplateProvider;
|
||||||
import com.gmp.mock.template.TemplateRegistry;
|
import com.gmp.mock.template.TemplateRegistry;
|
||||||
|
|
||||||
@@ -14,45 +21,125 @@ import com.gmp.mock.template.TemplateRegistry;
|
|||||||
public class DocxService {
|
public class DocxService {
|
||||||
|
|
||||||
private final TemplateRegistry templateRegistry;
|
private final TemplateRegistry templateRegistry;
|
||||||
|
private final FileCacheService fileCache;
|
||||||
|
|
||||||
public DocxService(TemplateRegistry templateRegistry) {
|
public DocxService(TemplateRegistry templateRegistry, FileCacheService fileCache) {
|
||||||
this.templateRegistry = templateRegistry;
|
this.templateRegistry = templateRegistry;
|
||||||
|
this.fileCache = fileCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExportResult export(String exportId) {
|
/**
|
||||||
|
* 生成文档,缓存到磁盘,返回 key 和文件名。
|
||||||
|
*/
|
||||||
|
public CachedResult export(String exportId, Map<String, Object> businessData) {
|
||||||
TemplateProvider provider = templateRegistry.get(exportId);
|
TemplateProvider provider = templateRegistry.get(exportId);
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
throw new TemplateNotFoundException(exportId);
|
throw new TemplateNotFoundException(exportId);
|
||||||
}
|
}
|
||||||
|
|
||||||
ExportFileWriter writer = outputStream -> {
|
|
||||||
try {
|
try {
|
||||||
// 1. poi-tl: compile template and render text placeholders
|
// 1. Pre-process: merge split runs
|
||||||
XWPFTemplate template = XWPFTemplate.compile(provider.getTemplateResource());
|
byte[] preprocessed = mergeSplitPlaceholders(provider.getTemplateResource());
|
||||||
template.render(provider.getDataModel());
|
|
||||||
|
|
||||||
// 2. Get the underlying document for post-processing
|
// 2. Build data model
|
||||||
NiceXWPFDocument doc = template.getXWPFDocument();
|
Map<String, Object> data = new java.util.HashMap<>(provider.getDataModel());
|
||||||
|
if (businessData != null) {
|
||||||
|
data.putAll(businessData);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Post-process (e.g. add table rows)
|
// 3. Render with poi-tl
|
||||||
provider.postProcess(doc);
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
XWPFTemplate template = XWPFTemplate.compile(new ByteArrayInputStream(preprocessed));
|
||||||
// 4. Write to output stream
|
template.render(data);
|
||||||
template.write(outputStream);
|
provider.postProcess(template.getXWPFDocument());
|
||||||
|
template.write(baos);
|
||||||
template.close();
|
template.close();
|
||||||
|
|
||||||
|
// 4. Cache to disk
|
||||||
|
String key = fileCache.store(baos.toByteArray());
|
||||||
|
|
||||||
|
return new CachedResult(key, provider.getFileName());
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("生成文档失败", e);
|
throw new UncheckedIOException("生成文档失败", e);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return new ExportResult(provider.getFileName(), writer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ExportResult(String fileName, ExportFileWriter writer) {
|
public record CachedResult(String key, String fileName) {}
|
||||||
|
|
||||||
|
// ====== Run merging (unchanged) ======
|
||||||
|
|
||||||
|
private byte[] mergeSplitPlaceholders(InputStream templateStream) throws IOException {
|
||||||
|
byte[] docxBytes;
|
||||||
|
try (InputStream is = templateStream) {
|
||||||
|
docxBytes = is.readAllBytes();
|
||||||
|
}
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(docxBytes));
|
||||||
|
ZipOutputStream zos = new ZipOutputStream(out)) {
|
||||||
|
ZipEntry entry;
|
||||||
|
while ((entry = zis.getNextEntry()) != null) {
|
||||||
|
zos.putNextEntry(new ZipEntry(entry.getName()));
|
||||||
|
byte[] content = zis.readAllBytes();
|
||||||
|
if ("word/document.xml".equals(entry.getName())) {
|
||||||
|
String xml = new String(content, "UTF-8");
|
||||||
|
xml = mergeRunsInXml(xml);
|
||||||
|
content = xml.getBytes("UTF-8");
|
||||||
|
}
|
||||||
|
zos.write(content);
|
||||||
|
zos.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
private String mergeRunsInXml(String xml) {
|
||||||
public interface ExportFileWriter {
|
StringBuilder result = new StringBuilder();
|
||||||
void write(OutputStream outputStream) throws IOException;
|
java.util.regex.Matcher m = Pattern.compile(
|
||||||
|
"(<w:r>.*?<w:t[^>]*?>)(.*?)(</w:t>.*?</w:r>)",
|
||||||
|
Pattern.DOTALL).matcher(xml);
|
||||||
|
var runs = new java.util.ArrayList<Run>();
|
||||||
|
while (m.find()) {
|
||||||
|
runs.add(new Run(m.start(), m.end(), m.group(1), m.group(2), m.group(3)));
|
||||||
|
}
|
||||||
|
if (runs.isEmpty()) return xml;
|
||||||
|
var merged = new java.util.ArrayList<MergedRun>();
|
||||||
|
int i = 0;
|
||||||
|
while (i < runs.size()) {
|
||||||
|
StringBuilder combined = new StringBuilder();
|
||||||
|
int start = i;
|
||||||
|
while (i < runs.size() && isPlaceholderPart(combined, runs.get(i).text)) {
|
||||||
|
combined.append(runs.get(i).text);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (combined.toString().matches("\\{\\{.*?\\}\\}")) {
|
||||||
|
Run first = runs.get(start);
|
||||||
|
Run last = runs.get(i - 1);
|
||||||
|
merged.add(new MergedRun(first.start, last.end, first.openTag + combined + first.closeTag));
|
||||||
|
} else {
|
||||||
|
for (int j = start; j < i; j++) {
|
||||||
|
Run r = runs.get(j);
|
||||||
|
merged.add(new MergedRun(r.start, r.end, r.fullMatch()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
int pos = 0;
|
||||||
|
for (MergedRun mr : merged) {
|
||||||
|
result.append(xml, pos, mr.start);
|
||||||
|
result.append(mr.xml);
|
||||||
|
pos = mr.end;
|
||||||
|
}
|
||||||
|
result.append(xml.substring(pos));
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPlaceholderPart(StringBuilder current, String text) {
|
||||||
|
if (current.isEmpty()) return text.contains("{{") || text.startsWith("{{");
|
||||||
|
return !text.contains("}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Run(int start, int end, String openTag, String text, String closeTag) {
|
||||||
|
String fullMatch() { return openTag + text + closeTag; }
|
||||||
|
}
|
||||||
|
private record MergedRun(int start, int end, String xml) {}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.gmp.mock.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件缓存服务。
|
||||||
|
* 生成的文件保存到本地磁盘,通过随机 key 索引,供下载接口使用。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class FileCacheService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(FileCacheService.class);
|
||||||
|
|
||||||
|
private final Path cacheDir;
|
||||||
|
|
||||||
|
public FileCacheService(@Value("${gmp.cache-dir:/tmp/gmp-cache}") String cacheDirPath) {
|
||||||
|
this.cacheDir = Path.of(cacheDirPath);
|
||||||
|
try {
|
||||||
|
Files.createDirectories(this.cacheDir);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("无法创建缓存目录: " + cacheDirPath, e);
|
||||||
|
}
|
||||||
|
log.info("File cache directory: {}", this.cacheDir.toAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成一个随机 key,并将数据写入缓存文件。
|
||||||
|
*
|
||||||
|
* @param data 文件字节数据
|
||||||
|
* @return 随机 key
|
||||||
|
*/
|
||||||
|
public String store(byte[] data) {
|
||||||
|
String key = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
Path file = cacheDir.resolve(key);
|
||||||
|
try {
|
||||||
|
Files.write(file, data);
|
||||||
|
log.debug("Cached file: {} ({} bytes)", key, data.length);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("缓存文件写入失败: " + key, e);
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 key 读取缓存文件。
|
||||||
|
*
|
||||||
|
* @param key 随机 key
|
||||||
|
* @return 文件字节数据,或 null(key 不存在)
|
||||||
|
*/
|
||||||
|
public byte[] retrieve(String key) {
|
||||||
|
Path file = cacheDir.resolve(sanitize(key));
|
||||||
|
if (!Files.exists(file)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Files.readAllBytes(file);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("读取缓存文件失败: {}", key, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把缓存文件内容写入输出流。
|
||||||
|
*
|
||||||
|
* @param key 随机 key
|
||||||
|
* @param out 输出流
|
||||||
|
* @return 是否写入成功
|
||||||
|
*/
|
||||||
|
public boolean writeToStream(String key, OutputStream out) throws IOException {
|
||||||
|
Path file = cacheDir.resolve(sanitize(key));
|
||||||
|
if (!Files.exists(file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Files.copy(file, out);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sanitize(String key) {
|
||||||
|
return key.replaceAll("[^a-zA-Z0-9\\-_]", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,8 +69,12 @@ public class AnnualReportProvider implements TemplateProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void postProcess(XWPFDocument document) {
|
public void postProcess(XWPFDocument document) {
|
||||||
|
List<XWPFTable> tables = document.getTables();
|
||||||
|
if (tables.isEmpty()) {
|
||||||
|
return; // 模板中无表格,跳过
|
||||||
|
}
|
||||||
// Add data rows to the first table in the document
|
// Add data rows to the first table in the document
|
||||||
XWPFTable table = document.getTables().get(0);
|
XWPFTable table = tables.get(0);
|
||||||
for (Map<String, String> metric : METRICS) {
|
for (Map<String, String> metric : METRICS) {
|
||||||
XWPFTableRow row = table.createRow();
|
XWPFTableRow row = table.createRow();
|
||||||
row.getCell(0).setText(metric.get("name"));
|
row.getCell(0).setText(metric.get("name"));
|
||||||
|
|||||||
Reference in New Issue
Block a user