From 117ecd77287794026954a5147f196bda81ddbf3b Mon Sep 17 00:00:00 2001
From: zhizhijie <zhizhijie@users.noreply.gitee.com>
Date: 星期五, 04 九月 2026 14:07:22 +0800
Subject: [PATCH] feat: 汇总工作簿月度块按备份母版还原(1-4月骨架→1-7月)试点 + fullCalcOnLoad + 09-04 测试报告

---
 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java |   91 ++++++++++++++++++++++++++++++
 docs/功能测试报告/功能测试报告_汇总工作簿月度块扩列还原试点_2026-09-04.md                                                   |   30 ++++++++++
 2 files changed, 121 insertions(+), 0 deletions(-)

diff --git "a/docs/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212_\346\261\207\346\200\273\345\267\245\344\275\234\347\260\277\346\234\210\345\272\246\345\235\227\346\211\251\345\210\227\350\277\230\345\216\237\350\257\225\347\202\271_2026-09-04.md" "b/docs/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212_\346\261\207\346\200\273\345\267\245\344\275\234\347\260\277\346\234\210\345\272\246\345\235\227\346\211\251\345\210\227\350\277\230\345\216\237\350\257\225\347\202\271_2026-09-04.md"
new file mode 100644
index 0000000..acaadd9
--- /dev/null
+++ "b/docs/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212/\345\212\237\350\203\275\346\265\213\350\257\225\346\212\245\345\221\212_\346\261\207\346\200\273\345\267\245\344\275\234\347\260\277\346\234\210\345\272\246\345\235\227\346\211\251\345\210\227\350\277\230\345\216\237\350\257\225\347\202\271_2026-09-04.md"
@@ -0,0 +1,30 @@
+# 鍔熻兘娴嬭瘯鎶ュ憡锛氭眹鎬诲伐浣滅翱鏈堝害鍧楁墿鍒�/杩樺師璇曠偣锛�2026-07锛夛紙2026-09-04锛�
+
+## 鍔熻兘璇存槑
+鐢ㄦ埛鎷嶆澘锛氫互銆�2026骞�7鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx銆嬶紙宸叉竻鐞嗕负鈥�1-4 鏈堥鏋垛�濓級涓烘瘝鐗堬紝鐢辩▼搴忔妸 5-7 鏈堟湀搴﹀潡鎸夊悓鏈熷浠芥瘝鐗堣繕鍘燂紝瀹炵幇鈥滈�愭湀鎵╁垪鐢熸垚 1-N 鏈堟眹鎬昏〃鈥濈殑绗竴姝ヨ瘯鐐癸紙M1锛岀洰鏍囨湡 2026-07锛夈�傚彛寰勶細1-4 鏈堜繚鐣欐瘝鐗堟暟鎹紱绱鍏紡鎭㈠涓� 1-7 鏈堬紱鍚屾瘮 R 鍒嗘瘝鐢� 2025 骞� 1-7 鏈堝垪锛�2025/2024 鍙傝�冨垪淇濇寔涓嶅姩锛涜緭鍑哄彟瀛樻柊鏂囦欢锛屼笉鏀规瘝鐗堛��
+
+浠g爜鏀瑰姩锛�
+- traffic-audit-server/.../reportexport/service/ReportExportService.java
+  - exportSummaryWorkbook 澧炲姞锛氬畾浣嶅悓鏈熷浠芥瘝鐗堬紙_澶囦唤_*_娓呯悊鍓峗*.xlsx锛屾寜 {骞磢骞磠鏈坿鏈� 鍖归厤锛屾悳绱㈤厤缃洰褰�/user.dir/涓婄骇鐩綍涓変釜鍊欓�夋牴锛夆啋 閫愭牸鎶娾�滃浠芥湁鍐呭鑰岄鏋剁己澶辨垨鍐呭涓嶅悓鈥濈殑鍗曞厓鏍煎洖濉紙鍚� 5-7 鏈堣〃澶淬�佹暟鎹�佸叕寮忎笌绱/鍚屾瘮鍏紡锛夛紱
+  - 鏂板 resolveSummaryDonor / restoreSummaryMonthlyBlocks / summaryCellText 涓変釜绉佹湁鏂规硶锛�
+  - 瀵煎嚭鏃剁疆 fullCalcOnLoad=true锛屼繚璇� Excel/WPS 鎵撳紑鍗抽噸绠楄鏀瑰啓/杩樺師鐨勫叕寮忋��
+
+## 娴嬭瘯鐜
+- 鍚庣 8090锛堝娆¢噸鍚悗鏈�鏂颁唬鐮佸氨缁級锛汳ySQL 127.0.0.1:3308銆�
+- 姣嶇増锛歞ocs/鐢熸垚姹囨�诲ぇ琛�/2026骞�7鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx锛�1-4 鏈堥鏋讹紝19 椤电锛夛紱澶囦唤锛歞ocs/鐢熸垚姹囨�诲ぇ琛�/_澶囦唤_2026骞�7鏈堥亾璺繍杈撻噺姹囨�昏〃_娓呯悊鍓峗20260904.xlsx銆�
+- 鏂规硶锛氳皟鐢� GET /api/report/export/summaryWorkbook?period=2026-07&mode=month锛屼繚瀛樹负 docs/鐢熸垚姹囨�诲ぇ琛�/杈撳嚭/閬撹矾杩愯緭閲忔眹鎬昏〃_2026-07.xlsx锛岀敤 openpyxl 涓庡浠芥瘝鐗堥�愭牸姣斿锛堝唴瀹�/鍏紡鏂囨湰锛岄潪鏍峰紡锛夈��
+
+## 鐢ㄤ緥涓庣粨鏋�
+1. 杩樺師骞垮害锛堟鍚戯級锛氭竻鐞嗘椂琚鐨� 7 涓彴璐�/鍚堟垚椤碉紙璐ц繍/鐝嚎鍖呰溅/鍩庡競瀹㈣繍/鍏氦/鍑虹/缃戠害杞�/杞ㄩ亾杞浮/涓彛寰勬槑缁嗭級缂哄け鍗曞厓鏍煎叏閮ㄨˉ榻愶紝missing=0锛涘叾浣欓〉锛堟垚鍝侀〉/闅愯棌椤碉級鏈棤宸紓锛屾湭鏀瑰姩銆傜粨璁� PASS銆�
+2. 閫愭牸瀵圭収锛堟鍚戯級锛氳緭鍑轰笌澶囦唤姣嶇増 19 椤靛叏閲忔瘮瀵癸紝real-diff=0锛涗粎 31 鏍间负鈥滄暣鏁� vs 46143.0鈥濈殑鏁板�肩瓑浠疯〃绀哄樊寮傦紙Excel 鏁板�肩浉鍚岋級銆傜粨璁� PASS銆�
+3. 绱/鍚屾瘮鍏紡锛堟鍚戯級锛氭娊鏌� 鐝嚎鍖呰溅/鍏氦/鍑虹/璐ц繍 绛夎 Q 鎭㈠涓� 1-7 鏈堝悇鏈堝�兼眰鍜岋紙濡� =G4+E4+C4+I4+K4+M4+O4锛夛紝R 鎭㈠涓� =Q/SUM(2025 骞� 1-7 鏈堝垪)-1锛屼笌澶囦唤涓�鑷淬�傜粨璁� PASS銆�
+4. 鎵撳紑鍗抽噸绠楋紙姝e悜锛夛細浜х墿 xl/workbook.xml 鍚� <calcPr fullCalcOnLoad="true"/>銆傜粨璁� PASS銆�
+5. 姣嶇増鏈鏀瑰姩锛堟鍚戯級锛氱敓鎴愬墠鍚庢瘝鐗堟枃浠跺瓧鑺傛湭鍙橈紙鍙澶嶅埗锛夈�傜粨璁� PASS銆�
+6. 缂栬瘧涓庡惎鍔細mvn compile EXIT=0锛涘悗绔噸鍚氨缁紱鎺ュ彛 200 杩斿洖 xlsx銆傜粨璁� PASS銆�
+
+## 鍙戠幇鐨勯棶棰樹笌澶勭疆
+1. 鍒濈増 donor 瀹氫綅鍙煡閰嶇疆鐩綍鐩稿璺緞锛岃�� spring-boot:run 鐨勫伐浣滅洰褰曟槸妯″潡鐩綍锛坱raffic-audit-server锛夛紝瀵艰嚧鈥滄湭鎵惧埌澶囦唤姣嶇増鈥濄�佽緭鍑轰粛涓洪鏋讹細宸叉敼涓轰笌 resolveAnyTemplate 鐩稿悓鐨� 3 鍊欓�夋牴鐩綍鎵弿锛屽娴嬭繕鍘熸垚鍔熴��
+2. POI setForceFormulaRecalculation 鏈啓 calcPr锛氭敼涓虹洿鎺ユ搷浣� XSSFWorkbook.getCTWorkbook() 鐨� calcPr.fullCalcOnLoad=true锛屽凡楠岃瘉鍐欏叆銆�
+
+## 缁撹
+M1 璇曠偣閫氳繃锛氫粠 1-4 鏈堥鏋舵瘝鐗堝彲绋嬪簭鍖栬繕鍘熷嚭涓庝汉宸� 7 鏈堟瘝鐗堝唴瀹归浂宸紓鐨勫畬鏁村伐浣滅翱锛堝惈 5-7 鏈堟湀搴﹀潡涓� 1-7 绱/鍚屾瘮鍏紡锛夛紝杈撳嚭鏂囦欢 docs/鐢熸垚姹囨�诲ぇ琛�/杈撳嚭/閬撹矾杩愯緭閲忔眹鎬昏〃_2026-07.xlsx銆備笅涓�姝� M1b锛氭湀浠�>7锛�8-12 鏈堬級鏃舵寜鈥滀笂涓�鏈堝潡鍏嬮殕+鍒楀紩鐢ㄥ钩绉烩�濊嚜鍔ㄦ彃鍏ユ柊鍒楋紙澶囦唤鏃犲搴旀湀浠芥椂鐢級锛屽啀鎺ュ叆 M2 鍙拌处鏁版嵁浠庡簱鍐呭~鍏呫��
\ No newline at end of file
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
index b1b1566..87fe99d 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -3207,9 +3207,20 @@
         }
         try (InputStream in = new FileInputStream(template);
              XSSFWorkbook wb = new XSSFWorkbook(in)) {
+            File donor = resolveSummaryDonor(year, month);
+            if (donor != null) {
+                try (InputStream din = new FileInputStream(donor); XSSFWorkbook dwb = new XSSFWorkbook(din)) {
+                    int restored = restoreSummaryMonthlyBlocks(wb, dwb);
+                    log.info("姹囨�诲伐浣滅翱鏈堝害鍧楄繕鍘熷畬鎴愶紝琛ラ綈/瑕嗙洊鍗曞厓鏍兼暟={}锛堟瘝鐗堬細{}锛屽浠芥瘝鐗堬細{}锛�", restored, template.getName(), donor.getName());
+                }
+            } else {
+                log.warn("姹囨�诲伐浣滅翱鏈壘鍒板悓鏈熷浠芥瘝鐗堬紙_澶囦唤_{}骞磠}鏈堚�︼級锛岃緭鍑哄皢淇濇寔姣嶇増鐜扮姸锛堝彲鑳戒负 1-4 鏈堥鏋讹級", year, month);
+            }
             dynamicSummaryYear(wb, year);
             applyTwoDecimalFormat(wb);
             autoFitContentColumns(wb); // 姹囨�昏〃瀵煎嚭鍚庢寜鍐呭鍔犲鍒楀锛堝彧鍔犲涓嶇缉绐勶級
+            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
+            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 杩樺師/鏀瑰啓鍏紡鍚庢墦寮�鍗抽噸绠�
             return toBytes(wb);
         }
     }
