feat: query annual report investment data from MySQL

This commit is contained in:
agentuser
2026-05-25 15:03:13 +08:00
parent 0c5c540e0f
commit 7b78091f0d
4 changed files with 160 additions and 72 deletions
+11
View File
@@ -47,6 +47,17 @@
<version>${poi-tl.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -0,0 +1,53 @@
package com.gmp.mock.template.annualReport;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
/**
* 从数据库查询年度投资数据。
*/
@Component
public class AnnualReportDataService {
private final JdbcTemplate jdbc;
public AnnualReportDataService(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
/**
* 按专业条线、年份汇总项目投资金额。
*
* @param years 年份列表,如 [2024, 2025, 2026]
* @return Map<projectTypeCode, Map<year, totalAmount>>
*/
public Map<String, Map<Integer, Long>> queryInvestmentByYear(List<Integer> years) {
if (years.isEmpty()) return Map.of();
// 构建 IN 子句
String placeholders = years.stream().map(y -> "?").reduce((a, b) -> a + "," + b).orElse("");
String sql = String.format("""
SELECT PROJECT_TYPE_CODE, REPLY_YEAR, ROUND(SUM(PROJECT_AMOUNT), 0) AS total
FROM pc_ip_project_version
WHERE REPLY_YEAR IN (%s) AND IS_DELETED = 2
GROUP BY PROJECT_TYPE_CODE, REPLY_YEAR
ORDER BY PROJECT_TYPE_CODE, REPLY_YEAR
""", placeholders);
Object[] params = years.toArray();
Map<String, Map<Integer, Long>> result = new HashMap<>();
jdbc.query(sql, params, rs -> {
String code = rs.getString("PROJECT_TYPE_CODE");
int year = rs.getInt("REPLY_YEAR");
long total = rs.getLong("total");
result.computeIfAbsent(code, k -> new HashMap<>()).put(year, total);
});
return result;
}
}
@@ -6,7 +6,6 @@ import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFTable;
@@ -20,35 +19,49 @@ import com.gmp.mock.template.TemplateProvider;
/**
* 年度报告模板填充器。
*
* 模板含一个固定表格,行结构如下:
* Row 0: 表头(序号 | 专业类别 | 合计 | yearA | yearB | yearC)
* Row 1: 总计(汇总行)
* Row 2~11: 各投资分类(电网基建/小型基建/技改/科技/信息化)
* 模板含一个固定表格,数据从数据库查询,按专业条线 + 年份汇总。
*
* postProcess() 遍历现有行,按行名填充生成的投资数据到单元格。
* 编码映射:
* 主网基建 ← TConstruct
* 配网基建 ← DEngineering
* 小型基建 ← SConstruct
* 生产技改 ← Imp
* 营销技改 ← MarketPM
* 其他技改 ← OImp
* 信息化项目 ← Info
* 科技项目 ← 无对应数据,填 0
*/
@Component
public class AnnualReportProvider implements TemplateProvider {
/** 行索引 → 生成策略 */
private static final List<RowConfig> ROW_CONFIGS = List.of(
new RowConfig(1, "总计", true), // 汇总行:自动求和
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) // 分类标题行(只有一项)
);
/** 行索引 → 对应 PROJECT_TYPE_CODE(null 表示标题汇总行或无数据) */
private static final Map<Integer, String> ROW_TO_CODE = new LinkedHashMap<>();
static {
ROW_TO_CODE.put(3, "TConstruct"); // 主网基建
ROW_TO_CODE.put(4, "DEngineering"); // 配网基建
ROW_TO_CODE.put(5, "SConstruct"); // 小型基建
ROW_TO_CODE.put(7, "Imp"); // 生产技改
ROW_TO_CODE.put(8, "MarketPM"); // 营销技改
ROW_TO_CODE.put(9, "OImp"); // 其他技改
ROW_TO_CODE.put(10, "Tech"); // 科技项目
ROW_TO_CODE.put(11, "Info"); // 信息化项目
}
/** 非标题行的行索引列表(需要实际填充数据) */
private static final List<Integer> DATA_ROW_INDEXES = List.of(3, 4, 7, 8, 9, 10, 11);
/** 分类标题行 → 对应的数据行索引列表 */
private static final Map<Integer, List<Integer>> CATEGORY_CHILDREN = new LinkedHashMap<>();
static {
CATEGORY_CHILDREN.put(2, List.of(3, 4)); // 电网基建
CATEGORY_CHILDREN.put(5, List.of(5)); // 小型基建
CATEGORY_CHILDREN.put(6, List.of(7, 8, 9)); // 技改项目
CATEGORY_CHILDREN.put(10, List.of(10)); // 科技项目
CATEGORY_CHILDREN.put(11, List.of(11)); // 信息化项目
}
private record RowConfig(int index, String name, boolean isHeader) {}
private final AnnualReportDataService dataService;
public AnnualReportProvider(AnnualReportDataService dataService) {
this.dataService = dataService;
}
@Override
public String getTemplateId() {
@@ -97,54 +110,61 @@ public class AnnualReportProvider implements TemplateProvider {
if (tables.isEmpty()) return;
XWPFTable table = tables.get(0);
// 1. 为每个数据行生成随机投资数据
List<Integer> years = List.of(2024, 2025, 2026);
Map<String, Map<Integer, Long>> rawData = dataService.queryInvestmentByYear(years);
// 1. 计算每个数据行
Map<Integer, long[]> rowData = new LinkedHashMap<>();
long grandTotal = 0;
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()) {
for (var entry : ROW_TO_CODE.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
String code = entry.getValue();
long a = 0, b = 0, c = 0;
if (code != null) {
Map<Integer, Long> byYear = rawData.getOrDefault(code, Map.of());
a = byYear.getOrDefault(2024, 0L);
b = byYear.getOrDefault(2025, 0L);
c = byYear.getOrDefault(2026, 0L);
}
rowData.put(rowIdx, new long[]{a + b + c, a, b, c});
}
// 2. 计算分类汇总和总计
Map<Integer, long[]> summaries = new LinkedHashMap<>();
long grandTotal = 0, grandA = 0, grandB = 0, grandC = 0;
for (var entry : CATEGORY_CHILDREN.entrySet()) {
int catRow = entry.getKey();
long catTotal = 0, catA = 0, catB = 0, catC = 0;
for (int child : entry.getValue()) {
long[] v = rowData.get(child);
if (v != null) {
catTotal += v[0]; catA += v[1]; catB += v[2]; catC += v[3];
}
}
summaries.put(catRow, new long[]{catTotal, catA, catB, catC});
grandTotal += catTotal; grandA += catA; grandB += catB; grandC += catC;
}
summaries.put(1, new long[]{grandTotal, grandA, grandB, grandC});
// 3. 填入表格
for (var entry : summaries.entrySet()) {
fillRow(table, entry.getKey(), entry.getValue());
}
for (var entry : rowData.entrySet()) {
// 跳过已在 summaries 中的标题行
if (summaries.containsKey(entry.getKey())) continue;
fillRow(table, entry.getKey(), entry.getValue());
}
}
private void fillRow(XWPFTable table, int rowIdx, long[] vals) {
XWPFTableRow row = table.getRow(rowIdx);
if (row == null) return;
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) {
@@ -153,11 +173,6 @@ public class AnnualReportProvider implements TemplateProvider {
}
}
private static long randomInvest() {
// 生成 500~50000 万元之间的随机投资额
return ThreadLocalRandom.current().nextLong(500, 50001);
}
private static String fmtLong(long v) {
return String.format("%,d", v);
}
+9
View File
@@ -7,3 +7,12 @@ spring:
gmp:
download-origin: https://closein.cn
spring:
datasource:
url: jdbc:mysql://localhost:3306/pc_ip_investapproval?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: PC_IP_INVESTAPPROVAL
password: Insigma123$%^
hikari:
maximum-pool-size: 5
minimum-idle: 1