From a08c70db67e49bf6cd125df04ebaf68c26f0d8dd Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期日, 20 九月 2026 16:19:16 +0800
Subject: [PATCH] feat(投资): 市州月报统一填报模板 + 县(市、区)列解析 + 表头行判定收紧

---
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java |  234 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 234 insertions(+), 0 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
new file mode 100644
index 0000000..ac01627
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java
@@ -0,0 +1,234 @@
+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 java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 鎶ヨ〃瀵煎嚭寮傛浠诲姟锛氭妸鑰楁椂鍗佸嚑绉掔殑銆婄敓鎴恄閬撹矾杩愯緭閲忔眹鎬昏〃銆嬬敓鎴愭尓鍒板悗鍙扮嚎绋嬶紝
+ * 鍓嶇鍙嬁銆屼换鍔″彿 + 杩涘害锛堟楠�/鐧惧垎姣旓級銆嶏紝瀹屾垚鍚庡崟鐙笅杞芥枃浠躲��
+ *
+ * 鐩殑锛�2026-09-20 鐢ㄦ埛涓婃姤銆岀偣涓嬭浇瑕佺瓑涓�浼氾紝鏁版嵁澶氫簡浼氫笉浼氬崱姝汇�嶏級锛�
+ * 1) 鐐瑰嚮鍚庣珛鍒绘湁鍙嶉锛屼笉鍐嶉暱鏃堕棿鏃犲搷搴旓紱
+ * 2) 鐢熸垚杩囩▼涓嶅崰鐢� HTTP 璇锋眰绾跨▼锛岄伩鍏嶅浜哄悓鏃剁偣鎶婂鍣ㄦ嫋姝伙紙宸ヤ綔绾跨▼鍥哄畾 1 涓紝澶氫綑璇锋眰鏄剧ず銆屾帓闃熶腑銆嶏級锛�
+ * 3) 鍚屼竴鎶ヨ〃鏈� 10 鍒嗛挓鍐呯殑缁撴灉鍙鐢紝閬垮厤姣忔閲嶇畻鏁存湰绾� 4 涓囨牸鍏紡銆�
+ *
+ * 缂撳瓨姝g‘鎬у畧鍗細鍙湁褰撱�岃窛涓婃鐢熸垚涓嶈秴杩� TTL銆嶄笖銆屾湡闂存病鏈変换浣曟柊鐨勬暟鎹鍏ワ紙import_batch 鏈�澶� id 鏈彉锛夈��
+ * 鏃舵墠澶嶇敤锛涘鍏ュ嵆澶辨晥锛岄伩鍏嶆妸杩囨湡鎶ヨ〃缁欑敤鎴枫�傚墠绔粛鎻愪緵銆岄噸鏂扮敓鎴愩�嶅己鍒跺埛鏂般��
+ */
+@Slf4j
+@Service
+public class ReportExportTaskService {
+
+    /** 姹囨�诲伐浣滅翱瀵煎嚭浠诲姟绫诲瀷 */
+    public static final String TYPE_SUMMARY_WORKBOOK = "summaryWorkbook";
+
+    /** 鐢熸垚缁撴灉淇濈暀鏃堕暱锛氭湡闂村彲鍙嶅涓嬭浇锛岃繃鍚庤嚜鍔ㄦ竻鐞� */
+    private static final long RESULT_TTL_MS = 30 * 60 * 1000L;
+    /** 鍚屼竴鎶ヨ〃鏈熺粨鏋滃鐢ㄦ椂闀匡紙涓旀湡闂存棤鏂板鍏ユ椂鎵嶅鐢級 */
+    private static final long CACHE_TTL_MS = 10 * 60 * 1000L;
+
+    @Resource
+    private ReportExportService reportService;
+    @Resource
+    private ImportBatchMapper importBatchMapper;
+
+    private final Map<String, Task> tasks = new ConcurrentHashMap<>();
+    /** 鍚屾姤琛ㄦ湡鏈�杩戜竴娆℃垚鍔熺粨鏋滐細key -> task */
+    private final Map<String, Task> cache = new ConcurrentHashMap<>();
+    private final ExecutorService pool = Executors.newFixedThreadPool(1, r -> {
+        Thread t = new Thread(r, "report-export-task");
+        t.setDaemon(true);
+        return t;
+    });
+
+    /** 涓�娆″鍑轰换鍔★紙鍐呭瓨鎬侊紝杩涚▼閲嶅惎鍗冲け鏁堬級 */
+    public static class Task {
+        private final String id = UUID.randomUUID().toString().replace("-", "");
+        private final String type;
+        private final String period;
+        private final String mode;
+        private final Integer toMonth;
+        private final long createdAt = System.currentTimeMillis();
+        /** 鐢熸垚鏃剁殑鏁版嵁鐗堟湰锛坕mport_batch 鏈�澶� id锛夛紝鐢ㄤ簬鍒ゆ柇缂撳瓨鏄惁杩囨湡 */
+        private final long importVersion;
+        private volatile String status = "PENDING"; // PENDING/RUNNING/DONE/FAILED
+        private volatile int percent = 0;
+        private volatile String step = "鎺掗槦涓�";
+        private volatile String error;
+        private volatile byte[] data;
+        private volatile String fileName;
+        private volatile boolean fromCache;
+        private volatile long finishedAt;
+
+        Task(String type, String period, String mode, Integer toMonth, long importVersion) {
+            this.type = type;
+            this.period = period;
+            this.mode = mode;
+            this.toMonth = toMonth;
+            this.importVersion = importVersion;
+        }
+
+        void progress(int pct, String stepText) {
+            if (pct > this.percent) this.percent = Math.min(pct, 99);
+            if (stepText != null) this.step = stepText;
+        }
+
+        public String getId() { return id; }
+
+        public String getStatus() { return status; }
+
+        public boolean isDone() { return "DONE".equals(status) && data != null; }
+
+        public boolean isFromCache() { return fromCache; }
+
+        public byte[] getData() { return data; }
+
+        public String getFileName() { return fileName; }
+
+        public String getError() { return error; }
+
+        public Map<String, Object> toView() {
+            Map<String, Object> m = new LinkedHashMap<>();
+            m.put("taskId", id);
+            m.put("type", type);
+            m.put("period", period);
+            m.put("mode", mode);
+            m.put("toMonth", toMonth);
+            m.put("status", status);
+            m.put("percent", "DONE".equals(status) ? 100 : percent);
+            m.put("step", step);
+            m.put("fromCache", fromCache);
+            m.put("fileName", fileName);
+            m.put("error", error);
+            long end = finishedAt > 0 ? finishedAt : System.currentTimeMillis();
+            m.put("elapsedMs", end - createdAt);
+            m.put("finishedAt", finishedAt > 0 ? new Date(finishedAt) : null);
+            return m;
+        }
+    }
+
+    /**
+     * 鎻愪氦瀵煎嚭浠诲姟銆俧orceRefresh=true 鏃跺拷鐣ョ紦瀛橀噸鏂扮敓鎴愶紱
+     * 鍚﹀垯鍦ㄣ�屽悓涓�鎶ヨ〃鏈熴��10 鍒嗛挓鍐呫�佹湡闂存棤鏂板鍏ャ�嶇殑鍓嶆彁涓嬬洿鎺ュ鐢ㄤ笂娆$粨鏋溿��
+     * 鍚屼竴鎶ヨ〃鏈熷凡鏈変换鍔″湪璺戞椂鐩存帴杩斿洖璇ヤ换鍔★紝閬垮厤閲嶅鎺掗槦銆�
+     */
+    public Task submit(String type, String period, String mode, Integer toMonth, boolean forceRefresh) {
+        cleanupExpired();
+        String key = cacheKey(type, period, mode, toMonth);
+        if (!forceRefresh) {
+            Task hit = cache.get(key);
+            if (cacheUsable(hit)) {
+                hit.fromCache = true;
+                log.info("鎶ヨ〃瀵煎嚭浠诲姟鍛戒腑缂撳瓨锛歿} {}锛堢敓鎴愪簬 {}锛�", type, period, new Date(hit.finishedAt));
+                return hit;
+            }
+        }
+        Task running = findRunning(key);
+        if (running != null) {
+            log.info("鎶ヨ〃瀵煎嚭浠诲姟宸插湪鎵ц锛屽鐢ㄨ繘琛屼腑鐨勪换鍔★細{} {}锛坽}锛�", type, period, running.id);
+            return running;
+        }
+        Task task = new Task(type, period, mode, toMonth, importVersion());
+        tasks.put(task.id, task);
+        pool.submit(() -> run(task));
+        return task;
+    }
+
+    public Task get(String taskId) {
+        if (taskId == null) return null;
+        cleanupExpired();
+        return tasks.get(taskId);
+    }
+
+    private void run(Task t) {
+        t.status = "RUNNING";
+        t.step = "寮�濮嬬敓鎴�";
+        long start = System.currentTimeMillis();
+        try {
+            byte[] data = reportService.exportSummaryWorkbook(t.period, t.mode, t.toMonth, t::progress);
+            t.data = data;
+            t.fileName = "鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃_" + t.period + ".xlsx";
+            t.step = "瀹屾垚";
+            t.status = "DONE";
+            t.finishedAt = System.currentTimeMillis();
+            cache.put(cacheKey(t.type, t.period, t.mode, t.toMonth), t);
+            log.info("鎶ヨ〃瀵煎嚭浠诲姟瀹屾垚锛歿} {} 鐢ㄦ椂 {} ms锛寋} 瀛楄妭", t.type, t.period, t.finishedAt - start, data.length);
+        } catch (Throwable e) {
+            t.status = "FAILED";
+            t.error = e.getMessage() == null ? e.toString() : e.getMessage();
+            t.step = "鐢熸垚澶辫触";
+            t.finishedAt = System.currentTimeMillis();
+            log.error("鎶ヨ〃瀵煎嚭浠诲姟澶辫触锛歿} {}锛堢敤鏃� {} ms锛�", t.type, t.period, t.finishedAt - start, e);
+        }
+    }
+
+    private Task findRunning(String key) {
+        for (Task t : tasks.values()) {
+            if (!key.equals(cacheKey(t.type, t.period, t.mode, t.toMonth))) continue;
+            if ("PENDING".equals(t.status) || "RUNNING".equals(t.status)) return t;
+        }
+        return null;
+    }
+
+    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;
+        long now = importVersion();
+        if (now < 0 || t.importVersion < 0) return false; // 鍙栦笉鍒版暟鎹増鏈椂涓嶅啋椋庨櫓澶嶇敤
+        return now == t.importVersion;
+    }
+
+    /** 鏁版嵁鐗堟湰锛漣mport_batch 鏈�澶� id锛氫换浣曚竴娆″鍏ラ兘浼氳瀹冨彉鍖栵紝鍙栦笉鍒拌繑鍥� -1锛堟鏃剁鐢ㄧ紦瀛橈級 */
+    private long importVersion() {
+        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();
+        } catch (Exception e) {
+            log.warn("璇诲彇瀵煎叆鏁版嵁鐗堟湰澶辫触锛屾湰娆$鐢ㄥ鍑虹紦瀛橈細{}", e.getMessage());
+            return -1L;
+        }
+    }
+
+    private String cacheKey(String type, String period, String mode, Integer toMonth) {
+        return type + "|" + period + "|" + mode + "|" + (toMonth == null ? "" : toMonth);
+    }
+
+    private void cleanupExpired() {
+        long now = System.currentTimeMillis();
+        for (Iterator<Map.Entry<String, Task>> it = tasks.entrySet().iterator(); it.hasNext(); ) {
+            Task t = it.next().getValue();
+            if (t.finishedAt > 0 && now - t.finishedAt > RESULT_TTL_MS) it.remove();
+        }
+        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();
+        }
+    }
+
+    @PreDestroy
+    public void shutdown() {
+        pool.shutdownNow();
+        try {
+            pool.awaitTermination(3, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+}

--
Gitblit v1.9.1