From 2b1af100a2402e65b44a929489754949198a19df Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期二, 06 十月 2026 00:29:34 +0800
Subject: [PATCH] fix(holiday): 国省道环比基期改为纯备用值,不进环比计算

---
 HANDOFF.md                                                                                         |   20 ++++++
 docs/database.md                                                                                   |    4 
 traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayWorkbookService.java    |   28 +++++++--
 traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayComparisonBaseTest.java |   99 +++++++++++++++++++++++++++++++++
 traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java        |   11 +--
 5 files changed, 148 insertions(+), 14 deletions(-)

diff --git a/HANDOFF.md b/HANDOFF.md
index ced9b27..2cf8844 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -4,6 +4,26 @@
 > **姣忓ぉ缁撴潫鏃�**锛氭妸褰撳ぉ杩涘睍銆佽俯杩囩殑鍧戙�佹柊闇�姹傘�佹湭瀹屾垚浜嬮」鏇存柊鍒版湰鏂囦欢锛岀劧鍚庡彲浠ユ斁蹇冨紑鏂板璇濄��
 > **鍗忎綔绾﹀畾锛�2026-08-31锛�**锛氳銆屾彁浜ゃ��= 鏈湴 commit + 鎺ㄩ�佸埌杩滅 git锛坥rigin main锛孏itblit锛夛紝涓嶅啀鍙仛鏈湴 commit銆�
 
