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 |  619 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 612 insertions(+), 7 deletions(-)

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 c1c3bd1..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
@@ -107,6 +107,10 @@
     @Value("${passenger.template-dir:docs/鍏矾鏃呭+鑳借��/杈撳嚭}")
     private String passengerTemplateDir;
 
+    /** 姹囨�诲伐浣滅翱妯℃澘鐩綍锛坅pplication.yml summary.template-dir锛� */
+    @Value("${summary.template-dir:docs/鐢熸垚姹囨�诲ぇ琛▆")
+    private String summaryTemplateDir;
+
     /** 璐ц繍鎶ヨ〃妯℃澘鐩綍锛坅pplication.yml freight.template-dir锛� */
     @Value("${freight.template-dir:docs/璐ц繍}")
     private String freightTemplateDir;
@@ -171,6 +175,43 @@
                     rows = addReadyCount(() -> energyMapper.selectCount(periodQw(period, mode, EnergyVehicleQuarterly::getReportPeriod)), "鑳借�楄溅杈嗗搴︽暟鎹�", notes);
                     auditTypes.add("H204");
                     break;
+                case "wycSplit": {
+                    rows = addReadyCount(() -> cityTaxiMapper.selectCount(periodQw(period, mode, CityTaxiMonthly::getReportPeriod)), "宸℃父鍑虹鏈堟姤", notes);
+                    try {
+                        Map<String, Object> inp = loadWycMonthlyInput(period, mode);
+                        if (inp == null) {
+                            rows = 0;
+                            notes.add("銆婄綉绾﹁溅璁㈠崟鍙婂叏鐪佹�婚噺.xlsx銆嬬己 " + period + " 琛岋紙闇�鍏堢淮鎶ゅ綋鏈堣鍗曚笌鍏ㄧ渷 pin锛�");
+                        } else {
+                            double pinK = (Double) inp.get("pinK"), pinCK = (Double) inp.get("pinCK");
+                            double pinZ = (Double) inp.get("pinZ"), pinCZ = (Double) inp.get("pinCZ");
+                            double orderSum = (Double) inp.get("orderSum");
+                            if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0 || orderSum <= 0) {
+                                rows = 0;
+                                notes.add("缃戠害杞﹁緭鍏� " + period + " 琛屼笉瀹屾暣锛堣鍗�/鍏ㄧ渷 pin 缂哄け锛�");
+                            }
+                        }
+                    } catch (Exception ex) {
+                        rows = 0;
+                        notes.add("缃戠害杞﹁緭鍏ヨ鍙栧け璐ワ細" + ex.getMessage());
+                    }
+                    break;
+                }
+                case "summaryWorkbook": {
+                    rows = addReadyCount(() -> cityTaxiMapper.selectCount(periodQw(period, mode, CityTaxiMonthly::getReportPeriod)), "宸℃父鍑虹鏈堟姤", notes)
+                         + addReadyCount(() -> cityBusMapper.selectCount(periodQw(period, mode, CityBusMonthly::getReportPeriod)), "鍩庡競鍏氦鏈堟姤", notes);
+                    if (rows <= 0) notes.add("姹囨�昏〃渚濊禆鐨勫叕浜�/鍑虹鏈堟姤鏁版嵁缂哄け");
+                    try {
+                        if (resolveSummaryMother(period, mode) == null) {
+                            rows = 0;
+                            notes.add("docs/鐢熸垚姹囨�诲ぇ琛� 涓嬬己灏戝彲澶嶅埗鐨勬瘝鐗堟眹鎬昏〃");
+                        }
+                    } catch (Exception ex) {
+                        rows = 0;
+                        notes.add("姣嶇増瀹氫綅澶辫触锛�" + ex.getMessage());
+                    }
+                    break;
+                }
                 default:
                     rows = 0;
                     notes.add("鏈煡鎶ヨ〃绫诲瀷锛�" + t);
@@ -181,6 +222,7 @@
                 auditRows += auditRowsOf(period, mode, at);
                 audited = audited || auditedMarked(period, mode, at);
             }
+            if (auditTypes.isEmpty()) audited = true;
             it.put("rows", rows);
             it.put("ready", rows > 0);
             it.put("auditRows", auditRows);
@@ -212,7 +254,8 @@
         for (String t : types) {
             if (isFreightType(t)) freight = true;
             else if (isPassengerType(t)) passenger = true;
-            else if ("cityTaxiDetail".equals(t) || isCityPassengerType(t)) { cityTaxi = true; cityBus = true; }
+            else if ("cityTaxiDetail".equals(t) || "wycSplit".equals(t)) { cityTaxi = true; }
+            else if (isCityPassengerType(t) || "summaryWorkbook".equals(t)) { cityTaxi = true; cityBus = true; }
             else if (isInvestType(t)) invest = true;
             else if ("energySummary".equals(t)) energy = true;
         }
@@ -256,7 +299,51 @@
             List<Integer> missing = new ArrayList<>();
             List<Integer> coverage = new ArrayList<>();
             if (Boolean.FALSE.equals(it.get("audited"))) problems.add("璇ユ姤琛ㄦ湡灏氭湭瀹℃牳閫氳繃");
