From 797d76ac40b997deca78b74aa1264e0827ae3f26 Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期二, 22 九月 2026 13:07:35 +0800
Subject: [PATCH] feat(报表生成): 导出缓存守卫改用数据指纹(无关导入不再失效 / 换母版必定失效)+ 失效提示
---
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java | 177 +++++++++++++++++++++++++++++++++++++++++++++++------------
1 files changed, 141 insertions(+), 36 deletions(-)
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java
index 6c430eb..b3a9ff5 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java
@@ -1,19 +1,27 @@
package com.trafficaudit.reportexport.service;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.trafficaudit.dataimport.entity.ImportBatch;
-import com.trafficaudit.dataimport.mapper.ImportBatchMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
+import javax.sql.DataSource;
import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
import java.util.Date;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
@@ -29,9 +37,11 @@
* 2) 鐢熸垚杩囩▼涓嶅崰鐢� HTTP 璇锋眰绾跨▼锛岄伩鍏嶅浜哄悓鏃剁偣鎶婂鍣ㄦ嫋姝伙紙宸ヤ綔绾跨▼鍥哄畾 1 涓紝澶氫綑璇锋眰鏄剧ず銆屾帓闃熶腑銆嶏級锛�
* 3) 鍚屼竴鎶ヨ〃鏈熺殑缁撴灉鍙鐢紙**钀界洏淇濆瓨锛岃繘绋嬮噸鍚悗渚濈劧鏈夋晥**锛夛紝閬垮厤姣忔閲嶇畻鏁存湰绾� 4 涓囨牸鍏紡銆�
*
- * 缂撳瓨姝g‘鎬у畧鍗細鍙湁褰撱�屾湡闂存病鏈変换浣曟柊鐨勬暟鎹鍏ワ紙import_batch 鏈�澶� id 鏈彉锛夈�嶄笖銆岀粨鏋滀笉瓒呰繃 7 澶┿�嶆椂鎵嶅鐢紱
- * 瀵煎叆鍗冲け鏁堬紝閬垮厤鎶婅繃鏈熸姤琛ㄧ粰鐢ㄦ埛銆傚墠绔粛鎻愪緵銆岄噸鏂扮敓鎴愩�嶅己鍒堕噸绠楋紙璺宠繃缂撳瓨锛夈��
- * 钀界洏浣嶇疆锛歿summary.template-dir}/_cache/<key>_v<鏁版嵁鐗堟湰>.xlsx
+ * 缂撳瓨姝g‘鎬у畧鍗紳**鏁版嵁鎸囩汗**锛氭寚绾圭敱銆屾眹鎬诲ぇ琛ㄤ緷璧栫殑婧愯〃鍐呭鏍¢獙鍜屻��+銆屾瘝鐗堢瓑鏂囦欢韬唤锛堝ぇ灏�/淇敼鏃堕棿锛夈�嶇畻鍑猴紝
+ * 鍙湁鎸囩汗涓�鑷达紙涓旂粨鏋滀笉瓒呰繃 7 澶╋級鎵嶅鐢ㄣ�傚洜姝わ細
+ * - 鎶曡祫/鑳借��/瀹℃牳绛�**涓庢眹鎬诲ぇ琛ㄦ棤鍏�**鐨勫鍏ヤ笉鍐嶈缂撳瓨澶辨晥锛�
+ * - 鎹㈡瘝鐗堛�佹敼鍙傜収浠躲�佸姩鐩稿叧婧愯〃锛堝惈鍘熷湴 UPDATE锛夐兘浼氳缂撳瓨澶辨晥锛屼笉浼氭妸鏃ц〃褰撴柊琛ㄥ彂鍑恒��
+ * 鍓嶇浠嶆彁渚涖�岄噸鏂扮敓鎴愩�嶅己鍒堕噸绠楋紙璺宠繃缂撳瓨锛夈�傝惤鐩樹綅缃細{summary.template-dir}/_cache/<key>_v<鎸囩汗>.xlsx
*/
@Slf4j
@Service
@@ -50,11 +60,24 @@
@Resource
private ReportExportService reportService;
@Resource
- private ImportBatchMapper importBatchMapper;
+ private DataSource dataSource;
/** 姹囨�诲伐浣滅翱鐩綍锛堜笌 ReportExportService 鍚岄厤缃級锛岃惤鐩樼紦瀛樻斁鍦ㄥ叾 _cache 瀛愮洰褰� */
@org.springframework.beans.factory.annotation.Value("${summary.template-dir:docs/鐢熸垚姹囨�诲ぇ琛▆")
private String summaryTemplateDir;
+
+ /** 鍩庡競瀹㈣繍鐩綍锛氱綉绾﹁溅璁㈠崟鍙婂叏鐪佹�婚噺.xlsx锛堝簱閲岀己璇ユ湀鏃剁殑鍏滃簳杈撳叆锛� */
+ @org.springframework.beans.factory.annotation.Value("${city-passenger.template-dir:docs/鍩庡競瀹㈣繍}")
+ private String cityPassengerTemplateDir;
+
+ /** 璁$畻鎸囩汗鏃�**鎺掗櫎**鐨勮〃锛氭姇璧勭嚎銆佽兘鑰楃嚎銆佸鏍哥嚎銆佺郴缁熻〃銆佸鍏ユ棩蹇椼��
+ * 杩欎簺琛ㄥ彉鍖栦笉褰卞搷姹囨�诲ぇ琛ㄥ唴瀹癸紝娌″繀瑕佽缂撳瓨澶辨晥锛涘叾浣欒〃涓�寰嬬撼鍏ワ紙瀹佸彲澶氱畻涓嶅彲鍑洪敊锛夈�� */
+ private static final Set<String> FINGERPRINT_EXCLUDE = new HashSet<>(Arrays.asList(
+ "investment_project", "investment_monthly", "investment_system",
+ "h204_vehicle_quarterly", "h204_auth_vehicle",
+ "audit_result", "audit_run", "audit_rule", "audit_explanation",
+ "sys_user", "sys_role", "sys_user_role", "sys_dept", "sys_operation_log",
+ "llm_desensitize_map", "import_batch"));
/** 钀界洏缂撳瓨鐩綍锛涘畾浣嶇瓥鐣ヤ笌妯℃澘涓�鑷达紙閰嶇疆鐩綍 / user.dir 鐩稿 / 涓婄骇鐩綍锛� */
private File cacheDir() {
@@ -95,8 +118,8 @@
private final String mode;
private final Integer toMonth;
private volatile long createdAt = System.currentTimeMillis();
- /** 鐢熸垚鏃剁殑鏁版嵁鐗堟湰锛坕mport_batch 鏈�澶� id锛夛紝鐢ㄤ簬鍒ゆ柇缂撳瓨鏄惁杩囨湡 */
- private final long importVersion;
+ /** 鐢熸垚鏃剁殑鏁版嵁鎸囩汗锛堟簮琛ㄦ牎楠屽拰 + 姣嶇増鏂囦欢韬唤锛夛紝鐢ㄤ簬鍒ゆ柇缂撳瓨鏄惁杩囨湡 */
+ private final String fingerprint;
private volatile String status = "PENDING"; // PENDING/RUNNING/DONE/FAILED
private volatile int percent = 0;
private volatile String step = "鎺掗槦涓�";
@@ -106,14 +129,16 @@
private volatile File cacheFile;
private volatile String fileName;
private volatile boolean fromCache;
+ /** 鏈銆屾病鍛戒腑缂撳瓨銆嶆槸鍥犱负瀛樺湪鏃х粨鏋滀絾鏁版嵁/姣嶇増宸插彉锛堢敤浜庣粰鐢ㄦ埛涓�鍙ヨВ閲婏級 */
+ private volatile boolean staleCache;
private volatile long finishedAt;
- Task(String type, String period, String mode, Integer toMonth, long importVersion) {
+ Task(String type, String period, String mode, Integer toMonth, String fingerprint) {
this.type = type;
this.period = period;
this.mode = mode;
this.toMonth = toMonth;
- this.importVersion = importVersion;
+ this.fingerprint = fingerprint;
}
void progress(int pct, String stepText) {
@@ -159,6 +184,7 @@
m.put("percent", "DONE".equals(status) ? 100 : percent);
m.put("step", step);
m.put("fromCache", fromCache);
+ m.put("staleCache", staleCache);
m.put("fileName", fileName);
m.put("error", error);
long end = finishedAt > 0 ? finishedAt : System.currentTimeMillis();
@@ -170,20 +196,22 @@
/**
* 鎻愪氦瀵煎嚭浠诲姟銆俧orceRefresh=true 鏃跺拷鐣ョ紦瀛橀噸鏂扮敓鎴愶紱
- * 鍚﹀垯鍦ㄣ�屽悓涓�鎶ヨ〃鏈熴��10 鍒嗛挓鍐呫�佹湡闂存棤鏂板鍏ャ�嶇殑鍓嶆彁涓嬬洿鎺ュ鐢ㄤ笂娆$粨鏋溿��
+ * 鍚﹀垯鍦ㄣ�屽悓涓�鎶ヨ〃鏈� + 鏁版嵁鎸囩汗涓�鑷� + 7 澶╁唴銆嶇殑鍓嶆彁涓嬬洿鎺ュ鐢ㄤ笂娆$粨鏋溿��
* 鍚屼竴鎶ヨ〃鏈熷凡鏈変换鍔″湪璺戞椂鐩存帴杩斿洖璇ヤ换鍔★紝閬垮厤閲嶅鎺掗槦銆�
*/
public Task submit(String type, String period, String mode, Integer toMonth, boolean forceRefresh) {
cleanupExpired();
String key = cacheKey(type, period, mode, toMonth);
+ String fingerprint = dataFingerprint();
if (!forceRefresh) {
Task hit = cache.get(key);
- if (cacheUsable(hit)) {
+ if (cacheUsable(hit, fingerprint)) {
hit.fromCache = true;
+ hit.staleCache = false; // 鍛戒腑缂撳瓨鏃朵笉鑳芥畫鐣欎笂涓�娆°�岀粨鏋滃凡澶辨晥銆嶇殑鎻愮ず
log.info("鎶ヨ〃瀵煎嚭浠诲姟鍛戒腑鍐呭瓨缂撳瓨锛歿} {}锛堢敓鎴愪簬 {}锛�", type, period, new Date(hit.finishedAt));
return hit;
}
- Task disk = loadFromDisk(key, type, period, mode, toMonth);
+ Task disk = loadFromDisk(key, type, period, mode, toMonth, fingerprint);
if (disk != null) {
tasks.put(disk.id, disk);
cache.put(key, disk);
@@ -197,7 +225,9 @@
log.info("鎶ヨ〃瀵煎嚭浠诲姟宸插湪鎵ц锛屽鐢ㄨ繘琛屼腑鐨勪换鍔★細{} {}锛坽}锛�", type, period, running.id);
return running;
}
- Task task = new Task(type, period, mode, toMonth, importVersion());
+ Task task = new Task(type, period, mode, toMonth, fingerprint);
+ // 鏈夋棫缁撴灉浣嗘病鍛戒腑锛堟暟鎹垨姣嶇増鍙樹簡锛夆啋 鍓嶇鎻愮ず涓�鍙ワ紝閬垮厤鐢ㄦ埛浠ヤ负"缂撳瓨鍧忎簡"
+ task.staleCache = !forceRefresh && hasStaleCache(type, period, mode, toMonth, fingerprint);
tasks.put(task.id, task);
pool.submit(() -> run(task));
return task;
@@ -240,23 +270,99 @@
return null;
}
- private boolean cacheUsable(Task t) {
- if (t == null || !"DONE".equals(t.status) || t.data == null) return false;
+ private boolean cacheUsable(Task t, String fingerprint) {
+ if (t == null || !"DONE".equals(t.status)) return false;
+ if (t.data == null && (t.cacheFile == null || !t.cacheFile.isFile())) return false;
if (System.currentTimeMillis() - t.finishedAt > CACHE_MAX_AGE_MS) return false;
- long now = importVersion();
- if (now < 0 || t.importVersion < 0) return false; // 鍙栦笉鍒版暟鎹増鏈椂涓嶅啋椋庨櫓澶嶇敤
- return now == t.importVersion;
+ if (fingerprint == null || t.fingerprint == null) return false; // 绠椾笉鍑烘寚绾规椂涓嶅啋椋庨櫓澶嶇敤
+ return fingerprint.equals(t.fingerprint);
}
- /** 鏁版嵁鐗堟湰锛漣mport_batch 鏈�澶� id锛氫换浣曚竴娆″鍏ラ兘浼氳瀹冨彉鍖栵紝鍙栦笉鍒拌繑鍥� -1锛堟鏃剁鐢ㄧ紦瀛橈級 */
- private long importVersion() {
+ /**
+ * 姹囨�诲ぇ琛ㄦ暟鎹寚绾癸紳銆屼緷璧栫殑婧愯〃鍐呭鏍¢獙鍜屻��+銆屾瘝鐗堢瓑鏂囦欢韬唤銆嶃��
+ * - 婧愯〃锛氬簱鍐呴櫎 FINGERPRINT_EXCLUDE锛堟姇璧�/鑳借��/瀹℃牳/绯荤粺/瀵煎叆鏃ュ織锛変互澶栫殑鍏ㄩ儴琛紝鐢� CHECKSUM TABLE 鍙栨牎楠屽拰锛�
+ * 鑳芥崟鎹夎鏁板彉鍖栥�佹柊澧炲垹闄ゃ��**鍘熷湴 UPDATE**锛�
+ * - 鏂囦欢锛氭瘝鐗堢洰褰曚笅鍏ㄩ儴鏂囦欢锛堟瘝鐗� / 鍙傜収浠� / _澶囦唤_ 鍥炲~浠讹級+銆婄綉绾﹁溅璁㈠崟鍙婂叏鐪佹�婚噺.xlsx銆嬶紝鍙栥�屽悕绉�+澶у皬+淇敼鏃堕棿銆嶃��
+ * 浠讳竴椤瑰彉鍖栧嵆鎸囩汗鍙樺寲 鈫� 缂撳瓨澶辨晥銆傝绠楀け璐ヨ繑鍥� null锛堟鏃剁鐢ㄧ紦瀛橈紝鍙噸绠椾笉澶嶇敤锛夈��
+ */
+ private String dataFingerprint() {
try {
- List<Object> v = importBatchMapper.selectObjs(new QueryWrapper<ImportBatch>().select("IFNULL(MAX(id),0)"));
- if (v == null || v.isEmpty() || v.get(0) == null) return 0L;
- return ((Number) v.get(0)).longValue();
+ List<String> parts = new ArrayList<>();
+ try (Connection conn = dataSource.getConnection()) {
+ List<String> tables = new ArrayList<>();
+ try (Statement st = conn.createStatement();
+ ResultSet rs = st.executeQuery("SELECT table_name FROM information_schema.tables "
+ + "WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE' ORDER BY table_name")) {
+ while (rs.next()) tables.add(rs.getString(1));
+ }
+ for (String t : tables) {
+ if (FINGERPRINT_EXCLUDE.contains(t)) continue;
+ try (Statement st = conn.createStatement();
+ ResultSet rs = st.executeQuery("CHECKSUM TABLE `" + t + "`")) {
+ if (rs.next()) parts.add("T:" + t + ":" + rs.getString(2));
+ }
+ }
+ }
+ for (File f : listIdentityFiles(resolveBaseDir(summaryTemplateDir))) {
+ parts.add("F:" + f.getName() + ":" + f.length() + ":" + f.lastModified());
+ }
+ File wyc = new File(resolveBaseDir(cityPassengerTemplateDir), "缃戠害杞﹁鍗曞強鍏ㄧ渷鎬婚噺.xlsx");
+ if (wyc.isFile()) parts.add("F:" + wyc.getName() + ":" + wyc.length() + ":" + wyc.lastModified());
+ return sha256Hex(String.join("|", parts)).substring(0, 16);
} catch (Exception e) {
- log.warn("璇诲彇瀵煎叆鏁版嵁鐗堟湰澶辫触锛屾湰娆$鐢ㄥ鍑虹紦瀛橈細{}", e.getMessage());
- return -1L;
+ log.warn("璁$畻鏁版嵁鎸囩汗澶辫触锛屾湰娆$鐢ㄥ鍑虹紦瀛橈細{}", e.getMessage());
+ return null;
+ }
+ }
+
+ /** 姣嶇増鐩綍绛夊熀纭�鐩綍瀹氫綅锛堜笌妯℃澘瑙f瀽鍚岀瓥鐣ワ細閰嶇疆鐩綍 / user.dir 鐩稿 / 涓婄骇鐩綍锛� */
+ private File resolveBaseDir(String dir) {
+ if (dir == null) return null;
+ String rel = dir;
+ while (rel.startsWith("./")) rel = rel.substring(2);
+ String[] roots = {rel, System.getProperty("user.dir") + "/" + rel, System.getProperty("user.dir") + "/../" + rel};
+ for (String root : roots) {
+ if (root == null || root.trim().isEmpty()) continue;
+ File f = new File(root);
+ if (f.isDirectory()) return f;
+ }
+ return null;
+ }
+
+ /** 鐩綍涓嬬殑鏂囦欢锛堟寜鍚嶅瓧鎺掑簭锛屾帓闄ゅ瓙鐩綍锛屼繚璇佹寚绾圭ǔ瀹氾級 */
+ private List<File> listIdentityFiles(File dir) {
+ List<File> out = new ArrayList<>();
+ if (dir == null) return out;
+ File[] fs = dir.listFiles(File::isFile);
+ if (fs == null) return out;
+ Arrays.sort(fs, Comparator.comparing(File::getName));
+ out.addAll(Arrays.asList(fs));
+ return out;
+ }
+
+ private String sha256Hex(String text) throws Exception {
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ byte[] d = md.digest(text.getBytes(StandardCharsets.UTF_8));
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < 8; i++) sb.append(String.format("%02x", d[i])); // 鍙栧墠 8 瀛楄妭锛�16 浣嶅崄鍏繘鍒讹紝澶熺敤涓旀枃浠跺悕鐭�
+ return sb.toString();
+ }
+
+ /** 璇ユ姤琛ㄦ湡鏄惁瀛樺湪銆屼笌褰撳墠鎸囩汗涓嶄竴鑷淬�嶇殑鏃х粨鏋滐紙鐢ㄤ簬鎻愮ず銆屼笂娆$粨鏋滃凡澶辨晥锛屾鍦ㄦ寜鏈�鏂版暟鎹噸绠椼�嶏級 */
+ private boolean hasStaleCache(String type, String period, String mode, Integer toMonth, String fingerprint) {
+ try {
+ File dir = cacheDir();
+ if (dir == null) return false;
+ String prefix = cacheFilePrefix(type, period, mode, toMonth);
+ File[] fs = dir.listFiles((d, n) -> n.startsWith(prefix + "_v") && n.endsWith(".xlsx"));
+ if (fs == null || fs.length == 0) return false;
+ String current = prefix + "_v" + fingerprint + ".xlsx";
+ for (File f : fs) {
+ if (!f.getName().equals(current)) return true;
+ }
+ return false;
+ } catch (Exception e) {
+ return false;
}
}
@@ -290,10 +396,10 @@
try {
File dir = cacheDir();
if (dir == null) return;
- long version = t.importVersion;
- if (version < 0) return;
+ String fingerprint = t.fingerprint;
+ if (fingerprint == null) return;
String prefix = cacheFilePrefix(t.type, t.period, t.mode, t.toMonth);
- File target = new File(dir, prefix + "_v" + version + ".xlsx");
+ File target = new File(dir, prefix + "_v" + fingerprint + ".xlsx");
java.nio.file.Files.write(target.toPath(), data);
File[] siblings = dir.listFiles((d, n) -> n.startsWith(prefix + "_v") && n.endsWith(".xlsx"));
if (siblings != null) {
@@ -308,18 +414,17 @@
}
}
- /** 浠庤惤鐩樼紦瀛橀噷鎵俱�屽悓涓�鎶ヨ〃鏈� + 鍚屼竴鏁版嵁鐗堟湰 + 鏈秴鏈熴�嶇殑缁撴灉锛涙病鏈夎繑鍥� null */
- private Task loadFromDisk(String key, String type, String period, String mode, Integer toMonth) {
+ /** 浠庤惤鐩樼紦瀛橀噷鎵俱�屽悓涓�鎶ヨ〃鏈� + 鍚屼竴鏁版嵁鎸囩汗 + 鏈秴鏈熴�嶇殑缁撴灉锛涙病鏈夎繑鍥� null */
+ private Task loadFromDisk(String key, String type, String period, String mode, Integer toMonth, String fingerprint) {
try {
+ if (fingerprint == null) return null; // 绠椾笉鍑烘寚绾规椂涓嶅啋椋庨櫓澶嶇敤
File dir = cacheDir();
if (dir == null) return null;
- long version = importVersion();
- if (version < 0) return null; // 鍙栦笉鍒版暟鎹増鏈椂涓嶅啋椋庨櫓澶嶇敤
- File f = new File(dir, cacheFilePrefix(type, period, mode, toMonth) + "_v" + version + ".xlsx");
+ File f = new File(dir, cacheFilePrefix(type, period, mode, toMonth) + "_v" + fingerprint + ".xlsx");
if (!f.isFile()) return null;
long finished = f.lastModified();
if (finished <= 0 || System.currentTimeMillis() - finished > CACHE_MAX_AGE_MS) return null;
- Task t = new Task(type, period, mode, toMonth, version);
+ Task t = new Task(type, period, mode, toMonth, fingerprint);
t.cacheFile = f;
t.createdAt = finished; // 澶嶇敤鍘嗗彶缁撴灉锛氫互鐢熸垚鏃堕棿浣滀负璧风偣锛岄伩鍏� elapsedMs 鍑虹幇璐熸暟
t.fileName = "鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃_" + period + ".xlsx";
--
Gitblit v1.9.1