+## 鏈�鏂帮紙2026-10-06锛�
+
+### 10-06锛氭櫘閫氬浗鐪侀亾銆岀幆姣斿熀鏈熴�嶆敼涓轰汉宸ュ鐢ㄥ�硷紙涓嶅啀鍙備笌璁$畻锛�
+
+- **鑳屾櫙**锛�10-04 閯傚窞妯婂搴勭珯涓皬瀹㈡祦閲忔柇宕栦笅璺岋紙35419 -> 8856锛夛紝棰嗗瑕佹眰鍓旈櫎銆傛墜宸ョ蹇姤鐨勫仛娉曟槸锛氬厛鐢ㄥ墧绔欏悗鐨勬暟鍊肩畻鍑� 10-04 鐜瘮锛屾妸 10-03 鍥界渷閬撳�兼尓鍒般�岀幆姣斿熀鏈熴�岰 鍒楀鐢紝鍐嶆妸 10-03 鎭㈠鍘熷�间互鍏嶅奖鍝嶇疮璁°�傜敤鎴锋槑纭彛寰勶細**銆岀幆姣斿熀鏈熴�嶅彧鐢ㄤ簬鏁版嵁澶囩敤锛屼笉鍙備笌浠讳綍璁$畻**銆�
+- **鏃ц涓猴紙閿欙級**锛歚HolidayStatService.nationalProvincialRingRatio` 鍦� 2026 骞村彧瑕� `comparison_base` 鏈夊�煎氨鎸夈�屽綋鏃� / 鐜瘮鍩烘湡 - 1銆嶇畻鍥界渷閬撶幆姣旓紱瀵煎嚭鏃� E 鍒楀啓 `=(D{琛寎-C{琛寎)/C{琛寎`銆傛寜鎵嬪伐绋跨殑 C 鍒楁憜娉曪紙鍩烘湡鏀惧湪琚瘮鏃ラ偅琛岋級锛岃繖鏍风畻杩樹細閿欎綅锛�10-03 鐜瘮琚畻鎴� +1.11%锛夈��
+- **鏂拌涓猴紙鏈宸叉敼锛�**锛氬浗鐪侀亾鐜瘮鍥哄畾銆屽綋鏃� / 鍓嶄竴鏃� - 1銆嶏紝2024/2025/2026 涓�鑷达紱`comparison_base` 鍙湪瀵煎叆/瀵煎嚭/椤甸潰灞曠ず鏃跺師鏍蜂繚瀛樹笌鍥炲啓锛屼笉鍙備笌鐜瘮銆佹姌绠椼�佺疮璁°�傚鍑� E 鍒楁敼涓� `=(D{琛寎-D{涓婁竴鏃ヨ})/D{涓婁竴鏃ヨ}`锛孋 鍒楃収甯稿洖鍐欏鐢ㄥ�笺��
+- **鏀瑰姩**锛歚HolidayStatService.java`锛坄nationalProvincialRingRatio` 鍘绘帀 year/comparisonBase 褰㈠弬锛夈�乣HolidayWorkbookService.java`锛堟柊澧� `ringRatioFormula`锛屽鍑烘寜涓婁竴琛屽悓骞� D 鍒楋級銆乣docs/database.md`锛坄comparison_base` 涓庤绠楀彛寰勪袱澶勶級銆�
+- **楠岃瘉**锛氭柊澧� `HolidayComparisonBaseTest`锛氣憼 搴撳唴瀛� `comparison_base=320.2418` 鏃讹紝10-04 鍥界渷閬撶幆姣斾粛鎸夊墠涓�鏃� 323.7837 璁$畻锛屼笖涓嶇瓑浜庢寜鍩烘湡绠楀嚭鐨勫�硷紱鈶� 瀵煎嚭鍏紡 = `=(D6-D5)/D5`銆俙mvn -o test` 鍏ㄧ豢锛�4 涓祴璇曠被锛夈��
+
+### 10-06锛氬緟纭鈥斺��10-04 鍥界渷閬撳墧绔欙紙閯傚窞妯婂搴勶級鎬庝箞杩涚郴缁�
+
+- **鎵嬪伐鍙e緞宸查�愰」鏍稿锛屼笌搴撳唴 `observation_station_flow` 瀹屽叏瀵瑰緱涓�**锛�
+  - 10-04 鍥界渷閬撳�硷紙2026/2025/2024锛�= 鍘熷�� 鈭� 妯婂搴勫綋鏃ュ�� -> 322.7695 / 337.5009 / 322.5277 涓囪締娆★紙妯婂搴� 8856 / 4492 / 2328锛夈��
+  - 10-04 鐜瘮鍒嗘瘝 = 2026-10-03 鍘熷�� 鈭� 妯婂搴� 10-03 鍊� = 323.7837 鈭� 3.5419 = **320.2418** 涓囪締娆� -> 鐜瘮 = 322.7695/320.2418 鈭� 1 = **+0.79%**銆�
+  - 10-03 鏄剧ず鍊间笌绱浠嶇敤鍘熷�� 323.7837锛岃鍓旂殑 320.2418 鍙惤鍒� C 鍒楀鐢ㄣ��
+- **瓒嬪娍缁撹锛堥渶鍏堣窡棰嗗瀵归綈锛�**锛氬彧鍓旀▕瀹跺簞锛�10-04 鍥界渷閬撶幆姣斿彧鏈� **+0.79%**锛涘悓鏃ラ珮閫熷皬瀹㈣溅 **+9.78%**銆侀搧璺� +9.94%銆佹皯鑸� +11.69%銆侀亾璺� +4.99%銆佹按璺� +4.33%銆�**鍓斾竴绔欏~涓嶅钩绾� 9 涓櫨鍒嗙偣鐨勮秼鍔垮樊**锛屼笌銆岃鍥界渷閬撳拰楂橀�熻秼鍔夸竴鑷淬�嶇殑棰勬湡骞朵笉鐩哥锛屽缓璁悓鏃惰閯傚窞鏍稿疄鍘熷璁板綍銆�
+- **寰呭喅绛栫殑浠g爜鏂规**锛氬洜 `comparison_base` 宸插畾涓虹函澶囩敤锛岀郴缁熸棤娉曚粠銆屽綋鏃ュ�笺�嶅弽鎺ㄥ墧绔欏熀鏈燂紝闇�瑕佸彟璁炬満鍒躲�傛帹鑽愭柊澧炪�屽紓甯哥珯鍓旈櫎銆嶉厤缃紙绔欑偣缂栧彿 + 鐢熸晥鏃ユ湡 + 骞翠唤锛夛紝绠楃幆姣斿垎姣嶆椂鐢� `observation_station_flow` 鐨勩�岃鏃ョ珯鐐瑰悎璁� 鈭� 鍓旂珯鍊笺�嶄綔鍩烘湡锛涙樉绀哄�间笌绱浠嶇敤鍘熷�笺��
+- **鍐崇瓥鐐�**锛氣憼 鍋氭垚鍙厤缃殑閫氱敤銆屽紓甯哥珯鍓旈櫎銆嶏紙鎺ㄨ崘锛屽彲澶嶇敤浜庡叾浠栬妭鍋囨棩/绔欑偣锛夛紝杩樻槸鍙寜鏈 10-04 涓�娆℃�у鐞嗭紱鈶� 鍓旂珯鍙奖鍝嶅浗鐪侀亾鐜瘮鍒嗘瘝锛岃繕鏄篃瑕佷笅娌夊埌闈炶惀涓氭�у皬瀹㈣溅/鍏矾/鍏ㄧぞ浼氱殑鐜瘮锛堟墜宸ョ瀹為檯鎶� 4.17% / 4.55% 涔熶竴璧锋敼浜嗭級銆�
+
 ## 鏈�鏂帮紙2026-10-05锛�
 
 ### 10-05 涓婂崍锛氬叧鏈哄揩鐓э紙褰撳墠鐘舵�侊級
diff --git a/docs/database.md b/docs/database.md
index 8367aa5..042b269 100644
--- a/docs/database.md
+++ b/docs/database.md
@@ -526,7 +526,7 @@
 | road_category | varchar(32) | 楂橀�熷叕璺� EXPRESSWAY / 鏅�氬浗鐪侀亾 NATIONAL_PROVINCIAL |
 | vehicle_type | varchar(32) | 灏忓杞� SMALL_PASSENGER / 涓皬瀹㈣溅 MEDIUM_SMALL_PASSENGER |
   | vehicle_count | decimal(30,15) | 杞︽祦閲忥紙涓囪締娆★級 |
-  | comparison_base | decimal(30,15) | 鏅�氬浗鐪侀亾鐜瘮鍩烘湡锛堜竾杈嗘锛屼粎褰撳勾浣跨敤锛� |
+  | comparison_base | decimal(30,15) | 鏅�氬浗鐪侀亾鐜瘮鍩烘湡锛堜竾杈嗘锛屼汉宸ュ鐢ㄥ�硷紝浠� 2026 骞村惎鐢ㄣ�佷笉鍙備笌璁$畻锛� |
   | created_at / updated_at | datetime | 鍒涘缓/鏇存柊鏃堕棿 |
 
 鍞竴閿細`(year, flow_date, road_category, vehicle_type)`銆�
@@ -544,7 +544,7 @@
 
 ### 璁$畻涓庡鍏ュ鍑哄彛寰�
   - 闈炶惀涓氭�т汉鍛樻祦鍔ㄩ噺 =锛堥珮閫熻溅娴� + 鏅�氬浗鐪侀亾杞︽祦锛壝� 褰撳勾鑺傚亣鏃ュ杩愮郴鏁般��
-  - 2026 骞存櫘閫氬浗鐪侀亾濡傚~鍐欑幆姣斿熀鏈燂紝鍒欏彧褰卞搷鏅�氬浗鐪侀亾杞︽祦閲忚嚜韬幆姣旓細濉啓鏃舵寜鈥滃綋鏃� / 鐜瘮鍩烘湡 - 1鈥濓紝鏈~鍐欐椂鎸夆�滃綋鏃� / 鍓嶄竴鏃� - 1鈥濓紱鎶樼畻浜哄憳娴佸姩閲忋�佹�讳汉鍛樻祦鍔ㄩ噺绛夌户缁寜 Excel 鍘熷叕寮忚绠椼��2024銆�2025 鍘嗗彶鏁版嵁涓嶄娇鐢ㄨ瀛楁銆�
+  - 鏅�氬浗鐪侀亾鐜瘮鍥哄畾鎸夆�滃綋鏃� / 鍓嶄竴鏃� - 1鈥濊绠楋紱2024/2025/2026 鍚勫勾涓�鑷淬�俙comparison_base`锛圗xcel 鏅�氬浗鐪侀亾琛� C 鍒椻�滅幆姣斿熀鏈熲�濓級鑷� 2026-10-05 璧蜂粎浣滀汉宸ュ鐢細瀵煎叆瀵煎嚭鍘熸牱淇濆瓨/鍥炲啓锛屼笉鍙備笌鐜瘮銆佹姌绠楁垨绱锛涘鍑烘椂 C 鍒楃収甯稿啓鍑猴紝E 鍒椻�滅幆姣斺�濆叕寮忓缁堝紩鐢ㄤ笂涓�琛屽悓骞� D 鍒椼��
   - 鍏矾浜哄憳娴佸姩閲� = 閬撹矾钀ヤ笟鎬у杩愰噺 + 闈炶惀涓氭�т汉鍛樻祦鍔ㄩ噺銆�
 - 鎬讳汉鍛樻祦鍔ㄩ噺 = 鍏矾 + 閾佽矾 + 姘戣埅 + 姘磋矾銆�
 - 瀵煎叆鎸夆�滃勾浠� + 鑺傚亣鏃ュ垎绫烩�濊鍙� `holiday_calendar` 涓� `day_index>=1` 鐨勬棩鏈燂紝灏嗗伐浣滅翱鈥滅N鏃モ�濊瑕嗙洊鍐欏叆褰撳勾瀹㈡祦/杞︽祦锛涘悓鏃惰鍙栧綋鍓嶅勾浠介搧璺�佹皯鑸殑鈥滃墠涓�鏃モ�濊锛屽啓鍏� `day_index=0` 瀵瑰簲鐨勬棩鏈熴��
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java
index 3015c71..a663821 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java
@@ -102,7 +102,7 @@
             BigDecimal nationalProvincial = vehicle.get(HolidayConstants.ROAD_NATIONAL_PROVINCIAL);
             BigDecimal comparisonBase = comparisonBaseByDate.get(day.getHolidayDate());
             BigDecimal nationalProvincialRingRatio = nationalProvincialRingRatio(
-                    year, nationalProvincial, previousNationalProvincial, comparisonBase);
+                    nationalProvincial, previousNationalProvincial);
             BigDecimal nonBusinessPersonFlow = null;
             if (coefficient != null && (expressway != null || nationalProvincial != null)) {
                 nonBusinessPersonFlow = HolidayNumberUtils.multiply(
@@ -252,12 +252,11 @@
         return total;
     }
 
-    private BigDecimal nationalProvincialRingRatio(int year, BigDecimal current,
-                                                   BigDecimal previous, BigDecimal comparisonBase) {
+    // The national/provincial ring ratio always compares the current day with the previous day of
+    // the same year. The stored comparison base (Excel column C) is kept as an operator backup
+    // value only and never feeds this calculation.
+    private BigDecimal nationalProvincialRingRatio(BigDecimal current, BigDecimal previous) {
         if (current == null) return null;
-        if (HolidayConstants.supportsComparisonBase(year) && comparisonBase != null) {
-            return ratio(current, comparisonBase);
-        }
         return previous == null ? null : ratio(current, previous);
     }
 
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayWorkbookService.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayWorkbookService.java
index 9c06e00..cdf5e8a 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayWorkbookService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayWorkbookService.java
@@ -568,6 +568,9 @@
                 setNumeric(sheet, row.getRowNum(), yearColumn, values.get(day.getValue()));
             }
             if (comparisonBaseColumn != null && HolidayConstants.supportsComparisonBase(year)) {
+                // Column C (comparison base) is written as an operator backup value only. The ring
+                // ratio in column E always compares the current day with the previous day row of
+                // the same year, so the base never changes the calculated percentage.
                 Map<LocalDate, BigDecimal> bases = loadComparisonBaseValues(year);
                 for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) {
                     if (!bases.containsKey(day.getValue())) continue;
@@ -575,16 +578,27 @@
                     if (row == null) continue;
                     setNumeric(sheet, row.getRowNum(), comparisonBaseColumn, bases.get(day.getValue()));
                 }
+                Integer previousRowIndex = null;
                 for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) {
-                    if (!bases.containsKey(day.getValue())) continue;
                     Row row = findDayRow(sheet, evaluator, day.getKey());
-                    if (row == null) continue;
-                    int rowIndex = row.getRowNum();
-                    int excelRow = rowIndex + 1;
-                    setFormula(sheet, rowIndex, 4, "=(D" + excelRow + "-C" + excelRow + ")/C" + excelRow);
+                    if (row == null) {
+                        previousRowIndex = null;
+                        continue;
+                    }
+                    if (previousRowIndex != null) {
+                        setFormula(sheet, row.getRowNum(), 4,
+                                ringRatioFormula(row.getRowNum() + 1, previousRowIndex + 1));
+                    }
+                    previousRowIndex = row.getRowNum();
                 }
             }
         }
+    }
+
+    // Ring ratio = (current day value - previous day value) / previous day value, always taken
+    // from the active year column (D). This intentionally ignores the comparison base column.
+    static String ringRatioFormula(int excelRow, int previousExcelRow) {
+        return "=(D" + excelRow + "-D" + previousExcelRow + ")/D" + previousExcelRow;
     }
 
     private void fillCoefficients(XSSFWorkbook workbook, FormulaEvaluator evaluator, int selectedYear, String holidayType) {
@@ -1013,7 +1027,9 @@
         if (row == null) row = sheet.createRow(rowIndex);
         Cell cell = row.getCell(columnIndex);
         if (cell == null) cell = row.createCell(columnIndex);
-        cell.setCellFormula(formula);
+        // POI's setCellFormula rejects a leading '='; the helper accepts either form.
+        String normalized = formula.startsWith("=") ? formula.substring(1) : formula;
+        cell.setCellFormula(normalized);
     }
 
     private Map<String, Object> toMap(HolidayCalendar calendar) {
diff --git a/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayComparisonBaseTest.java b/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayComparisonBaseTest.java
new file mode 100644
index 0000000..62c7366
--- /dev/null
+++ b/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayComparisonBaseTest.java
@@ -0,0 +1,99 @@
+package com.trafficaudit.holiday.service;
+
+import com.trafficaudit.holiday.HolidayConstants;
+import com.trafficaudit.holiday.HolidayNumberUtils;
+import com.trafficaudit.holiday.entity.HolidayCalendar;
+import com.trafficaudit.holiday.entity.HolidayPassengerFlow;
+import com.trafficaudit.holiday.entity.HolidayVehicleFlow;
+import com.trafficaudit.holiday.mapper.HolidayPassengerFlowMapper;
+import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * The Excel comparison base column (C) is an operator backup value only: it must never change the
+ * national/provincial ring ratio. This locks in that behaviour.
+ */
+class HolidayComparisonBaseTest {
+
+    @Test
+    void ringRatioUsesPreviousDayEvenWhenComparisonBaseIsStored() throws Exception {
+        HolidayConfigService configService = mock(HolidayConfigService.class);
+        HolidayPassengerFlowMapper passengerMapper = mock(HolidayPassengerFlowMapper.class);
+        HolidayVehicleFlowMapper vehicleMapper = mock(HolidayVehicleFlowMapper.class);
+
+        when(configService.coefficientOf(2026, HolidayConstants.CATEGORY_HOLIDAY)).thenReturn(new BigDecimal("2.4"));
+        when(passengerMapper.selectList(any())).thenReturn(new ArrayList<HolidayPassengerFlow>());
+        when(vehicleMapper.selectList(any())).thenReturn(vehicleRows());
+
+        HolidayStatService service = new HolidayStatService();
+        inject(service, "configService", configService);
+        inject(service, "passengerFlowMapper", passengerMapper);
+        inject(service, "vehicleFlowMapper", vehicleMapper);
+
+        List<Map<String, Object>> rows = service.buildOverviewRows(2026, HolidayConstants.TYPE_NATIONAL_DAY, calendar());
+
+        BigDecimal ringRatio = (BigDecimal) rows.get(3).get("nationalProvincialRingRatio");
+        assertEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("323.7837")), ringRatio);
+        assertNotEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("320.2418")), ringRatio);
+        assertEquals(new BigDecimal("320.2418"), rows.get(2).get("comparisonBase"));
+    }
+
+    @Test
+    void exportFormulaComparesPreviousRowInsteadOfComparisonBaseColumn() {
+        assertEquals("=(D6-D5)/D5", HolidayWorkbookService.ringRatioFormula(6, 5));
+    }
+
+    private static BigDecimal ratio(BigDecimal current, BigDecimal base) {
+        return HolidayNumberUtils.normalize(HolidayNumberUtils.divide(current, base).subtract(BigDecimal.ONE));
+    }
+
+    private static List<HolidayVehicleFlow> vehicleRows() {
+        List<HolidayVehicleFlow> rows = new ArrayList<>();
+        rows.add(vehicle("2026-10-03", "323.7837", "320.2418"));
+        rows.add(vehicle("2026-10-04", "322.7695", null));
+        return rows;
+    }
+
+    private static HolidayVehicleFlow vehicle(String date, String value, String base) {
+        HolidayVehicleFlow row = new HolidayVehicleFlow();
+        row.setYear(2026);
+        row.setFlowDate(LocalDate.parse(date));
+        row.setRoadCategory(HolidayConstants.ROAD_NATIONAL_PROVINCIAL);
+        row.setVehicleType(HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER);
+        row.setVehicleCount(new BigDecimal(value));
+        row.setComparisonBase(base == null ? null : new BigDecimal(base));
+        return row;
+    }
+
+    private static List<HolidayCalendar> calendar() {
+        List<HolidayCalendar> days = new ArrayList<>();
+        for (int i = 1; i <= 4; i++) {
+            HolidayCalendar day = new HolidayCalendar();
+            day.setYear(2026);
+            day.setHolidayType(HolidayConstants.TYPE_NATIONAL_DAY);
+            day.setDayIndex(i);
+            day.setHolidayDate(LocalDate.of(2026, 10, i));
+            days.add(day);
+        }
+        return days;
+    }
+
+    private static void inject(Object target, String field, Object value) throws Exception {
+        Field declared = target.getClass().getDeclaredField(field);
+        declared.setAccessible(true);
+        declared.set(target, value);
+    }
+}

--
Gitblit v1.9.1