-            if (isFreightType(t)) {
+            if ("wycSplit".equals(t)) {
+                coverage.addAll(taxiCover);
+                missing = missingMonths(taxiCover, month);
+                try {
+                    Map<String, Object> inp = loadWycMonthlyInput(period, mode);
+                    if (inp == null) {
+                        problems.add("缂虹綉绾﹁溅杈撳叆锛氳鍦ㄣ�婄綉绾﹁溅璁㈠崟鍙婂叏鐪佹�婚噺.xlsx銆嬭ˉ " + period + " 琛�");
+                    } else {
+                        double pinK = (Double) inp.get("pinK"), pinCK = (Double) inp.get("pinCK");
+                        double pinZ = (Double) inp.get("pinZ"), pinCZ = (Double) inp.get("pinCZ");
+                        double orderSum = (Double) inp.get("orderSum");
+                        if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0 || orderSum <= 0) {
+                            problems.add("缃戠害杞﹁緭鍏� " + period + " 琛屼笉瀹屾暣锛堣鍗�/鍏ㄧ渷 pin 缂哄け锛�");
+                        } else {
+                            addSum(summary, "鍏ㄧ渷缃戠害杞﹀杩愰噺", pinK, "涓囦汉");
+                            addSum(summary, "鍏朵腑鍩庡競鍐呭杩愰噺", pinCK, "涓囦汉");
+                            addSum(summary, "鍏ㄧ渷缃戠害杞﹀懆杞噺", pinZ, "涓囦汉鍏噷");
+                            addSum(summary, "鍏朵腑鍩庡競鍐呭懆杞噺", pinCZ, "涓囦汉鍏噷");
+                            addSum(summary, "閮ㄧ骇璁㈠崟鍚堣", orderSum, "鍗�");
+                        }
+                    }
+                } catch (Exception ex) {
+                    problems.add("缃戠害杞﹁緭鍏ヨ鍙栧け璐ワ細" + ex.getMessage());
+                }
+            } else if ("summaryWorkbook".equals(t)) {
+                java.util.Set<Integer> cov = new java.util.TreeSet<>();
+                cov.addAll(busCover);
+                cov.addAll(taxiCover);
+                coverage.addAll(cov);
+                missing = missingMonths(cov, month);
+                try {
+                    if (resolveSummaryMother(period, mode) == null) {
+                        problems.add("docs/鐢熸垚姹囨�诲ぇ琛� 涓嬬己灏戝彲澶嶅埗鐨勬瘝鐗堟眹鎬昏〃");
+                    } else {
+                        Map<Integer, Map<String, double[]>> busM = loadCityBusByMonth(yearPrefix, month);
+                        Map<Integer, Map<String, double[]>> taxiM = loadCityTaxiByMonth(yearPrefix, month);
+                        Map<String, double[]> bc = busM.get(month);
+                        Map<String, double[]> tc = taxiM.get(month);
+                        if (bc != null && bc.get("鍏ㄧ渷") != null) addSum(summary, "鍩庡競鍏氦瀹㈣繍閲忥紙鍏ㄧ渷褰撴湀锛�", bc.get("鍏ㄧ渷")[0], "涓囦汉");
+                        if (tc != null && tc.get("鍏ㄧ渷") != null) addSum(summary, "宸℃父鍑虹瀹㈣繍閲忥紙鍏ㄧ渷褰撴湀锛�", tc.get("鍏ㄧ渷")[0], "涓囦汉");
+                    }
+                } catch (Exception ex) {
+                    problems.add(ex.getMessage());
+                }
+            } else if (isFreightType(t)) {
                 coverage.addAll(freightCover);
                 missing = missingMonths(freightCover, month);
                 freightPreview(period, month, summary, problems);
@@ -954,14 +1041,15 @@
         return org.apache.poi.ss.util.CellReference.convertNumToColString(colIdx);
     }
 
