From 2b39c4eb64b194e23f338873e4467eda2b18ccc3 Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期二, 22 九月 2026 11:28:56 +0800
Subject: [PATCH] feat(报表生成): 汇总大表导出结果落盘缓存,无新导入即秒回复用

---
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java |  135 +++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 124 insertions(+), 11 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 ac01627..6c430eb 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
@@ -8,6 +8,7 @@
 
 import javax.annotation.PreDestroy;
 import javax.annotation.Resource;
+import java.io.File;
 import java.util.Date;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
@@ -26,10 +27,11 @@
  * 鐩殑锛�2026-09-20 鐢ㄦ埛涓婃姤銆岀偣涓嬭浇瑕佺瓑涓�浼氾紝鏁版嵁澶氫簡浼氫笉浼氬崱姝汇�嶏級锛�
  * 1) 鐐瑰嚮鍚庣珛鍒绘湁鍙嶉锛屼笉鍐嶉暱鏃堕棿鏃犲搷搴旓紱
  * 2) 鐢熸垚杩囩▼涓嶅崰鐢� HTTP 璇锋眰绾跨▼锛岄伩鍏嶅浜哄悓鏃剁偣鎶婂鍣ㄦ嫋姝伙紙宸ヤ綔绾跨▼鍥哄畾 1 涓紝澶氫綑璇锋眰鏄剧ず銆屾帓闃熶腑銆嶏級锛�
- * 3) 鍚屼竴鎶ヨ〃鏈� 10 鍒嗛挓鍐呯殑缁撴灉鍙鐢紝閬垮厤姣忔閲嶇畻鏁存湰绾� 4 涓囨牸鍏紡銆�
+ * 3) 鍚屼竴鎶ヨ〃鏈熺殑缁撴灉鍙鐢紙**钀界洏淇濆瓨锛岃繘绋嬮噸鍚悗渚濈劧鏈夋晥**锛夛紝閬垮厤姣忔閲嶇畻鏁存湰绾� 4 涓囨牸鍏紡銆�
  *
- * 缂撳瓨姝g‘鎬у畧鍗細鍙湁褰撱�岃窛涓婃鐢熸垚涓嶈秴杩� TTL銆嶄笖銆屾湡闂存病鏈変换浣曟柊鐨勬暟鎹鍏ワ紙import_batch 鏈�澶� id 鏈彉锛夈��
- * 鏃舵墠澶嶇敤锛涘鍏ュ嵆澶辨晥锛岄伩鍏嶆妸杩囨湡鎶ヨ〃缁欑敤鎴枫�傚墠绔粛鎻愪緵銆岄噸鏂扮敓鎴愩�嶅己鍒跺埛鏂般��
+ * 缂撳瓨姝g‘鎬у畧鍗細鍙湁褰撱�屾湡闂存病鏈変换浣曟柊鐨勬暟鎹鍏ワ紙import_batch 鏈�澶� id 鏈彉锛夈�嶄笖銆岀粨鏋滀笉瓒呰繃 7 澶┿�嶆椂鎵嶅鐢紱
+ * 瀵煎叆鍗冲け鏁堬紝閬垮厤鎶婅繃鏈熸姤琛ㄧ粰鐢ㄦ埛銆傚墠绔粛鎻愪緵銆岄噸鏂扮敓鎴愩�嶅己鍒堕噸绠楋紙璺宠繃缂撳瓨锛夈��
+ * 钀界洏浣嶇疆锛歿summary.template-dir}/_cache/<key>_v<鏁版嵁鐗堟湰>.xlsx
  */
 @Slf4j
 @Service
@@ -40,13 +42,41 @@
 
     /** 鐢熸垚缁撴灉淇濈暀鏃堕暱锛氭湡闂村彲鍙嶅涓嬭浇锛岃繃鍚庤嚜鍔ㄦ竻鐞� */
     private static final long RESULT_TTL_MS = 30 * 60 * 1000L;
-    /** 鍚屼竴鎶ヨ〃鏈熺粨鏋滃鐢ㄦ椂闀匡紙涓旀湡闂存棤鏂板鍏ユ椂鎵嶅鐢級 */
-    private static final long CACHE_TTL_MS = 10 * 60 * 1000L;
+    /** 鍚屼竴鎶ヨ〃鏈熺粨鏋滃鐢ㄤ笂闄愶細鍙鏈熼棿娌℃湁鏂板鍏ュ氨涓�鐩存部鐢紱瓒呰繃璇ュぉ鏁颁笉鍐嶅鐢紙闃叉闄堝勾缁撴灉琚綋鏂拌〃鍙戝嚭锛� */
+    private static final long CACHE_MAX_AGE_MS = 7L * 24 * 60 * 60 * 1000;
+    /** 钀界洏缂撳瓨瀛愮洰褰曪紙鐩稿 summary.template-dir锛� */
+    private static final String CACHE_SUBDIR = "_cache";
 
     @Resource
     private ReportExportService reportService;
     @Resource
     private ImportBatchMapper importBatchMapper;
