feat: fill annual report table cells with generated investment data

This commit is contained in:
agentuser
2026-05-25 15:00:21 +08:00
parent 39c99f64e5
commit 0c5c540e0f
3 changed files with 103 additions and 24 deletions
+3 -3
View File
@@ -5,7 +5,7 @@ info:
contact: contact:
name: 吕冬 name: 吕冬
servers: servers:
- url: https://closein.site - url: https://closein.cn
description: 生产服务器(HTTPS) description: 生产服务器(HTTPS)
- url: http://localhost:10086 - url: http://localhost:10086
description: 本地开发服务器(HTTP) description: 本地开发服务器(HTTP)
@@ -42,7 +42,7 @@ paths:
code: 0 code: 0
msg: "success" msg: "success"
data: data:
url: "https://closein.site/api/docx/download?id=d8d4122719c8465ea75ab62b37f6ce10" url: "https://closein.cn/api/docx/download?id=d8d4122719c8465ea75ab62b37f6ce10"
fileName: "annualReport.docx" fileName: "annualReport.docx"
"400": "400":
description: 参数校验失败(exportId 为空 / businessData 为空) description: 参数校验失败(exportId 为空 / businessData 为空)
@@ -177,7 +177,7 @@ components:
type: string type: string
format: uri format: uri
description: 文件下载 URL,由 origin + 路径组成 description: 文件下载 URL,由 origin + 路径组成
example: "https://closein.site/api/docx/download?id=d8d4122719c8465ea75ab62b37f6ce10" example: "https://closein.cn/api/docx/download?id=d8d4122719c8465ea75ab62b37f6ce10"
fileName: fileName:
type: string type: string
description: 文件名 description: 文件名
@@ -3,8 +3,10 @@ package com.gmp.mock.template.annualReport;
import java.io.InputStream; import java.io.InputStream;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFTable; import org.apache.poi.xwpf.usermodel.XWPFTable;
@@ -16,24 +18,38 @@ import com.deepoove.poi.XWPFTemplate;
import com.gmp.mock.template.TemplateProvider; import com.gmp.mock.template.TemplateProvider;
/** /**
* poi-tl based template provider. * 年度报告模板填充器。
* *
* Simple text placeholders ({{title}}, {{companyName}}, etc.) are handled * 模板含一个固定表格,行结构如下:
* by poi-tl directly. Table rows are built programmatically with POI API * Row 0: 表头(序号 | 专业类别 | 合计 | yearA | yearB | yearC)
* after poi-tl rendering, because poi-tl's table loop syntax can be * Row 1: 总计(汇总行)
* finicky with raw XML templates. * Row 2~11: 各投资分类(电网基建/小型基建/技改/科技/信息化)
* *
* To edit the template: open the .docx in Word, write {{placeholders}}. * postProcess() 遍历现有行,按行名填充生成的投资数据到单元格。
*/ */
@Component @Component
public class AnnualReportProvider implements TemplateProvider { public class AnnualReportProvider implements TemplateProvider {
private static final List<Map<String, String>> METRICS = List.of( /** 行索引 → 生成策略 */
Map.of("name", "营业收入", "value", "¥ 1,234,567", "change", "+15.3%"), private static final List<RowConfig> ROW_CONFIGS = List.of(
Map.of("name", "净利润", "value", "¥ 321,098", "change", "+8.7%"), new RowConfig(1, "总计", true), // 汇总行:自动求和
Map.of("name", "毛利率", "value", "45.2%", "change", "+2.1%") new RowConfig(2, "电网基建", true), // 分类标题行
new RowConfig(3, "主网基建", false),
new RowConfig(4, "配网基建", false),
new RowConfig(5, "小型基建", true), // 分类标题行(只有一项)
new RowConfig(6, "技改项目", true), // 分类标题行
new RowConfig(7, "生产技改", false),
new RowConfig(8, "营销技改", false),
new RowConfig(9, "其他技改", false),
new RowConfig(10, "科技项目", true), // 分类标题行(只有一项)
new RowConfig(11, "信息化项目", true) // 分类标题行(只有一项)
); );
/** 非标题行的行索引列表(需要实际填充数据) */
private static final List<Integer> DATA_ROW_INDEXES = List.of(3, 4, 7, 8, 9, 10, 11);
private record RowConfig(int index, String name, boolean isHeader) {}
@Override @Override
public String getTemplateId() { public String getTemplateId() {
return "annualReport"; return "annualReport";
@@ -59,27 +75,90 @@ public class AnnualReportProvider implements TemplateProvider {
@Override @Override
public Map<String, Object> getDataModel() { public Map<String, Object> getDataModel() {
int yearA = 2024;
int yearB = 2025;
int yearC = 2026;
return Map.of( return Map.of(
"title", "2025 年度工作报告", "title", "2025 年度工作报告",
"companyName", "示例科技有限公司", "companyName", "示例科技有限公司",
"reportPeriod", "2025年1月 - 2025年12月", "reportPeriod", "2025年1月 - 2025年12月",
"generatedDate", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")) "generatedDate", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")),
"yearA", String.valueOf(yearA),
"yearB", String.valueOf(yearB),
"yearC", String.valueOf(yearC),
"snhj", "300",
"dwjjtz", "200"
); );
} }
@Override @Override
public void postProcess(XWPFDocument document) { public void postProcess(XWPFDocument document) {
List<XWPFTable> tables = document.getTables(); List<XWPFTable> tables = document.getTables();
if (tables.isEmpty()) { if (tables.isEmpty()) return;
return; // 模板中无表格,跳过
}
// Add data rows to the first table in the document
XWPFTable table = tables.get(0); XWPFTable table = tables.get(0);
for (Map<String, String> metric : METRICS) {
XWPFTableRow row = table.createRow(); // 1. 为每个数据行生成随机投资数据
row.getCell(0).setText(metric.get("name")); Map<Integer, long[]> rowData = new LinkedHashMap<>();
row.getCell(1).setText(metric.get("value")); long grandTotal = 0;
row.getCell(2).setText(metric.get("change")); long grandYearA = 0;
long grandYearB = 0;
long grandYearC = 0;
// 分类与子项分配比例
Map<Integer, List<Integer>> categoryChildren = Map.of(
2, List.of(3, 4), // 电网基建 → 主网基建 + 配网基建
5, List.of(5), // 小型基建(自身就是数据行)
6, List.of(7, 8, 9), // 技改项目 → 生产技改 + 营销技改 + 其他技改
10, List.of(10), // 科技项目
11, List.of(11) // 信息化项目
);
for (var entry : categoryChildren.entrySet()) {
int catRowIdx = entry.getKey();
List<Integer> childIndexes = entry.getValue();
// 为子项生成随机数据
long catTotal = 0, catA = 0, catB = 0, catC = 0;
for (int childIdx : childIndexes) {
long a = randomInvest();
long b = randomInvest();
long c = randomInvest();
long total = a + b + c;
rowData.put(childIdx, new long[]{total, a, b, c});
catTotal += total; catA += a; catB += b; catC += c;
} }
// 分类标题行 = 子项之和
rowData.put(catRowIdx, new long[]{catTotal, catA, catB, catC});
grandTotal += catTotal; grandYearA += catA; grandYearB += catB; grandYearC += catC;
}
// 总计行
rowData.put(1, new long[]{grandTotal, grandYearA, grandYearB, grandYearC});
// 2. 填充表格
for (var entry : rowData.entrySet()) {
int rowIdx = entry.getKey();
long[] vals = entry.getValue();
XWPFTableRow row = table.getRow(rowIdx);
if (row == null) continue;
setCellText(row, 2, fmtLong(vals[0])); // 合计
setCellText(row, 3, fmtLong(vals[1])); // yearA
setCellText(row, 4, fmtLong(vals[2])); // yearB
setCellText(row, 5, fmtLong(vals[3])); // yearC
}
}
private void setCellText(XWPFTableRow row, int col, String text) {
if (row.getCell(col) != null) {
row.getCell(col).setText(text);
}
}
private static long randomInvest() {
// 生成 500~50000 万元之间的随机投资额
return ThreadLocalRandom.current().nextLong(500, 50001);
}
private static String fmtLong(long v) {
return String.format("%,d", v);
} }
} }
+1 -1
View File
@@ -6,4 +6,4 @@ spring:
name: gmp-mock-service name: gmp-mock-service
gmp: gmp:
download-origin: https://closein.site download-origin: https://closein.cn