-    /** 鏃呭鍒嗗競宸炴湀鍊硷紙0=瀹㈣繍閲�/1=鍛ㄨ浆閲�/2=涓綋瀹㈣繍閲�/3=涓綋鍛ㄨ浆閲忥級 */
+    /** 鏃呭鍒嗗競宸炴湀鍊硷紙0=瀹㈣繍閲�/1=鍛ㄨ浆閲�/2=涓綋瀹㈣繍閲�/3=涓綋鍛ㄨ浆閲忥紱0/1=浼佷笟H2031+涓綋锛�2026-09-03 鍙e緞纭锛� */
     private double passengerMonthVal(Map<Integer, Map<String, PassengerAgg>> data, Map<Integer, Map<String, double[]>> indi,
                                      int year, int month, String region, int metricIdx) {
-        if (metricIdx <= 1) {
+        if (metricIdx == 0 || metricIdx == 1) { // 瀹㈣繍閲�/鍛ㄨ浆閲忔�昏锛氫紒涓�+涓綋
             PassengerAgg agg = aggOf(data, year, month, region);
-            Double val = agg == null ? null
+            double ent = agg == null ? 0.0
                 : (metricIdx == 0 ? agg.passengerTotal / 10000.0 : agg.turnoverTotal / 10000.0);
-            return val == null ? 0.0 : val;
+            double ind = individualVal(indi, year, month, region, metricIdx == 0 ? 0 : 1);
+            return ent + ind;
         }
         return individualVal(indi, year, month, region, metricIdx - 2);
     }
@@ -2106,7 +2194,7 @@
 
     /**
      * 涓彛寰勫垎绫绘湀搴︽暟鎹細骞�*100+鏈� -> (甯傚窞/鍏ㄧ渷 -> double[5][2])
-     * 缁村害 0=鎬婚噺 1=鍏矾鐝嚎(h2031) 2=鍩庨檯鍩庝埂鍏氦(cityBus) 3=宸℃父鍑虹 4=缃戠害杞︼紱[瀹㈣繍閲�(涓囦汉), 鍛ㄨ浆閲�(涓囦汉鍏噷)]
+     * 缁村害 0=鎬婚噺 1=鍏矾鐝嚎(h2031+涓綋) 2=鍩庨檯鍩庝埂鍏氦(cityBus) 3=宸℃父鍑虹 4=缃戠害杞︼紱[瀹㈣繍閲�(涓囦汉), 鍛ㄨ浆閲�(涓囦汉鍏噷)]
      */
     private Map<Integer, Map<String, double[][]>> loadMidClassMap() {
         Map<Integer, Map<String, double[][]>> mid = new HashMap<>();
@@ -2128,6 +2216,25 @@
                 + nz(e.getPassengerClass4()) + nz(e.getPassengerCharter())) / 10000.0;
             double turn = (nz(e.getTurnoverClass1()) + nz(e.getTurnoverClass2()) + nz(e.getTurnoverClass3())
                 + nz(e.getTurnoverClass4()) + nz(e.getTurnoverCharter())) / 10000.0;
+            addMidClass(mid, key, city, 1, pass, turn);
+            addMidClass(mid, key, "婀栧寳鐪�", 1, pass, turn);
+        }
+        for (PassengerIndividualMonthly e : passengerIndividualMapper.selectList(null)) { // 涓綋骞跺叆鍏矾鐝嚎/鎬婚噺锛�09-03 鍙e緞锛�
+            if (e.getReportPeriod() == null) continue;
+            String[] parts = e.getReportPeriod().split("-");
+            if (parts.length != 2) continue;
+            int y, m;
+            try {
+                y = Integer.parseInt(parts[0]);
+                m = Integer.parseInt(parts[1]);
+            } catch (NumberFormatException ex) {
+                continue;
+            }
+            String city = RegionUtil.cityByCode(e.getRegionCode());
+            if (city == null) continue;
+            int key = y * 100 + m;
+            double pass = nz(e.getPassengerCount()) / 10000.0;
+            double turn = nz(e.getTurnover()) / 10000.0;
             addMidClass(mid, key, city, 1, pass, turn);
             addMidClass(mid, key, "婀栧寳鐪�", 1, pass, turn);
         }
@@ -3087,6 +3194,400 @@
         return bos.toByteArray();
     }
 