+
+    /** 姹囨�诲伐浣滅翱鐩綍锛堜笌 ReportExportService 鍚岄厤缃級锛岃惤鐩樼紦瀛樻斁鍦ㄥ叾 _cache 瀛愮洰褰� */
+    @org.springframework.beans.factory.annotation.Value("${summary.template-dir:docs/鐢熸垚姹囨�诲ぇ琛▆")
+    private String summaryTemplateDir;
+
+    /** 钀界洏缂撳瓨鐩綍锛涘畾浣嶇瓥鐣ヤ笌妯℃澘涓�鑷达紙閰嶇疆鐩綍 / user.dir 鐩稿 / 涓婄骇鐩綍锛� */
+    private File cacheDir() {
+        String rel = summaryTemplateDir;
+        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 base = new File(root);
+            if (base.isDirectory()) {
+                File dir = new File(base, CACHE_SUBDIR);
+                if (!dir.isDirectory() && !dir.mkdirs()) continue;
+                return dir;
+            }
+        }
+        return null;
+    }
+
+    /** 缂撳瓨鏂囦欢鍚嶅墠缂�锛堜笉鍚暟鎹増鏈笌鎵╁睍鍚嶏級锛歬ey 閲岀殑闈炴硶瀛楃缁熶竴鏇挎崲 */
+    private String cacheFilePrefix(String type, String period, String mode, Integer toMonth) {
+        return (cacheKey(type, period, mode, toMonth)).replaceAll("[^0-9A-Za-z_-]", "_");
+    }
 
     private final Map<String, Task> tasks = new ConcurrentHashMap<>();
     /** 鍚屾姤琛ㄦ湡鏈�杩戜竴娆℃垚鍔熺粨鏋滐細key -> task */
@@ -64,7 +94,7 @@
         private final String period;
         private final String mode;
         private final Integer toMonth;
-        private final long createdAt = System.currentTimeMillis();
+        private volatile long createdAt = System.currentTimeMillis();
         /** 鐢熸垚鏃剁殑鏁版嵁鐗堟湰锛坕mport_batch 鏈�澶� id锛夛紝鐢ㄤ簬鍒ゆ柇缂撳瓨鏄惁杩囨湡 */
         private final long importVersion;
         private volatile String status = "PENDING"; // PENDING/RUNNING/DONE/FAILED
@@ -72,6 +102,8 @@
         private volatile String step = "鎺掗槦涓�";
         private volatile String error;
         private volatile byte[] data;
+        /** 钀界洏缂撳瓨鏂囦欢锛氬唴瀛樻暟鎹娓呯悊鍚庝粛鍙粠杩欓噷鍙栵紙璺ㄨ繘绋嬮噸鍚鐢級 */
+        private volatile File cacheFile;
         private volatile String fileName;
         private volatile boolean fromCache;
         private volatile long finishedAt;
@@ -93,11 +125,24 @@
 
         public String getStatus() { return status; }
 
-        public boolean isDone() { return "DONE".equals(status) && data != null; }
+        public boolean isDone() {
+            return "DONE".equals(status) && (data != null || (cacheFile != null && cacheFile.isFile()));
+        }
 
         public boolean isFromCache() { return fromCache; }
 
-        public byte[] getData() { return data; }
+        public byte[] getData() {
+            if (data != null) return data;
+            File f = cacheFile;
+            if (f != null && f.isFile()) {
+                try {
+                    return java.nio.file.Files.readAllBytes(f.toPath());
+                } catch (Exception e) {
+                    log.warn("璇诲彇瀵煎嚭缂撳瓨鏂囦欢澶辫触锛歿} {}", f.getAbsolutePath(), e.getMessage());
+                }
+            }
+            return null;
+        }
 
         public String getFileName() { return fileName; }
 
