Archived
refactor: streaming download, Git LFS templates, deploy workflow
- Replace base64 JSON response with StreamingResponseBody (zero-buffer) - Error responses return JSON via GlobalExceptionHandler (400/404) - Add git-lfs track for *.docx to keep repo lean - Add deploy/deploy.sh: one-click build + restart - Add deploy/clean-lfs.sh: periodic LFS cache cleanup - Add deploy/gmp-service.service: systemd service unit - Remove unused ApiResponse and DocxExportData DTOs - Fix build.sh/run.sh jar name (gmp-mock-service -> gmp-service) - Add TemplateNotFoundException for missing templates
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
*.docx filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.doc filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.dotx filter=lfs diff=lfs merge=lfs -text
|
||||||
+1
-1
@@ -8,7 +8,7 @@ mvn clean package -DskipTests
|
|||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "===== 构建成功 ====="
|
echo "===== 构建成功 ====="
|
||||||
JAR_PATH="target/gmp-mock-service.jar"
|
JAR_PATH="target/gmp-service.jar"
|
||||||
if [ -f "$JAR_PATH" ]; then
|
if [ -f "$JAR_PATH" ]; then
|
||||||
SIZE=$(du -h "$JAR_PATH" | cut -f1)
|
SIZE=$(du -h "$JAR_PATH" | cut -f1)
|
||||||
echo "JAR 文件: $JAR_PATH ($SIZE)"
|
echo "JAR 文件: $JAR_PATH ($SIZE)"
|
||||||
|
|||||||
Executable
+74
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Git LFS 缓存清理脚本
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# ./deploy/clean-lfs.sh # 交互式预览
|
||||||
|
# ./deploy/clean-lfs.sh --prune # 执行清理
|
||||||
|
# ./deploy/clean-lfs.sh --force # 强制清理所有缓存
|
||||||
|
#
|
||||||
|
# Git LFS 会把所有历史版本的二进制文件缓存在 .git/lfs/objects/ 下。
|
||||||
|
# 这些缓存不会自动删除,时间久了会占用大量磁盘空间。
|
||||||
|
# 此脚本用于安全地清理不再需要的 LFS 缓存对象。
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
LFS_DIR="$(git rev-parse --git-dir 2>/dev/null || echo ".git")/lfs/objects"
|
||||||
|
cd "$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
|
||||||
|
|
||||||
|
echo "===== Git LFS 缓存分析 ====="
|
||||||
|
echo "仓库: $(pwd)"
|
||||||
|
|
||||||
|
# 统计当前缓存大小
|
||||||
|
if [ -d "$LFS_DIR" ]; then
|
||||||
|
CACHE_SIZE=$(du -sh "$LFS_DIR" 2>/dev/null | cut -f1)
|
||||||
|
CACHE_FILES=$(find "$LFS_DIR" -type f 2>/dev/null | wc -l)
|
||||||
|
echo "LFS 缓存目录: .git/lfs/objects/"
|
||||||
|
echo "缓存文件数: ${CACHE_FILES:-0}"
|
||||||
|
echo "缓存总大小: ${CACHE_SIZE:-0}"
|
||||||
|
else
|
||||||
|
echo "LFS 缓存目录不存在,尚无缓存文件。"
|
||||||
|
CACHE_SIZE="0B"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查 git lfs 是否可用
|
||||||
|
if ! git lfs version &>/dev/null; then
|
||||||
|
echo "错误: git lfs 未安装。"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 分析可清理的空间
|
||||||
|
echo "===== 可清理空间分析 ====="
|
||||||
|
git lfs prune --dry-run 2>&1 | head -20
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
--prune)
|
||||||
|
echo "===== 执行清理 ====="
|
||||||
|
git lfs prune
|
||||||
|
echo ""
|
||||||
|
# 统计清理后
|
||||||
|
if [ -d "$LFS_DIR" ]; then
|
||||||
|
NEW_SIZE=$(du -sh "$LFS_DIR" 2>/dev/null | cut -f1)
|
||||||
|
echo "清理完成: ${CACHE_SIZE} -> ${NEW_SIZE}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
--force)
|
||||||
|
echo "===== 强制清理所有 LFS 缓存 ====="
|
||||||
|
echo "注意: 这将删除所有不在当前 checkout 中的 LFS 对象。"
|
||||||
|
echo "如果你需要切换回旧版本,LFS 对象会被重新下载。"
|
||||||
|
echo ""
|
||||||
|
git lfs prune --force 2>&1 | tail -5
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "这是预览模式,未执行任何操作。"
|
||||||
|
echo ""
|
||||||
|
echo "执行清理: bash $0 --prune"
|
||||||
|
echo "强制清理: bash $0 --force"
|
||||||
|
echo ""
|
||||||
|
echo "提示: 定期执行 'git lfs prune' 可以删除旧版本 LFS 缓存。"
|
||||||
|
echo " Git LFS 的默认保留策略是最近 3 天的对象 + 当前 HEAD 可达的对象。"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# gmp-service 一键部署脚本
|
||||||
|
# 用法: ./deploy/deploy.sh
|
||||||
|
#
|
||||||
|
# 它会: 构建 -> 停止旧服务 -> 替换jar -> 启动新服务
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
SERVICE_NAME="gmp-service"
|
||||||
|
JAR_SRC="$PROJECT_DIR/target/gmp-service.jar"
|
||||||
|
JAR_DST="/opt/$SERVICE_NAME/gmp-service.jar"
|
||||||
|
LOG_DIR="/var/log/$SERVICE_NAME"
|
||||||
|
|
||||||
|
echo "===== 1. Maven 构建 ====="
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
mvn clean package -DskipTests
|
||||||
|
|
||||||
|
if [ ! -f "$JAR_SRC" ]; then
|
||||||
|
echo "构建失败,未生成 JAR 文件"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "===== 2. 停止旧服务 ====="
|
||||||
|
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||||
|
sudo systemctl stop "$SERVICE_NAME"
|
||||||
|
echo "旧服务已停止"
|
||||||
|
else
|
||||||
|
echo "服务未运行,跳过"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "===== 3. 部署新 JAR ====="
|
||||||
|
sudo mkdir -p /opt/$SERVICE_NAME "$LOG_DIR"
|
||||||
|
sudo cp "$JAR_SRC" "$JAR_DST"
|
||||||
|
sudo chmod 644 "$JAR_DST"
|
||||||
|
|
||||||
|
echo "===== 4. 启动服务 ====="
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable "$SERVICE_NAME"
|
||||||
|
sudo systemctl start "$SERVICE_NAME"
|
||||||
|
|
||||||
|
echo "===== 等待服务启动 ====="
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||||
|
echo "服务已成功启动!"
|
||||||
|
systemctl status "$SERVICE_NAME" --no-pager | head -15
|
||||||
|
else
|
||||||
|
echo "服务启动失败,查看日志: sudo journalctl -u $SERVICE_NAME -n 50 --no-pager"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "部署完成!"
|
||||||
|
echo " 服务: $SERVICE_NAME"
|
||||||
|
echo " JAR: $JAR_DST"
|
||||||
|
echo " 日志: journalctl -u $SERVICE_NAME -f"
|
||||||
|
echo " 端口: $(grep 'server.port' "$PROJECT_DIR/src/main/resources/application.yml" 2>/dev/null || echo '6666')"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=GMP Mock Service - Spring Boot Application
|
||||||
|
Documentation=
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=agentuser
|
||||||
|
WorkingDirectory=/opt/gmp-service
|
||||||
|
ExecStart=/usr/bin/java -Xms256m -Xmx512m -jar /opt/gmp-service/gmp-service.jar
|
||||||
|
ExecStop=/bin/kill -SIGTERM $MAINPID
|
||||||
|
SuccessExitStatus=143
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10
|
||||||
|
StandardOutput=append:/var/log/gmp-service/stdout.log
|
||||||
|
StandardError=append:/var/log/gmp-service/stderr.log
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
JAR_PATH="target/gmp-mock-service.jar"
|
JAR_PATH="target/gmp-service.jar"
|
||||||
|
|
||||||
if [ ! -f "$JAR_PATH" ]; then
|
if [ ! -f "$JAR_PATH" ]; then
|
||||||
echo "JAR 文件不存在,请先执行 build.sh 进行构建"
|
echo "JAR 文件不存在,请先执行 build.sh 进行构建"
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
package com.gmp.mock.controller;
|
package com.gmp.mock.controller;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
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.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.DocxExportData;
|
|
||||||
import com.gmp.mock.dto.DocxExportRequest;
|
import com.gmp.mock.dto.DocxExportRequest;
|
||||||
import com.gmp.mock.service.DocxService;
|
import com.gmp.mock.service.DocxService;
|
||||||
|
import com.gmp.mock.service.DocxService.ExportResult;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/docx")
|
@RequestMapping("/api/docx")
|
||||||
@@ -26,11 +26,18 @@ public class DocxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/export")
|
@PostMapping("/export")
|
||||||
public ResponseEntity<ApiResponse<DocxExportData>> exportDocx(@Valid @RequestBody DocxExportRequest request) throws IOException {
|
public ResponseEntity<StreamingResponseBody> exportDocx(@Valid @RequestBody DocxExportRequest request) {
|
||||||
DocxExportData data = docxService.export(request.getExportId());
|
|
||||||
if (data == null) {
|
ExportResult result = docxService.export(request.getExportId());
|
||||||
return ResponseEntity.ok(ApiResponse.error(404, "模板不存在"));
|
|
||||||
}
|
StreamingResponseBody stream = outputStream ->
|
||||||
return ResponseEntity.ok(ApiResponse.success(data));
|
result.writer().write(outputStream);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.parseMediaType(
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"))
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
|
"attachment; filename=\"" + result.fileName() + "\"")
|
||||||
|
.body(stream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
package com.gmp.mock.dto;
|
|
||||||
|
|
||||||
public class ApiResponse<T> {
|
|
||||||
|
|
||||||
private int code;
|
|
||||||
private T data;
|
|
||||||
private String msg;
|
|
||||||
|
|
||||||
private ApiResponse(int code, T data, String msg) {
|
|
||||||
this.code = code;
|
|
||||||
this.data = data;
|
|
||||||
this.msg = msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> ApiResponse<T> success(T data) {
|
|
||||||
return new ApiResponse<>(0, data, "success");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> ApiResponse<T> success(T data, String msg) {
|
|
||||||
return new ApiResponse<>(0, data, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> ApiResponse<T> error(int code, String msg) {
|
|
||||||
return new ApiResponse<>(code, null, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCode() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCode(int code) {
|
|
||||||
this.code = code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public T getData() {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setData(T data) {
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMsg() {
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMsg(String msg) {
|
|
||||||
this.msg = msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package com.gmp.mock.dto;
|
|
||||||
|
|
||||||
public class DocxExportData {
|
|
||||||
|
|
||||||
private String fileName;
|
|
||||||
private String fileBase64;
|
|
||||||
|
|
||||||
public DocxExportData(String fileName, String fileBase64) {
|
|
||||||
this.fileName = fileName;
|
|
||||||
this.fileBase64 = fileBase64;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFileName() {
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFileName(String fileName) {
|
|
||||||
this.fileName = fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFileBase64() {
|
|
||||||
return fileBase64;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFileBase64(String fileBase64) {
|
|
||||||
this.fileBase64 = fileBase64;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
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;
|
||||||
@@ -7,16 +8,23 @@ 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;
|
||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
public ResponseEntity<ApiResponse<Void>> handleValidation(MethodArgumentNotValidException ex) {
|
public ResponseEntity<Map<String, Object>> 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.ok(ApiResponse.error(400, message));
|
return ResponseEntity.badRequest()
|
||||||
|
.body(Map.of("code", 400, "msg", message));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(TemplateNotFoundException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleTemplateNotFound(TemplateNotFoundException ex) {
|
||||||
|
return ResponseEntity.status(404)
|
||||||
|
.body(Map.of("code", 404, "msg", ex.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package com.gmp.mock.service;
|
package com.gmp.mock.service;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Base64;
|
import java.io.OutputStream;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.gmp.mock.dto.DocxExportData;
|
|
||||||
import com.gmp.mock.template.TemplateProvider;
|
import com.gmp.mock.template.TemplateProvider;
|
||||||
import com.gmp.mock.template.TemplateRegistry;
|
import com.gmp.mock.template.TemplateRegistry;
|
||||||
|
|
||||||
@@ -20,18 +19,34 @@ public class DocxService {
|
|||||||
this.templateRegistry = templateRegistry;
|
this.templateRegistry = templateRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DocxExportData export(String exportId) throws IOException {
|
/**
|
||||||
|
* @return ExportResult containing file name and a writer that writes directly to an OutputStream
|
||||||
|
* @throws TemplateNotFoundException if templateId is not registered
|
||||||
|
*/
|
||||||
|
public ExportResult export(String exportId) {
|
||||||
TemplateProvider provider = templateRegistry.get(exportId);
|
TemplateProvider provider = templateRegistry.get(exportId);
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
return null;
|
throw new TemplateNotFoundException(exportId);
|
||||||
}
|
}
|
||||||
|
|
||||||
try (XWPFDocument document = new XWPFDocument();
|
// Return a writer callback instead of buffering everything in memory.
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
// The document is generated and written directly to the HTTP response stream.
|
||||||
|
ExportFileWriter writer = outputStream -> {
|
||||||
|
try (XWPFDocument document = new XWPFDocument()) {
|
||||||
provider.generate(document);
|
provider.generate(document);
|
||||||
document.write(baos);
|
document.write(outputStream);
|
||||||
String base64 = Base64.getEncoder().encodeToString(baos.toByteArray());
|
outputStream.flush();
|
||||||
return new DocxExportData(provider.getFileName(), base64);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new ExportResult(provider.getFileName(), writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ExportResult(String fileName, ExportFileWriter writer) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ExportFileWriter {
|
||||||
|
void write(OutputStream outputStream) throws IOException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.gmp.mock.service;
|
||||||
|
|
||||||
|
public class TemplateNotFoundException extends RuntimeException {
|
||||||
|
public TemplateNotFoundException(String templateId) {
|
||||||
|
super("模板不存在: " + templateId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,18 @@
|
|||||||
package com.gmp.mock.template.annualReport;
|
package com.gmp.mock.template.annualReport;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import com.gmp.mock.template.TemplateProvider;
|
import com.gmp.mock.template.TemplateProvider;
|
||||||
@@ -10,6 +20,20 @@ import com.gmp.mock.template.TemplateProvider;
|
|||||||
@Component
|
@Component
|
||||||
public class AnnualReportProvider implements TemplateProvider {
|
public class AnnualReportProvider implements TemplateProvider {
|
||||||
|
|
||||||
|
private static final Map<String, String> DEFAULT_VARS = Map.ofEntries(
|
||||||
|
Map.entry("title", "2025 年度工作报告"),
|
||||||
|
Map.entry("companyName", "示例科技有限公司"),
|
||||||
|
Map.entry("reportPeriod", "2025年1月 - 2025年12月"),
|
||||||
|
Map.entry("metric1Name", "营业收入"),
|
||||||
|
Map.entry("metric1Value", "¥ 1,234,567"),
|
||||||
|
Map.entry("metric1Change","+15.3%"),
|
||||||
|
Map.entry("metric2Name", "净利润"),
|
||||||
|
Map.entry("metric2Value", "¥ 321,098"),
|
||||||
|
Map.entry("metric2Change","+8.7%"),
|
||||||
|
Map.entry("generatedDate",
|
||||||
|
LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")))
|
||||||
|
);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getTemplateId() {
|
public String getTemplateId() {
|
||||||
return "annualReport";
|
return "annualReport";
|
||||||
@@ -22,5 +46,55 @@ public class AnnualReportProvider implements TemplateProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void generate(XWPFDocument document) throws IOException {
|
public void generate(XWPFDocument document) throws IOException {
|
||||||
|
ClassPathResource resource = new ClassPathResource("templates/annualReportTemplate.docx");
|
||||||
|
if (!resource.exists()) {
|
||||||
|
throw new IOException("模板文件未找到: templates/annualReportTemplate.docx");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] templateBytes;
|
||||||
|
try (InputStream is = resource.getInputStream()) {
|
||||||
|
templateBytes = is.readAllBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace ${placeholders} in word/document.xml inside the ZIP
|
||||||
|
byte[] modified = replacePlaceholdersInDocx(templateBytes);
|
||||||
|
|
||||||
|
// Read the modified docx and copy content into the output document
|
||||||
|
try (InputStream modifiedIs = new ByteArrayInputStream(modified);
|
||||||
|
XWPFDocument templateDoc = new XWPFDocument(modifiedIs)) {
|
||||||
|
|
||||||
|
for (var para : templateDoc.getParagraphs()) {
|
||||||
|
var newPara = document.createParagraph();
|
||||||
|
newPara.getCTP().set(para.getCTP().copy());
|
||||||
|
}
|
||||||
|
for (var table : templateDoc.getTables()) {
|
||||||
|
var newTable = document.createTable();
|
||||||
|
newTable.getCTTbl().set(table.getCTTbl().copy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] replacePlaceholdersInDocx(byte[] docxBytes) throws IOException {
|
||||||
|
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");
|
||||||
|
for (Map.Entry<String, String> var : DEFAULT_VARS.entrySet()) {
|
||||||
|
xml = xml.replace("${" + var.getKey() + "}", var.getValue());
|
||||||
|
}
|
||||||
|
content = xml.getBytes("UTF-8");
|
||||||
|
}
|
||||||
|
zos.write(content);
|
||||||
|
zos.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user