+    /**
+     * 鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx锛圥0-P1 楠ㄦ灦锛夛細鏁存湰澶嶅埗姣嶇増銆奩YYY骞碝鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx銆嬶紝
+     * 淇濈暀鍏ㄩ儴椤电/鐗堝紡/鍏紡/2025 骞寸紦瀛樺�硷紝浠呮妸琛ㄥご骞翠唤鍔ㄦ�佸寲涓虹洰鏍囧勾锛涙暟鎹~鍏呮寜 P2 璧烽�愭鎺ュ叆銆�
+     */
+    public byte[] exportSummaryWorkbook(String period, String mode) throws Exception {
+        int year = Integer.parseInt(period.contains("-") ? period.split("-")[0] : period);
+        int month = period.contains("-") ? monthOf(period, mode) : 12;
+        File template = resolveSummaryMother(period, mode);
+        if (template == null) {
+            throw new RuntimeException("鏈壘鍒版眹鎬昏〃姣嶇増銆�" + year + "骞�" + month + "鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx銆嬶紙docs/鐢熸垚姹囨�诲ぇ琛級");
+        }
+        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);
+        }
+    }
+
+    /** 姹囨�昏〃姣嶇増瀹氫綅锛氫粎绮剧‘鍖归厤 {year}骞磠month}鏈堚�︼紙P0-P1 楠ㄦ灦鏈熶笉鍏佽璺ㄦ湀/璺ㄥ勾闈欓粯鍥為��锛�
+     *  鐩爣鏈熸棤瀵瑰簲姣嶇増鏃舵槑纭姤閿欙紝鏈堜唤鎵╁睍瑙勫垯寰� P0 姣嶇増瀹氱鍚庡啀鏀惧紑锛� */
+    private File resolveSummaryMother(String period, String mode) throws Exception {
+        String year = period != null && period.contains("-") ? period.split("-")[0] : (period == null ? "" : period);
+        int month = period != null && period.contains("-") ? monthOf(period, mode) : 12;
+        String name = year + "骞�" + month + "鏈堥亾璺繍杈撻噺姹囨�昏〃.xlsx";
+        return resolveAnyTemplate(summaryTemplateDir, name);
+    }
+
+    // ==================== 缃戠害杞﹀杩愰噺鎷嗗垎琛紙鏈堝害锛� ====================
+
+    /** 缃戠害杞﹀杩愰噺鎷嗗垎琛細杈撳叆=閮ㄧ骇璁㈠崟锛堢綉绾﹁溅璁㈠崟鍙婂叏鐪佹�婚噺.xlsx G-W锛�+ 鍏ㄧ渷 pin锛圔-E锛�+ 宸℃父鍑虹鏈堟姤锛圖B锛夛紱
+     *  绠楁硶涓庛�婄綉绾﹁溅鎷嗗垎.xlsx銆嬫湀椤典竴鑷达細瀹㈣繍閲�=璁㈠崟鍗犳瘮脳鍏ㄧ渷鎬婚噺锛涘煄甯傚唴=宸℃父鍑虹鍩庡競鍐呭崰姣斾唬鐞嗐�佹姹変繚鎸佽嚜韬叾浣欐寜鍓╀綑鍒嗘憡锛�
+     *  鍛ㄨ浆閲�=瀹㈣繍閲徝楀贰娓稿嚭绉熷钩鍧囪繍璺濆崰姣斿垎鎽婂叏鐪佸懆杞噺 pin锛屽煄甯傚唴鍛ㄨ浆閲忓悓娉曘�� */
+    public byte[] exportWycSplit(String period, String mode) throws Exception {
+        if (period == null || !period.contains("-")) {
+            throw new RuntimeException("缃戠害杞﹀杩愰噺鎷嗗垎琛ㄤ粎鏀寔鎸夋湀鐢熸垚锛岃閫夋嫨鍏蜂綋鏈堜唤");
+        }
+        int year = Integer.parseInt(period.split("-")[0]);
+        int month = parseMonth(period);
+        Map<String, Object> inp = loadWycMonthlyInput(period, mode);
+        if (inp == null) {
+            throw new RuntimeException("銆婄綉绾﹁溅璁㈠崟鍙婂叏鐪佹�婚噺.xlsx銆嬬己灏戞姤琛ㄦ湡 " + period + " 鐨勬暟鎹锛岃鍏堢淮鎶ゅ綋鏈堣鍗曚笌鍏ㄧ渷 pin");
+        }
+        double pinK = (Double) inp.get("pinK");
+        double pinCK = (Double) inp.get("pinCK");
+        double pinZ = (Double) inp.get("pinZ");
+        double pinCZ = (Double) inp.get("pinCZ");
+        double orderSum = (Double) inp.get("orderSum");
+        double[] orderArr = (double[]) inp.get("orders");
+        if (pinK <= 0 || pinCK <= 0 || pinZ <= 0 || pinCZ <= 0 || orderSum <= 0) {
+            throw new RuntimeException("缃戠害杞﹁緭鍏� " + period + " 琛屼笉瀹屾暣锛氬叏鐪� pin / 璁㈠崟缂哄け锛岃鏍稿銆婄綉绾﹁溅璁㈠崟鍙婂叏鐪佹�婚噺.xlsx銆�");
+        }
+        List<String> cities = RegionUtil.cityList();
+        int n = cities.size();
+        if (orderArr == null || orderArr.length < n) {
+            throw new RuntimeException("缃戠害杞﹁鍗曞垪鏁颁笉瓒筹紙搴斾负 17 甯傚窞璁㈠崟鍒楋級");
+        }
+        Map<String, double[]> taxi = taxiMonthlyOf(period);
+        java.util.List<String> missingTaxi = new java.util.ArrayList<>();
+        double[] Dtax = new double[n];
+        double[] Etax = new double[n];
+        double[] avgD = new double[n];
+        double[] avgDc = new double[n];
+        for (int i = 0; i < n; i++) {
+            double[] a = taxi.get(cities.get(i));
+            if (a == null || a[0] <= 0) {
+                missingTaxi.add(cities.get(i));
+                continue;
+            }
+            Dtax[i] = a[0];
+            Etax[i] = a[2];
+            avgD[i] = a[4];
+            avgDc[i] = a[5];
+        }
+        if (!missingTaxi.isEmpty()) {
+            throw new RuntimeException("宸℃父鍑虹鏈堟姤缂哄皯浠ヤ笅甯傚窞鏁版嵁锛�" + String.join("銆�", missingTaxi));
+        }
+        double[] C = new double[n];
+        double[] H = new double[n];
+        double[] I = new double[n];
+        double[] Gl = new double[n];
+        double[] Hl = new double[n];
+        double[] Il = new double[n];
+        double sumG = 0;
+        double sumDl = 0;
+        double sumEl = 0;
+        for (int i = 0; i < n; i++) {
+            C[i] = orderArr[i] / orderSum * pinK;
+            double F = Etax[i] / Dtax[i];
+            double G = C[i] * F;
+            sumG += G;
+            double Dl = C[i] * avgD[i];
+            double El = H[i] * avgDc[i];
+            sumDl += Dl;
+            sumEl += El;
+        }
+        // 鍩庡競鍐咃細姝︽眽淇濇寔鑷韩锛屽叾浣欐寜 G 鍓╀綑鍗犳瘮鍒嗘憡鑷冲叏鐪佸煄甯傚唴 pin
+        for (int i = 0; i < n; i++) {
+            if (i == 0) {
+                H[i] = C[i] * (Etax[i] / Dtax[i]);
+            } else {
+                H[i] = (C[i] * (Etax[i] / Dtax[i])) / (sumG - C[0] * (Etax[0] / Dtax[0])) * (pinCK - C[0] * (Etax[0] / Dtax[0]));
+            }
+            I[i] = C[i] - H[i];
+        }
+        // 鍛ㄨ浆閲忥細鎸� 瀹㈣繍閲徝楀贰娓稿嚭绉熷钩鍧囪繍璺� 鍗犳瘮鍒嗘憡鍏ㄧ渷鍛ㄨ浆閲� pin
+        double whG = C[0] * (Etax[0] / Dtax[0]);
+        double whDl = C[0] * avgD[0];
+        double[] El2 = new double[n];
+        double sumEl2 = 0;
+        for (int i = 0; i < n; i++) {
+            double Dl = C[i] * avgD[i];
+            double El2v = H[i] * avgDc[i];
+            El2[i] = El2v;
+            sumEl2 += El2v;
+            Gl[i] = Dl / sumDl * pinZ;
+        }
+        for (int i = 0; i < n; i++) {
+            if (i == 0) {
+                Hl[i] = Gl[i];
+            } else {
+                Hl[i] = El2[i] / (sumEl2 - El2[0]) * (pinCZ - Gl[0]);
+            }
+            Il[i] = Gl[i] - Hl[i];
+        }
+        return writeWycSplitWorkbook(period, cities, orderArr, C, Dtax, Etax, H, I, avgD, avgDc, Gl, Hl, Il, pinK, pinCK, pinZ, pinCZ, orderSum);
+    }
+
+    private byte[] writeWycSplitWorkbook(String period, List<String> cities, double[] orders, double[] C, double[] Dtax,
+                                         double[] Etax, double[] H, double[] I, double[] avgD, double[] avgDc,
+                                         double[] Gl, double[] Hl, double[] Il,
+                                         double pinK, double pinCK, double pinZ, double pinCZ, double orderSum) throws Exception {
+        XSSFWorkbook wb = new XSSFWorkbook();
+        XSSFSheet sh = wb.createSheet(period + "鎷嗗垎");
+        int n = cities.size();
+        // 瀹㈣繍閲忓潡
+        Row t = sh.createRow(0);
+        t.createCell(0).setCellValue("缃戠害杞﹀杩愰噺鎷嗗垎锛堜竾浜猴級  鎶ヨ〃鏈燂細" + period);
+        String[] head1 = {"甯傚窞", "璁㈠崟鎬绘暟锛堝崟锛�", "缃戠害杞﹀杩愰噺锛堜竾浜猴級", "宸℃父鍑虹瀹㈣繍閲忥紙涓囦汉锛�", "宸℃父鍑虹鍩庡競鍐呭杩愰噺锛堜竾浜猴級", "鍩庡競鍐呭崰姣�", "缃戠害杞﹀煄甯傚唴瀹㈣繍閲忥紙涓囦汉锛�", "缃戠害杞﹀煄闄呭煄涔″杩愰噺锛堜竾浜猴級"};
+        Row h1 = sh.createRow(1);
+        for (int j = 0; j < head1.length; j++) h1.createCell(j).setCellValue(head1[j]);
+        int r = 2;
+        writeWycRow(sh, r++, "鍏ㄧ渷", orderSum, pinK, sum(Dtax), sum(Etax), pinK > 0 ? pinCK / pinK : 0, pinCK, pinK - pinCK);
+        for (int i = 0; i < n; i++) {
+            writeWycRow(sh, r++, cities.get(i), orders[i], C[i], Dtax[i], Etax[i], Dtax[i] > 0 ? Etax[i] / Dtax[i] : 0, H[i], I[i]);
+        }
+        r += 1;
+        Row t2 = sh.createRow(r++);
+        t2.createCell(0).setCellValue("鏃呭鍛ㄨ浆閲忔媶鍒嗭紙涓囦汉鍏噷锛�");
+        String[] head2 = {"甯傚窞", "鍑虹杞﹀钩鍧囪繍璺濓紙鍏噷锛�", "鍑虹杞﹀煄甯傚唴骞冲潎杩愯窛锛堝叕閲岋級", "缃戠害杞︽梾瀹㈠懆杞噺锛堜竾浜哄叕閲岋級", "缃戠害杞﹀煄甯傚唴鏃呭鍛ㄨ浆閲忥紙涓囦汉鍏噷锛�", "缃戠害杞﹀煄闄呭煄涔℃梾瀹㈠懆杞噺锛堜竾浜哄叕閲岋級"};
+        Row h2 = sh.createRow(r++);
+        for (int j = 0; j < head2.length; j++) h2.createCell(j).setCellValue(head2[j]);
+        writeWycRow2(sh, r++, "鍏ㄧ渷", 0, 0, pinZ, pinCZ, pinZ - pinCZ);
+        for (int i = 0; i < n; i++) {
+            writeWycRow2(sh, r++, cities.get(i), avgD[i], avgDc[i], Gl[i], Hl[i], Il[i]);
+        }
+        r += 1;
+        String[] notes = {
+            "娉細1) 瀹㈣繍閲� C = 璁㈠崟鍗犳瘮 脳 鍏ㄧ渷缃戠害杞﹀杩愰噺锛涘煄甯傚唴 H锛氭姹�=鑷韩锛屽叾浣欐寜 G 鍓╀綑鍗犳瘮鍒嗘憡鑷冲叏鐪佸煄甯傚唴 pin锛涘煄闄呭煄涔� = 鎬婚噺 - 鍩庡競鍐呫��",
+            "    2) 鍛ㄨ浆閲� G = 瀹㈣繍閲� 脳 宸℃父鍑虹骞冲潎杩愯窛 鐨勫崰姣� 脳 鍏ㄧ渷缃戠害杞﹀懆杞噺 pin锛涘煄甯傚唴鍛ㄨ浆閲忓悓娉曪紙鐢ㄥ煄甯傚唴骞冲潎杩愯窛锛夈��",
+            "    3) 杈撳叆锛氳鍗� = 褰撴湀鍒嗘瀽鎶ュ憡闄勪欢1锛�17 甯傚窞璁㈠崟鏁帮級锛涘叏鐪� pin = 缃戠害杞︽�绘暟.xlsx锛涘贰娓稿嚭绉� = 宸℃父鍑虹鏈堟姤锛堢郴缁熷鍏ワ紝鍚钩鍧囪繍璺濓級銆�",
+            "    4) 姝︽眽鏃犲煄闄呭煄涔★紱鏁板�间繚鐣欏叏绮惧害锛岀晫闈�/鏂囦欢鏄剧ず涓や綅灏忔暟銆�"
+        };
+        for (String nt : notes) {
+            Row nr = sh.createRow(r++);
+            nr.createCell(0).setCellValue(nt);
+        }
+        for (int j = 0; j < 8; j++) sh.setColumnWidth(j, 18 * 256);
+        return toBytes(wb);
+    }
+
+    private void writeWycRow(Sheet sh, int r, String name, double ord, double k, double dt, double et, double f, double h, double i) {
+        Row row = sh.createRow(r);
+        row.createCell(0).setCellValue(name);
+        row.createCell(1).setCellValue(ord);
+        row.createCell(2).setCellValue(k);
+        row.createCell(3).setCellValue(dt);
+        row.createCell(4).setCellValue(et);
+        row.createCell(5).setCellValue(f);
+        row.createCell(6).setCellValue(h);
+        row.createCell(7).setCellValue(i);
+    }
+
+    private void writeWycRow2(Sheet sh, int r, String name, double ad, double adc, double g, double h, double i) {
+        Row row = sh.createRow(r);
+        row.createCell(0).setCellValue(name);
+        row.createCell(1).setCellValue(ad);
+        row.createCell(2).setCellValue(adc);
+        row.createCell(3).setCellValue(g);
+        row.createCell(4).setCellValue(h);
+        row.createCell(5).setCellValue(i);
+    }
+
+    private double sum(double[] a) {
+        double s = 0;
+        for (double v : a) s += v;
+        return s;
+    }
+
+    /** 璇诲彇 docs/鍩庡競瀹㈣繍/缃戠害杞﹁鍗曞強鍏ㄧ渷鎬婚噺.xlsx锛氭寜 period(yyyy-MM) 琛屽彇 pin(B-E) 涓� 17 甯傚窞璁㈠崟(G-W) */
+    private Map<String, Object> loadWycMonthlyInput(String period, String mode) throws Exception {
+        String target = period;
+        File f;
+        try {
+            f = resolveAnyTemplate(templateDir, "缃戠害杞﹁鍗曞強鍏ㄧ渷鎬婚噺.xlsx");
+        } catch (Exception e) {
+            throw new RuntimeException("鏈壘鍒般�婄綉绾﹁溅璁㈠崟鍙婂叏鐪佹�婚噺.xlsx銆嬶紝璇锋斁鍒� docs/鍩庡競瀹㈣繍 鐩綍");
+        }
+        try (InputStream in = new FileInputStream(f);
+             org.apache.poi.ss.usermodel.Workbook wb = org.apache.poi.ss.usermodel.WorkbookFactory.create(in)) {
+            org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(0);
+            org.apache.poi.ss.usermodel.DataFormatter df = new org.apache.poi.ss.usermodel.DataFormatter();
+            List<String> cities = RegionUtil.cityList();
+            int n = cities.size();
+            for (int i = 1; i <= sh.getLastRowNum(); i++) {
+                Row row = sh.getRow(i);
+                if (row == null) continue;
+                Cell c0 = row.getCell(0);
+                if (c0 == null) continue;
+                String m = df.formatCellValue(c0).trim();
+                if (m == null || m.isEmpty()) continue;
+                if (m.length() > 7) m = m.substring(0, 7);
+                if (!target.equals(m)) continue;
+                Map<String, Object> map = new HashMap<>();
+                map.put("pinK", cellNum(row, 1, df, 0.0));
+                map.put("pinCK", cellNum(row, 2, df, 0.0));
+                map.put("pinZ", cellNum(row, 3, df, 0.0));
+                map.put("pinCZ", cellNum(row, 4, df, 0.0));
+                double[] orders = new double[n];
+                double orderSum = 0;
+                for (int k = 0; k < n; k++) {
+                    orders[k] = cellNum(row, 6 + k, df, 0.0);
+                    orderSum += orders[k];
+                }
+                if (orderSum <= 0) {
+                    orderSum = cellNum(row, 5, df, 0.0); // F 鍒楀悎璁″厹搴�
+                }
+                map.put("orders", orders);
+                map.put("orderSum", orderSum);
+                return map;
+            }
+            return null;
+        }
+    }
+
+    private double cellNum(Row row, int idx, org.apache.poi.ss.usermodel.DataFormatter df, double def) {
+        Cell c = row.getCell(idx);
+        if (c == null) return def;
+        if (c.getCellType() == CellType.NUMERIC) return c.getNumericCellValue();
+        String s = df.formatCellValue(c).replace(",", "").trim();
+        if (s == null || s.isEmpty()) return def;
+        try {
+            return Double.parseDouble(s);
+        } catch (Exception e) {
+            return def;
+        }
+    }
+
+    /** 宸℃父鍑虹鏈堟姤锛圖B锛夋寜鏈堝姞杞斤細甯傚窞 -> [瀹㈣繍閲�, 鍛ㄨ浆閲�, 鍩庡競鍐呭杩愰噺, 鍩庡競鍐呭懆杞噺, 骞冲潎杩愯窛, 鍩庡競鍐呭钩鍧囪繍璺漖 */
+    private Map<String, double[]> taxiMonthlyOf(String period) {
+        Map<String, double[]> map = new java.util.LinkedHashMap<>();
+        List<CityTaxiMonthly> rows = cityTaxiMapper.selectList(
+            new LambdaQueryWrapper<CityTaxiMonthly>().eq(CityTaxiMonthly::getReportPeriod, period));
+        for (CityTaxiMonthly r : rows) {
+            String city = RegionUtil.normalizeCityName(r.getCity());
+            if (city == null || city.isEmpty()) city = "鏈煡";
+            double[] a = map.computeIfAbsent(city, k -> new double[6]);
+            a[0] += nz(r.getPassengerVolume());
+            a[1] += nz(r.getTurnover());
+            a[2] += nz(r.getPassengerCity());
+            a[3] += nz(r.getTurnoverCity());
+            if (r.getAvgDistance() != null && r.getAvgDistance() > 0) a[4] = r.getAvgDistance();
+            if (r.getAvgDistanceCity() != null && r.getAvgDistanceCity() > 0) a[5] = r.getAvgDistanceCity();
+        }
+        for (double[] a : map.values()) {
+            if (a[4] <= 0 && a[0] > 0) a[4] = round(a[1] / a[0], 2);
+            if (a[5] <= 0 && a[2] > 0) a[5] = round(a[3] / a[2], 2);
+        }
+        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骞�";
+        String newPrefix = year + "骞�";
+        if (oldPrefix.equals(newPrefix)) return;
+        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
+            Sheet sh = wb.getSheetAt(i);
+            if (sh == null) continue;
+            for (Row row : sh) {
+                if (row == null) continue;
+                for (Cell c : row) {
+                    if (c == null || c.getCellType() != CellType.STRING) continue;
+                    String v = c.getStringCellValue();
+                    if (v != null && v.contains(oldPrefix)) {
+                        c.setCellValue(v.replace(oldPrefix, newPrefix));
+                    }
+                }
+            }
+        }
+    }
+
     /** 鐢熸垚_鎶曡祫鎶ヨ〃绯诲垪.zip锛氶瀹夋帓 + 鍗佷簲浜� + 浜垮厓 + 缁忔祹寮哄幙 */
     public byte[] exportInvestSeries(String period, String mode) throws Exception {
         java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream();
@@ -3127,6 +3628,7 @@
             case "passengerMidDetail": return exportPassengerMidDetail(period, mode);
             case "passengerMidRank": return exportPassengerMidRank(period, mode);
             case "passengerMidAnalysis": return exportPassengerMidAnalysis(period, mode);
+            case "summaryWorkbook": return exportSummaryWorkbook(period, mode);
             case "energySummary": return exportEnergySummary(period);
             case "investPlan": return investPlan(period, mode);
             case "investFiveYearLogistics": return investFiveYearLogistics(period, mode);
@@ -3138,6 +3640,7 @@
             case "cityPassengerCitySum": return exportCityPassengerCitySum("鐢熸垚_鍩庡競瀹㈣繍瀹㈣繍閲忓垎甯傚窞绱.xlsx", period, mode);
             case "cityPassengerDetailSum": return exportCityPassengerCitySum("鐢熸垚_鍩庡競瀹㈣繍鍚勫競宸炴槑缁嗚〃.xlsx", period, mode);
             case "cityPassengerSummary": return exportCityPassengerSummary(period, mode);
+            case "wycSplit": return exportWycSplit(period, mode);
             default: return null;
         }
     }