@@ -135,8 +180,16 @@
             Task hit = cache.get(key);
             if (cacheUsable(hit)) {
                 hit.fromCache = true;
-                log.info("鎶ヨ〃瀵煎嚭浠诲姟鍛戒腑缂撳瓨锛歿} {}锛堢敓鎴愪簬 {}锛�", type, period, new Date(hit.finishedAt));
+                log.info("鎶ヨ〃瀵煎嚭浠诲姟鍛戒腑鍐呭瓨缂撳瓨锛歿} {}锛堢敓鎴愪簬 {}锛�", type, period, new Date(hit.finishedAt));
                 return hit;
+            }
+            Task disk = loadFromDisk(key, type, period, mode, toMonth);
+            if (disk != null) {
+                tasks.put(disk.id, disk);
+                cache.put(key, disk);
+                log.info("鎶ヨ〃瀵煎嚭浠诲姟鍛戒腑钀界洏缂撳瓨锛歿} {}锛堢敓鎴愪簬 {}锛屾枃浠� {}锛�", type, period,
+                        new Date(disk.finishedAt), disk.cacheFile.getName());
+                return disk;
             }
         }
         Task running = findRunning(key);
@@ -168,6 +221,7 @@
             t.status = "DONE";
             t.finishedAt = System.currentTimeMillis();
             cache.put(cacheKey(t.type, t.period, t.mode, t.toMonth), t);
+            writeToDisk(t, data);
             log.info("鎶ヨ〃瀵煎嚭浠诲姟瀹屾垚锛歿} {} 鐢ㄦ椂 {} ms锛寋} 瀛楄妭", t.type, t.period, t.finishedAt - start, data.length);
         } catch (Throwable e) {
             t.status = "FAILED";
@@ -188,7 +242,7 @@
 
     private boolean cacheUsable(Task t) {
         if (t == null || !"DONE".equals(t.status) || t.data == null) return false;
-        if (System.currentTimeMillis() - t.finishedAt > CACHE_TTL_MS) 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;
@@ -218,7 +272,66 @@
         }
         for (Iterator<Map.Entry<String, Task>> it = cache.entrySet().iterator(); it.hasNext(); ) {
             Task t = it.next().getValue();
-            if (t.finishedAt <= 0 || now - t.finishedAt > CACHE_TTL_MS) it.remove();
+            if (t.finishedAt <= 0 || now - t.finishedAt > CACHE_MAX_AGE_MS) it.remove();
+        }
+        File dir = cacheDir();
+        if (dir == null) return;
+        File[] fs = dir.listFiles((d, n) -> n.endsWith(".xlsx"));
+        if (fs == null) return;
+        for (File f : fs) {
+            if (now - f.lastModified() > CACHE_MAX_AGE_MS && !f.delete()) {
+                log.warn("娓呯悊杩囨湡瀵煎嚭缂撳瓨澶辫触锛歿}", f.getAbsolutePath());
+            }
+        }
+    }
+
+    /** 鐢熸垚鎴愬姛鍚庢妸缁撴灉钀界洏锛屼緵杩涚▼閲嶅惎鍚庡鐢紱鍐欑洏澶辫触鍙憡璀︼紝涓嶅奖鍝嶆湰娆″鍑� */
+    private void writeToDisk(Task t, byte[] data) {
+        try {
+            File dir = cacheDir();
+            if (dir == null) return;
+            long version = t.importVersion;
+            if (version < 0) return;
+            String prefix = cacheFilePrefix(t.type, t.period, t.mode, t.toMonth);
+            File target = new File(dir, prefix + "_v" + version + ".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) {
+                for (File f : siblings) {
+                    if (!f.getName().equals(target.getName()) && !f.delete()) {
+                        log.warn("娓呯悊鏃х増鏈鍑虹紦瀛樺け璐ワ細{}", f.getAbsolutePath());
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.warn("瀵煎嚭缁撴灉钀界洏澶辫触锛堜笉褰卞搷鏈瀵煎嚭锛屼粎鏃犳硶璺ㄩ噸鍚鐢級锛歿}", e.getMessage());
+        }
+    }
+
+    /** 浠庤惤鐩樼紦瀛橀噷鎵俱�屽悓涓�鎶ヨ〃鏈� + 鍚屼竴鏁版嵁鐗堟湰 + 鏈秴鏈熴�嶇殑缁撴灉锛涙病鏈夎繑鍥� null */
+    private Task loadFromDisk(String key, String type, String period, String mode, Integer toMonth) {
+        try {
+            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");
+            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);
+            t.cacheFile = f;
+            t.createdAt = finished; // 澶嶇敤鍘嗗彶缁撴灉锛氫互鐢熸垚鏃堕棿浣滀负璧风偣锛岄伩鍏� elapsedMs 鍑虹幇璐熸暟
+            t.fileName = "鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃_" + period + ".xlsx";
+            t.status = "DONE";
+            t.percent = 100;
+            t.step = "瀹屾垚";
+            t.finishedAt = finished;
+            t.fromCache = true;
+            return t;
+        } catch (Exception e) {
+            log.warn("璇诲彇钀界洏瀵煎嚭缂撳瓨澶辫触锛歿}", e.getMessage());
+            return null;
         }
     }
 

--
Gitblit v1.9.1