@@ -3476,6 +3487,86 @@
         return map;
     }
 
+    /** 瀹氫綅鍚屾湡澶囦唤姣嶇増锛堟竻鐞嗗墠鐨勫畬鏁存湀姣嶇増锛夛紝鐢ㄤ簬鎶婇鏋舵瘝鐗堢己澶辩殑 5-7 鏈堟湀搴﹀潡鎸夊浠借繕鍘燂紱
+     *  涓� resolveAnyTemplate 鐩稿悓绛栫暐锛氬湪閰嶇疆鐩綍銆乽ser.dir 鐩稿鐩綍銆佷笂绾х洰褰曚笁涓�欓�夋牴涓壂鎻� */
+    private File resolveSummaryDonor(int year, int month) {
+        String rel = summaryTemplateDir;
+        while (rel.startsWith("./")) rel = rel.substring(2);
+        String[] roots = {
+            summaryTemplateDir,
+            System.getProperty("user.dir") + "/" + rel,
+            System.getProperty("user.dir") + "/../" + rel
+        };
+        for (String root : roots) {
+            if (root == null || root.trim().isEmpty()) continue;
+            File dir = new File(root);
+            File[] files = dir.listFiles((d, name) ->
+                name.startsWith("_澶囦唤_") && name.contains(year + "骞�" + month + "鏈�") && name.contains("閬撹矾杩愯緭閲忔眹鎬昏〃"));
+            if (files != null && files.length > 0) return files[0];
+        }
+        return null;
+    }
+
+    /** 鎸夊悓鏈熷浠芥瘝鐗堣繕鍘熸湀搴﹀潡锛氬嚒澶囦唤鏈夊唴瀹硅�屾瘝鐗堝悓鏍肩己澶�/鍐呭涓嶅悓锛堟竻鐞嗘椂琚鎺夌殑 5-7 鏈堝�间笌绱鍏紡锛夌殑鍗曞厓鏍硷紝鏁存牸鍥炲~骞惰鐩� */
+    private int restoreSummaryMonthlyBlocks(XSSFWorkbook wb, XSSFWorkbook donor) {
+        int restored = 0;
+        for (int i = 0; i < donor.getNumberOfSheets(); i++) {
+            Sheet ds = donor.getSheetAt(i);
+            if (ds == null) continue;
+            Sheet bs = wb.getSheet(ds.getSheetName());
+            if (bs == null) continue;
+            for (int r = ds.getFirstRowNum(); r <= ds.getLastRowNum(); r++) {
+                Row dr = ds.getRow(r);
+                if (dr == null) continue;
+                Row br = bs.getRow(r);
+                for (Cell dc : dr) {
+                    if (dc == null) continue;
+                    CellType t = dc.getCellType();
+                    if (t == CellType.BLANK) continue;
+                    String dtx = summaryCellText(dc);
+                    if (dtx == null || dtx.isEmpty()) continue;
+                    Cell bc = (br == null) ? null : br.getCell(dc.getColumnIndex());
+                    if (bc != null && dtx.equals(summaryCellText(bc))) continue;
+                    Row tr = (bc != null) ? br : bs.getRow(r);
+                    if (tr == null) tr = bs.createRow(r);
+                    Cell tc = (bc != null) ? bc : tr.getCell(dc.getColumnIndex());
+                    if (tc == null) tc = tr.createCell(dc.getColumnIndex());
+                    tc.setBlank();
+                    if (t == CellType.FORMULA) {
+                        String f = dc.getCellFormula();
+                        if (f != null) tc.setCellFormula(f);
+                    } else if (t == CellType.STRING) {
+                        tc.setCellValue(dc.getStringCellValue());
+                    } else if (t == CellType.NUMERIC) {
+                        tc.setCellValue(dc.getNumericCellValue());
+                    } else if (t == CellType.BOOLEAN) {
+                        tc.setCellValue(dc.getBooleanCellValue());
+                    }
+                    restored++;
+                }
+            }
+        }
+        return restored;
+    }
+
+    /** 鍗曞厓鏍煎唴瀹规枃鏈寲锛堝叕寮忓甫 = 鍓嶇紑锛夛紝鐢ㄤ簬璺ㄧ翱姣斿 */
+    private String summaryCellText(Cell c) {
+        if (c == null) return null;
+        switch (c.getCellType()) {
+            case FORMULA: {
+                String f = c.getCellFormula();
+                return f == null ? "" : "=" + f;
+            }
+            case STRING:
+                return c.getStringCellValue();
+            case NUMERIC:
+                return Double.toString(c.getNumericCellValue());
+            case BOOLEAN:
+                return Boolean.toString(c.getBooleanCellValue());
+            default:
+                return "";
+        }
+    }
     /** 姹囨�诲伐浣滅翱骞翠唤鍔ㄦ�佸寲锛氫粎鎶� 2026骞� 鍓嶇紑鏇挎崲涓虹洰鏍囧勾锛�2025/2024 鍙傝�冨垪涓嶅姩锛� */
     private void dynamicSummaryYear(XSSFWorkbook wb, int year) {
         String oldPrefix = "2026骞�";

--
Gitblit v1.9.1