@@ -3162,6 +3665,8 @@
         REPORT_FILE_NAMES.put("cityPassengerCitySum", "鐢熸垚_鍩庡競瀹㈣繍瀹㈣繍閲忓垎甯傚窞绱.xlsx");
         REPORT_FILE_NAMES.put("cityPassengerDetailSum", "鐢熸垚_鍩庡競瀹㈣繍鍚勫競宸炴槑缁嗚〃.xlsx");
         REPORT_FILE_NAMES.put("cityPassengerSummary", "鐢熸垚_鍩庡競瀹㈣繍姹囨��.xlsx");
+        REPORT_FILE_NAMES.put("summaryWorkbook", "鐢熸垚_閬撹矾杩愯緭閲忔眹鎬昏〃.xlsx");
+        REPORT_FILE_NAMES.put("wycSplit", "鐢熸垚_缃戠害杞﹀杩愰噺鎷嗗垎琛�.xlsx");
     }
 
     private void addZipEntry(java.util.zip.ZipOutputStream zos, String name, byte[] data) throws Exception {
@@ -3412,6 +3917,7 @@
     }
 
     private byte[] toBytesHssf(HSSFWorkbook wb) throws Exception {
+        applyTwoDecimalFormat(wb);
         try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
             wb.write(out);
             wb.close();
@@ -4145,7 +4651,106 @@
     private double nz(Double v) {
         return v == null ? 0.0 : v;
     }
+    /** 鐢熸垚琛ㄦ暟鍊煎垪缁熶竴鏍峰紡锛氫繚鐣欏叏绮惧害鏁板�硷紝鏄剧ず涓や綅灏忔暟锛涙暣鏁板�间笌 %/鏃ユ湡/绉戝璁℃暟绛夋棦鏈夋牱寮忎笉鏀瑰彉 */
+    private void applyTwoDecimalFormat(org.apache.poi.ss.usermodel.Workbook wb) {
+        if (wb == null) return;
+        org.apache.poi.ss.usermodel.DataFormat df = wb.createDataFormat();
+        short fmtTwo = df.getFormat("0.00");
+        for (int s = 0; s < wb.getNumberOfSheets(); s++) {
+            org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(s);
+            if (sh == null) continue;
+            for (org.apache.poi.ss.usermodel.Row row : sh) {
+                if (row == null) continue;
+                for (org.apache.poi.ss.usermodel.Cell cell : row) {
+                    if (cell == null) continue;
+                    try {
+                        CellType ct = cell.getCellType();
+                        if (ct != CellType.NUMERIC && ct != CellType.FORMULA) continue;
+                        if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) continue;
+                        boolean integral = false;
+                        if (ct == CellType.NUMERIC) {
+                            integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue();
+                        } else if (cell.getCachedFormulaResultType() == CellType.NUMERIC) {
+                            integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue();
+                        }
+                        if (integral) continue; // 鏁存暟涓嶆樉绀� .00锛堝惈鍏紡缁撴灉涓烘暣鏁扮殑鏍硷級
+                        org.apache.poi.ss.usermodel.CellStyle cs = cell.getCellStyle();
+                        if (cs == null) continue;
+                        String f = cs.getDataFormatString();
+                        if (f == null) f = "";
+                        if (f.contains("%") || f.contains("E") || f.contains("0.00")) continue;
+                        boolean plain = f.isEmpty() || "General".equals(f) || "general".equals(f)
+                            || "0".equals(f) || "#,##0".equals(f) || "0.0".equals(f) || "#,##0.0".equals(f);
+                        if (!plain) continue;
+                        org.apache.poi.ss.usermodel.CellStyle ns = wb.createCellStyle();
+                        ns.cloneStyleFrom(cs);
+                        ns.setDataFormat(fmtTwo);
+                        cell.setCellStyle(ns);
+                    } catch (Exception ignore) {
+                        // 涓埆鍗曞厓鏍兼牸寮忓紓甯镐笉闃诲瀵煎嚭
+                    }
+                }
+            }
+        }
+    }
+
+    /** 姹囨�昏〃瀵煎嚭鍚庢寜鍐呭鑷姩鍔犲鍒楀锛堝彧鍔犲涓嶇缉绐勶紱璺宠繃鍚堝苟鍗曞厓鏍兼爣棰橈紝闃查暱鏍囬鎶婂崟鍒楁拺鐖嗭級 */
+    private void autoFitContentColumns(org.apache.poi.ss.usermodel.Workbook wb) {
+        if (wb == null) return;
+        org.apache.poi.ss.usermodel.DataFormatter dfmt = new org.apache.poi.ss.usermodel.DataFormatter();
+        for (int s = 0; s < wb.getNumberOfSheets(); s++) {
+            org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(s);
+            if (sh == null) continue;
+            java.util.Set<String> merged = new java.util.HashSet<>();
+            for (org.apache.poi.ss.util.CellRangeAddress ra : sh.getMergedRegions()) {
+                for (int r = ra.getFirstRow(); r <= ra.getLastRow(); r++) {
+                    for (int c = ra.getFirstColumn(); c <= ra.getLastColumn(); c++) {
+                        merged.add(r + ":" + c);
+                    }
+                }
+            }
+            int maxCol = -1;
+            for (org.apache.poi.ss.usermodel.Row row : sh) {
+                if (row == null) continue;
+                maxCol = Math.max(maxCol, (int) row.getLastCellNum() - 1);
+            }
+            if (maxCol < 0) continue;
+            int[] need = new int[maxCol + 1];
+            for (org.apache.poi.ss.usermodel.Row row : sh) {
+                if (row == null) continue;
+                for (org.apache.poi.ss.usermodel.Cell cell : row) {
+                    int c = cell.getColumnIndex();
+                    if (c > maxCol || merged.contains(row.getRowNum() + ":" + c)) continue;
+                    String txt;
+                    try {
+                        txt = dfmt.formatCellValue(cell);
+                    } catch (Exception ignore) {
+                        continue;
+                    }
+                    if (txt == null || txt.isEmpty()) continue;
+                    int units = 0;
+                    for (int i = 0; i < txt.length(); i++) {
+                        units += isWideChar(txt.charAt(i)) ? 2 : 1;
+                    }
+                    if (units > need[c]) need[c] = Math.min(units, 34); // 闀挎枃鏈笉鎶婂崟鍒楁拺鐖�
+                }
+            }
+            for (int c = 0; c <= maxCol; c++) {
+                if (need[c] <= 0) continue;
+                int target = Math.min(need[c] + 2, 46) * 256;
+                if (target > sh.getColumnWidth(c)) sh.setColumnWidth(c, target);
+            }
+        }
+    }
+
+    /** CJK / 鍏ㄨ瀛楃鎸変袱鍊嶅搴﹁锛堣繎浼� Excel 鏄剧ず鍒楀锛� */
+    private boolean isWideChar(char ch) {
+        return (ch >= 0x2E80 && ch <= 0x9FFF) || (ch >= 0xF900 && ch <= 0xFAFF)
+            || (ch >= 0xFF00 && ch <= 0xFFEF);
+    }
+
     private byte[] toBytes(XSSFWorkbook wb) throws Exception {
+        applyTwoDecimalFormat(wb);
         try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
             wb.write(out);
             wb.close();

--
Gitblit v1.9.1