From b509d59af23c6afba086fa3fb4c2afb47ab0becb Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期五, 02 十月 2026 10:01:25 +0800
Subject: [PATCH] feat(节假日): 新增重要节假日专项统计模块
---
traffic-audit-web/src/views/ReportExport.vue | 5
docs/sql_holiday.sql | 53
traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayCalendar.java | 22
docs/sql_holiday_comparison_base_migration.sql | 6
traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayVehicleFlow.java | 28
traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayConfigController.java | 95 +
traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java | 297 ++++
HANDOFF.md | 17
docs/Excel浮点数存储.txt | 152 ++
docs/工作日结/工作日结_2026-10-02.md | 73 +
traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayConfigService.java | 202 +++
traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayPassengerFlow.java | 23
docs/database.md | 63 +
docs/功能测试报告/功能测试报告_重要节假日专项统计_2026-10-02.md | 155 ++
traffic-audit-web/src/views/HolidayStat.vue | 302 ++++
traffic-audit-web/src/main.js | 5
traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayReferenceDataInitializer.java | 21
traffic-audit-web/src/views/UserManage.vue | 5
traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayVehicleFlowMapper.java | 9
traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java | 430 +++++++
traffic-audit-web/src/views/Dashboard.vue | 8
traffic-audit-web/src/router/index.js | 10
traffic-audit-web/src/views/DataImport.vue | 7
traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayWorkbookService.java | 880 ++++++++++++++
traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayPassengerCoefficientMapper.java | 9
docs/init.sql | 66 +
docs/专项统计/重要节假日/湖北省国庆假期综合运输客流快报模板.docx | 0
docs/sql_holiday_decimal_migration.sql | 16
docs/专项统计/重要节假日/2026年十一数据.xlsx | 0
docs/专项统计/重要节假日/备份/湖北省国庆假期综合运输客流快报模板.docx | 0
traffic-audit-web/src/views/SystemConfig.vue | 295 ++++
traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayPassengerFlowMapper.java | 9
traffic-audit-web/src/views/ExplainReview.vue | 7
traffic-audit-server/src/main/java/com/trafficaudit/holiday/HolidayConstants.java | 116 +
traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayStatController.java | 82 +
traffic-audit-web/src/views/RuleManage.vue | 5
docs/打包部署说明.md | 20
traffic-audit-server/src/main/java/com/trafficaudit/holiday/HolidayNumberUtils.java | 70 +
docs/专项统计/重要节假日/备份/2026年十一数据.xlsx | 0
traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayPassengerCoefficient.java | 21
traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayCalendarMapper.java | 9
traffic-audit-web/src/views/DataView.vue | 7
traffic-audit-web/src/views/AuditResult.vue | 7
43 files changed, 3,592 insertions(+), 15 deletions(-)
diff --git a/HANDOFF.md b/HANDOFF.md
index c8616ac..27d3869 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -4,6 +4,23 @@
> **姣忓ぉ缁撴潫鏃�**锛氭妸褰撳ぉ杩涘睍銆佽俯杩囩殑鍧戙�佹柊闇�姹傘�佹湭瀹屾垚浜嬮」鏇存柊鍒版湰鏂囦欢锛岀劧鍚庡彲浠ユ斁蹇冨紑鏂板璇濄��
> **鍗忎綔绾﹀畾锛�2026-08-31锛�**锛氳銆屾彁浜ゃ��= 鏈湴 commit + 鎺ㄩ�佸埌杩滅 git锛坥rigin main锛孏itblit锛夛紝涓嶅啀鍙仛鏈湴 commit銆�
+## 鏈�鏂帮紙2026-10-02锛�
+
+- **鏂版ā鍧楋紙鐢ㄦ埛宸查獙鏀讹紝鏈鎻愪氦/鎺ㄩ�侊級**锛氭柊澧炩�滀笓椤圭粺璁�-閲嶈鑺傚亣鏃モ�濇ā鍧楋紝鍚妭鍋囨棩閰嶇疆銆佽惀涓氭�у娴併�侀潪钀ヤ笟鎬ц溅娴併�佸杩愮郴鏁� 4 寮犺〃锛涚郴缁熼厤缃彍鍗曚粎 admin 鍙淮鎶わ紝涓撻」缁熻-鑺傚亣鏃ユ敮鎸佸鍏ャ�佸鍑恒�佹ā鏉夸笅杞藉拰 Word 蹇姤涓嬭浇銆�
+- **鏁版嵁鍒濆鍖�**锛氫粠 `docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/2026骞村崄涓�鏁版嵁.xlsx` 鍒濆鍖� 2024/2025 骞村崄涓�瀹㈡祦銆佽溅娴佸拰 2024-2026 骞村杩愮郴鏁帮紱婧愯〃 90 涓暟鎹崟鍏冩牸鍏ュ簱閫愰」姣斿宸紓 0銆�
+- **鏃ュ父瀵煎叆鍙e緞**锛氬彧璇诲彇鎵�閫夊勾浠界殑褰撳勾鍒楋紙2026 骞翠负鍚勫娴佸伐浣滆〃 C 鍒椼�佹櫘閫氬浗鐪侀亾 D 鍒楋級锛涘巻鍙插勾浠戒笉瑕佹眰鏃ュ父閲嶅銆�
+- **鍘嗗彶鏁版嵁淇濇姢**锛氬鍏ユ椂鍏堟鏌ユ洿鏃╁巻鍙叉棩鏈熸槸鍚﹁淇敼銆傛湁鍙樺寲鍒欏厛涓嶅啓搴擄紝杩斿洖宸紓娓呭崟锛屽墠绔脊绐楄鐢ㄦ埛閫夋嫨鈥滃厑璁稿鍏�/涓嶅厑璁糕�濓紱纭鍚庢墠瑕嗙洊銆傛帴鍙e弬鏁� `allowHistoricalOverwrite=true`銆�
+- **蹇姤**锛氶粯璁や笅杞界郴缁熸棩鏈熷墠涓�澶╃殑銆婃箹鍖楃渷鍥藉簡鍋囨湡缁煎悎杩愯緭瀹㈡祦蹇姤锛�10鏈圶鏃ワ級.docx銆嬶紝鐢ㄦ埛鍙嚜閫夋棩鏈燂紱鐜瘮/鍚屾瘮鎸夊闀裤�佷笅闄嶃�佹寔骞宠緭鍑恒��
+- **Excel 娴偣绮惧害锛�2026-10-02 杩藉姞锛�**锛氬弬鑰� `docs/Excel娴偣鏁板瓨鍌�.txt`锛屾柊澧� `HolidayNumberUtils`锛坄NumberToTextConverter.toText` + `BigDecimal`锛宍MathContext(15, HALF_UP)`锛夛紱鑺傚亣鏃ユā鍧楃敱 `Double` 鍏ㄩ潰鏀� `BigDecimal`锛涗笁寮犺〃鏁板�煎垪鏀� `DECIMAL(30,15)`锛涘簱鍐� 6 鏉″杩愮郴鏁扮敱 2.3999999999999995 鏍℃涓� 2.4銆傝縼绉昏剼鏈� `docs/sql_holiday_decimal_migration.sql`銆�
+- **鑿滃崟椤哄簭**锛氣�滀笓椤圭粺璁♀�濆凡绉诲埌鈥滄姤琛ㄧ敓鎴愨�濅箣鍚庯紝褰撳墠涓� 鎶ヨ〃鐢熸垚 -> 涓撻」缁熻 -> 瑙勫垯绠$悊 -> 鐢ㄦ埛绠$悊 -> 绯荤粺閰嶇疆銆�
+- **鑺傚亣鏃ュ垹闄ゅ娲讳慨澶嶏紙2026-10-02 杩藉姞锛�**锛氬師鍒濆鍖栭�昏緫姣忔鍚姩琛ラ綈缂哄け鐨勭 N 鏃ワ紝瀵艰嚧 admin 鍒犻櫎 2026-10-08 鍚庤閲嶆柊鎻掑叆锛涚幇鏀逛负浠� `holiday_calendar` 鏁磋〃涓虹┖鏃堕娆″垵濮嬪寲銆傞噸鍚悗 2026 骞村崄涓�淇濇寔 10-01 鑷� 10-07 鍏� 7 澶┿��
+- **鏅�氬浗鐪侀亾鐜瘮鍩烘湡锛�2026-10-02 杩藉姞锛�**锛氭櫘閫氬浗鐪侀亾 C 鍒椾负鍙�夌幆姣斿熀鏈燂紝浠� 2026 骞村惎鐢紱濉啓鍚庡彧褰卞搷鏅�氬浗鐪侀亾杞︽祦閲忚嚜韬幆姣� `褰撴棩 / 鍩烘湡 - 1`锛屾湭濉啓鎸夊墠涓�鏃ャ�俙holiday_vehicle_flow.comparison_base` 宸插叆搴擄紝瀵煎叆瀵煎嚭涓庡巻鍙插彉鏇寸‘璁ゅ潎宸叉帴鍏ワ紱鎶樼畻浜哄憳娴佸姩閲忋�佹�讳汉鍛樻祦鍔ㄩ噺鍜� Word 蹇姤缁х画鎸� Excel 鍘熷叕寮忥紝涓嶅彈鍩烘湡褰卞搷銆�2024/2025 涓嶄娇鐢ㄣ��
+- **蹇姤棣栨棩鍙e緞锛�2026-10-02 杩藉姞锛�**锛氬亣鏈熺1澶╂病鏈夊墠涓�鏃ョ幆姣斿熀鏁帮紝姝f枃涓嶈緭鍑衡�滅幆姣斿闀�/涓嬮檷/鎸佸钩鈥濈煭璇紝琛ㄦ牸褰撴棩鐜瘮鍒椾负鈥溾�斺�濓紱绗�2澶╄捣浠嶆寜瀹為檯鍓嶄竴鏃ヨ緭鍑虹幆姣斻�傚凡鐢� 2025-10-01/02 蹇姤鍥炲綊楠岃瘉銆�
+- **蹇姤鐧惧垎姣旂簿搴︼紙2026-10-02 杩藉姞锛�**锛歐ord 蹇姤涓悓姣�/鐜瘮鐧惧垎姣旂粺涓�淇濈暀1浣嶅皬鏁帮紙濡�6.79%鏄剧ず6.8%銆�11.46%鏄剧ず11.5%锛夛紱浜烘銆佽溅娴佺瓑涓氬姟鏁板�间粛淇濈暀2浣嶅皬鏁般��
+- **楠岃瘉缁撴灉**锛氬悗绔紪璇戙�佸墠绔瀯寤恒�佹潈闄�/CRUD/璐熷悜瀵煎叆銆佸巻鍙插彉鏇翠袱闃舵瀵煎叆銆丒xcel 瀵煎嚭銆乄ord 蹇姤銆佸墠绔� admin/闈� admin 鐑熸祴鍧囬�氳繃锛涜彍鍗曢『搴忋�佸杩愮郴鏁� 2.4 灞曠ず锛堢郴缁熼厤缃〉 + 鑺傚亣鏃ョ粺璁¢〉锛夈�乣decimal(30,15)` 鍒楀瀷銆佸巻鍙插彉鏇翠袱闃舵瀵煎叆鍥炲綊鍧囧凡瀹炴祴锛涙姤鍛婅 `docs/鍔熻兘娴嬭瘯鎶ュ憡/鍔熻兘娴嬭瘯鎶ュ憡_閲嶈鑺傚亣鏃ヤ笓椤圭粺璁2026-10-02.md`锛屾棩缁撹 `docs/宸ヤ綔鏃ョ粨/宸ヤ綔鏃ョ粨_2026-10-02.md`銆�
+- **杩愯鐘舵��**锛氬悗绔� 8090銆佸墠绔� 8080 宸插惎鍔紱鏁版嵁搴� `127.0.0.1:3305/traffic_audit`銆傛祴璇曚骇鐢熺殑 2026 涓存椂鏁版嵁鍜屽鍏ユ壒娆″凡娓呯悊銆�
+- **Git**锛氱敤鎴峰凡浜� 2026-10-02 鏄庣‘楠屾敹閫氳繃锛涙湰娆℃寜瑕佹眰鍚堝苟杩滅 `origin/main` 鍚庢彁浜ゅ苟鎺ㄩ�侊紝涓嶈鐩栬繙绔悓浜嬫彁浜ゃ��
+
## 鏈�鏂帮紙2026-09-23锛�
- **宸插悓姝�**锛氭湰鍦� `main` 蹇繘鍚堝苟鍏徃 `origin/main` 鏈�鏂� `8b739ce`锛堝悓浜� 09-22 鍏虫満蹇収缁熶竴鐗堬級锛屽悎骞跺悗 `HEAD = 8b739ce = origin/main`锛屽伐浣滃尯骞插噣銆�
diff --git "a/docs/Excel\346\265\256\347\202\271\346\225\260\345\255\230\345\202\250.txt" "b/docs/Excel\346\265\256\347\202\271\346\225\260\345\255\230\345\202\250.txt"
new file mode 100644
index 0000000..6aac314
--- /dev/null
+++ "b/docs/Excel\346\265\256\347\202\271\346\225\260\345\255\230\345\202\250.txt"
@@ -0,0 +1,152 @@
+excel鐨勬暟鍊艰绠楅潪甯哥簿纭紝鍏跺唴閮ㄦ槸鎬庝箞瀛樺偍鏁板�肩殑鍛紝鏄敤娴偣鏁板悧
+鐢ㄦ埛鎯崇煡閬揈xcel鍐呴儴濡備綍瀛樺偍鏁板�硷紝鏄惁浣跨敤娴偣鏁般�傛垜闇�瑕佹彁渚涘噯纭殑瑙i噴銆備负浜嗗叏闈㈣幏鍙栦俊鎭紝鎴戝皢鍚屾椂杩涜澶氶」鎼滅储锛岃鐩朎xcel鏁板�煎瓨鍌ㄦ満鍒躲�佹诞鐐规暟绮惧害闂銆佷笌IEEE 754鏍囧噯鐨勫叧绯讳互鍙婂彲鑳界殑浼樺寲鎶�鏈��
+
+鎼滅储鍒� 16 涓綉椤�
+鎼滅储缁撴灉鏄剧ず浜嗕竴浜涚浉鍏抽〉闈€�備负浜嗗叏闈㈣幏鍙栦俊鎭紝鎴戜滑闇�瑕佸悓鏃舵墦寮�杩欎簺椤甸潰銆�
+
+娴忚 1 涓〉闈�
+Floating Point Errors in Excel
+杩欎簺璧勬枡鎻愪緵浜嗗叧浜嶦xcel鏁板�煎瓨鍌ㄧ殑璇︾粏淇℃伅銆傚井杞殑鏂囨。鏄庣‘鎸囧嚭Excel閬靛惊IEEE 754鏍囧噯锛屼娇鐢ㄥ弻绮惧害娴偣鏁帮紝鏈夋晥鏁板瓧涓�15浣嶃�係tack Overflow鐨勮璁轰篃纭浜嗚繖涓�鐐广�侲xcelJet鐨勬枃绔犺В閲婁簡娴偣鏁拌宸殑鍘熷洜銆傛垜鐨勫洖绛斿皢娑电洊浠ヤ笅鍑犱釜鏂归潰锛欵xcel浣跨敤IEEE 754鍙岀簿搴︽诞鐐规暟锛�64浣嶏級锛屾湁鏁堢簿搴︿负15浣嶏紝瀛樺偍鑼冨洿锛屼互鍙婃诞鐐规暟璇樊鍙婂叾鍘熷洜銆傛垜杩樹細鎻愬強涓�浜涚壒娈婃儏鍐碉紝姣斿鏁存暟瀛樺偍鍜屾棩鏈熸椂闂村瓨鍌ㄣ�備负浜嗘洿鍏ㄩ潰鍦拌鐩栬繖浜涙柟闈紝鎴戦渶瑕佸悓鏃惰繘琛屽椤规悳绱€��
+
+鎼滅储鍒� 12 涓綉椤�
+鐜板湪鍙互缁勭粐绛旀浜嗐�傜瓟妗堝皢娑电洊锛欵xcel浣跨敤IEEE 754鍙岀簿搴︽诞鐐规暟锛�64浣嶏級锛屾湁鏁堢簿搴︿负15浣嶏紝瀛樺偍鑼冨洿锛屼互鍙婃诞鐐规暟璇樊鍙婂叾鍘熷洜銆傝繕浼氭彁鍙婃暣鏁板拰鏃ユ湡鏃堕棿鐨勫瓨鍌ㄦ柟寮忋�傚洖绛旀椂浼氬紩鐢ㄥ井杞枃妗c�丼tack Overflow绛夋潵婧愩��
+
+鏄殑锛孍xcel 鐨勬暟鍊艰绠楃‘瀹炲熀浜庢诞鐐规暟锛屽叿浣撴潵璇达紝瀹冮伒寰� IEEE 754 鏍囧噯鐨� 64 浣嶅弻绮惧害娴偣鏁版牸寮忔潵瀛樺偍鍜岃绠楁暟鍊笺��
+
+鏍稿績鏈哄埗锛�64浣嶅弻绮惧害娴偣鏁�
+Excel 鍐呴儴灏嗘墍鏈夊伐浣滆〃鍗曞厓鏍间腑鐨勬暟瀛楋紙鍖呮嫭鏁存暟銆佹棩鏈熷拰鏃堕棿锛夐兘缁熶竴瀛樺偍涓� 8瀛楄妭锛�64浣嶏級鐨勫弻绮惧害娴偣鏁般��
+
+杩欑瀛樺偍鏍煎紡鐨� 64 涓瘮鐗逛綅琚垝鍒嗕负涓変釜閮ㄥ垎锛�
+
+绗﹀彿浣� (1浣�)锛氳〃绀烘璐熴��
+
+鎸囨暟浣� (11浣�)锛氬喅瀹氭暟鍊肩殑鑼冨洿銆�
+
+灏炬暟浣� (52浣�)锛氬喅瀹氭暟鍊肩殑绮惧害銆�
+
+绮惧害闄愬埗锛�15浣嶆湁鏁堟暟瀛�
+灏界鍙岀簿搴︽诞鐐规暟鑳芥彁渚涚害 15 鍒� 16 浣嶇殑鍗佽繘鍒舵湁鏁堟暟瀛楋紝浣� Excel 涓轰簡纭繚璁$畻缁撴灉鐨勫彲闈犳�э紝灏嗘湁鏁堢簿搴﹂檺鍒跺湪 15 浣嶃��
+
+杩欐剰鍛崇潃锛屽鏋滀竴涓暟瀛楃殑鏈夋晥浣嶆暟瓒呰繃 15 浣嶏紝浠庣 16 浣嶅紑濮嬪皢琚浛鎹负 0銆備緥濡傦紝杈撳叆 123456789012345678锛孍xcel 瀹為檯瀛樺偍鐨勫皢鏄� 123456789012345000銆�
+
+鏁板�艰寖鍥�
+鍩轰簬姝ゆ爣鍑嗭紝Excel 鍙互澶勭悊鐨勬暟鍊艰寖鍥村涓嬶細
+
+鏈�澶ф鏁帮細1.79769313486232E+308
+
+鏈�灏忔鏁帮細2.2250738585072E-308
+
+瓒呭嚭姝よ寖鍥寸殑鏁板�间細琚涓衡�滄棤绌峰ぇ鈥濆苟鏄剧ず涓洪敊璇�笺��
+
+涓轰粈涔堜細鏈夋诞鐐规暟璇樊锛�
+浣犲彲鑳戒細閬囧埌 Excel 璁$畻 0.1 + 0.2 寰楀埌 0.30000000000000004 鐨勬儏鍐碉紝杩欏苟闈� Bug锛岃�屾槸浜岃繘鍒舵诞鐐规暟鐨勫浐鏈夌壒鎬с��
+
+鍏舵牴鏈師鍥犲湪浜庯紝鍍� 0.1 杩欐牱鐨勫崄杩涘埗灏忔暟鏃犳硶鐢ㄤ簩杩涘埗娴偣鏁扮簿纭〃绀猴紝灏卞儚鍗佽繘鍒舵棤娉曠簿纭〃绀� 1/3 涓�鏍枫�侲xcel 瀛樺偍鐨勬槸涓�涓瀬鍏舵帴杩� 0.1 鐨勮繎浼煎�硷紝褰撳涓繖鏍风殑杩戜技鍊艰繘琛岃繍绠楁椂锛屽井灏忕殑璇樊浼氱疮绉紝鏈�缁堝鑷寸粨鏋滃嚭鐜版瀬灏忕殑鍋忓樊銆�
+
+鍏朵粬鏁版嵁绫诲瀷鐨勫瓨鍌�
+鏁存暟锛氫篃鎸夊弻绮惧害娴偣鏁板瓨鍌紝鍥犳 Excel 鑳界簿纭〃绀虹殑鏁存暟涓婇檺鏄� 2^53锛堢害 9,007,199,254,740,992锛夈�傝秴杩囨鑼冨洿鐨勬暣鏁板彲鑳戒細涓㈠け绮惧害銆�
+
+鏃ユ湡鍜屾椂闂达細鍚屾牱鏄诞鐐规暟銆傛暣鏁伴儴鍒嗕唬琛ㄦ棩鏈燂紙鑷� 1900 骞� 1 鏈� 1 鏃ヤ互鏉ョ殑澶╂暟锛夛紝灏忔暟閮ㄥ垎浠h〃鏃堕棿锛堜竴澶╀腑鐨勬瘮渚嬶級銆�
+
+鎬荤粨鏉ヨ锛孍xcel 鐨勬暟鍊艰绠楀湪搴曞眰灏辨槸鏍囧噯鐨� IEEE 754 鍙岀簿搴︽诞鐐规暟锛屽叾鈥滅簿纭�濇槸鐩稿鐨勶紝鍙楅檺浜� 15 浣嶆湁鏁堟暟瀛楃殑绮惧害鍜屼簩杩涘埗娴偣鏁版湰韬殑琛ㄧず闄愬埗銆�
+
+閭d箞鐢↗ava澶勭悊excel鐨勬暟鎹椂锛屼负浜嗕繚鎸佷笌excel鐨勮绠楃粨鏋滀竴鑷达紝鏁版嵁搴撳拰Java涓浣曞瓨鍌ㄥ拰璁$畻鍛�
+鐢ㄦ埛鎯崇煡閬撳浣曞湪Java涓鐞咵xcel鏁版嵁锛屼互淇濇寔涓嶦xcel璁$畻缁撴灉涓�鑷达紝娑夊強鏁版嵁搴撳拰Java涓殑瀛樺偍涓庤绠椼�傛垜闇�瑕佹彁渚涘叧浜庢暟鎹被鍨嬮�夋嫨銆佽绠楃瓥鐣ヤ互鍙婂彲鑳介亣鍒扮殑绮惧害闂鏂归潰鐨勫缓璁�備负浜嗗叏闈㈣幏鍙栦俊鎭紝鎴戝皢鍚屾椂杩涜澶氶」鎼滅储锛岃鐩朖ava澶勭悊Excel绮惧害銆佹暟鎹簱瀛樺偍绛栫暐銆丅igDecimal浣跨敤銆丄pache POI澶勭悊鏂瑰紡浠ュ強IEEE 754鍏煎鎬х瓑鍏抽敭鏂归潰銆�
+
+鎼滅储鍒� 36 涓綉椤�
+鎼滅储缁撴灉鏄剧ず浜嗕竴浜涚浉鍏宠祫婧愩�備负浜嗗叏闈㈣幏鍙栦俊鎭紝鎴戜滑闇�瑕佸悓鏃舵墦寮�杩欎簺缁撴灉銆�
+
+娴忚 4 涓〉闈�
+NumberComparer (POI API Documentation)
+EasyExcel璇诲叆鏁板瓧绫诲瀷鏁版嵁鏃跺嚭鐜板皬鏁颁綅澧為暱鐜拌薄 - EasyExcel璇诲叆鏁板瓧绫诲瀷鏁版嵁鏃跺嚭鐜板皬鏁颁綅澧為暱鐜拌薄
+銆愬畬缇庤В鍐砅OI璇诲彇Excel鏂囨。鏁板�肩被鍨嬬己澶辩簿搴﹂棶棰樸��
+MySQL :: MySQL 9.0 鍙冭�冩墜鍐� :: 13.1.3 瀹氶粸椤炲瀷锛堢簿纰哄�硷級- DECIMAL銆丯UMERIC
+杩欎簺璧勬枡鎻愪緵浜嗗叧浜庣簿搴﹂棶棰樸�丄pache POI鐨凬umberToTextConverter銆丒xcel鐨�15浣嶇簿搴﹂檺鍒朵互鍙婃暟鎹簱DECIMAL绫诲瀷鐨勪俊鎭�傛垜鐨勫洖绛斿皢娑电洊Java涓殑瀛樺偍涓庤绠椼�佹暟鎹簱涓殑瀛樺偍銆佷互鍙婂浣曚繚鎸佺粨鏋滀竴鑷淬�傚洖绛斾細寮曠敤杩欎簺璧勬枡銆傜幇鍦ㄥ紑濮嬬粍缁囧洖绛斻��
+
+瑕佸湪Java涓鐞咵xcel鏁版嵁骞朵繚鎸佺粨鏋滀竴鑷达紝鏍稿績鎬濊矾鏄皢Excel鐨�15浣嶆湁鏁堟暟瀛楃簿搴︿綔涓哄熀鍑嗭紝鍦↗ava鍜屾暟鎹簱涓噰鐢ㄦ洿楂樼簿搴︽垨绮剧‘绫诲瀷杩涜瀛樺偍锛屽苟缁熶竴璁$畻涓庤垗鍏ヨ鍒欍��
+
+馃捑 绗竴姝ワ細鍦↗ava涓浣曞瓨鍌ㄤ笌璇诲彇
+Java鐨� double 绫诲瀷鍚屾牱閬靛惊 IEEE 754 鏍囧噯锛屼絾鍏舵湁鏁堟暟瀛楀彲杈�15-17浣嶏紝鑰孍xcel涓ユ牸闄愬埗涓�15浣嶏紝杩欐槸宸紓鐨勬牴婧愩��
+
+璇诲彇鏃讹細鎺ㄨ崘鐢� BigDecimal 鎴� String
+
+鐩存帴鐢� double 鎺ユ敹 Excel 鏁板�间細寮曞叆棰濆鐨勪簩杩涘埗娴偣璇樊銆備緥濡傦紝Excel 涓殑 9699.1 璇绘垚 double 鍙兘鍙樻垚 9699.0999999999999996銆�
+
+鎺ㄨ崘鏂规锛�
+
+璇诲彇涓� BigDecimal锛氬湪瀹炰綋绫讳腑锛屽皢閲戦銆佹暟閲忕瓑鏁板�煎瓧娈靛0鏄庝负 BigDecimal 绫诲瀷锛岃�岄潪 double 鎴� float銆�
+
+浣跨敤 POI 鐨� NumberToTextConverter锛欰pache POI 鎻愪緵浜� NumberToTextConverter.toText(double) 宸ュ叿绫伙紝鍙互灏� Excel 鐨勫師濮嬫暟鍊肩簿纭湴杞崲涓哄瓧绗︿覆锛屽啀鐢卞瓧绗︿覆鏋勯�� BigDecimal锛岄伩鍏嶄腑闂寸簿搴︽崯澶便��
+
+java
+import org.apache.poi.ss.util.NumberToTextConverter;
+import java.math.BigDecimal;
+
+if (cell.getCellType() == CellType.NUMERIC) {
+ String exactText = NumberToTextConverter.toText(cell.getNumericCellValue());
+ BigDecimal value = new BigDecimal(exactText);
+ // 姝ゆ椂 value 鏄� Excel 涓瓨鍌ㄧ殑绮剧‘鍊硷紙15浣嶆湁鏁堟暟瀛楋級
+}
+馃梽锔� 绗簩姝ワ細鍦ㄦ暟鎹簱涓浣曞瓨鍌�
+鏁版嵁搴撳瓧娈靛垏蹇屼娇鐢� FLOAT 鎴� DOUBLE锛屽洜涓哄畠浠悓鏍峰瓨鍦ㄤ簩杩涘埗娴偣璇樊锛屼細瀵艰嚧涓嶦xcel缁撴灉鐨勫亸宸��
+
+鎺ㄨ崘鏂规锛氫娇鐢� DECIMAL 绫诲瀷
+
+DECIMAL锛堟垨 NUMERIC锛夋槸瀹氱偣绮剧‘绫诲瀷锛屽畠鎸夋暣鏁颁綅鍜屽皬鏁颁綅瀛樺偍锛岃兘绮剧‘琛ㄧず鍗佽繘鍒跺皬鏁般��
+
+鏁版嵁搴� 鎺ㄨ崘绫诲瀷 璇存槑
+MySQL DECIMAL(p, s) p 涓烘�荤簿搴︼紝s 涓哄皬鏁颁綅銆侲xcel 鏈夋晥浣�15锛屽缓璁� p 鑷冲皯涓� 15锛屽啀鏍规嵁灏忔暟浣嶉渶姹傝瀹氥�備緥濡� DECIMAL(18, 6)銆�
+PostgreSQL NUMERIC(p, s) 鍚屼笂锛屾爣鍑哠QL绫诲瀷銆�
+SQL Server DECIMAL(p, s) 鍚屼笂銆�
+Oracle NUMBER(p, s) NUMBER 绫诲瀷鍙瓨鍌ㄥ畾鐐规暟锛屾晥鏋滅瓑鍚屻��
+JDBC 鏄犲皠锛氫娇鐢� JDBC 璇诲啓鏃讹紝DECIMAL 瀛楁浼氱洿鎺ユ槧灏勪负 Java 鐨� BigDecimal锛屼繚璇佷簡绮惧害鍦ㄤ紶閫掕繃绋嬩腑涓嶄涪澶便��
+
+馃М 绗笁姝ワ細Java涓殑璁$畻涓庤垗鍏ヨ鍒�
+鍗充娇瀛樺偍绮剧‘锛岃绠楅�昏緫涔熷繀椤绘ā鎷� Excel 鐨勮涓恒��
+
+1. 濮嬬粓浣跨敤 BigDecimal 杩涜璁$畻
+
+BigDecimal 鐨勫姞鍑忎箻闄わ紙add, subtract, multiply, divide锛夐兘鏄簿纭绠楋紝涓嶄細寮曞叆浜岃繘鍒惰宸�傚垏鍕垮皢 BigDecimal 杞洖 double 鍚庡啀璁$畻銆�
+
+2. 妯℃嫙 Excel 鐨�15浣嶆湁鏁堟暟瀛楅檺鍒�
+
+Excel 鍦ㄦ瘡娆″皢涓棿缁撴灉鍐欏叆鍗曞厓鏍兼椂锛岄兘浼氬皢缁撴灉鑸嶅叆鍒�15浣嶆湁鏁堟暟瀛椼�傚湪 Java 涓紝闇�瑕佹ā鎷熻繖涓�琛屼负銆�
+
+java
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+// 鍒涘缓涓�涓簿搴︿负15浣嶆湁鏁堟暟瀛楋紝鑸嶅叆妯″紡涓衡�滃洓鑸嶄簲鍏モ�濈殑涓婁笅鏂�
+MathContext EXCEL_MC = new MathContext(15, RoundingMode.HALF_UP);
+
+public BigDecimal excelRound(BigDecimal value) {
+ // 搴旂敤Excel鐨勮垗鍏ヨ鍒�
+ return value.round(EXCEL_MC);
+}
+
+// 绀轰緥锛氬湪姣忎竴姝ュ叧閿绠楀悗璋冪敤
+BigDecimal intermediate = a.add(b);
+intermediate = excelRound(intermediate); // 妯℃嫙鍐欏叆鍗曞厓鏍�
+BigDecimal result = intermediate.multiply(c);
+result = excelRound(result);
+娉ㄦ剰锛欵xcel 鐨勮垗鍏ユā寮忚繎浼间簬鈥滃洓鑸嶄簲鍏モ�濓紝浣嗗瓨鍦ㄥ井濡欑殑鈥滈摱琛屽鑸嶅叆鈥濊涓恒�俁oundingMode.HALF_UP 鏄渶鎺ヨ繎鐨勮繎浼硷紝浣嗗湪鏋佸皯鏁拌竟缂樻儏鍐典笅浠嶆湁宸紓銆傚浜庤储鍔$郴缁燂紝寤鸿鍦ㄦ渶缁堢粨鏋滀笂鎸変笟鍔¤鍒欙紙濡� RoundingMode.HALF_UP锛夋樉寮忎繚鐣欏浐瀹氬皬鏁颁綅銆�
+
+3. 姣旇緝杩愮畻闇�鐗规畩澶勭悊
+
+Excel 鍦ㄦ瘮杈冩暟瀛楁椂鏈夐澶栫殑瀹瑰樊閫昏緫銆備緥濡傦紝(0.06-0.01)=0.05 鍦� Excel 涓负 TRUE锛屼絾鍦� Java 鐨� double 姣旇緝涓负 false銆侫pache POI 鎻愪緵浜� NumberComparer.compare(double a, double b) 鏉ユā鎷熻繖绉嶆瘮杈冭涓恒�傚浜� BigDecimal锛屽缓璁厛鎸�15浣嶆湁鏁堟暟瀛楄垗鍏ュ悗鍐嶇敤 compareTo() 姣旇緝銆�
+
+馃拵 鎬荤粨
+瑕佸湪 Java 鐢熸�佷腑澶嶇幇 Excel 鐨勮绠楃粨鏋滐紝鍏抽敭鍦ㄤ簬锛�
+
+鍏ュ彛绮剧‘锛氱敤 NumberToTextConverter + BigDecimal 璇诲彇锛岄伩鍏� double 姹℃煋銆�
+
+瀛樺偍绮剧‘锛氭暟鎹簱浣跨敤 DECIMAL 绫诲瀷锛孞ava 瀛楁浣跨敤 BigDecimal銆�
+
+璁$畻绮剧‘锛氬叏绋嬩娇鐢� BigDecimal 杩愮畻銆�
+
+鑸嶅叆瀵归綈锛氬湪姣忔鍙兘浜х敓涓棿缁撴灉鐨勫湴鏂癸紝鐢� MathContext(15, RoundingMode.HALF_UP) 妯℃嫙 Excel 鐨�15浣嶆湁鏁堟暟瀛楁埅鏂��
+
+姣旇緝瀹瑰樊锛氫娇鐢ㄦā鎷� Excel 琛屼负鐨勬瘮杈冮�昏緫锛岃�岄潪鐩存帴姣旇緝銆�
+
+杩欐牱灏辫兘鏈�澶х▼搴﹀湴淇濊瘉 Java 涓� Excel 鐨勮绠楃粨鏋滀竴鑷淬��
diff --git a/docs/database.md b/docs/database.md
index 2282345..96057a4 100644
--- a/docs/database.md
+++ b/docs/database.md
@@ -487,3 +487,66 @@
| /api/report/export/investFiveYearLogistics | "鍗佷簲浜�"鐗╂祦椤圭洰杩涘睍 | 鍥哄畾鍚嶅崟杩囨护锛屽競宸炲皬鑺�+椤圭洰琛� |
| /api/report/export/investBillion | 浜垮厓鎶曡祫椤圭洰 | 鎶曡祫鎬昏〃 + 瑙勪笂椤圭洰琛ㄤ袱sheet锛屼嚎鍏冨悕鍗曡繃婊� |
| /api/report/export/investCounty | 缁忔祹寮哄幙鎶曡祫缁熻 | 缁忔祹寮哄幙鍚嶅崟杩囨护 |
+
+
+## 涔濄�佷笓椤圭粺璁�-閲嶈鑺傚亣鏃ユā鍧�
+
+> 鏁版嵁鏉ユ簮涓庢ā鏉匡細`docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/2026骞村崄涓�鏁版嵁.xlsx`锛涘揩鎶ユā鏉匡細`docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/婀栧寳鐪佸浗搴嗗亣鏈熺患鍚堣繍杈撳娴佸揩鎶ユā鏉�.docx`銆傞娆″惎鍔ㄤ細浠庡伐浣滅翱鍒濆鍖� 2024/2025 骞村崄涓�瀹㈡祦銆佽溅娴佹暟鎹紝骞跺垵濮嬪寲 2024-2026 骞村杩愮郴鏁般��
+
+### holiday_calendar 鑺傚亣鏃ユ棩鏈熼厤缃�
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| id | bigint | PK |
+| year | int | 骞翠唤 |
+| holiday_type | varchar(32) | 鏄ヨ妭 SPRING_FESTIVAL / 浜斾竴 LABOR_DAY / 鍗佷竴 NATIONAL_DAY |
+| holiday_date | date | 鑺傚亣鏃ュ叿浣撴棩鏈� |
+| day_index | int | 绗嚑鏃ワ紝浠� 1 寮�濮� |
+| created_at / updated_at | datetime | 鍒涘缓/鏇存柊鏃堕棿 |
+
+鍞竴閿細`(year, holiday_type, day_index)`銆乣(year, holiday_type, holiday_date)`銆備粎 admin 鍙�氳繃绯荤粺閰嶇疆鑿滃崟缁存姢銆�
+
+### holiday_passenger_flow 鑺傚亣鏃ョ患鍚堣繍杈撹惀涓氭�у娴�
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| id | bigint | PK |
+| year | int | 骞翠唤 |
+| flow_date | date | 鏃ユ湡 |
+| transport_mode | varchar(32) | 閾佽矾 RAIL / 姘戣埅 CIVIL_AVIATION / 閬撹矾 ROAD / 姘磋矾 WATER |
+| passenger_count | decimal(30,15) | 鍙戦�佹梾瀹㈡暟锛堜竾浜烘锛� |
+| created_at / updated_at | datetime | 鍒涘缓/鏇存柊鏃堕棿 |
+
+鍞竴閿細`(year, flow_date, transport_mode)`銆�
+
+### holiday_vehicle_flow 鑺傚亣鏃ョ患鍚堣繍杈撻潪钀ヤ笟鎬ц溅娴�
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| id | bigint | PK |
+| year | int | 骞翠唤 |
+| flow_date | date | 鏃ユ湡 |
+| 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) | 鏅�氬浗鐪侀亾鐜瘮鍩烘湡锛堜竾杈嗘锛屼粎褰撳勾浣跨敤锛� |
+ | created_at / updated_at | datetime | 鍒涘缓/鏇存柊鏃堕棿 |
+
+鍞竴閿細`(year, flow_date, road_category, vehicle_type)`銆�
+
+### holiday_passenger_coefficient 瀹㈣繍绯绘暟閰嶇疆
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| id | bigint | PK |
+| year | int | 骞翠唤 |
+| date_category | varchar(32) | 鑺傚亣鏃� HOLIDAY / 闈炶妭鍋囨棩 NON_HOLIDAY |
+| coefficient | decimal(30,15) | 瀹㈣繍绯绘暟 |
+| created_at / updated_at | datetime | 鍒涘缓/鏇存柊鏃堕棿 |
+
+鍞竴閿細`(year, date_category)`銆備粎 admin 鍙�氳繃绯荤粺閰嶇疆鑿滃崟缁存姢銆�
+
+### 璁$畻涓庡鍏ュ鍑哄彛寰�
+ - 闈炶惀涓氭�т汉鍛樻祦鍔ㄩ噺 =锛堥珮閫熻溅娴� + 鏅�氬浗鐪侀亾杞︽祦锛壝� 褰撳勾鑺傚亣鏃ュ杩愮郴鏁般��
+ - 2026 骞存櫘閫氬浗鐪侀亾濡傚~鍐欑幆姣斿熀鏈燂紝鍒欏彧褰卞搷鏅�氬浗鐪侀亾杞︽祦閲忚嚜韬幆姣旓細濉啓鏃舵寜鈥滃綋鏃� / 鐜瘮鍩烘湡 - 1鈥濓紝鏈~鍐欐椂鎸夆�滃綋鏃� / 鍓嶄竴鏃� - 1鈥濓紱鎶樼畻浜哄憳娴佸姩閲忋�佹�讳汉鍛樻祦鍔ㄩ噺绛夌户缁寜 Excel 鍘熷叕寮忚绠椼��2024銆�2025 鍘嗗彶鏁版嵁涓嶄娇鐢ㄨ瀛楁銆�
+ - 鍏矾浜哄憳娴佸姩閲� = 閬撹矾钀ヤ笟鎬у杩愰噺 + 闈炶惀涓氭�т汉鍛樻祦鍔ㄩ噺銆�
+- 鎬讳汉鍛樻祦鍔ㄩ噺 = 鍏矾 + 閾佽矾 + 姘戣埅 + 姘磋矾銆�
+- 瀵煎叆鎸夆�滃勾浠� + 鑺傚亣鏃ュ垎绫烩�濊鍙� `holiday_calendar` 鐨勬棩鏈燂紝灏嗗伐浣滅翱鈥滅N鏃モ�濊瑕嗙洊鍐欏叆褰撳勾瀹㈡祦/杞︽祦锛涙棩甯稿彧璇诲彇鎵�閫夊勾浠界殑褰撳勾鍒楋紝鍘嗗彶骞翠唤浠呴娆″垵濮嬪寲銆�
+- 瀵煎叆鍓嶆鏌ユ瘮鏈鏈�鏂板鍏ユ棩鏇存棭鐨勫巻鍙叉棩鏈熸槸鍚﹀彂鐢熷彉鍖栵紱濡傛湁鍙樺寲鍏堜笉鍐欏簱锛岃繑鍥炲樊寮傛竻鍗曪紝鐢ㄦ埛纭鍚庢墠鍏佽瑕嗙洊銆�
+- 瀵煎嚭浠ユ簮宸ヤ綔绨夸负搴曠锛屽洖濉簱鍐呮暟鎹笌瀹㈣繍绯绘暟锛涘揩鎶ユ寜鏃ユ湡鐢熸垚 Word 骞舵浛鎹㈡ā鏉夸腑鐨勫綋鏃ャ�佺疮璁°�佺幆姣斿拰鍚屾瘮鍐呭銆�
diff --git a/docs/init.sql b/docs/init.sql
index 7edbe73..dcc9932 100644
--- a/docs/init.sql
+++ b/docs/init.sql
@@ -619,3 +619,69 @@
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_type_period (report_type, report_period)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鎶ヨ〃鏈熷鏍搁�氳繃鏍囪';
+
+
+-- ============================================
+-- 鍗佷竴銆佷笓椤圭粺璁�-閲嶈鑺傚亣鏃�
+-- ============================================
+
+-- 鑺傚亣鏃ユ棩鏈熼厤缃�
+DROP TABLE IF EXISTS holiday_calendar;
+CREATE TABLE holiday_calendar (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL COMMENT '骞翠唤',
+ holiday_type VARCHAR(32) NOT NULL COMMENT '鑺傚亣鏃ュ垎绫伙細SPRING_FESTIVAL/LABOR_DAY/NATIONAL_DAY',
+ holiday_date DATE NOT NULL COMMENT '鑺傚亣鏃ュ叿浣撴棩鏈�',
+ day_index INT NOT NULL COMMENT '绗嚑鏃ワ紝浠� 1 寮�濮�',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_holiday_calendar_seq (year, holiday_type, day_index),
+ UNIQUE KEY uk_holiday_calendar_date (year, holiday_type, holiday_date),
+ KEY idx_holiday_calendar_date (holiday_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='閲嶈鑺傚亣鏃ユ棩鏈熼厤缃�';
+
+-- 鑺傚亣鏃ョ患鍚堣繍杈撹惀涓氭�у娴�
+DROP TABLE IF EXISTS holiday_passenger_flow;
+CREATE TABLE holiday_passenger_flow (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL COMMENT '骞翠唤',
+ flow_date DATE NOT NULL COMMENT '鏃ユ湡',
+ transport_mode VARCHAR(32) NOT NULL COMMENT '杩愯緭鏂瑰紡锛歊AIL/CIVIL_AVIATION/ROAD/WATER',
+ passenger_count DECIMAL(30,15) NOT NULL COMMENT '鍙戦�佹梾瀹㈡暟锛堜竾浜烘锛�',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_holiday_passenger (year, flow_date, transport_mode),
+ KEY idx_holiday_passenger_date (flow_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鑺傚亣鏃ョ患鍚堣繍杈撹惀涓氭�у娴佹儏鍐�';
+
+-- 鑺傚亣鏃ョ患鍚堣繍杈撻潪钀ヤ笟鎬ц溅娴�
+DROP TABLE IF EXISTS holiday_vehicle_flow;
+CREATE TABLE holiday_vehicle_flow (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL COMMENT '骞翠唤',
+ flow_date DATE NOT NULL COMMENT '鏃ユ湡',
+ road_category VARCHAR(32) NOT NULL COMMENT '閬撹矾鍒嗙被锛欵XPRESSWAY/NATIONAL_PROVINCIAL',
+ vehicle_type VARCHAR(32) NOT NULL COMMENT '杞﹀瀷鍒嗙被锛歋MALL_PASSENGER/MEDIUM_SMALL_PASSENGER',
+ vehicle_count DECIMAL(30,15) NOT NULL COMMENT '杞︽祦閲忥紙涓囪締娆★級',
+ comparison_base DECIMAL(30,15) NULL COMMENT '鏅�氬浗鐪侀亾鐜瘮鍩烘湡锛堜竾杈嗘锛屼粎褰撳勾浣跨敤锛�',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_holiday_vehicle (year, flow_date, road_category, vehicle_type),
+ KEY idx_holiday_vehicle_date (flow_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鑺傚亣鏃ョ患鍚堣繍杈撻潪钀ヤ笟鎬ц溅娴佹儏鍐�';
+
+-- 瀹㈣繍绯绘暟閰嶇疆
+DROP TABLE IF EXISTS holiday_passenger_coefficient;
+CREATE TABLE holiday_passenger_coefficient (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL COMMENT '骞翠唤',
+ date_category VARCHAR(32) NOT NULL COMMENT '鏃ユ湡鍒嗙被锛欻OLIDAY/NON_HOLIDAY',
+ coefficient DECIMAL(30,15) NOT NULL COMMENT '瀹㈣繍绯绘暟',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_holiday_coefficient (year, date_category)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='瀹㈣繍绯绘暟閰嶇疆';
diff --git a/docs/sql_holiday.sql b/docs/sql_holiday.sql
new file mode 100644
index 0000000..ca39066
--- /dev/null
+++ b/docs/sql_holiday.sql
@@ -0,0 +1,53 @@
+-- Holiday statistics module. Run against the traffic_audit database.
+CREATE TABLE IF NOT EXISTS holiday_calendar (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL,
+ holiday_type VARCHAR(32) NOT NULL,
+ holiday_date DATE NOT NULL,
+ day_index INT NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_holiday_calendar_seq (year, holiday_type, day_index),
+ UNIQUE KEY uk_holiday_calendar_date (year, holiday_type, holiday_date),
+ KEY idx_holiday_calendar_date (holiday_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS holiday_passenger_flow (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL,
+ flow_date DATE NOT NULL,
+ transport_mode VARCHAR(32) NOT NULL,
+ passenger_count DECIMAL(30,15) NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_holiday_passenger (year, flow_date, transport_mode),
+ KEY idx_holiday_passenger_date (flow_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS holiday_vehicle_flow (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL,
+ flow_date DATE NOT NULL,
+ road_category VARCHAR(32) NOT NULL,
+ vehicle_type VARCHAR(32) NOT NULL,
+ vehicle_count DECIMAL(30,15) NOT NULL,
+ comparison_base DECIMAL(30,15) NULL COMMENT '鏅�氬浗鐪侀亾鐜瘮鍩烘湡锛堜竾杈嗘锛屼粎褰撳勾浣跨敤锛�',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_holiday_vehicle (year, flow_date, road_category, vehicle_type),
+ KEY idx_holiday_vehicle_date (flow_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE IF NOT EXISTS holiday_passenger_coefficient (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL,
+ date_category VARCHAR(32) NOT NULL,
+ coefficient DECIMAL(30,15) NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_holiday_coefficient (year, date_category)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/docs/sql_holiday_comparison_base_migration.sql b/docs/sql_holiday_comparison_base_migration.sql
new file mode 100644
index 0000000..6c73353
--- /dev/null
+++ b/docs/sql_holiday_comparison_base_migration.sql
@@ -0,0 +1,6 @@
+-- Add the optional ordinary national/provincial road comparison base.
+-- The field is used only for the 2026 holiday data.
+ALTER TABLE holiday_vehicle_flow
+ ADD COLUMN comparison_base DECIMAL(30,15) NULL
+ COMMENT '鏅�氬浗鐪侀亾鐜瘮鍩烘湡锛堜竾杈嗘锛屼粎褰撳勾浣跨敤锛�'
+ AFTER vehicle_count;
diff --git a/docs/sql_holiday_decimal_migration.sql b/docs/sql_holiday_decimal_migration.sql
new file mode 100644
index 0000000..6235c17
--- /dev/null
+++ b/docs/sql_holiday_decimal_migration.sql
@@ -0,0 +1,16 @@
+-- Convert holiday statistics numeric columns from binary floating point to exact decimal storage.
+-- Run once against the traffic_audit database.
+ALTER TABLE holiday_passenger_flow
+ MODIFY COLUMN passenger_count DECIMAL(30,15) NOT NULL COMMENT '鍙戦�佹梾瀹㈡暟锛堜竾浜烘锛�';
+
+ALTER TABLE holiday_vehicle_flow
+ MODIFY COLUMN vehicle_count DECIMAL(30,15) NOT NULL COMMENT '杞︽祦閲忥紙涓囪締娆★級';
+
+ALTER TABLE holiday_passenger_coefficient
+ MODIFY COLUMN coefficient DECIMAL(30,15) NOT NULL COMMENT '瀹㈣繍绯绘暟';
+
+-- The seeded coefficient was averaged from repeated 2.4 cells through a Java double.
+-- Normalize the migrated value to the exact Excel decimal.
+UPDATE holiday_passenger_coefficient
+SET coefficient = 2.4
+WHERE year IN (2024, 2025, 2026);
diff --git "a/docs/\344\270\223\351\241\271\347\273\237\350\256\241/\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245/2026\345\271\264\345\215\201\344\270\200\346\225\260\346\215\256.xlsx" "b/docs/\344\270\223\351\241\271\347\273\237\350\256\241/\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245/2026\345\271\264\345\215\201\344\270\200\346\225\260\346\215\256.xlsx"
new file mode 100644
index 0000000..cc214e3
--- /dev/null
+++ "b/docs/\344\270\223\351\241\271\347\273\237\350\256\241/\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245/2026\345\271\264\345\215\201\344\270\200\346\225\260\346\215\256.xlsx"
Binary files differ
diff --git "a/docs/\344\270\223\351\241\271\347\273\237\350\256\241/\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245/\345\244\207\344\273\275/2026\345\271\264\345\215\201\344\270\200\346\225\260\346\215\256.xlsx" "b/docs/\344\270\223\351\241\271\347\273\237\350\256\241/\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245/\345\244\207\344\273\275/2026\345\271\264\345\215\201\344\270\200\346\225\260\346\215\256.xlsx"
new file mode 100644
index 0000000..d75764f
--- /dev/null
+++ "b/docs/\344\270\223\351\241\271\347\273\237\350\256\241/\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245/\345\244\207\344\273\275/2026\345\271\264\345\215\201\344\270\200\346\225\260\346\215\256.xlsx"
Binary files differ
diff --git "a/docs/\344\270\223\351\241\271\347\273\237\350\256\241/\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245/\345\244\207\344\273\275/\346\271\226\345\214\227\347\234\201\345\233\275\345\272\206\345\201\207\346\234\237\347\273\274\345\220\210\350\277\220\350\276\223\345\256\242\346\265\201\345\277\253\346\212\245\346\250\241\346\235\277.docx" "b/docs/\344\270\223\351\241\271\347\273\237\350\256\241/\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245/\345\244\207\344\273\275/\346\271\226\345\214\227\347\234\201\345\233\275\345\272\206\345\201\207\346\234\237\347\273\274\345\220\210\350\277\220\350\276\223\345\256\242\346\265\201\345\277\253\346\212\245\346\250\241\346\235\277.docx"
new file mode 100644
index 0000000..26794b6
--- /dev/null
+++ "b/docs/\344\270\223\351\241\271\347\273\237\350\256\241/\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245/\345\244\207\344\273\275/\346\271\226\345\214\227\347\234\201\345\233\275\345\272\206\345\201\207\346\234\237\347\273\274\345\220\210\350\277\220\350\276\223\345\256\242\346\265\201\345\277\253\346\212\245\346\250\241\346\235\277.docx"
Binary files differ
diff --git "a/docs/\344\270\223\351\241\271\347\273\237\350\256\241/\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245/\346\271\226\345\214\227\347\234\201\345\233\275\345\272\206\345\201\207\346\234\237\347\273\274\345\220\210\350\277\220\350\276\223\345\256\242\346\265\201\345\277\253\346\212\245\346\250\241\346\235\277.docx" "b/docs/\344\270\223\351\241\271\347\273\237\350\256\241/\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245/\346\271\226\345\214\227\347\234\201\345\233\275\345\272\206\345\201\207\346\234\237\347\273\274\345\220\210\350\277\220\350\276\223\345\256\242\346\265\201\345\277\253\346\212\245\346\250\241\346\235\277.docx"
new file mode 100644
index 0000000..26794b6
--- /dev/null
+++ "b/docs/\344\270\223\351\241\271\347\273\237\350\256\241/\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245/\346\271\226\345\214\227\347\234\201\345\233\275\345\272\206\345\201\207\346\234\237\347\273\274\345\220\210\350\277\220\350\276\223\345\256\242\346\265\201\345\277\253\346\212\245\346\250\241\346\235\277.docx"
Binary files differ
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_\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245\344\270\223\351\241\271\347\273\237\350\256\241_2026-10-02.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_\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245\344\270\223\351\241\271\347\273\237\350\256\241_2026-10-02.md"
new file mode 100644
index 0000000..e5b76c5
--- /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_\351\207\215\350\246\201\350\212\202\345\201\207\346\227\245\344\270\223\351\241\271\347\273\237\350\256\241_2026-10-02.md"
@@ -0,0 +1,155 @@
+# 鍔熻兘娴嬭瘯鎶ュ憡_閲嶈鑺傚亣鏃ヤ笓椤圭粺璁2026-10-02
+
+## 涓�銆佸姛鑳借鏄�
+
+鏂板鈥滀笓椤圭粺璁�-閲嶈鑺傚亣鏃モ�濇ā鍧楋紝瑕嗙洊浠ヤ笅鑳藉姏锛�
+
+1. 绯荤粺閰嶇疆鑿滃崟鏂板鈥滆妭鍋囨棩绠$悊鈥濆拰鈥滃杩愮郴鏁扮鐞嗏�濓紝浠� admin 鍙淮鎶ゃ��
+2. 鑺傚亣鏃ョ鐞嗗瓧娈碉細骞翠唤銆佽妭鍋囨棩鍒嗙被锛堟槬鑺�/浜斾竴/鍗佷竴锛夈�佹棩鏈熴�佺鍑犳棩銆�
+3. 瀹㈣繍绯绘暟绠$悊瀛楁锛氬勾浠姐�佹棩鏈熷垎绫伙紙鑺傚亣鏃�/闈炶妭鍋囨棩锛夈�佸杩愮郴鏁般��
+4. 涓撻」缁熻-鑺傚亣鏃ヨ彍鍗曟敮鎸佹寜骞翠唤銆佽妭鍋囨棩鍒嗙被鏌ョ湅瀹㈡祦涓庤溅娴佺患鍚堟暟鎹��
+5. 鏀寔瀵煎叆銆�2026骞村崄涓�鏁版嵁.xlsx銆嬫ā鏉匡紝鎸夋墍閫夊勾浠借鍙栧綋骞村垪锛�2026 骞翠负瀹㈡祦 C 鍒椼�佹櫘閫氬浗鐪侀亾 D 鍒楋級锛屾寜鈥滅鍑犳棩鈥濆啓鍏ュ綋骞村娴佸拰杞︽祦锛�2024/2025 鍘嗗彶鏁版嵁浠呴娆″垵濮嬪寲锛屼笉瑕佹眰鏃ュ父閲嶅瀵煎叆銆�
+6. 瀵煎叆鏃跺厛妫�鏌モ�滄瘮鏈鏈�鏂板鍏ユ棩鏇存棭鈥濈殑鍘嗗彶鏃ユ湡鏄惁琚敼鍔紱濡傛湁鍙樺寲锛屽厛涓嶅啓搴撳苟鍒楀嚭宸紓锛岀敱鐢ㄦ埛閫夋嫨鏄惁鍏佽瑕嗙洊銆�
+7. 鏀寔瀵煎嚭 Excel锛屾ā鏉夸负銆�2026骞村崄涓�鏁版嵁.xlsx銆嬨��
+8. 鏀寔涓嬭浇銆婃箹鍖楃渷鍥藉簡鍋囨湡缁煎悎杩愯緭瀹㈡祦蹇姤锛�10鏈圶鏃ワ級.docx銆嬶紝榛樿鏃ユ湡涓虹郴缁熸棩鏈熷墠涓�澶╋紝涔熷厑璁哥敤鎴疯嚜閫夋棩鏈燂紱鐜瘮銆佸悓姣旀寜鈥滃闀�/涓嬮檷/鎸佸钩鈥濊緭鍑恒��
+9. 搴旂敤棣栨鍚姩鏃讹紝浠庢ā鏉垮伐浣滅翱鍒濆鍖� 2024/2025 骞村崄涓�瀹㈡祦銆佽溅娴侊紝浠ュ強 2024-2026 骞村杩愮郴鏁般��
+
+## 浜屻�佹祴璇曠幆澧�
+
+| 椤圭洰 | 鍐呭 |
+|------|------|
+| 宸ヤ綔鐩綍 | `D:\Codex\MyProject\trafficAudit` |
+| 鍚庣 | Spring Boot锛宍http://localhost:8090` |
+| 鍓嶇 | Vue 2 + Element UI锛宍http://localhost:8080` |
+| 鏁版嵁搴� | MySQL `127.0.0.1:3305/traffic_audit` |
+| 婧愬伐浣滅翱 | `docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/2026骞村崄涓�鏁版嵁.xlsx` |
+| 蹇姤妯℃澘 | `docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/婀栧寳鐪佸浗搴嗗亣鏈熺患鍚堣繍杈撳娴佸揩鎶ユā鏉�.docx` |
+| 缂栬瘧 | `mvn -q -DskipTests compile -f traffic-audit-server/pom.xml` 閫氳繃 |
+| 鍓嶇鏋勫缓 | `npm run build` 閫氳繃锛屼粎鏈夊師椤圭洰 bundle 浣撶Н warning |
+
+## 涓夈�佺敤渚嬭〃
+
+| 缂栧彿 | 绫诲瀷 | 杈撳叆/鎿嶄綔 | 棰勬湡 | 瀹為檯 | 缁撹 |
+|------|------|-----------|------|------|------|
+| T01 | 鍒濆鍖� | 鍚姩鍚庣 | 鑷姩寤�/鍒濆鍖� 4 寮犺妭鍋囨棩琛� | 琛ㄥ瓨鍦紝鍒濆鍖栧畬鎴� | 閫氳繃 |
+| T02 | 鍒濆鍖� | 鏌ヨ鑺傚亣鏃ラ厤缃� | 2024/2026 骞� 7 澶╋紝2025 骞� 8 澶� | 2024 骞� 7 澶╋紝2025 骞� 8 澶╋紝2026 骞� 7 澶� | 閫氳繃 |
+| T03 | 鍒濆鍖� | 鏌ヨ瀹㈡祦銆佽溅娴� | 2024/2025 鏁版嵁浠庢簮宸ヤ綔绨胯鍙� | 瀹㈡祦 28/32 琛岋紱杞︽祦 14/16 琛� | 閫氳繃 |
+| T04 | 鍒濆鍖� | 鏌ヨ瀹㈣繍绯绘暟 | 2024-2026 骞寸郴鏁版潵鑷簮宸ヤ綔绨� | 姣忓勾 HOLIDAY/NON_HOLIDAY 鍧� 2.4锛屼笌婧愯〃涓�鑷� | 閫氳繃 |
+| T05 | 鍏ュ簱鎶芥煡 | 婧愬伐浣滅翱 vs 鏁版嵁搴撻�愰」姣斿 | 鏃犲樊寮� | 90 涓崟鍏冩牸姣斿锛屽樊寮� 0 | 閫氳繃 |
+| T06 | 鏉冮檺 | 鏈櫥褰曡闂妭鍋囨棩鎺ュ彛 | 鎷︽埅 | HTTP 401锛屾彁绀烘湭鐧诲綍 | 閫氳繃 |
+| T07 | 鏉冮檺 | 闈� admin 淇濆瓨鑺傚亣鏃� | 鎷掔粷 | code=403锛屾彁绀轰粎 admin 鍙鐞� | 閫氳繃 |
+| T08 | 鏉冮檺 | 闈� admin 淇濆瓨瀹㈣繍绯绘暟 | 鎷掔粷 | code=403锛屾彁绀轰粎 admin 鍙鐞� | 閫氳繃 |
+| T09 | 姝e悜 | admin 鏂板/缂栬緫/鍒犻櫎鑺傚亣鏃� 2030 娴嬭瘯璁板綍 | 鎴愬姛涓旀棤鑴忔暟鎹� | 鏂板銆佺紪杈戞垚鍔燂紱閲嶅閰嶇疆杩斿洖 400锛涘垹闄ゆ垚鍔� | 閫氳繃 |
+| T10 | 姝e悜 | admin 鏂板/缂栬緫/鍒犻櫎瀹㈣繍绯绘暟 2030 娴嬭瘯璁板綍 | 鎴愬姛涓旀棤鑴忔暟鎹� | 鏂板銆佺紪杈戞垚鍔燂紱闈炴硶绯绘暟杩斿洖 400锛涘垹闄ゆ垚鍔� | 閫氳繃 |
+| T11 | 姝e悜 | 瀵煎叆婧愬伐浣滅翱 2025 骞村崄涓�鏁版嵁 | 瑕嗙洊鍐欏叆 | 瀹㈡祦 32 鏉°�佽溅娴� 16 鏉★紝澶辫触 0 | 閫氳繃 |
+| T12 | 姝e悜 | 瀵煎叆涓存椂 2026 娴嬭瘯宸ヤ綔绨匡紙鍚屽�奸獙璇佹寔骞筹級 | 鍐欏叆骞剁敓鎴愬揩鎶� | 瀹㈡祦 8 鏉°�佽溅娴� 4 鏉★紝澶辫触 0锛涙祴璇曟暟鎹凡娓呯悊 | 閫氳繃 |
+| T13 | 璐熷悜 | 閫夋嫨鏈厤缃勾浠藉鍏� | 鎻愮ず鍏堢淮鎶よ妭鍋囨棩 | code=400锛屾彁绀衡�滆鍏堝湪绯荤粺閰嶇疆涓淮鎶よ妭鍋囨棩鈥� | 閫氳繃 |
+| T14 | 璐熷悜 | 瀵煎叆闈� Excel 鏂囦欢 | 瑙f瀽澶辫触骞舵彁绀� | code=400锛屾彁绀� unsupported file type | 閫氳繃 |
+| T15 | 璐熷悜 | 涓嶄紶鏂囦欢瀵煎叆 | 涓氬姟鎻愮ず锛屼笉杩斿洖 500 | code=400锛屾彁绀衡�滆閫夋嫨瑕佸鍏ョ殑 Excel 鏂囦欢鈥� | 閫氳繃 |
+| T16 | 瀵煎嚭 | 瀵煎嚭 2025 骞村崄涓� Excel | 甯﹀簱鍐呮暟鎹紝鍏紡淇濈暀 | 鏂囦欢鍙 openpyxl 鎵撳紑锛�2025 瀹㈡祦/杞︽祦鏁版嵁姝g‘ | 閫氳繃 |
+| T17 | 蹇姤 | 涓嬭浇 2025-10-01 蹇姤 | 棣栨棩姝f枃涓嶈緭鍑衡�滅幆姣斺�︹�︹�濓紱琛ㄦ牸鐜瘮鍒椾负鈥溾�斺�濓紝姝f枃/琛ㄦ牸瀹屾暣 | 姝f枃涓衡��10鏈�1鏃ワ紙鍋囨湡绗�1澶╋級鈥︹�︿竾浜烘锛屾瘮2025骞村悓鏈熷闀�6.8%銆傗�濓紝鏃犫�滅幆姣斺�濈煭璇紱琛ㄦ牸鐜瘮鍒椾负鈥溾�斺�� | 閫氳繃 |
+| T18 | 蹇姤 | 涓嬭浇 2025-10-02 蹇姤 | 鐜瘮涓轰笅闄嶅苟杈撳嚭缁濆鍊� | 杈撳嚭鈥滅幆姣斾笅闄�11.5%鈥� | 閫氳繃 |
+| T19 | 蹇姤 | 涓嬭浇 2025-10-03 蹇姤 | 鐜瘮涓烘骞惰緭鍑虹粷瀵瑰�� | 杈撳嚭鈥滅幆姣斿闀�2.4%鈥� | 閫氳繃 |
+| T20 | 蹇姤 | 涓存椂鏋勯�� 2026 绗� 1/2 鏃ョ浉鍚屾暟鎹� | 鐜瘮涓� 0 鏃惰緭鍑衡�滄寔骞斥�� | 杈撳嚭鈥滅幆姣旀寔骞斥�� | 閫氳繃 |
+| T21 | 骞跺彂/绋冲畾 | 閲嶅惎鍚庤繛缁敓鎴愬揩鎶� | 涓嶄覆鏂囨。 | 涓や唤涓嶅悓鏃ユ湡蹇姤鍐呭姝g‘ | 閫氳繃 |
+| T22 | 鍓嶇 | admin 鎵撳紑鈥滀笓椤圭粺璁�-鑺傚亣鏃モ�� | 椤甸潰娓叉煋銆佹暟鎹〃鏄剧ず | 鏍囬銆佽彍鍗曘�佽〃鏍兼甯� | 閫氳繃 |
+| T23 | 鍓嶇 | admin 鎵撳紑鈥滅郴缁熼厤缃�� | 涓や釜 Tab 姝e父 | 鑺傚亣鏃ョ鐞嗐�佸杩愮郴鏁扮鐞嗘甯� | 閫氳繃 |
+| T24 | 鍓嶇 | 闈� admin 璁块棶绯荤粺閰嶇疆 | 鑿滃崟闅愯棌涓旂洿璁胯閲嶅畾鍚� | 鑿滃崟涓嶅彲瑙侊紝鐩磋鍥炲伐浣滃彴 | 閫氳繃 |
+| T25 | 鍓嶇 | 鐐瑰嚮鈥滀笅杞芥ā鏉库�� | HTTP 200锛屾枃浠跺彲涓嬭浇 | 娴忚鍣ㄥ疄闄呰姹傝繑鍥� 200 | 閫氳繃 |
+| T26 | 鎺у埗鍙� | 椤甸潰鐑熸祴鏀堕泦 JS 閿欒 | 鏃犻敊璇� | 鏃� pageerror/console error | 閫氳繃 |
+| T27 | 鍘嗗彶妫�鏌� | 宸叉湁 2026 绗�1鏃ユ暟鎹悗锛屽鍏ョ1鏃ヨ淇敼涓旂2鏃ユ甯哥殑鏂囦欢锛堜笉纭锛� | 涓嶅啓搴擄紝杩斿洖鍘嗗彶鍙樺寲 | `confirmationRequired=true`锛屽垪鍑� 6 鏉″樊寮傦紱搴撳唴绗�1鏃ヤ粛涓哄師鍊� | 閫氳繃 |
+| T28 | 鍘嗗彶妫�鏌� | 鍚屼笂鏂囦欢锛岀敤鎴风‘璁ゅ厑璁稿鍏� | 瑕嗙洊鍘嗗彶鏃ユ暟鎹� | 杩斿洖鎴愬姛 12 鏉★紱搴撳唴绗�1鏃ョ敱 100/200/300 鏇存柊涓� 110/210/310 | 閫氳繃 |
+| T29 | 鍓嶇纭 | 椤甸潰瀵煎叆鍚巻鍙插彉鍖栫殑鏂囦欢 | 寮圭獥鏄剧ず宸紓鍜屸�滃厑璁稿鍏�/涓嶅厑璁糕�� | 寮圭獥鍑虹幇涓斿寘鍚師鍊�/瀵煎叆鍊硷紱鐐瑰嚮鍏佽鍚庡鍏ユ垚鍔� | 閫氳繃 |
+| T30 | 榛樿鏃ユ湡 | 鎵撳紑蹇姤鏃ユ湡閫夋嫨 | 榛樿绯荤粺鏃ユ湡鍓嶄竴澶╋紝鍙墜宸ヤ慨鏀� | 椤甸潰榛樿鍙� 2026-10-01锛屾棩鏈熸帶浠跺彲閫夋嫨 | 閫氳繃 |
+| T31 | 鑿滃崟椤哄簭 | 鏌ョ湅宸︿晶鑿滃崟 | 涓撻」缁熻浣嶄簬鎶ヨ〃鐢熸垚涔嬪悗 | 椤哄簭涓� 鎶ヨ〃鐢熸垚 -> 涓撻」缁熻 -> 瑙勫垯绠$悊 -> 鐢ㄦ埛绠$悊 -> 绯荤粺閰嶇疆 | 閫氳繃 |
+| T32 | 绮惧害 | Excel 瀹㈣繍绯绘暟 2.4 鍏ュ簱鍚庢煡璇� | 涓嶅嚭鐜� 2.3999999999999995 | 搴撳唴 2024/2025/2026 骞寸郴鏁板潎涓� 2.4锛屾帴鍙h繑鍥� coefficient=2.4 | 閫氳繃 |
+| T33 | 绮惧害 | 绯荤粺閰嶇疆-瀹㈣繍绯绘暟绠$悊鍒楄〃 | 鏄剧ず 2.4 | 娴忚鍣ㄥ疄娴嬩袱琛屽潎鏄剧ず 2.4 | 閫氳繃 |
+| T34 | 绮惧害 | 鑺傚亣鏃ョ粺璁¢〉瀹㈣繍绯绘暟鍒� | 鏄剧ず 2.4 | overview 鎺ュ彛涓庨〉闈㈠潎鏄剧ず 2.4 | 閫氳繃 |
+| T35 | 绮惧害 | 妫�鏌ヨ妭鍋囨棩鏁板�煎垪绫诲瀷 | decimal(30,15) | passenger_count/vehicle_count/coefficient/comparison_base 鍧囦负 decimal(30,15) | 閫氳繃 |
+| T36 | 瀵煎叆鍙e緞 | 瀵煎叆鍚� 2024/2025 鍘嗗彶鍒椾笌瀹㈣繍绯绘暟鍒楃殑 2026 宸ヤ綔绨� | 鍙 2026 褰撳勾鍒楋紝涓嶅啓 2024/2025锛屼笉璇荤郴鏁� | 鍐欏叆 12 鏉★紙瀹㈡祦 8 + 杞︽祦 4锛夛紝2024/2025 鍒椾笌绯绘暟鍒楁湭鍏ュ簱 | 閫氳繃 |
+| T37 | 鍘嗗彶妫�鏌ュ洖褰� | 宸叉湁 2026 绗�1/2 鏃ユ暟鎹悗锛屽鍏ョ1鏃ヨ鏀瑰姩鐨勬枃浠讹紙涓嶇‘璁わ級 | 涓嶅啓搴撳苟鍒楀嚭宸紓 | confirmationRequired=true锛屽垪鍑� 6 鏉″樊寮傦紝搴撳唴绗�1鏃ヤ粛涓� 100/100/100/100/200/300 | 閫氳繃 |
+| T38 | 鍘嗗彶妫�鏌ュ洖褰� | 鍚屼笂鏂囦欢锛岀敤鎴风‘璁ゅ厑璁稿鍏� | 瑕嗙洊鍘嗗彶鏃ユ暟鎹� | 杩斿洖鎴愬姛 12 鏉★紝搴撳唴绗�1鏃ユ洿鏂颁负 110/110/110/110/210/310锛岀2鏃ヤ笉鍙� | 閫氳繃 |
+| T39 | 椤甸潰琛ㄥご | 鑺傚亣鏃ユ暟鎹竴瑙堣〃澶村姞鍗曚綅 | 鍒楀ご甯﹀崟浣� | 瀹炴祴锛氶搧璺�/姘戣埅/閬撹矾/姘磋矾锛堜竾浜烘锛夛紝楂橀�熻溅娴�/鍥界渷閬撹溅娴侊紙涓囪締娆★級锛岄潪钀ヤ笟鎬т汉鍛樻祦鍔ㄩ噺/鎬讳汉鍛樻祦鍔ㄩ噺锛堜竾浜烘锛夛紝瀹㈣繍绯绘暟鏃犲崟浣� | 閫氳繃 |
+| T40 | 閰嶇疆鍥炲綊 | admin 鍒犻櫎 2026-10-08 鍚庨噸鍚悗绔� | 鍒犻櫎缁撴灉淇濇寔锛屼笉鍐嶈嚜鍔ㄨˉ鍥� | 閲嶅惎鍚庡簱鍐呫�佹帴鍙e拰椤甸潰鍧囦负 2026-10-01 鑷� 10-07 鍏� 7 澶� | 閫氳繃 |
+| T41 | 鐜瘮鍩烘湡瀵煎叆 | 2026 骞存櫘閫氬浗鐪侀亾 C 鍒楀~ 320銆丏 鍒楀~ 360 | C 鍒椾綔涓虹幆姣斿熀鏈熶繚瀛橈紝D 鍒椾负褰撴棩杞︽祦 | 搴撳唴 comparison_base=320锛寁ehicle_count=360锛涙帴鍙e潎杩斿洖 | 閫氳繃 |
+| T42 | 鐜瘮鍩烘湡瀵煎嚭 | 瀵煎嚭宸插鍏ュ熀鏈熺殑 2026 骞存暟鎹� | 鏅�氬浗鐪侀亾 C 鍒楀洖濉熀鏈燂紝D 鍒楀洖濉綋鏃ヨ溅娴� | 瀵煎嚭鏂囦欢 C4=320銆丏4=360 | 閫氳繃 |
+| T43 | 鍥界渷閬撹溅娴佺幆姣� | 2026-10-02 鍥界渷閬撳綋鏃�360銆佸熀鏈�320 | 浠呭浗鐪侀亾鐩存帴鐜瘮浣跨敤鍩烘湡 | 鍥界渷閬撹溅娴佺幆姣斾负12.50%锛涘揩鎶ユ�讳汉鍛樻祦鍔ㄩ噺浠嶄负17.00%锛屼笉鍙楀熀鏈熷奖鍝� | 閫氳繃 |
+| T44 | 鏄犲皠瀵煎嚭 | 鏈夊熀鏈熸椂瀵煎嚭鏅�氬浗鐪侀亾 E 鍒楀叕寮� | E 浣跨敤褰撴棩涓庣幆姣斿熀鏈熻绠� | 瀵煎嚭 E4 鍏紡涓� `=(D4-C4)/C4` | 閫氳繃 |
+| T45 | 鍘嗗彶鍩烘湡鍙樻洿 | 宸叉湁 2026 绗�1鏃ュ熀鏈熷悗锛屽鍏ユ柊鐨勭1鏃ュ熀鏈熷�� | 涓嶇‘璁ゆ椂闆跺啓鍏ュ苟鍒楀嚭宸紓 | confirmationRequired=true锛屾彁绀哄師鍊�300/瀵煎叆鍊�310锛屽簱鍐呬粛涓�300 | 閫氳繃 |
+| T46 | 鍘嗗彶骞翠唤闅旂 | 瀵煎嚭 2024/2025 骞存暟鎹� | 鍘嗗彶骞翠唤涓嶄娇鐢ㄧ幆姣斿熀鏈� | 涓や唤瀵煎嚭鏂囦欢鏅�氬浗鐪侀亾 C3:C10 鍧囦负绌� | 閫氳繃 |
+| T47 | 蹇姤棣栨棩鍙e緞 | 涓嬭浇 2025-10-01 蹇姤骞舵鏌ユ鏂� | 绗�1澶╀笉鐢熸垚鈥滅幆姣斿闀�/涓嬮檷/鎸佸钩鈥濈煭璇� | 姝f枃涓衡��10鏈�1鏃ワ紙鍋囨湡绗�1澶╋級锛屽叏鐪佸叏绀句細璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺1714.41涓囦汉娆★紝姣�2025骞村悓鏈熷闀�6.8%銆傗�濓紱绗簩澶╀粛杈撳嚭鈥滅幆姣斾笅闄�11.5%鈥� | 閫氳繃 |
+| T48 | 蹇姤鐧惧垎姣旂簿搴� | 涓嬭浇 2025-10-01/02 蹇姤妫�鏌ユ鏂囧拰琛ㄦ牸 | 鍚屾瘮/鐜瘮鐧惧垎姣斾繚鐣�1浣嶅皬鏁帮紝浜烘暟/杞︽祦淇濈暀2浣嶅皬鏁� | 6.79%鏄剧ず涓�6.8%锛�11.46%鏄剧ず涓�11.5%锛涗汉鏁�1714.41銆�1518.02浠嶄负2浣嶅皬鏁� | 閫氳繃 |
+
+## 鍥涖�佸彂鐜扮殑闂涓庡缃�
+
+1. 妯℃澘涓嬭浇鏁存璺緞浣跨敤 `encodeURIComponent`锛屾妸 `/` 缂栫爜涓� `%2F`锛孴omcat 杩斿洖 400銆�
+ - 澶勭疆锛氬墠绔敼涓烘寜璺緞娈电紪鐮侊紝淇濈暀鏂滄潬锛屽娴嬭繑鍥� 200銆�
+2. 瀵煎叆鍘熷厛娌℃湁鍘嗗彶鏁版嵁鍙樻洿纭锛屽彲鑳介潤榛樿鐩栧凡鍙戝竷鍘嗗彶鏃ャ��
+ - 澶勭疆锛氭柊澧炰袱闃舵瀵煎叆锛涙湭纭鏃堕浂鍐欏叆锛岀‘璁ゅ悗鎵嶅厑璁歌鐩栧巻鍙叉棩銆�
+3. 蹇姤鐢熸垚鏈嶅姟鍘熷厛鎶婂綋鍓� DOM 鏂囨。鏀惧湪瀹炰緥瀛楁锛屽瓨鍦ㄥ苟鍙戜覆鎵伴闄┿��
+ - 澶勭疆锛氭敼涓� `ThreadLocal` 淇濆瓨褰撳墠鏂囨。锛屽苟鍦ㄨ姹傜粨鏉熸椂娓呯悊锛涢噸鍚悗澶嶆祴杩炵画鐢熸垚閫氳繃銆�
+4. 瀵煎叆鎺ュ彛涓嶄紶鏂囦欢鏃剁敱妗嗘灦鎶涘嚭 500銆�
+5. 鑺傚亣鏃ュ垵濮嬪寲姣忔鍚姩閮戒細琛ラ綈缂哄け鐨勭 N 鏃ワ紝瀵艰嚧 admin 鍒犻櫎 2026-10-08 鍚庯紝閲嶅惎鍙堣鑷姩鎻掑洖銆�
+ - 澶勭疆锛氭枃浠跺弬鏁版敼涓哄彲閫夛紝缁熶竴杩斿洖涓氬姟 400 鎻愮ず銆�
+ - 澶勭疆锛歚seedNationalDayCalendar()` 鏀逛负浠呭湪 `holiday_calendar` 鏁磋〃涓虹┖鏃舵墽琛岄娆″垵濮嬪寲锛涢潪绌洪厤缃畬鍏ㄧ敱 admin 缁存姢銆傚娴嬮噸鍚悗 2026 骞村崄涓�淇濇寔 7 澶┿��
+
+## 浜斻�佹暟鎹竻鐞�
+
+- 娴嬭瘯涓骇鐢熺殑 2026 瀹㈡祦/杞︽祦涓存椂鏁版嵁宸插垹闄ゃ��
+- 娴嬭瘯浜х敓鐨� `import_batch` 鑺傚亣鏃ュ鍏ヨ褰曞凡鍒犻櫎銆�
+- 2024/2025 鍒濆鍖栨暟鎹拰 2024-2026 瀹㈣繍绯绘暟淇濈暀锛岀鍚堥渶姹傘��
+
+## 鍏�佺粨璁�
+
+閲嶈鑺傚亣鏃ヤ笓椤圭粺璁℃ā鍧楃殑姝e悜娴佺▼銆佹潈闄愭帶鍒躲�佹暟鎹垵濮嬪寲銆佸鍏ュ鍑恒�佸揩鎶ョ敓鎴愬拰鍓嶇椤甸潰鐑熸祴鍧囬�氳繃锛涙簮宸ヤ綔绨� 90 涓暟鎹崟鍏冩牸鍏ュ簱鎶芥煡鏃犲樊寮傘�傚彂鐜扮殑闂宸蹭慨澶嶅苟瀹屾垚鍥炲綊锛屽綋鍓嶅悗绔� 8090銆佸墠绔� 8080 姝e父杩愯銆�
+
+## 涓冦�佽ˉ鍏呭洖褰掞紙2026-10-02锛孍xcel 娴偣绮惧害涓庤彍鍗曢『搴忥級
+
+鑳屾櫙锛欵xcel 鍐呴儴鎸� IEEE 754 鍙岀簿搴︽诞鐐瑰瓨鍌ㄦ暟鍊硷紙鏈夋晥 15 浣嶏級锛屽簱鍐呭杩愮郴鏁板師涓� 2.3999999999999995锛岄〉闈㈡樉绀哄紓甯搞�傚弬鑰� `docs/Excel娴偣鏁板瓨鍌�.txt` 鍏ㄩ潰鏁存敼銆�
+
+淇敼鍐呭锛�
+
+1. 鏂板鏁板�煎伐鍏� `HolidayNumberUtils`锛欵xcel 鍗曞厓鏍兼暟鍊肩粺涓�鐢� `NumberToTextConverter.toText(double)` 杞枃鏈啀鍏� `BigDecimal`锛岃绠椾娇鐢� `MathContext(15, HALF_UP)`锛屼笌 Excel 15 浣嶆湁鏁堟暟瀛椾繚鎸佷竴鑷淬��
+2. 鑺傚亣鏃ユā鍧楀疄浣撱�佹湇鍔°�佸鍏ュ鍑恒�佸揩鎶ョ粺涓�鐢� `Double` 鏀逛负 `BigDecimal`銆�
+3. 涓夊紶琛ㄦ暟鍊煎垪鏀逛负 `DECIMAL(30,15)`锛歚holiday_passenger_flow.passenger_count`銆乣holiday_vehicle_flow.vehicle_count`銆乣holiday_passenger_coefficient.coefficient`锛涜縼绉昏剼鏈� `docs/sql_holiday_decimal_migration.sql`銆�
+4. 搴撳唴 6 鏉″杩愮郴鏁帮紙2024-2026 骞淬�佽妭鍋囨棩/闈炶妭鍋囨棩锛夌敱 2.3999999999999995 鏍℃涓� 2.4銆�
+5. 鈥滀笓椤圭粺璁♀�濊彍鍗曠Щ鍔ㄥ埌鈥滄姤琛ㄧ敓鎴愨�濅箣鍚庯紱鍓嶇瀹㈣繍绯绘暟灞曠ず缁熶竴鍘婚櫎娴偣灏惧樊銆�
+
+楠岃瘉缁撴灉锛�
+
+- `mvn -q -DskipTests compile` 閫氳繃锛沗npm run build` 閫氳繃锛堜粎鍘熸湁 bundle 浣撶Н璀﹀憡锛夈��
+- 鑿滃崟椤哄簭瀹炴祴锛氭姤琛ㄧ敓鎴� -> 涓撻」缁熻 -> 瑙勫垯绠$悊 -> 鐢ㄦ埛绠$悊 -> 绯荤粺閰嶇疆銆�
+- 绯荤粺閰嶇疆-瀹㈣繍绯绘暟绠$悊鍒楄〃娴忚鍣ㄥ疄娴嬫樉绀� 2.4锛涙帴鍙� `coefficient=2.4`銆�
+- 涓夊紶琛ㄦ暟鍊煎垪鍧囦负 `decimal(30,15)`銆�
+- 鍘嗗彶鍙樻洿涓ら樁娈靛鍏ュ洖褰掞細涓嶇‘璁ゆ椂闆跺啓鍏ュ苟鍒楀嚭 6 鏉″樊寮傦紝纭鍚庣1鏃ョ敱 100/100/100/100/200/300 瑕嗙洊涓� 110/110/110/110/210/310锛岀2鏃ヤ笉鍙樸��
+- 瀵煎叆鍙e緞澶嶆牳锛氫粎鍐欏叆鎵�閫夊勾浠藉綋骞村垪锛堟湰鐢ㄤ緥 12 鏉★級锛�2024/2025 鍘嗗彶鍒椾笌瀹㈣繍绯绘暟鍒椾笉璇讳笉鍏ュ簱銆�
+- 娴嬭瘯浜х敓鐨� 2026 瀹㈡祦/杞︽祦鍙� `import_batch` 璁板綍宸叉竻鐞嗭紱2024/2025 鍘嗗彶鏁版嵁涓� 2024-2026 瀹㈣繍绯绘暟淇濈暀銆�
+- 椤甸潰琛ㄥご琛ュ崟浣嶏紙娴忚鍣ㄥ疄娴嬶級锛氶搧璺�/姘戣埅/閬撹矾/姘磋矾锛堜竾浜烘锛夈�侀珮閫熻溅娴�/鍥界渷閬撹溅娴侊紙涓囪締娆★級銆侀潪钀ヤ笟鎬т汉鍛樻祦鍔ㄩ噺/鎬讳汉鍛樻祦鍔ㄩ噺锛堜竾浜烘锛夛紱瀹㈣繍绯绘暟淇濈暀鏃犲崟浣嶃��
+
+## 鍏�佽ˉ鍏呭洖褰掞紙2026-10-02锛岃妭鍋囨棩鍒犻櫎鍚庨噸鍚娲伙級
+
+鐜拌薄锛歛dmin 宸插湪绯荤粺閰嶇疆涓垹闄� 2026-10-08锛屼絾鍚庣閲嶅惎鍚庤琛屽啀娆″嚭鐜般��
+
+鏍瑰洜锛歚HolidayReferenceDataInitializer` 鍦ㄦ瘡娆″簲鐢ㄥ惎鍔ㄦ椂璋冪敤 `seedNationalDayCalendar()`锛屽師閫昏緫鎸夊浐瀹氬ぉ鏁伴亶鍘嗗苟琛ラ綈鎵�鏈夌己澶辩殑绗� N 鏃ワ紝鍥犳鎶� admin 宸插垹闄ょ殑鏃ユ湡閲嶆柊鎻掑叆銆�
+
+淇涓庨獙璇侊細
+
+- `HolidayWorkbookService.seedNationalDayCalendar()` 澧炲姞闈炵┖淇濇姢锛氳妭鍋囨棩琛ㄥ彧瑕佸凡鏈変换涓�閰嶇疆锛屽氨涓嶅啀杩涜鑷姩琛ョ銆�
+- 鍚庣缂栬瘧閫氳繃锛岄噸鍚悗鏁版嵁搴� 2026 骞村崄涓�涓� `2026-10-01` 鑷� `2026-10-07` 鍏� 7 澶┿��
+- 鎺ュ彛楠岃瘉锛氳妭鍋囨棩閰嶇疆 7 鏉★紝缁熻涓�瑙� 7 琛岋紝鏈�鍚庝竴鏃� `dayIndex=7`銆乣date=2026-10-07`銆�
+
+## 涔濄�佽ˉ鍏呭洖褰掞紙2026-10-02锛屾櫘閫氬浗鐪侀亾鐜瘮鍩烘湡锛�
+
+鑳屾櫙锛氭櫘閫氬浗鐪侀亾 `C` 鍒椾负鈥滅幆姣斿熀鏈熲�濄�傚綋鏅�氬浗鐪侀亾娴侀噺瓒嬪娍涓庨珮閫熷叕璺笉涓�鑷存椂锛屼互楂橀�熶负鍑嗭紝鎵嬪伐濉啓鍩烘湡锛岀幆姣旀寜 `褰撴棩 / 鐜瘮鍩烘湡 - 1`锛涙湭濉啓鏃朵粛鎸� `褰撴棩 / 鍓嶄竴鏃� - 1`銆�
+
+瀹炵幇鍙e緞锛�
+
+- `holiday_vehicle_flow` 鏂板鍙┖瀛楁 `comparison_base DECIMAL(30,15)`锛岃縼绉昏剼鏈� `docs/sql_holiday_comparison_base_migration.sql`銆�
+- 浠� 2026 骞村惎鐢細2024銆�2025 鍘嗗彶鏁版嵁涓嶅鍏ャ�佷笉瀵煎嚭銆佷笉鍙備笌鍩烘湡璁$畻銆�
+- 2026 骞存櫘閫氬浗鐪侀亾 C 鍒楀鍏ヤ负 `comparison_base`锛孌 鍒楀鍏ヤ负 `vehicle_count`锛涘鍑烘椂 C/D 鍒楀垎鍒洖濉��
+- 鍩烘湡鍙奖鍝嶆櫘閫氬浗鐪侀亾杞︽祦閲忚嚜韬殑鐜瘮锛氭湁鍩烘湡鏃� `褰撴棩 / 鍩烘湡 - 1`锛屾棤鍩烘湡鏃� `褰撴棩 / 鍓嶄竴鏃� - 1`銆�
+- 鎶樼畻浜哄憳娴佸姩閲忋�佸叕璺汉鍛樻祦鍔ㄩ噺銆侀潪钀ヤ笟鎬т汉鍛樻祦鍔ㄩ噺銆佸叏绀句細浜哄憳娴佸姩閲忓拰 Word 蹇姤缁х画鎸� Excel 鍘熸湁鍏紡璁$畻锛屼笉鍙楀熀鏈熷奖鍝嶃��
+- 瀵煎嚭鏃跺鏌愭棩濉啓鍩烘湡锛屾櫘閫氬浗鐪侀亾 E 鍒楀叕寮忓啓涓� `=(D-C)/C`銆�
+- 鍩烘湡鏀瑰彉鍚屾牱绾冲叆鍘嗗彶鏃ユ湡鍙樻洿妫�鏌ワ紝鐢ㄦ埛纭鍓嶄笉鍐欏簱銆�
+- 蹇姤绗�1澶╂病鏈夊墠涓�鏃ュ彲姣斿熀鏁帮紝姝f枃涓嶈緭鍑衡�滅幆姣斿闀�/涓嬮檷/鎸佸钩鈥濈煭璇紱琛ㄦ牸褰撴棩鐜瘮鍒椾负鈥溾�斺�濄�傜2澶╄捣浠嶆寜瀹為檯鍓嶄竴鏃ヨ绠楀苟杈撳嚭鐜瘮銆�
+- 蹇姤涓殑鍚屾瘮/鐜瘮鐧惧垎姣旂粺涓�淇濈暀1浣嶅皬鏁帮紙鍥涜垗浜斿叆锛夛紱浜烘銆佽溅娴佺瓑涓氬姟鏁板�间粛淇濈暀2浣嶅皬鏁般��
diff --git "a/docs/\345\267\245\344\275\234\346\227\245\347\273\223/\345\267\245\344\275\234\346\227\245\347\273\223_2026-10-02.md" "b/docs/\345\267\245\344\275\234\346\227\245\347\273\223/\345\267\245\344\275\234\346\227\245\347\273\223_2026-10-02.md"
new file mode 100644
index 0000000..7a5316f
--- /dev/null
+++ "b/docs/\345\267\245\344\275\234\346\227\245\347\273\223/\345\267\245\344\275\234\346\227\245\347\273\223_2026-10-02.md"
@@ -0,0 +1,73 @@
+# 宸ヤ綔鏃ョ粨_2026-10-02
+
+## 涓�銆佷粖鏃ュ畬鎴�
+
+1. 鏂板鈥滀笓椤圭粺璁�-閲嶈鑺傚亣鏃モ�濆畬鏁存ā鍧椼��
+ - 鍚庣鏂板 4 寮犺〃锛氳妭鍋囨棩閰嶇疆銆佽惀涓氭�у娴併�侀潪钀ヤ笟鎬ц溅娴併�佸杩愮郴鏁般��
+ - 鍚庣鏂板鑺傚亣鏃ラ厤缃�佺粺璁°�佸伐浣滅翱瀵煎叆瀵煎嚭銆乄ord 蹇姤鏈嶅姟銆�
+ - 鍓嶇鏂板鈥滀笓椤圭粺璁�-鑺傚亣鏃モ�濆拰鈥滅郴缁熼厤缃�濊彍鍗�/椤甸潰銆�
+2. 鏉冮檺鎺у埗宸插疄鐜般��
+ - 鑺傚亣鏃ョ鐞嗐�佸杩愮郴鏁扮鐞嗕粎 admin 鍙淮鎶わ紝闈� admin 杩斿洖 403锛岃彍鍗曢殣钘忎笖鐩磋閲嶅畾鍚戙��
+3. 妯℃澘鍒濆鍖栧凡瀹炵幇銆�
+ - 浠庛��2026骞村崄涓�鏁版嵁.xlsx銆嬪垵濮嬪寲 2024/2025 骞村崄涓�瀹㈡祦鍜岃溅娴併��
+ - 浠庡悓涓�宸ヤ綔绨垮垵濮嬪寲 2024-2026 骞村杩愮郴鏁般��
+4. 瀵煎叆瀵煎嚭宸插疄鐜般��
+ - 瀵煎叆浠呰鍙栨墍閫夊勾浠界殑褰撳勾鍒楋紙2026 骞翠负瀹㈡祦 C 鍒椼�佹櫘閫氬浗鐪侀亾 D 鍒楋級锛屽巻鍙� 2024/2025 鏁版嵁鍙仛鍒濆鍖栵紝涓嶈姹傛棩甯搁噸瀵笺��
+ - 瀵煎叆鎸夆�滃勾浠� + 鑺傚亣鏃ュ垎绫烩�濆尮閰嶁�滅N鏃モ�濓紝瑕嗙洊鍐欏叆瀹㈡祦涓庤溅娴併��
+ - 瀵煎叆鍓嶆鏌ユ洿鏃╃殑鍘嗗彶鏃ユ湡鏄惁琚敼鍔紱鏈夊彉鍖栨椂鍏堜笉鍐欏簱骞跺垪鍑哄樊寮傦紝鐢ㄦ埛纭鍚庢墠鍏佽瑕嗙洊銆�
+ - 瀵煎嚭浠ユ簮宸ヤ綔绨夸负妯℃澘锛屽洖濉暟鎹簱鏁版嵁骞朵繚鐣欏叕寮忋��
+5. 蹇姤宸插疄鐜般��
+ - 涓嬭浇鏂囦欢鍚嶏細`婀栧寳鐪佸浗搴嗗亣鏈熺患鍚堣繍杈撳娴佸揩鎶ワ紙10鏈圶鏃ワ級.docx`銆�
+ - 鐜瘮/鍚屾瘮鏂囨鎸夋鏁板闀裤�佽礋鏁颁笅闄嶃��0 鎸佸钩杈撳嚭锛岀櫨鍒嗘瘮淇濈暀1浣嶅皬鏁般��
+6. 鏂囨。宸插悓姝ャ��
+ - 鏇存柊 `docs/sql_holiday.sql`銆乣docs/init.sql`銆乣docs/database.md`銆乣docs/鎵撳寘閮ㄧ讲璇存槑.md`銆�
+7. Excel 娴偣绮惧害鏁存敼锛�2026-10-02 杩藉姞锛夈��
+8. 鑺傚亣鏃ュ垹闄ゅ悗閲嶅惎澶嶆椿淇锛�2026-10-02 杩藉姞锛夈��
+9. 鏅�氬浗鐪侀亾鐜瘮鍩烘湡锛�2026-10-02 杩藉姞锛夈��
+ - 鏅�氬浗鐪侀亾 C 鍒楃‘璁や负鈥滅幆姣斿熀鏈熲�濓細濉啓鍚庢寜鈥滃綋鏃�/鐜瘮鍩烘湡-1鈥濊绠楃幆姣旓紝鏈~鍐欐椂浠嶆寜鈥滃綋鏃�/鍓嶄竴鏃�-1鈥濄��
+ - `holiday_vehicle_flow` 鏂板 `comparison_base` 瀛楁锛涗粎 2026 骞村鍏ャ�佸鍑哄拰鍙備笌璁$畻锛�2024/2025 鍘嗗彶鏁版嵁涓嶄娇鐢ㄣ��
+ - 鎸� Excel 鍘熷叕寮忓彛寰勶紝鍩烘湡鍙奖鍝嶆櫘閫氬浗鐪侀亾杞︽祦閲忚嚜韬幆姣旓紱鎶樼畻浜哄憳娴佸姩閲忋�佸叕璺汉鍛樻祦鍔ㄩ噺銆佹�讳汉鍛樻祦鍔ㄩ噺鍜� Word 蹇姤鍧囦笉鍙楀奖鍝嶃��
+ - 鍩烘湡鍙樺寲浠嶇撼鍏ュ巻鍙叉棩鏈熷彉鏇寸‘璁わ紱瀵煎嚭鏃跺搴旀棩 E 鍒楀叕寮忔敼涓� `(D-C)/C`銆�
+ - 蹇姤绗�1澶╂鏂囦笉杈撳嚭鈥滅幆姣斿闀�/涓嬮檷/鎸佸钩鈥濈煭璇紝琛ㄦ牸鐜瘮鍒椾负鈥溾�斺�濓紱绗�2澶╄捣淇濈暀鐜瘮銆�
+ - 蹇姤鍚屾瘮/鐜瘮鐧惧垎姣旀敼涓轰繚鐣�1浣嶅皬鏁帮紱浜烘銆佽溅娴佺瓑鏁板�间粛淇濈暀2浣嶅皬鏁般��
+ - 鍘熼�昏緫姣忔鍚姩閮戒細琛ラ綈缂哄け鐨勭 N 鏃ワ紝瀵艰嚧 admin 鍒犻櫎 2026-10-08 鍚庨噸鍚閲嶆柊鎻掑叆銆�
+ - 淇涓轰粎褰� `holiday_calendar` 鏁磋〃涓虹┖鏃舵墽琛岄娆″垵濮嬪寲锛涘娴嬮噸鍚悗 2026 骞村崄涓�淇濇寔 7 澶┿��
+ - 鏂板 `HolidayNumberUtils`锛孍xcel 鏁板�肩粺涓�鐢� `NumberToTextConverter.toText(double)` 杞� `BigDecimal`锛岃绠楃敤 `MathContext(15, HALF_UP)`銆�
+ - 鑺傚亣鏃ユā鍧楀疄浣�/鏈嶅姟/瀵煎叆瀵煎嚭/蹇姤鐢� `Double` 鏀逛负 `BigDecimal`锛涗笁寮犺〃鏁板�煎垪鏀逛负 `DECIMAL(30,15)`锛涘簱鍐� 6 鏉″杩愮郴鏁扮敱 2.3999999999999995 鏍℃涓� 2.4銆�
+ - 鈥滀笓椤圭粺璁♀�濊彍鍗曠Щ鍒扳�滄姤琛ㄧ敓鎴愨�濅箣鍚庯紱鍓嶇绯绘暟灞曠ず鍘婚櫎娴偣灏惧樊銆�
+
+## 浜屻�佹祴璇曠粨鏋滄憳瑕�
+
+- 鍚庣缂栬瘧閫氳繃锛屽墠绔敓浜ф瀯寤洪�氳繃銆�
+- 婧愬伐浣滅翱涓庢暟鎹簱鎶芥煡锛�90 涓崟鍏冩牸锛屽樊寮� 0銆�
+- 鏉冮檺銆佹鍚� CRUD銆佽礋鍚戝鍏ャ�佸巻鍙叉暟鎹彉鏇寸‘璁ゃ�丒xcel 瀵煎嚭銆乄ord 蹇姤銆侀〉闈㈢儫娴嬪叏閮ㄩ�氳繃銆�
+- 鍘嗗彶鍙樻洿绔埌绔疄娴嬶細鏈‘璁ゆ椂鏁版嵁涓嶈惤搴擄紝纭鍚庡巻鍙叉棩鍊兼寜瀵煎叆鏂囦欢鏇存柊锛涙祴璇曟暟鎹凡娓呯悊銆�
+- 鏃犲ご娴忚鍣ㄧ儫娴嬶細admin 椤甸潰姝e父锛岄潪 admin 绯荤粺閰嶇疆闅愯棌骞堕噸瀹氬悜锛屾ā鏉夸笅杞� HTTP 200锛屾棤 JS 閿欒銆�
+- 绮惧害鍥炲綊锛氱郴缁熼厤缃�-瀹㈣繍绯绘暟鍒楄〃涓庤妭鍋囨棩缁熻椤靛潎鏄剧ず 2.4锛屾帴鍙� `coefficient=2.4`锛屼笁寮犺〃鏁板�煎垪涓� `decimal(30,15)`銆�
+- 鑿滃崟鍥炲綊锛氫晶杈硅彍鍗曢『搴忎负 鎶ヨ〃鐢熸垚 -> 涓撻」缁熻 -> 瑙勫垯绠$悊 -> 鐢ㄦ埛绠$悊 -> 绯荤粺閰嶇疆銆�
+- 鍘嗗彶鍙樻洿涓ら樁娈靛鍏ュ洖褰掞細涓嶇‘璁ゆ椂闆跺啓鍏ュ苟鍒� 6 鏉″樊寮傦紝纭鍚庣1鏃ヨ鐩栦负 110/110/110/110/210/310锛岀2鏃ヤ笉鍙橈紱瀵煎叆鍙e緞浠呰褰撳勾鍒椼��
+- 椤甸潰琛ㄥご琛ュ崟浣嶏細鑺傚亣鏃ユ暟鎹竴瑙堝垪澶村凡甯﹀崟浣嶏紙涓囦汉娆�/涓囪締娆★級锛屾祻瑙堝櫒瀹炴祴閫氳繃銆�
+- 鍒犻櫎澶嶆椿鍥炲綊锛歛dmin 鍒犻櫎 2026-10-08 鍚庨噸鍚紝鏁版嵁搴� 7 澶┿�佺粺璁℃帴鍙� 7 琛岋紝鏈�鍚庝竴鏃ヤ负 `2026-10-07`锛屾湭鍐嶈嚜鍔ㄨˉ鍥炪��
+- 鐜瘮鍩烘湡鍥炲綊锛氬鍏� C=320銆丏=360 鍚庡簱鍐呭拰鎺ュ彛鍧囨纭紱鍥界渷閬撹溅娴佺幆姣斾负 12.50%锛屽鍑� E 鍒楀叕寮忎负 `(D-C)/C`锛涘揩鎶ユ�讳汉鍛樻祦鍔ㄩ噺浠嶄负 17.00%銆佹湭鍙楀熀鏈熷奖鍝嶏紱鍘嗗彶鍩烘湡鍙樺寲杩斿洖纭鎻愮ず涓旀湭鍐欏簱锛�2024/2025 瀵煎嚭 C 鍒椾繚鎸佷负绌恒��
+- 蹇姤棣栨棩鍥炲綊锛�2025-10-01 姝f枃鏃犫�滅幆姣斺�濈煭璇紝浠呬繚鐣欏悓姣旓紱2025-10-02 浠嶈緭鍑衡�滅幆姣斾笅闄�11.5%鈥濄��
+- 蹇姤鐧惧垎姣旂簿搴﹀洖褰掞細姝f枃鍜岃〃鏍煎悓姣�/鐜瘮鍧囦负1浣嶅皬鏁帮紝濡�6.79%鏄剧ず6.8%銆�11.46%鏄剧ず11.5%锛涗汉鏁�1714.41/1518.02浠嶄繚鐣�2浣嶅皬鏁般��
+- 娴嬭瘯鎶ュ憡锛歚docs/鍔熻兘娴嬭瘯鎶ュ憡/鍔熻兘娴嬭瘯鎶ュ憡_閲嶈鑺傚亣鏃ヤ笓椤圭粺璁2026-10-02.md`銆�
+
+## 涓夈�侀仐鐣欓棶棰樹笌寰呭姙
+
+1. 鐪熷疄 2026 骞村崄涓�鏁版嵁灏氭湭瀵煎叆锛涢〉闈㈤粯璁ゅ揩鎶ユ棩鏈熶负绯荤粺鏃ユ湡鍓嶄竴澶╋紝褰撳墠绯荤粺鏃ユ湡 2026-10-02 鏃堕粯璁ゅ彇 2026-10-01锛岀敤鎴蜂篃鍙墜宸ラ�夋嫨鏃ユ湡銆�
+2. 鏄ヨ妭銆佷簲涓�鐩墠鎻愪緵浜嗛厤缃垎绫诲拰閫氱敤琛ㄧ粨鏋勶紝浣嗘湭鎻愪緵鍒濆鍖栨暟鎹紝闇�瑕佸悗缁寜瀵瑰簲骞村害缁存姢銆�
+3. 蹇姤妯℃澘鍥哄畾涓哄浗搴嗙増鏈紝鍏朵粬鑺傚亣鏃ュ闇�蹇姤闇�鍙︽彁渚涙ā鏉裤��
+4. 姝e紡鍙戝竷鍓嶄粛闇�纭楠岃瘉鐮佸紑鍏崇瓑涓婄嚎閰嶇疆銆�
+
+## 鍥涖�佹槑鏃ヨ鍒�
+
+1. 鏍规嵁鐢ㄦ埛鎻愪緵鐨� 2026 骞村崄涓�姣忔棩鏁版嵁锛屾寚瀵兼垨鎵ц瀵煎叆骞舵牳瀵圭敓鎴愬揩鎶ャ��
+2. 濡傜敤鎴锋彁渚涙槬鑺傘�佷簲涓�鏃ユ湡鍜屾ā鏉匡紝缁х画鎵╁睍瀵瑰簲鍒濆鍖栦笌蹇姤妯℃澘閫傞厤銆�
+3. 鏍规嵁璇曠敤鍙嶉寰皟椤甸潰灞曠ず鍜屽鍑烘牸寮忋��
+
+## 浜斻�佽繍琛岀姸鎬�
+
+- 鍚庣锛�8090 宸插惎鍔紝鏃ュ織鍑虹幇 `Started TrafficAuditApplication`銆�
+- 鍓嶇锛�8080 宸插惎鍔紝璁块棶 `http://localhost:8080`銆�
+- 鏁版嵁搴擄細`127.0.0.1:3305/traffic_audit`銆�
diff --git "a/docs/\346\211\223\345\214\205\351\203\250\347\275\262\350\257\264\346\230\216.md" "b/docs/\346\211\223\345\214\205\351\203\250\347\275\262\350\257\264\346\230\216.md"
index 5d98f70..6d56bda 100644
--- "a/docs/\346\211\223\345\214\205\351\203\250\347\275\262\350\257\264\346\230\216.md"
+++ "b/docs/\346\211\223\345\214\205\351\203\250\347\275\262\350\257\264\346\230\216.md"
@@ -63,7 +63,9 @@
鈹� 鈹溾攢 init.sql 寤哄簱寤鸿〃 + 棰勭疆璐﹀彿锛堝繀椤绘墽琛岋級
鈹� 鈹溾攢 sql_city_bus.sql 鍩庡競瀹㈣繍-鍏氦鏈堝害琛紙蹇呴』鎵ц锛�
鈹� 鈹溾攢 sql_city_taxi.sql 鍩庡競瀹㈣繍-鍑虹鏈堝害琛�/鎺堟潈琛紙蹇呴』鎵ц锛�
-鈹� 鈹斺攢 sql_wyc.sql 缃戠害杞﹀叏鐪佹�婚噺/璁㈠崟鍒嗗競宸炶〃锛堝繀椤绘墽琛岋級
+鈹� 鈹溾攢 sql_wyc.sql 缃戠害杞﹀叏鐪佹�婚噺/璁㈠崟鍒嗗競宸炶〃锛堝繀椤绘墽琛岋級
+鈹� 鈹溾攢 sql_holiday.sql 涓撻」缁熻-閲嶈鑺傚亣鏃ヨ〃锛堝繀椤绘墽琛岋級
+鈹� 鈹斺攢 sql_holiday_comparison_base_migration.sql 鑺傚亣鏃ユ棫搴撳崌绾э細鏅�氬浗鐪侀亾鐜瘮鍩烘湡瀛楁
鈹斺攢 README-閮ㄧ讲璇存槑.txt
```
@@ -71,20 +73,22 @@
### 5.1 鍑嗗鏁版嵁搴擄紙涓ゆ潯璺嚎浜岄�変竴锛�**涓嶈娣风潃鍋�**锛�
-#### 璺嚎 A锛氬叏鏂扮┖搴擄紙瑕佽窇 4 涓剼鏈級
+#### 璺嚎 A锛氬叏鏂扮┖搴擄紙瑕佽窇 5 涓剼鏈級
-`init.sql` 鑷甫 `CREATE DATABASE` 涓� `USE traffic_audit`锛屽彲鐙珛鎵ц锛涘彟澶� 3 涓剼鏈病鏈� `USE` 璇彞锛屽繀椤荤敤 `-D traffic_audit`锛堟垨 `mysql ... traffic_audit < 鏂囦欢`锛夋寚瀹氬簱锛�
+`init.sql` 鑷甫 `CREATE DATABASE` 涓� `USE traffic_audit`锛屽彲鐙珛鎵ц锛涘彟澶� 4 涓剼鏈病鏈� `USE` 璇彞锛屽繀椤荤敤 `-D traffic_audit`锛堟垨 `mysql ... traffic_audit < 鏂囦欢`锛夋寚瀹氬簱锛�
```bash
cd db
-mysql --default-character-set=utf8mb4 -u root -p < init.sql # 寤哄簱 + 24 寮犲熀纭�琛� + 棰勭疆璐﹀彿
+mysql --default-character-set=utf8mb4 -u root -p < init.sql # 寤哄簱 + 28 寮犲熀纭�琛� + 棰勭疆璐﹀彿
mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_city_bus.sql
mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_city_taxi.sql
mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_wyc.sql
+mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_holiday.sql
```
> 鈿狅笍 **`init.sql` 鏄牬鍧忔�х殑**锛氶噷闈㈢殑鍩虹琛ㄩ兘甯� `DROP TABLE IF EXISTS`銆�**宸叉湁鏁版嵁鐨勫簱涓婂崈涓囦笉瑕佸啀璺� init.sql**锛屽惁鍒欒〃鏁版嵁浼氳娓呯┖閲嶅缓銆�
-> 4 涓剼鏈叡瑕嗙洊 29 寮犺〃锛坕nit.sql 24 寮� + 鍩庡競瀹㈣繍 3 寮� + 缃戠害杞� 2 寮狅級銆�
+> 宸查儴缃茶繃鏃╂湡鑺傚亣鏃ユā鍧楃殑鐜版湁搴擄紝鍗囩骇鍓嶅厛鎵ц `sql_holiday_comparison_base_migration.sql` 娣诲姞 `comparison_base` 瀛楁锛涘叏鏂板簱鐩存帴浣跨敤宸叉洿鏂板悗鐨� `sql_holiday.sql`銆�
+> 5 涓剼鏈叡瑕嗙洊 33 寮犺〃锛坕nit.sql 28 寮� + 鍩庡競瀹㈣繍 3 寮� + 缃戠害杞� 2 寮狅級銆�
#### 璺嚎 B锛氭妸鐜扮綉/寮�鍙戞満鐨勬暟鎹暣濂楁惉杩囧幓锛堟帹鑽愶紝涓嶇敤璺戣剼鏈級
@@ -105,11 +109,11 @@
mysql --default-character-set=utf8mb4 -u root -p < traffic_audit_full_YYYYMMDD.sql
```
-- `--databases` 浼氭妸 `CREATE DATABASE` + `USE` 涓�璧峰啓杩涘浠斤紝鎵�浠ョ洰鏍囨満鍣�**涓嶇敤鍏堝缓搴撱�佷篃涓嶇敤璺戦偅 4 涓剼鏈�**銆�
+- `--databases` 浼氭妸 `CREATE DATABASE` + `USE` 涓�璧峰啓杩涘浠斤紝鎵�浠ョ洰鏍囨満鍣�**涓嶇敤鍏堝缓搴撱�佷篃涓嶇敤璺戦偅 5 涓剼鏈�**銆�
- 澶囦唤閲屽寘鍚� `sys_user`锛岃处鍙峰瘑鐮佷笌婧愭満鍣ㄥ畬鍏ㄤ竴鑷达紙鏈満鏄� `admin / 123456`锛夛紝涓嶄細琚噸缃垚 `admin/admin123`銆�
- 鈿狅笍 澶囦唤閲屾瘡寮犺〃閮藉甫 `DROP TABLE IF EXISTS`锛堝叡 29 寮狅級锛�**鐩爣搴撹鏄凡鏈夋暟鎹紝浼氳鐩存帴瑕嗙洊**銆傝杩樺師鍒扮┖搴擄紝鎴栧厛鑷繁澶囦唤鐩爣搴撱��
- 鏈宸茬敓鎴愬苟**楠岃瘉杩�**澶囦唤锛歚traffic_audit_full_20260913.sql`锛�45.5 MB锛�8.0.29 瀹㈡埛绔鍑猴級銆�
- 楠岃瘉鏂瑰紡锛氭妸澶囦唤搴撳悕鏀规垚涓存椂搴撳鍏ワ紝閫愯〃姣斿 `SHOW CREATE TABLE`锛堣〃缁撴瀯锛変笌 `COUNT(*)`锛堣鏁帮級鈥斺��**29 寮犺〃缁撴瀯涓庤鏁板潎 0 宸紓**锛屾娊鏌� `wyc_order_monthly` 2026-08 鏁版嵁涓�鑷达紝楠岃瘉鍚庡凡鍒犻櫎涓存椂搴撱��
+ 楠岃瘉鏂瑰紡锛氭妸澶囦唤搴撳悕鏀规垚涓存椂搴撳鍏ワ紝閫愯〃姣斿 `SHOW CREATE TABLE`锛堣〃缁撴瀯锛変笌 `COUNT(*)`锛堣鏁帮級鈥斺��**29 寮犺〃缁撴瀯涓庤鏁板潎 0 宸紓锛堟柊澧炶妭鍋囨棩妯″潡鍚庝负 33 寮狅級**锛屾娊鏌� `wyc_order_monthly` 2026-08 鏁版嵁涓�鑷达紝楠岃瘉鍚庡凡鍒犻櫎涓存椂搴撱��
#### 璺嚎 B2锛氱敤 Navicat 鐨勩�屽浠� / 杩樺師澶囦唤銆嶏紙.nb3锛屾渶璐磋繎鏃ュ父涔犳儻锛�
@@ -204,7 +208,7 @@
| 瀵煎嚭鎶ャ�屾湭鎵惧埌姹囨�昏〃姣嶇増銆奩YYY骞碝鏈堚�︺�嬨�� | `docs/鐢熸垚姹囨�诲ぇ琛╜ 缂鸿鏈堟瘝鐗� |
| 鐧诲綍鎺ュ彛 500 | `jwt.secret` 濉簡闈炴硶瀛楃锛堝涓枃锛夛紱鏀规垚 32 浣嶅崄鍏繘鍒朵覆 |
| 椤甸潰鑳藉紑浣嗘帴鍙� 404 | 璧� nginx 鏂规鏃舵病閰� `/api` 鍙嶄唬锛涙敼鐢� jar 鍐呭祵璁块棶 8090 |
-| 鏁版嵁搴撹繛鎺ュけ璐� | 妫�鏌� `application.yml`锛涚‘璁� 4 涓� SQL 鑴氭湰宸叉墽琛� |
+| 鏁版嵁搴撹繛鎺ュけ璐� | 妫�鏌� `application.yml`锛涚‘璁� 5 涓� SQL 鑴氭湰宸叉墽琛� |
| 绔彛鍗犵敤 | 鏀� `server.port`锛涘墠绔紑鍙戠鍙� 8080 |
## 鍗併�佸紑鍙戠幆澧冩棩甯稿惎鍔�
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/HolidayConstants.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/HolidayConstants.java
new file mode 100644
index 0000000..a3e18cc
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/HolidayConstants.java
@@ -0,0 +1,116 @@
+package com.trafficaudit.holiday;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+public final class HolidayConstants {
+ public static final int COMPARISON_BASE_YEAR = 2026;
+
+ public static final String TYPE_SPRING_FESTIVAL = "SPRING_FESTIVAL";
+
+ public static final String TYPE_LABOR_DAY = "LABOR_DAY";
+ public static final String TYPE_NATIONAL_DAY = "NATIONAL_DAY";
+
+ public static final String CATEGORY_HOLIDAY = "HOLIDAY";
+ public static final String CATEGORY_NON_HOLIDAY = "NON_HOLIDAY";
+
+ public static final String MODE_RAIL = "RAIL";
+ public static final String MODE_CIVIL_AVIATION = "CIVIL_AVIATION";
+ public static final String MODE_ROAD = "ROAD";
+ public static final String MODE_WATER = "WATER";
+
+ public static final String ROAD_EXPRESSWAY = "EXPRESSWAY";
+ public static final String ROAD_NATIONAL_PROVINCIAL = "NATIONAL_PROVINCIAL";
+
+ public static final String VEHICLE_SMALL_PASSENGER = "SMALL_PASSENGER";
+ public static final String VEHICLE_MEDIUM_SMALL_PASSENGER = "MEDIUM_SMALL_PASSENGER";
+
+ public static final Map<String, String> HOLIDAY_TYPE_LABELS;
+ public static final Map<String, String> DATE_CATEGORY_LABELS;
+ public static final Map<String, String> TRANSPORT_MODE_LABELS;
+ public static final Map<String, String> ROAD_CATEGORY_LABELS;
+ public static final Map<String, String> VEHICLE_TYPE_LABELS;
+
+ private static final Set<String> HOLIDAY_TYPES;
+ private static final Set<String> DATE_CATEGORIES;
+ private static final Set<String> TRANSPORT_MODES;
+ private static final Set<String> ROAD_CATEGORIES;
+ private static final Set<String> VEHICLE_TYPES;
+
+ static {
+ Map<String, String> holidayTypes = new LinkedHashMap<>();
+ holidayTypes.put(TYPE_SPRING_FESTIVAL, "鏄ヨ妭");
+ holidayTypes.put(TYPE_LABOR_DAY, "浜斾竴");
+ holidayTypes.put(TYPE_NATIONAL_DAY, "鍗佷竴");
+ HOLIDAY_TYPE_LABELS = Collections.unmodifiableMap(holidayTypes);
+ HOLIDAY_TYPES = new LinkedHashSet<>(holidayTypes.keySet());
+
+ Map<String, String> dateCategories = new LinkedHashMap<>();
+ dateCategories.put(CATEGORY_HOLIDAY, "鑺傚亣鏃�");
+ dateCategories.put(CATEGORY_NON_HOLIDAY, "闈炶妭鍋囨棩");
+ DATE_CATEGORY_LABELS = Collections.unmodifiableMap(dateCategories);
+ DATE_CATEGORIES = new LinkedHashSet<>(dateCategories.keySet());
+
+ Map<String, String> transportModes = new LinkedHashMap<>();
+ transportModes.put(MODE_RAIL, "閾佽矾");
+ transportModes.put(MODE_CIVIL_AVIATION, "姘戣埅");
+ transportModes.put(MODE_ROAD, "閬撹矾");
+ transportModes.put(MODE_WATER, "姘磋矾");
+ TRANSPORT_MODE_LABELS = Collections.unmodifiableMap(transportModes);
+ TRANSPORT_MODES = new LinkedHashSet<>(transportModes.keySet());
+
+ Map<String, String> roadCategories = new LinkedHashMap<>();
+ roadCategories.put(ROAD_EXPRESSWAY, "楂橀�熷叕璺�");
+ roadCategories.put(ROAD_NATIONAL_PROVINCIAL, "鏅�氬浗鐪侀亾");
+ ROAD_CATEGORY_LABELS = Collections.unmodifiableMap(roadCategories);
+ ROAD_CATEGORIES = new LinkedHashSet<>(roadCategories.keySet());
+
+ Map<String, String> vehicleTypes = new LinkedHashMap<>();
+ vehicleTypes.put(VEHICLE_SMALL_PASSENGER, "灏忓杞�");
+ vehicleTypes.put(VEHICLE_MEDIUM_SMALL_PASSENGER, "涓皬瀹㈣溅");
+ VEHICLE_TYPE_LABELS = Collections.unmodifiableMap(vehicleTypes);
+ VEHICLE_TYPES = new LinkedHashSet<>(vehicleTypes.keySet());
+ }
+
+ private HolidayConstants() {
+ }
+
+ public static boolean supportsComparisonBase(int year) {
+ return year == COMPARISON_BASE_YEAR;
+ }
+
+ public static boolean isValidHolidayType(String value) {
+ return HOLIDAY_TYPES.contains(value);
+ }
+
+ public static boolean isValidDateCategory(String value) {
+ return DATE_CATEGORIES.contains(value);
+ }
+
+ public static boolean isValidTransportMode(String value) {
+ return TRANSPORT_MODES.contains(value);
+ }
+
+ public static boolean isValidRoadCategory(String value) {
+ return ROAD_CATEGORIES.contains(value);
+ }
+
+ public static boolean isValidVehicleType(String value) {
+ return VEHICLE_TYPES.contains(value);
+ }
+
+ public static Map<String, Object> optionMap() {
+ Map<String, Object> options = new LinkedHashMap<>();
+ options.put("holidayTypes", HOLIDAY_TYPE_LABELS);
+ options.put("dateCategories", DATE_CATEGORY_LABELS);
+ options.put("transportModes", TRANSPORT_MODE_LABELS);
+ options.put("roadCategories", ROAD_CATEGORY_LABELS);
+ options.put("vehicleTypes", VEHICLE_TYPE_LABELS);
+ options.put("years", Arrays.asList(2024, 2025, 2026));
+ return options;
+ }
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/HolidayNumberUtils.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/HolidayNumberUtils.java
new file mode 100644
index 0000000..fb0ea58
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/HolidayNumberUtils.java
@@ -0,0 +1,70 @@
+package com.trafficaudit.holiday;
+
+import org.apache.poi.ss.util.NumberToTextConverter;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+public final class HolidayNumberUtils {
+
+ public static final MathContext EXCEL_MC = new MathContext(15, RoundingMode.HALF_UP);
+ public static final BigDecimal ZERO = BigDecimal.ZERO;
+
+ private HolidayNumberUtils() {
+ }
+
+ public static BigDecimal excelValue(double value) {
+ return normalize(new BigDecimal(NumberToTextConverter.toText(value)));
+ }
+
+ public static BigDecimal decimalValue(Object value) {
+ if (value == null || value.toString().trim().isEmpty()) return null;
+ return normalize(new BigDecimal(value.toString().trim()));
+ }
+
+ public static BigDecimal normalize(BigDecimal value) {
+ if (value == null) return null;
+ BigDecimal rounded = value.round(EXCEL_MC);
+ return rounded.signum() == 0 ? ZERO : rounded.stripTrailingZeros();
+ }
+
+ public static BigDecimal valueOrZero(BigDecimal value) {
+ return value == null ? ZERO : value;
+ }
+
+ public static BigDecimal sumNullable(BigDecimal left, BigDecimal right) {
+ if (left == null && right == null) return null;
+ return normalize(valueOrZero(left).add(valueOrZero(right)));
+ }
+
+ public static BigDecimal multiply(BigDecimal left, BigDecimal right) {
+ if (left == null || right == null) return null;
+ return normalize(left.multiply(right));
+ }
+
+ public static BigDecimal divide(BigDecimal dividend, BigDecimal divisor) {
+ if (dividend == null || divisor == null || divisor.signum() == 0) return null;
+ return normalize(dividend.divide(divisor, EXCEL_MC));
+ }
+
+ public static boolean sameValue(BigDecimal left, BigDecimal right) {
+ if (left == null || right == null) return false;
+ return normalize(left).compareTo(normalize(right)) == 0;
+ }
+
+ public static String changeText(BigDecimal value) {
+ if (value == null) return "";
+ return normalize(value).toPlainString();
+ }
+
+ public static String format1(BigDecimal value) {
+ if (value == null) return "";
+ return value.setScale(1, RoundingMode.HALF_UP).toPlainString();
+ }
+
+ public static String format2(BigDecimal value) {
+ if (value == null) return "";
+ return value.setScale(2, RoundingMode.HALF_UP).toPlainString();
+ }
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayConfigController.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayConfigController.java
new file mode 100644
index 0000000..63c5a07
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayConfigController.java
@@ -0,0 +1,95 @@
+package com.trafficaudit.holiday.controller;
+
+import com.trafficaudit.common.Result;
+import com.trafficaudit.holiday.HolidayConstants;
+import com.trafficaudit.holiday.entity.HolidayCalendar;
+import com.trafficaudit.holiday.entity.HolidayPassengerCoefficient;
+import com.trafficaudit.holiday.service.HolidayConfigService;
+import com.trafficaudit.security.utils.JwtUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/holiday/config")
+public class HolidayConfigController {
+
+ @Resource
+ private HolidayConfigService configService;
+ @Resource
+ private JwtUtils jwtUtils;
+
+ @GetMapping("/options")
+ public Result<Map<String, Object>> options() {
+ return Result.ok(HolidayConstants.optionMap());
+ }
+
+ @GetMapping("/holidays")
+ public Result<List<HolidayCalendar>> holidays(@RequestParam(value = "year", required = false) Integer year,
+ @RequestParam(value = "holidayType", required = false) String holidayType) {
+ return Result.ok(configService.listHolidays(year, holidayType));
+ }
+
+ @PostMapping("/holidays/save")
+ public Result<HolidayCalendar> saveHoliday(@RequestHeader(value = "Authorization", required = false) String auth,
+ @RequestBody Map<String, Object> params) {
+ if (!isAdmin(auth)) return Result.error(403, "浠� admin 鍙互绠$悊鑺傚亣鏃ラ厤缃�");
+ try {
+ return Result.ok(configService.saveHoliday(params));
+ } catch (Exception e) {
+ return Result.error(400, e.getMessage());
+ }
+ }
+
+ @PostMapping("/holidays/delete")
+ public Result<Void> deleteHoliday(@RequestHeader(value = "Authorization", required = false) String auth,
+ @RequestParam("id") Long id) {
+ if (!isAdmin(auth)) return Result.error(403, "浠� admin 鍙互绠$悊鑺傚亣鏃ラ厤缃�");
+ try {
+ configService.deleteHoliday(id);
+ return Result.ok();
+ } catch (Exception e) {
+ return Result.error(400, e.getMessage());
+ }
+ }
+
+ @GetMapping("/coefficients")
+ public Result<List<HolidayPassengerCoefficient>> coefficients(@RequestParam(value = "year", required = false) Integer year) {
+ return Result.ok(configService.listCoefficients(year));
+ }
+
+ @PostMapping("/coefficients/save")
+ public Result<HolidayPassengerCoefficient> saveCoefficient(@RequestHeader(value = "Authorization", required = false) String auth,
+ @RequestBody Map<String, Object> params) {
+ if (!isAdmin(auth)) return Result.error(403, "浠� admin 鍙互绠$悊瀹㈣繍绯绘暟");
+ try {
+ return Result.ok(configService.saveCoefficient(params));
+ } catch (Exception e) {
+ return Result.error(400, e.getMessage());
+ }
+ }
+
+ @PostMapping("/coefficients/delete")
+ public Result<Void> deleteCoefficient(@RequestHeader(value = "Authorization", required = false) String auth,
+ @RequestParam("id") Long id) {
+ if (!isAdmin(auth)) return Result.error(403, "浠� admin 鍙互绠$悊瀹㈣繍绯绘暟");
+ try {
+ configService.deleteCoefficient(id);
+ return Result.ok();
+ } catch (Exception e) {
+ return Result.error(400, e.getMessage());
+ }
+ }
+
+ private boolean isAdmin(String auth) {
+ return "admin".equals(jwtUtils.resolveUsername(auth));
+ }
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayStatController.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayStatController.java
new file mode 100644
index 0000000..d6265b2
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayStatController.java
@@ -0,0 +1,82 @@
+package com.trafficaudit.holiday.controller;
+
+import com.trafficaudit.common.Result;
+import com.trafficaudit.holiday.service.HolidayBulletinService;
+import com.trafficaudit.holiday.service.HolidayStatService;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.net.URLEncoder;
+import java.time.LocalDate;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/holiday")
+public class HolidayStatController {
+
+ @Resource
+ private HolidayStatService statService;
+ @Resource
+ private HolidayBulletinService bulletinService;
+
+ @GetMapping("/overview")
+ public Result<Map<String, Object>> overview(@RequestParam("year") int year,
+ @RequestParam("holidayType") String holidayType) {
+ try {
+ return Result.ok(statService.overview(year, holidayType));
+ } catch (Exception e) {
+ return Result.error(400, e.getMessage());
+ }
+ }
+
+ @PostMapping("/import")
+ public Result<Map<String, Object>> importWorkbook(@RequestParam(value = "file", required = false) MultipartFile file,
+ @RequestParam("year") int year,
+ @RequestParam("holidayType") String holidayType,
+ @RequestParam(value = "allowHistoricalOverwrite", defaultValue = "false") boolean allowHistoricalOverwrite) {
+ try {
+ return Result.ok(statService.importWorkbook(file, year, holidayType, allowHistoricalOverwrite));
+ } catch (Exception e) {
+ return Result.error(400, e.getMessage());
+ }
+ }
+
+ @GetMapping("/export")
+ public ResponseEntity<byte[]> exportWorkbook(@RequestParam("year") int year,
+ @RequestParam("holidayType") String holidayType) throws Exception {
+ byte[] data = statService.exportWorkbook(year, holidayType);
+ String fileName = year + "骞�" + holidayTypeFileLabel(holidayType) + "鏁版嵁.xlsx";
+ String encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded)
+ .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
+ .body(data);
+ }
+
+ @GetMapping("/bulletin")
+ public ResponseEntity<byte[]> bulletin(@RequestParam(value = "date", required = false) String date) throws Exception {
+ LocalDate targetDate = (date == null || date.trim().isEmpty())
+ ? LocalDate.now().minusDays(1) : LocalDate.parse(date);
+ byte[] data = bulletinService.generate(targetDate);
+ String fileName = String.format("婀栧寳鐪佸浗搴嗗亣鏈熺患鍚堣繍杈撳娴佸揩鎶ワ紙10鏈�%d鏃ワ級.docx", targetDate.getDayOfMonth());
+ String encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded)
+ .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"))
+ .body(data);
+ }
+
+ private String holidayTypeFileLabel(String holidayType) {
+ if ("SPRING_FESTIVAL".equals(holidayType)) return "鏄ヨ妭";
+ if ("LABOR_DAY".equals(holidayType)) return "浜斾竴";
+ return "鍗佷竴";
+ }
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayCalendar.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayCalendar.java
new file mode 100644
index 0000000..3676039
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayCalendar.java
@@ -0,0 +1,22 @@
+package com.trafficaudit.holiday.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("holiday_calendar")
+public class HolidayCalendar {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private Integer year;
+ private String holidayType;
+ private LocalDate holidayDate;
+ private Integer dayIndex;
+ private LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayPassengerCoefficient.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayPassengerCoefficient.java
new file mode 100644
index 0000000..cd09b3c
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayPassengerCoefficient.java
@@ -0,0 +1,21 @@
+package com.trafficaudit.holiday.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("holiday_passenger_coefficient")
+public class HolidayPassengerCoefficient {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private Integer year;
+ private String dateCategory;
+ private BigDecimal coefficient;
+ private LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayPassengerFlow.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayPassengerFlow.java
new file mode 100644
index 0000000..4aacf85
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayPassengerFlow.java
@@ -0,0 +1,23 @@
+package com.trafficaudit.holiday.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("holiday_passenger_flow")
+public class HolidayPassengerFlow {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private Integer year;
+ private LocalDate flowDate;
+ private String transportMode;
+ private BigDecimal passengerCount;
+ private LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayVehicleFlow.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayVehicleFlow.java
new file mode 100644
index 0000000..c9dd32e
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayVehicleFlow.java
@@ -0,0 +1,28 @@
+package com.trafficaudit.holiday.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("holiday_vehicle_flow")
+public class HolidayVehicleFlow {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private Integer year;
+ private LocalDate flowDate;
+ private String roadCategory;
+ private String vehicleType;
+ private BigDecimal vehicleCount;
+ @TableField(updateStrategy = FieldStrategy.IGNORED)
+ private BigDecimal comparisonBase;
+ private LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayCalendarMapper.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayCalendarMapper.java
new file mode 100644
index 0000000..26b1149
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayCalendarMapper.java
@@ -0,0 +1,9 @@
+package com.trafficaudit.holiday.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.trafficaudit.holiday.entity.HolidayCalendar;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface HolidayCalendarMapper extends BaseMapper<HolidayCalendar> {
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayPassengerCoefficientMapper.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayPassengerCoefficientMapper.java
new file mode 100644
index 0000000..faac8c3
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayPassengerCoefficientMapper.java
@@ -0,0 +1,9 @@
+package com.trafficaudit.holiday.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.trafficaudit.holiday.entity.HolidayPassengerCoefficient;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface HolidayPassengerCoefficientMapper extends BaseMapper<HolidayPassengerCoefficient> {
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayPassengerFlowMapper.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayPassengerFlowMapper.java
new file mode 100644
index 0000000..543e691
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayPassengerFlowMapper.java
@@ -0,0 +1,9 @@
+package com.trafficaudit.holiday.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.trafficaudit.holiday.entity.HolidayPassengerFlow;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface HolidayPassengerFlowMapper extends BaseMapper<HolidayPassengerFlow> {
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayVehicleFlowMapper.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayVehicleFlowMapper.java
new file mode 100644
index 0000000..a99a00c
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayVehicleFlowMapper.java
@@ -0,0 +1,9 @@
+package com.trafficaudit.holiday.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.trafficaudit.holiday.entity.HolidayVehicleFlow;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface HolidayVehicleFlowMapper extends BaseMapper<HolidayVehicleFlow> {
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java
new file mode 100644
index 0000000..1c686ee
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java
@@ -0,0 +1,430 @@
+package com.trafficaudit.holiday.service;
+
+import com.trafficaudit.holiday.HolidayNumberUtils;
+import com.trafficaudit.holiday.service.HolidayStatService.DailyMetrics;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.nio.file.Files;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+@Slf4j
+@Service
+public class HolidayBulletinService {
+
+ private static final String WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
+ private static final BigDecimal EPSILON = new BigDecimal("0.0000001");
+
+ @Value("${holiday.bulletin-template:docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/婀栧寳鐪佸浗搴嗗亣鏈熺患鍚堣繍杈撳娴佸揩鎶ユā鏉�.docx}")
+ private String bulletinTemplatePath;
+
+ private final HolidayStatService statService;
+ private static final ThreadLocal<Document> CURRENT_DOCUMENT = new ThreadLocal<>();
+
+ public HolidayBulletinService(HolidayStatService statService) {
+ this.statService = statService;
+ }
+
+ public byte[] generate(LocalDate date) {
+ DailyMetrics current = statService.findMetricsByDate(date);
+ if (current == null) {
+ throw new IllegalArgumentException("鎵�閫夋棩鏈熶笉鍦ㄥ凡閰嶇疆鐨勮妭鍋囨棩鑼冨洿鍐�");
+ }
+ DailyMetrics previousDay = current.getDayIndex() > 1
+ ? statService.dailyMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex() - 1) : null;
+ DailyMetrics previousYear = statService.dailyMetrics(current.getYear() - 1, current.getHolidayType(), current.getDayIndex());
+ DailyMetrics currentCumulative = statService.cumulativeMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex());
+ DailyMetrics previousYearCumulative = statService.cumulativeMetrics(current.getYear() - 1, current.getHolidayType(), current.getDayIndex());
+
+ if (current.getTotalPersonFlow() == null) {
+ throw new IllegalArgumentException("鎵�閫夋棩鏈熺殑鍏ㄧぞ浼氳法鍖哄煙浜哄憳娴佸姩閲忔病鏈夎冻澶熸暟鎹紝鏃犳硶鐢熸垚蹇姤");
+ }
+
+ String currentTotal = formatNumber(current.getTotalPersonFlow());
+ String cumulativeTotalYi = formatNumber(currentCumulative.getTotalPersonFlow() == null
+ ? null : HolidayNumberUtils.divide(currentCumulative.getTotalPersonFlow(), BigDecimal.valueOf(10000)));
+ String ringRatioClause = previousDay == null ? "" : "锛岀幆姣�" + ratioPhrase(
+ current.getTotalPersonFlow(), previousDay.getTotalPersonFlow());
+ String paragraph1 = String.format("%d鏈�%d鏃ワ紙鍋囨湡绗�%d澶╋級锛屽叏鐪佸叏绀句細璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺%s涓囦汉娆�%s锛屾瘮2025骞村悓鏈�%s銆�",
+ date.getMonthValue(), date.getDayOfMonth(), current.getDayIndex(), currentTotal,
+ ringRatioClause,
+ ratioPhrase(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow()));
+ String paragraph2 = String.format("鍋囨湡浠ユ潵锛屽叏鐪佸叏绀句細璺ㄥ尯鍩熶汉鍛樻祦鍔ㄩ噺绱%s浜夸汉娆★紝姣�2025骞村悓鏈�%s銆�",
+ cumulativeTotalYi,
+ ratioPhrase(currentCumulative.getTotalPersonFlow(),
+ previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow()));
+
+ File template = resolveTemplateFile();
+ if (!template.exists() || !template.isFile()) {
+ throw new IllegalArgumentException("蹇姤妯℃澘鏂囦欢涓嶅瓨鍦�: " + template.getAbsolutePath());
+ }
+
+ try {
+ byte[] templateBytes;
+ try (InputStream in = new FileInputStream(template)) {
+ templateBytes = readAll(in);
+ }
+ byte[] documentXml = readZipEntry(templateBytes, "word/document.xml");
+ Document document = parseDocument(documentXml);
+ CURRENT_DOCUMENT.set(document);
+ try {
+ replaceParagraph(document, text -> text.startsWith("10鏈�") && text.contains("鍋囨湡绗�"), paragraph1);
+ replaceParagraph(document, text -> text.startsWith("鍋囨湡浠ユ潵"), paragraph2);
+ fillTable(document, current, previousDay, previousYear, currentCumulative, previousYearCumulative);
+ byte[] replacedXml = serializeDocument(document);
+ return replaceZipEntry(templateBytes, "word/document.xml", replacedXml);
+ } finally {
+ CURRENT_DOCUMENT.remove();
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException("蹇姤鐢熸垚澶辫触: " + e.getMessage(), e);
+ }
+ }
+
+ private void fillTable(Document document, DailyMetrics current, DailyMetrics previousDay,
+ DailyMetrics previousYear, DailyMetrics currentCumulative,
+ DailyMetrics previousYearCumulative) {
+ Element table = firstElement(document, "tbl");
+ if (table == null) {
+ throw new IllegalArgumentException("蹇姤妯℃澘涓壘涓嶅埌鏁版嵁琛�");
+ }
+ writeTableRow(table, 2, new String[]{
+ number(current.getNonBusinessPersonFlow()),
+ ratio(current.getNonBusinessPersonFlow(), previousDay == null ? null : previousDay.getNonBusinessPersonFlow()),
+ ratio(current.getNonBusinessPersonFlow(), previousYear == null ? null : previousYear.getNonBusinessPersonFlow()),
+ "鈥�",
+ number(currentCumulative.getNonBusinessPersonFlow()),
+ ratio(currentCumulative.getNonBusinessPersonFlow(),
+ previousYearCumulative == null ? null : previousYearCumulative.getNonBusinessPersonFlow()),
+ "鈥�"
+ });
+ writeTableRow(table, 3, new String[]{
+ number(current.getRoad()),
+ ratio(current.getRoad(), previousDay == null ? null : previousDay.getRoad()),
+ ratio(current.getRoad(), previousYear == null ? null : previousYear.getRoad()),
+ "鈥�",
+ number(currentCumulative.getRoad()),
+ ratio(currentCumulative.getRoad(), previousYearCumulative == null ? null : previousYearCumulative.getRoad()),
+ "鈥�"
+ });
+ writeTableRow(table, 4, new String[]{
+ number(current.getRoadPersonFlow()),
+ ratio(current.getRoadPersonFlow(), previousDay == null ? null : previousDay.getRoadPersonFlow()),
+ ratio(current.getRoadPersonFlow(), previousYear == null ? null : previousYear.getRoadPersonFlow()),
+ "鈥�",
+ number(currentCumulative.getRoadPersonFlow()),
+ ratio(currentCumulative.getRoadPersonFlow(), previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow()),
+ "鈥�"
+ });
+ writeTableRow(table, 5, transportRow(current.getRail(), previousDay == null ? null : previousDay.getRail(),
+ previousYear == null ? null : previousYear.getRail(), currentCumulative.getRail(),
+ previousYearCumulative == null ? null : previousYearCumulative.getRail()));
+ writeTableRow(table, 6, transportRow(current.getCivilAviation(), previousDay == null ? null : previousDay.getCivilAviation(),
+ previousYear == null ? null : previousYear.getCivilAviation(), currentCumulative.getCivilAviation(),
+ previousYearCumulative == null ? null : previousYearCumulative.getCivilAviation()));
+ writeTableRow(table, 7, transportRow(current.getWater(), previousDay == null ? null : previousDay.getWater(),
+ previousYear == null ? null : previousYear.getWater(), currentCumulative.getWater(),
+ previousYearCumulative == null ? null : previousYearCumulative.getWater()));
+ writeTableRow(table, 8, transportRow(current.getTotalPersonFlow(), previousDay == null ? null : previousDay.getTotalPersonFlow(),
+ previousYear == null ? null : previousYear.getTotalPersonFlow(), currentCumulative.getTotalPersonFlow(),
+ previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow()));
+ }
+
+ private String[] transportRow(BigDecimal current, BigDecimal previousDay, BigDecimal previousYear,
+ BigDecimal currentCumulative, BigDecimal previousYearCumulative) {
+ return new String[]{
+ number(current),
+ ratio(current, previousDay),
+ ratio(current, previousYear),
+ "鈥�",
+ number(currentCumulative),
+ ratio(currentCumulative, previousYearCumulative),
+ "鈥�"
+ };
+ }
+
+ private void writeTableRow(Element table, int rowIndex, String[] values) {
+ List<Element> rows = childElements(table, "tr");
+ if (rowIndex >= rows.size()) return;
+ Element row = rows.get(rowIndex);
+ List<Element> cells = cellRefs(row);
+ int logicalColumn = 0;
+ for (Element cell : cells) {
+ int span = gridSpan(cell);
+ if (logicalColumn >= 2 && logicalColumn < 9) {
+ int valueIndex = logicalColumn - 2;
+ if (valueIndex < values.length) {
+ setCellText(cell, values[valueIndex]);
+ }
+ }
+ logicalColumn += span;
+ }
+ }
+
+ private List<Element> cellRefs(Element row) {
+ List<Element> cells = new ArrayList<>();
+ for (Element child : childElements(row, "tc")) cells.add(child);
+ return cells;
+ }
+
+ private int gridSpan(Element cell) {
+ Element tcPr = firstChildElement(cell, "tcPr");
+ if (tcPr == null) return 1;
+ Element span = firstChildElement(tcPr, "gridSpan");
+ if (span == null) return 1;
+ String value = span.getAttributeNS(WORD_NS, "val");
+ try {
+ return Integer.parseInt(value);
+ } catch (Exception e) {
+ return 1;
+ }
+ }
+
+ private void setCellText(Element cell, String text) {
+ Element tcPr = firstChildElement(cell, "tcPr");
+ Element templateParagraph = firstChildElement(cell, "p");
+ Element paragraph = createElement("p");
+ if (templateParagraph != null) {
+ Element pPr = firstChildElement(templateParagraph, "pPr");
+ if (pPr != null) paragraph.appendChild(pPr.cloneNode(true));
+ }
+ Element run = createElement("r");
+ Element t = createElement("t");
+ t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve");
+ t.setTextContent(text == null ? "" : text);
+ run.appendChild(t);
+ paragraph.appendChild(run);
+
+ List<Node> removals = new ArrayList<>();
+ Node child = cell.getFirstChild();
+ while (child != null) {
+ Node next = child.getNextSibling();
+ if (child instanceof Element && "p".equals(localName(child))) {
+ removals.add(child);
+ }
+ child = next;
+ }
+ for (Node node : removals) cell.removeChild(node);
+ if (tcPr != null && tcPr.getNextSibling() != null) {
+ cell.insertBefore(paragraph, tcPr.getNextSibling());
+ } else if (tcPr != null) {
+ cell.appendChild(paragraph);
+ } else {
+ cell.insertBefore(paragraph, cell.getFirstChild());
+ }
+ }
+
+ private void replaceParagraph(Document document, ParagraphMatcher matcher, String replacement) {
+ NodeList paragraphs = document.getElementsByTagNameNS(WORD_NS, "p");
+ for (int i = 0; i < paragraphs.getLength(); i++) {
+ Element paragraph = (Element) paragraphs.item(i);
+ String text = paragraphText(paragraph);
+ if (matcher.matches(text)) {
+ setParagraphText(paragraph, replacement);
+ return;
+ }
+ }
+ throw new IllegalArgumentException("蹇姤妯℃澘缂哄皯鍙浛鎹㈡钀�: " + replacement);
+ }
+
+ private void setParagraphText(Element paragraph, String text) {
+ Element pPr = firstChildElement(paragraph, "pPr");
+ Element firstRun = firstChildElement(paragraph, "r");
+ Element rPr = firstRun == null ? null : firstChildElement(firstRun, "rPr");
+ List<Node> removals = new ArrayList<>();
+ Node child = paragraph.getFirstChild();
+ while (child != null) {
+ Node next = child.getNextSibling();
+ if (child != pPr) removals.add(child);
+ child = next;
+ }
+ for (Node node : removals) paragraph.removeChild(node);
+
+ Element run = createElement("r");
+ if (rPr != null) run.appendChild(rPr.cloneNode(true));
+ Element t = createElement("t");
+ t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve");
+ t.setTextContent(text);
+ run.appendChild(t);
+ paragraph.appendChild(run);
+ }
+
+ private String paragraphText(Element paragraph) {
+ StringBuilder builder = new StringBuilder();
+ NodeList texts = paragraph.getElementsByTagNameNS(WORD_NS, "t");
+ for (int i = 0; i < texts.getLength(); i++) {
+ builder.append(texts.item(i).getTextContent());
+ }
+ return builder.toString();
+ }
+
+ private String ratioPhrase(BigDecimal current, BigDecimal base) {
+ if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return "鏃犲彲姣旀暟鎹�";
+ BigDecimal ratio = HolidayNumberUtils.divide(current, base);
+ BigDecimal percent = HolidayNumberUtils.multiply(ratio.subtract(BigDecimal.ONE), BigDecimal.valueOf(100));
+ if (percent == null || percent.abs().compareTo(EPSILON) < 0) return "鎸佸钩";
+ return (percent.signum() > 0 ? "澧為暱" : "涓嬮檷") + HolidayNumberUtils.format1(percent.abs()) + "%";
+ }
+
+ private String ratio(BigDecimal current, BigDecimal base) {
+ if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return "鈥�";
+ return ratioPhrase(current, base);
+ }
+
+ private String number(BigDecimal value) {
+ return value == null ? "鈥�" : formatNumber(value);
+ }
+
+ private String formatNumber(BigDecimal value) {
+ if (value == null) return "";
+ return HolidayNumberUtils.format2(value);
+ }
+
+ private File resolveTemplateFile() {
+ List<File> candidates = new ArrayList<>();
+ File configured = new File(bulletinTemplatePath);
+ candidates.add(configured);
+ if (!configured.isAbsolute()) {
+ candidates.add(new File(System.getProperty("user.dir"), bulletinTemplatePath));
+ candidates.add(new File(System.getProperty("user.dir"), "../" + bulletinTemplatePath));
+ }
+ for (File candidate : candidates) {
+ try {
+ if (candidate.exists() && candidate.isFile()) {
+ return candidate.getCanonicalFile();
+ }
+ } catch (Exception ignored) {
+ // Keep checking.
+ }
+ }
+ return configured;
+ }
+
+ private byte[] readZipEntry(byte[] zipBytes, String entryName) throws Exception {
+ try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
+ ZipEntry entry;
+ while ((entry = zin.getNextEntry()) != null) {
+ if (entryName.equals(entry.getName())) {
+ return readAll(zin);
+ }
+ }
+ }
+ throw new IllegalArgumentException("妯℃澘涓己灏� " + entryName);
+ }
+
+ private byte[] replaceZipEntry(byte[] zipBytes, String entryName, byte[] replacement) throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zipBytes));
+ ZipOutputStream zout = new ZipOutputStream(out)) {
+ ZipEntry entry;
+ byte[] buffer = new byte[8192];
+ while ((entry = zin.getNextEntry()) != null) {
+ ZipEntry target = new ZipEntry(entry.getName());
+ zout.putNextEntry(target);
+ if (entryName.equals(entry.getName())) {
+ zout.write(replacement);
+ } else {
+ int length;
+ while ((length = zin.read(buffer)) > 0) {
+ zout.write(buffer, 0, length);
+ }
+ }
+ zout.closeEntry();
+ }
+ }
+ return out.toByteArray();
+ }
+
+ private Document parseDocument(byte[] xml) throws Exception {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ return factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml));
+ }
+
+ private byte[] serializeDocument(Document document) throws Exception {
+ Transformer transformer = TransformerFactory.newInstance().newTransformer();
+ transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ transformer.transform(new DOMSource(document), new StreamResult(out));
+ return out.toByteArray();
+ }
+
+ private Element firstElement(Document document, String localName) {
+ NodeList list = document.getElementsByTagNameNS(WORD_NS, localName);
+ return list.getLength() == 0 ? null : (Element) list.item(0);
+ }
+
+ private Element createElement(String localName) {
+ Document document = CURRENT_DOCUMENT.get();
+ if (document == null) {
+ throw new IllegalStateException("Word document is not initialized");
+ }
+ return document.createElementNS(WORD_NS, "w:" + localName);
+ }
+
+ private List<Element> childElements(Element parent, String localName) {
+ List<Element> result = new ArrayList<>();
+ Node child = parent.getFirstChild();
+ while (child != null) {
+ if (child instanceof Element && localName.equals(localName(child))) {
+ result.add((Element) child);
+ }
+ child = child.getNextSibling();
+ }
+ return result;
+ }
+
+ private Element firstChildElement(Element parent, String localName) {
+ Node child = parent.getFirstChild();
+ while (child != null) {
+ if (child instanceof Element && localName.equals(localName(child))) {
+ return (Element) child;
+ }
+ child = child.getNextSibling();
+ }
+ return null;
+ }
+
+ private String localName(Node node) {
+ return node.getLocalName() == null ? node.getNodeName().replaceFirst("^.*:", "") : node.getLocalName();
+ }
+
+ private byte[] readAll(InputStream in) throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[8192];
+ int length;
+ while ((length = in.read(buffer)) > 0) {
+ out.write(buffer, 0, length);
+ }
+ return out.toByteArray();
+ }
+
+ private interface ParagraphMatcher {
+ boolean matches(String text);
+ }
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayConfigService.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayConfigService.java
new file mode 100644
index 0000000..6b281c7
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayConfigService.java
@@ -0,0 +1,202 @@
+package com.trafficaudit.holiday.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.trafficaudit.holiday.HolidayConstants;
+import com.trafficaudit.holiday.entity.HolidayCalendar;
+import com.trafficaudit.holiday.entity.HolidayPassengerCoefficient;
+import com.trafficaudit.holiday.mapper.HolidayCalendarMapper;
+import com.trafficaudit.holiday.mapper.HolidayPassengerCoefficientMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class HolidayConfigService {
+
+ @Resource
+ private HolidayCalendarMapper holidayCalendarMapper;
+
+ @Resource
+ private HolidayPassengerCoefficientMapper coefficientMapper;
+
+ public List<HolidayCalendar> listHolidays(Integer year, String holidayType) {
+ LambdaQueryWrapper<HolidayCalendar> wrapper = new LambdaQueryWrapper<HolidayCalendar>()
+ .eq(year != null, HolidayCalendar::getYear, year)
+ .eq(holidayType != null && !holidayType.trim().isEmpty(), HolidayCalendar::getHolidayType, holidayType)
+ .orderByAsc(HolidayCalendar::getYear)
+ .orderByAsc(HolidayCalendar::getHolidayType)
+ .orderByAsc(HolidayCalendar::getDayIndex);
+ return holidayCalendarMapper.selectList(wrapper);
+ }
+
+ public List<HolidayCalendar> findHolidayDays(int year, String holidayType) {
+ return holidayCalendarMapper.selectList(new LambdaQueryWrapper<HolidayCalendar>()
+ .eq(HolidayCalendar::getYear, year)
+ .eq(HolidayCalendar::getHolidayType, holidayType)
+ .orderByAsc(HolidayCalendar::getDayIndex));
+ }
+
+ public HolidayCalendar findHolidayByDate(LocalDate date) {
+ return holidayCalendarMapper.selectOne(new LambdaQueryWrapper<HolidayCalendar>()
+ .eq(HolidayCalendar::getHolidayDate, date)
+ .orderByAsc(HolidayCalendar::getDayIndex)
+ .last("limit 1"));
+ }
+
+ @Transactional
+ public HolidayCalendar saveHoliday(Map<String, Object> params) {
+ Long id = longValue(params.get("id"));
+ Integer year = intValue(params.get("year"));
+ String holidayType = stringValue(params.get("holidayType"));
+ LocalDate holidayDate = dateValue(params.get("holidayDate"));
+ Integer dayIndex = intValue(params.get("dayIndex"));
+ if (year == null || year < 2000 || year > 2100) {
+ throw new IllegalArgumentException("骞翠唤涓嶆纭�");
+ }
+ if (!HolidayConstants.isValidHolidayType(holidayType)) {
+ throw new IllegalArgumentException("鑺傚亣鏃ュ垎绫讳笉姝g‘");
+ }
+ if (holidayDate == null || holidayDate.getYear() != year) {
+ throw new IllegalArgumentException("鏃ユ湡蹇呴』灞炰簬鎵�閫夊勾浠�");
+ }
+ if (dayIndex == null || dayIndex < 1) {
+ throw new IllegalArgumentException("绗嚑鏃ュ繀椤讳粠 1 寮�濮�");
+ }
+ LambdaQueryWrapper<HolidayCalendar> duplicate = new LambdaQueryWrapper<HolidayCalendar>()
+ .eq(HolidayCalendar::getYear, year)
+ .eq(HolidayCalendar::getHolidayType, holidayType)
+ .and(w -> w.eq(HolidayCalendar::getHolidayDate, holidayDate)
+ .or().eq(HolidayCalendar::getDayIndex, dayIndex));
+ if (id != null) {
+ duplicate.ne(HolidayCalendar::getId, id);
+ }
+ if (holidayCalendarMapper.selectCount(duplicate) > 0) {
+ throw new IllegalArgumentException("鍚屼竴鑺傚亣鏃ヤ笅鐨勬棩鏈熸垨绗嚑鏃ュ凡瀛樺湪");
+ }
+
+ HolidayCalendar entity = id == null ? new HolidayCalendar() : holidayCalendarMapper.selectById(id);
+ if (entity == null) {
+ throw new IllegalArgumentException("鑺傚亣鏃ヨ褰曚笉瀛樺湪");
+ }
+ if (id == null) {
+ entity.setCreatedAt(LocalDateTime.now());
+ }
+ entity.setYear(year);
+ entity.setHolidayType(holidayType);
+ entity.setHolidayDate(holidayDate);
+ entity.setDayIndex(dayIndex);
+ entity.setUpdatedAt(LocalDateTime.now());
+ if (id == null) {
+ holidayCalendarMapper.insert(entity);
+ } else {
+ holidayCalendarMapper.updateById(entity);
+ }
+ return entity;
+ }
+
+ @Transactional
+ public void deleteHoliday(Long id) {
+ if (id == null) {
+ throw new IllegalArgumentException("缂哄皯璁板綍 id");
+ }
+ holidayCalendarMapper.deleteById(id);
+ }
+
+ public List<HolidayPassengerCoefficient> listCoefficients(Integer year) {
+ return coefficientMapper.selectList(new LambdaQueryWrapper<HolidayPassengerCoefficient>()
+ .eq(year != null, HolidayPassengerCoefficient::getYear, year)
+ .orderByAsc(HolidayPassengerCoefficient::getYear)
+ .orderByAsc(HolidayPassengerCoefficient::getDateCategory));
+ }
+
+ public BigDecimal coefficientOf(int year, String dateCategory) {
+ HolidayPassengerCoefficient row = coefficientMapper.selectOne(
+ new LambdaQueryWrapper<HolidayPassengerCoefficient>()
+ .eq(HolidayPassengerCoefficient::getYear, year)
+ .eq(HolidayPassengerCoefficient::getDateCategory, dateCategory)
+ .last("limit 1"));
+ return row == null ? null : row.getCoefficient();
+ }
+
+ @Transactional
+ public HolidayPassengerCoefficient saveCoefficient(Map<String, Object> params) {
+ Long id = longValue(params.get("id"));
+ Integer year = intValue(params.get("year"));
+ String dateCategory = stringValue(params.get("dateCategory"));
+ BigDecimal coefficient = decimalValue(params.get("coefficient"));
+ if (year == null || year < 2000 || year > 2100) {
+ throw new IllegalArgumentException("骞翠唤涓嶆纭�");
+ }
+ if (!HolidayConstants.isValidDateCategory(dateCategory)) {
+ throw new IllegalArgumentException("鏃ユ湡鍒嗙被涓嶆纭�");
+ }
+ if (coefficient == null || coefficient.compareTo(BigDecimal.ZERO) <= 0) {
+ throw new IllegalArgumentException("瀹㈣繍绯绘暟蹇呴』澶т簬 0");
+ }
+ LambdaQueryWrapper<HolidayPassengerCoefficient> duplicate = new LambdaQueryWrapper<HolidayPassengerCoefficient>()
+ .eq(HolidayPassengerCoefficient::getYear, year)
+ .eq(HolidayPassengerCoefficient::getDateCategory, dateCategory);
+ if (id != null) {
+ duplicate.ne(HolidayPassengerCoefficient::getId, id);
+ }
+ if (coefficientMapper.selectCount(duplicate) > 0) {
+ throw new IllegalArgumentException("璇ュ勾浠藉拰鏃ユ湡鍒嗙被鐨勫杩愮郴鏁板凡瀛樺湪");
+ }
+
+ HolidayPassengerCoefficient entity = id == null ? new HolidayPassengerCoefficient() : coefficientMapper.selectById(id);
+ if (entity == null) {
+ throw new IllegalArgumentException("瀹㈣繍绯绘暟璁板綍涓嶅瓨鍦�");
+ }
+ if (id == null) {
+ entity.setCreatedAt(LocalDateTime.now());
+ }
+ entity.setYear(year);
+ entity.setDateCategory(dateCategory);
+ entity.setCoefficient(coefficient);
+ entity.setUpdatedAt(LocalDateTime.now());
+ if (id == null) {
+ coefficientMapper.insert(entity);
+ } else {
+ coefficientMapper.updateById(entity);
+ }
+ return entity;
+ }
+
+ @Transactional
+ public void deleteCoefficient(Long id) {
+ if (id == null) {
+ throw new IllegalArgumentException("缂哄皯璁板綍 id");
+ }
+ coefficientMapper.deleteById(id);
+ }
+
+ private Long longValue(Object value) {
+ if (value == null || value.toString().trim().isEmpty()) return null;
+ return Long.valueOf(value.toString());
+ }
+
+ private Integer intValue(Object value) {
+ if (value == null || value.toString().trim().isEmpty()) return null;
+ return Integer.valueOf(value.toString());
+ }
+
+ private BigDecimal decimalValue(Object value) {
+ if (value == null || value.toString().trim().isEmpty()) return null;
+ return com.trafficaudit.holiday.HolidayNumberUtils.decimalValue(value);
+ }
+
+ private String stringValue(Object value) {
+ return value == null ? null : value.toString().trim();
+ }
+
+ private LocalDate dateValue(Object value) {
+ if (value == null || value.toString().trim().isEmpty()) return null;
+ return LocalDate.parse(value.toString());
+ }
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayReferenceDataInitializer.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayReferenceDataInitializer.java
new file mode 100644
index 0000000..fcec0f8
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayReferenceDataInitializer.java
@@ -0,0 +1,21 @@
+package com.trafficaudit.holiday.service;
+
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+@Component
+@Order(100)
+public class HolidayReferenceDataInitializer implements ApplicationRunner {
+
+ @Resource
+ private HolidayWorkbookService workbookService;
+
+ @Override
+ public void run(ApplicationArguments args) {
+ workbookService.seedReferenceDataIfNeeded();
+ }
+}
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
new file mode 100644
index 0000000..8ccd624
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java
@@ -0,0 +1,297 @@
+package com.trafficaudit.holiday.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.trafficaudit.dataimport.entity.ImportBatch;
+import com.trafficaudit.dataimport.mapper.ImportBatchMapper;
+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 lombok.Data;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class HolidayStatService {
+
+ @Resource
+ private HolidayConfigService configService;
+ @Resource
+ private HolidayWorkbookService workbookService;
+ @Resource
+ private HolidayPassengerFlowMapper passengerFlowMapper;
+ @Resource
+ private HolidayVehicleFlowMapper vehicleFlowMapper;
+ @Resource
+ private ImportBatchMapper importBatchMapper;
+
+ public Map<String, Object> overview(int year, String holidayType) {
+ List<HolidayCalendar> calendar = configService.findHolidayDays(year, holidayType);
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("year", year);
+ result.put("holidayType", holidayType);
+ result.put("holidayTypeLabel", HolidayConstants.HOLIDAY_TYPE_LABELS.get(holidayType));
+ result.put("days", calendar);
+ result.put("rows", buildOverviewRows(year, holidayType, calendar));
+ result.put("templateFileName", workbookService.referenceFileName());
+ return result;
+ }
+
+ @Transactional
+ public Map<String, Object> importWorkbook(MultipartFile file, int year, String holidayType, boolean allowHistoricalOverwrite) {
+ HolidayWorkbookService.ImportSummary summary = workbookService.importWorkbook(file, year, holidayType, allowHistoricalOverwrite);
+ if (summary.isConfirmationRequired()) {
+ return importResult(summary);
+ }
+ ImportBatch batch = new ImportBatch();
+ batch.setFileName(file == null ? null : file.getOriginalFilename());
+ batch.setImportType("HOLIDAY_FLOW");
+ batch.setReportPeriod(year + "-" + holidayType);
+ batch.setTotalRows(summary.getSuccessRows() + summary.getFailRows());
+ batch.setSuccessRows(summary.getSuccessRows());
+ batch.setFailRows(summary.getFailRows());
+ batch.setErrorDetail(joinErrors(summary.getErrors()));
+ batch.setCreatedAt(LocalDateTime.now());
+ importBatchMapper.insert(batch);
+
+ return importResult(summary);
+ }
+
+ private Map<String, Object> importResult(HolidayWorkbookService.ImportSummary summary) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("passengerRows", summary.getPassengerRows());
+ result.put("vehicleRows", summary.getVehicleRows());
+ result.put("skippedRows", summary.getSkippedRows());
+ result.put("successRows", summary.getSuccessRows());
+ result.put("failRows", summary.getFailRows());
+ result.put("errors", summary.getErrors());
+ result.put("confirmationRequired", summary.isConfirmationRequired());
+ result.put("historicalChanges", summary.getHistoricalChanges());
+ return result;
+ }
+
+ public byte[] exportWorkbook(int year, String holidayType) {
+ return workbookService.exportWorkbook(year, holidayType);
+ }
+
+ public List<Map<String, Object>> buildOverviewRows(int year, String holidayType, List<HolidayCalendar> calendar) {
+ Map<LocalDate, Map<String, BigDecimal>> passengerByDate = loadPassengerByDate(year);
+ Map<LocalDate, Map<String, BigDecimal>> vehicleByDate = loadVehicleByDate(year);
+ Map<LocalDate, BigDecimal> comparisonBaseByDate = loadComparisonBaseByDate(year);
+ BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY);
+ List<Map<String, Object>> rows = new ArrayList<>();
+ BigDecimal previousNationalProvincial = null;
+ for (HolidayCalendar day : calendar) {
+ Map<String, BigDecimal> passenger = passengerByDate.getOrDefault(day.getHolidayDate(), Collections.emptyMap());
+ Map<String, BigDecimal> vehicle = vehicleByDate.getOrDefault(day.getHolidayDate(), Collections.emptyMap());
+ BigDecimal expressway = vehicle.get(HolidayConstants.ROAD_EXPRESSWAY);
+ BigDecimal nationalProvincial = vehicle.get(HolidayConstants.ROAD_NATIONAL_PROVINCIAL);
+ BigDecimal comparisonBase = comparisonBaseByDate.get(day.getHolidayDate());
+ BigDecimal nationalProvincialRingRatio = nationalProvincialRingRatio(
+ year, nationalProvincial, previousNationalProvincial, comparisonBase);
+ BigDecimal nonBusinessPersonFlow = null;
+ if (coefficient != null && (expressway != null || nationalProvincial != null)) {
+ nonBusinessPersonFlow = HolidayNumberUtils.multiply(
+ HolidayNumberUtils.sumNullable(expressway, nationalProvincial), coefficient);
+ }
+ Map<String, Object> row = new LinkedHashMap<>();
+ row.put("dayIndex", day.getDayIndex());
+ row.put("date", day.getHolidayDate());
+ row.put("rail", passenger.get(HolidayConstants.MODE_RAIL));
+ row.put("civilAviation", passenger.get(HolidayConstants.MODE_CIVIL_AVIATION));
+ row.put("road", passenger.get(HolidayConstants.MODE_ROAD));
+ row.put("water", passenger.get(HolidayConstants.MODE_WATER));
+ row.put("expressway", expressway);
+ row.put("nationalProvincial", nationalProvincial);
+ row.put("comparisonBase", comparisonBase);
+ row.put("nationalProvincialRingRatio", nationalProvincialRingRatio);
+ row.put("coefficient", coefficient);
+ row.put("nonBusinessPersonFlow", nonBusinessPersonFlow);
+ row.put("totalPersonFlow", totalPersonFlow(passenger, nonBusinessPersonFlow));
+ rows.add(row);
+ previousNationalProvincial = nationalProvincial;
+ }
+ return rows;
+ }
+
+ public DailyMetrics dailyMetrics(int year, String holidayType, int dayIndex) {
+ List<HolidayCalendar> calendar = configService.findHolidayDays(year, holidayType);
+ HolidayCalendar target = null;
+ for (HolidayCalendar day : calendar) {
+ if (day.getDayIndex() != null && day.getDayIndex() == dayIndex) {
+ target = day;
+ break;
+ }
+ }
+ if (target == null) return null;
+
+ Map<LocalDate, Map<String, BigDecimal>> passengerByDate = loadPassengerByDate(year);
+ Map<LocalDate, Map<String, BigDecimal>> vehicleByDate = loadVehicleByDate(year);
+ Map<LocalDate, BigDecimal> comparisonBaseByDate = loadComparisonBaseByDate(year);
+ Map<String, BigDecimal> passenger = passengerByDate.getOrDefault(target.getHolidayDate(), Collections.emptyMap());
+ Map<String, BigDecimal> vehicle = vehicleByDate.getOrDefault(target.getHolidayDate(), Collections.emptyMap());
+ BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY);
+ BigDecimal expressway = vehicle.get(HolidayConstants.ROAD_EXPRESSWAY);
+ BigDecimal nationalProvincial = vehicle.get(HolidayConstants.ROAD_NATIONAL_PROVINCIAL);
+ BigDecimal comparisonBase = comparisonBaseByDate.get(target.getHolidayDate());
+ BigDecimal nonBusiness = null;
+ if (coefficient != null && (expressway != null || nationalProvincial != null)) {
+ nonBusiness = HolidayNumberUtils.multiply(
+ HolidayNumberUtils.sumNullable(expressway, nationalProvincial), coefficient);
+ }
+
+ DailyMetrics metrics = new DailyMetrics();
+ metrics.setYear(year);
+ metrics.setHolidayType(holidayType);
+ metrics.setDayIndex(dayIndex);
+ metrics.setDate(target.getHolidayDate());
+ metrics.setRail(passenger.get(HolidayConstants.MODE_RAIL));
+ metrics.setCivilAviation(passenger.get(HolidayConstants.MODE_CIVIL_AVIATION));
+ metrics.setRoad(passenger.get(HolidayConstants.MODE_ROAD));
+ metrics.setWater(passenger.get(HolidayConstants.MODE_WATER));
+ metrics.setExpressway(expressway);
+ metrics.setNationalProvincial(nationalProvincial);
+ metrics.setComparisonBase(comparisonBase);
+ metrics.setCoefficient(coefficient);
+ metrics.setNonBusinessPersonFlow(nonBusiness);
+ metrics.setRoadPersonFlow(sumNullable(metrics.getRoad(), nonBusiness));
+ metrics.setTotalPersonFlow(totalPersonFlow(passenger, nonBusiness));
+ return metrics;
+ }
+
+ public DailyMetrics cumulativeMetrics(int year, String holidayType, int dayIndex) {
+ DailyMetrics cumulative = new DailyMetrics();
+ cumulative.setYear(year);
+ cumulative.setHolidayType(holidayType);
+ cumulative.setDayIndex(dayIndex);
+ for (int day = 1; day <= dayIndex; day++) {
+ DailyMetrics item = dailyMetrics(year, holidayType, day);
+ if (item == null) continue;
+ cumulative.setRail(sumNullable(cumulative.getRail(), item.getRail()));
+ cumulative.setCivilAviation(sumNullable(cumulative.getCivilAviation(), item.getCivilAviation()));
+ cumulative.setRoad(sumNullable(cumulative.getRoad(), item.getRoad()));
+ cumulative.setWater(sumNullable(cumulative.getWater(), item.getWater()));
+ cumulative.setExpressway(sumNullable(cumulative.getExpressway(), item.getExpressway()));
+ cumulative.setNationalProvincial(sumNullable(cumulative.getNationalProvincial(), item.getNationalProvincial()));
+ cumulative.setNonBusinessPersonFlow(sumNullable(cumulative.getNonBusinessPersonFlow(), item.getNonBusinessPersonFlow()));
+ cumulative.setRoadPersonFlow(sumNullable(cumulative.getRoadPersonFlow(), item.getRoadPersonFlow()));
+ cumulative.setTotalPersonFlow(sumNullable(cumulative.getTotalPersonFlow(), item.getTotalPersonFlow()));
+ }
+ return cumulative;
+ }
+
+
+ public DailyMetrics findMetricsByDate(LocalDate date) {
+ HolidayCalendar calendar = configService.findHolidayByDate(date);
+ if (calendar == null) return null;
+ return dailyMetrics(calendar.getYear(), calendar.getHolidayType(), calendar.getDayIndex());
+ }
+
+ private Map<LocalDate, Map<String, BigDecimal>> loadPassengerByDate(int year) {
+ Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>();
+ List<HolidayPassengerFlow> rows = passengerFlowMapper.selectList(
+ new LambdaQueryWrapper<HolidayPassengerFlow>().eq(HolidayPassengerFlow::getYear, year));
+ for (HolidayPassengerFlow row : rows) {
+ result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>())
+ .put(row.getTransportMode(), row.getPassengerCount());
+ }
+ return result;
+ }
+
+ private Map<LocalDate, Map<String, BigDecimal>> loadVehicleByDate(int year) {
+ Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>();
+ List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList(
+ new LambdaQueryWrapper<HolidayVehicleFlow>().eq(HolidayVehicleFlow::getYear, year));
+ for (HolidayVehicleFlow row : rows) {
+ result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>())
+ .put(row.getRoadCategory(), row.getVehicleCount());
+ }
+ return result;
+ }
+
+ private Map<LocalDate, BigDecimal> loadComparisonBaseByDate(int year) {
+ Map<LocalDate, BigDecimal> result = new LinkedHashMap<>();
+ List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList(
+ new LambdaQueryWrapper<HolidayVehicleFlow>()
+ .eq(HolidayVehicleFlow::getYear, year)
+ .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL));
+ for (HolidayVehicleFlow row : rows) {
+ if (row.getComparisonBase() != null) result.put(row.getFlowDate(), row.getComparisonBase());
+ }
+ return result;
+ }
+
+ private BigDecimal totalPersonFlow(Map<String, BigDecimal> passenger, BigDecimal nonBusinessPersonFlow) {
+ BigDecimal total = nonBusinessPersonFlow;
+ total = sumNullable(total, passenger.get(HolidayConstants.MODE_ROAD));
+ total = sumNullable(total, passenger.get(HolidayConstants.MODE_RAIL));
+ total = sumNullable(total, passenger.get(HolidayConstants.MODE_CIVIL_AVIATION));
+ total = sumNullable(total, passenger.get(HolidayConstants.MODE_WATER));
+ return total;
+ }
+
+ private BigDecimal nationalProvincialRingRatio(int year, BigDecimal current,
+ BigDecimal previous, BigDecimal comparisonBase) {
+ if (current == null) return null;
+ if (HolidayConstants.supportsComparisonBase(year) && comparisonBase != null) {
+ return ratio(current, comparisonBase);
+ }
+ return previous == null ? null : ratio(current, previous);
+ }
+
+ private BigDecimal ratio(BigDecimal current, BigDecimal base) {
+ BigDecimal quotient = HolidayNumberUtils.divide(current, base);
+ return quotient == null ? null : HolidayNumberUtils.normalize(quotient.subtract(BigDecimal.ONE));
+ }
+
+ private BigDecimal sumNullable(BigDecimal left, BigDecimal right) {
+ return HolidayNumberUtils.sumNullable(left, right);
+ }
+
+ private String joinErrors(List<String> errors) {
+ if (errors == null || errors.isEmpty()) return null;
+ StringBuilder builder = new StringBuilder();
+ int limit = Math.min(errors.size(), 200);
+ for (int i = 0; i < limit; i++) {
+ if (i > 0) builder.append('\n');
+ builder.append(errors.get(i));
+ }
+ if (errors.size() > limit) {
+ builder.append("\n... ").append(errors.size() - limit).append(" more");
+ }
+ return builder.toString();
+ }
+
+ @Data
+ public static class DailyMetrics {
+ private int year;
+ private String holidayType;
+ private int dayIndex;
+ private LocalDate date;
+ private BigDecimal rail;
+ private BigDecimal civilAviation;
+ private BigDecimal road;
+ private BigDecimal water;
+ private BigDecimal expressway;
+ private BigDecimal nationalProvincial;
+ private BigDecimal comparisonBase;
+ private BigDecimal coefficient;
+ private BigDecimal nonBusinessPersonFlow;
+ private BigDecimal roadPersonFlow;
+ private BigDecimal totalPersonFlow;
+ }
+}
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
new file mode 100644
index 0000000..3fc5d2e
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayWorkbookService.java
@@ -0,0 +1,880 @@
+package com.trafficaudit.holiday.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.trafficaudit.holiday.HolidayConstants;
+import com.trafficaudit.holiday.HolidayNumberUtils;
+import com.trafficaudit.holiday.entity.HolidayCalendar;
+import com.trafficaudit.holiday.entity.HolidayPassengerCoefficient;
+import com.trafficaudit.holiday.entity.HolidayPassengerFlow;
+import com.trafficaudit.holiday.entity.HolidayVehicleFlow;
+import com.trafficaudit.holiday.mapper.HolidayPassengerCoefficientMapper;
+import com.trafficaudit.holiday.mapper.HolidayPassengerFlowMapper;
+import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.DataFormatter;
+import org.apache.poi.ss.usermodel.FormulaEvaluator;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.usermodel.WorkbookFactory;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Service
+public class HolidayWorkbookService {
+
+ private static final Map<String, String> PASSENGER_SHEET_MODE = new LinkedHashMap<>();
+ private static final int FIRST_DATA_ROW_INDEX = 2;
+ private static final int[] REFERENCE_YEARS = {2024, 2025, 2026};
+
+ static {
+ PASSENGER_SHEET_MODE.put("閾佽矾", HolidayConstants.MODE_RAIL);
+ PASSENGER_SHEET_MODE.put("姘戣埅", HolidayConstants.MODE_CIVIL_AVIATION);
+ PASSENGER_SHEET_MODE.put("閬撹矾", HolidayConstants.MODE_ROAD);
+ PASSENGER_SHEET_MODE.put("姘磋矾", HolidayConstants.MODE_WATER);
+ }
+
+ @Resource
+ private HolidayConfigService configService;
+ @Resource
+ private HolidayPassengerFlowMapper passengerFlowMapper;
+ @Resource
+ private HolidayVehicleFlowMapper vehicleFlowMapper;
+ @Resource
+ private HolidayPassengerCoefficientMapper coefficientMapper;
+
+ @Value("${holiday.reference-path:docs/涓撻」缁熻/閲嶈鑺傚亣鏃�/2026骞村崄涓�鏁版嵁.xlsx}")
+ private String referencePath;
+
+ public File resolveReferenceFile() {
+ List<File> candidates = new ArrayList<>();
+ File configured = new File(referencePath);
+ candidates.add(configured);
+ if (!configured.isAbsolute()) {
+ candidates.add(new File(System.getProperty("user.dir"), referencePath));
+ candidates.add(new File(System.getProperty("user.dir"), "../" + referencePath));
+ }
+ for (File candidate : candidates) {
+ try {
+ if (candidate.exists() && candidate.isFile()) {
+ return candidate.getCanonicalFile();
+ }
+ } catch (Exception ignored) {
+ // Keep checking the remaining candidate paths.
+ }
+ }
+ return configured;
+ }
+
+ public String referenceFileName() {
+ return resolveReferenceFile().getName();
+ }
+
+ @Transactional
+ public ImportSummary importWorkbook(MultipartFile file, int year, String holidayType, boolean allowHistoricalOverwrite) {
+ if (file == null || file.isEmpty()) {
+ throw new IllegalArgumentException("璇烽�夋嫨瑕佸鍏ョ殑 Excel 鏂囦欢");
+ }
+ List<HolidayCalendar> holidayDays = configService.findHolidayDays(year, holidayType);
+ if (holidayDays.isEmpty()) {
+ throw new IllegalArgumentException("鎵�閫夊勾浠藉拰鑺傚亣鏃ュ皻鏈厤缃棩鏈燂紝璇峰厛鍦ㄧ郴缁熼厤缃腑缁存姢鑺傚亣鏃�");
+ }
+ Map<Integer, LocalDate> dateByDay = new LinkedHashMap<>();
+ for (HolidayCalendar day : holidayDays) {
+ dateByDay.put(day.getDayIndex(), day.getHolidayDate());
+ }
+
+ if (!allowHistoricalOverwrite) {
+ ImportSummary historicalSummary = detectHistoricalChanges(file, year, dateByDay);
+ if (historicalSummary.isConfirmationRequired()) {
+ return historicalSummary;
+ }
+ }
+
+ ImportSummary summary = new ImportSummary();
+ try (InputStream in = file.getInputStream(); Workbook workbook = WorkbookFactory.create(in)) {
+ FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
+ importPassengerSheets(workbook, evaluator, year, dateByDay, summary);
+ importVehicleSheets(workbook, evaluator, year, dateByDay, summary);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("宸ヤ綔绨胯В鏋愬け璐�: " + e.getMessage(), e);
+ }
+ return summary;
+ }
+
+ private ImportSummary detectHistoricalChanges(MultipartFile file, int year, Map<Integer, LocalDate> dateByDay) {
+ ImportSummary summary = new ImportSummary();
+ try (InputStream in = file.getInputStream(); Workbook workbook = WorkbookFactory.create(in)) {
+ FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
+ int latestDay = findLatestIncomingDay(workbook, evaluator, year, dateByDay);
+ if (latestDay <= 1) {
+ return summary;
+ }
+ Map<LocalDate, Map<String, BigDecimal>> existingPassenger = loadPassengerByDate(year);
+ Map<LocalDate, Map<String, BigDecimal>> existingVehicle = loadVehicleByDate(year);
+ detectPassengerHistory(workbook, evaluator, year, dateByDay, latestDay, existingPassenger, summary);
+ detectVehicleHistory(workbook, evaluator, year, dateByDay, latestDay, existingVehicle, summary);
+ if (!summary.getHistoricalChanges().isEmpty()) {
+ summary.setConfirmationRequired(true);
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException("宸ヤ綔绨胯В鏋愬け璐�: " + e.getMessage(), e);
+ }
+ return summary;
+ }
+
+ private int findLatestIncomingDay(Workbook workbook, FormulaEvaluator evaluator, int year, Map<Integer, LocalDate> dateByDay) {
+ int latest = 0;
+ for (Integer dayIndex : dateByDay.keySet()) {
+ if (hasPassengerValue(workbook, evaluator, year, dayIndex)
+ || hasVehicleValue(workbook, evaluator, year, dayIndex)) {
+ latest = Math.max(latest, dayIndex);
+ }
+ }
+ return latest;
+ }
+
+ private boolean hasPassengerValue(Workbook workbook, FormulaEvaluator evaluator, int year, int dayIndex) {
+ for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) {
+ Sheet sheet = workbook.getSheet(entry.getKey());
+ if (sheet == null) continue;
+ Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7);
+ if (yearColumn == null) continue;
+ Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + dayIndex - 1);
+ if (row == null) continue;
+ BigDecimal value = numberValue(row.getCell(yearColumn), evaluator);
+ if (value != null) return true;
+ }
+ return false;
+ }
+
+ private boolean hasVehicleValue(Workbook workbook, FormulaEvaluator evaluator, int year, int dayIndex) {
+ Sheet expressway = workbook.getSheet("楂橀�熷叕璺�");
+ if (expressway != null) {
+ Integer yearColumn = findYearColumn(expressway, evaluator, year, 2, 7);
+ if (yearColumn != null) {
+ Row row = expressway.getRow(FIRST_DATA_ROW_INDEX + dayIndex - 1);
+ if (row != null && numberValue(row.getCell(yearColumn), evaluator) != null) return true;
+ }
+ }
+ Sheet nationalProvincial = workbook.getSheet("鏅�氬浗鐪侀亾");
+ if (nationalProvincial != null) {
+ Integer yearColumn = findYearColumn(nationalProvincial, evaluator, year, 3, 8);
+ if (yearColumn != null) {
+ Row row = nationalProvincial.getRow(FIRST_DATA_ROW_INDEX + dayIndex - 1);
+ if (row != null && numberValue(row.getCell(yearColumn), evaluator) != null) return true;
+ }
+ }
+ return false;
+ }
+
+ private void detectPassengerHistory(Workbook workbook, FormulaEvaluator evaluator, int year,
+ Map<Integer, LocalDate> dateByDay, int latestDay,
+ Map<LocalDate, Map<String, BigDecimal>> existing,
+ ImportSummary summary) {
+ for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) {
+ Sheet sheet = workbook.getSheet(entry.getKey());
+ if (sheet == null) continue;
+ Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7);
+ if (yearColumn == null) continue;
+ for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) {
+ if (day.getKey() >= latestDay) continue;
+ Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1);
+ if (row == null) continue;
+ BigDecimal incoming = numberValue(row.getCell(yearColumn), evaluator);
+ if (incoming == null) continue;
+ Map<String, BigDecimal> existingDay = existing.get(day.getValue());
+ BigDecimal oldValue = existingDay == null ? null : existingDay.get(entry.getValue());
+ if (oldValue != null && !sameValue(oldValue, incoming)) {
+ summary.addHistoricalChange("绗�" + day.getKey() + "鏃� " + day.getValue() + " "
+ + entry.getKey() + "锛氬師鍊� " + formatChangeValue(oldValue) + "锛屽鍏ュ�� " + formatChangeValue(incoming));
+ }
+ }
+ }
+ }
+
+ private void detectVehicleHistory(Workbook workbook, FormulaEvaluator evaluator, int year,
+ Map<Integer, LocalDate> dateByDay, int latestDay,
+ Map<LocalDate, Map<String, BigDecimal>> existing,
+ ImportSummary summary) {
+ detectVehicleSheetHistory(workbook, evaluator, year, dateByDay, latestDay, existing, summary,
+ "楂橀�熷叕璺�", HolidayConstants.ROAD_EXPRESSWAY, 2, 7);
+ detectVehicleSheetHistory(workbook, evaluator, year, dateByDay, latestDay, existing, summary,
+ "鏅�氬浗鐪侀亾", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, 3, 8);
+ detectComparisonBaseHistory(workbook, evaluator, year, dateByDay, latestDay, existing, summary);
+ }
+
+ private void detectVehicleSheetHistory(Workbook workbook, FormulaEvaluator evaluator, int year,
+ Map<Integer, LocalDate> dateByDay, int latestDay,
+ Map<LocalDate, Map<String, BigDecimal>> existing,
+ ImportSummary summary, String sheetName, String roadCategory,
+ int startColumn, int endColumn) {
+ Sheet sheet = workbook.getSheet(sheetName);
+ if (sheet == null) return;
+ Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn);
+ if (yearColumn == null) return;
+ for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) {
+ if (day.getKey() >= latestDay) continue;
+ Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1);
+ if (row == null) continue;
+ BigDecimal incoming = numberValue(row.getCell(yearColumn), evaluator);
+ if (incoming == null) continue;
+ Map<String, BigDecimal> existingDay = existing.get(day.getValue());
+ BigDecimal oldValue = existingDay == null ? null : existingDay.get(roadCategory);
+ if (oldValue != null && !sameValue(oldValue, incoming)) {
+ summary.addHistoricalChange("绗�" + day.getKey() + "鏃� " + day.getValue() + " "
+ + sheetName + "锛氬師鍊� " + formatChangeValue(oldValue) + "锛屽鍏ュ�� " + formatChangeValue(incoming));
+ }
+ }
+ }
+
+ private void detectComparisonBaseHistory(Workbook workbook, FormulaEvaluator evaluator, int year,
+ Map<Integer, LocalDate> dateByDay, int latestDay,
+ Map<LocalDate, Map<String, BigDecimal>> existingVehicle,
+ ImportSummary summary) {
+ if (!HolidayConstants.supportsComparisonBase(year)) return;
+ Sheet sheet = workbook.getSheet("鏅�氬浗鐪侀亾");
+ if (sheet == null) return;
+ Map<LocalDate, BigDecimal> existingBase = loadComparisonBaseByDate(year);
+ for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) {
+ if (day.getKey() >= latestDay) continue;
+ Map<String, BigDecimal> existingDay = existingVehicle.get(day.getValue());
+ if (existingDay == null || existingDay.get(HolidayConstants.ROAD_NATIONAL_PROVINCIAL) == null) continue;
+ Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1);
+ if (row == null) continue;
+ BigDecimal incoming;
+ try {
+ incoming = numberValue(row.getCell(2), evaluator);
+ } catch (Exception e) {
+ summary.addError("鏅�氬浗鐪侀亾 绗�" + (row.getRowNum() + 1) + "琛岀幆姣斿熀鏈燂細" + e.getMessage());
+ continue;
+ }
+ BigDecimal oldValue = existingBase.get(day.getValue());
+ if (!sameNullableValue(oldValue, incoming)) {
+ summary.addHistoricalChange("绗�" + day.getKey() + "鏃� " + day.getValue() + " 鏅�氬浗鐪侀亾鐜瘮鍩烘湡锛氬師鍊� "
+ + nullableChangeValue(oldValue) + "锛屽鍏ュ�� " + nullableChangeValue(incoming));
+ }
+ }
+ }
+
+ Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>();
+ private Map<LocalDate, Map<String, BigDecimal>> loadPassengerByDate(int year) {
+ List<HolidayPassengerFlow> rows = passengerFlowMapper.selectList(
+ new LambdaQueryWrapper<HolidayPassengerFlow>().eq(HolidayPassengerFlow::getYear, year));
+ for (HolidayPassengerFlow row : rows) {
+ result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>())
+ .put(row.getTransportMode(), row.getPassengerCount());
+ }
+ return result;
+ }
+
+ private Map<LocalDate, Map<String, BigDecimal>> loadVehicleByDate(int year) {
+ Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>();
+ List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList(
+ new LambdaQueryWrapper<HolidayVehicleFlow>().eq(HolidayVehicleFlow::getYear, year));
+ for (HolidayVehicleFlow row : rows) {
+ result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>())
+ .put(row.getRoadCategory(), row.getVehicleCount());
+ }
+ return result;
+ }
+
+ private Map<LocalDate, BigDecimal> loadComparisonBaseByDate(int year) {
+ Map<LocalDate, BigDecimal> result = new LinkedHashMap<>();
+ List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList(
+ new LambdaQueryWrapper<HolidayVehicleFlow>()
+ .eq(HolidayVehicleFlow::getYear, year)
+ .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL));
+ for (HolidayVehicleFlow row : rows) result.put(row.getFlowDate(), row.getComparisonBase());
+ return result;
+ }
+
+ private boolean sameValue(BigDecimal left, BigDecimal right) {
+ return HolidayNumberUtils.sameValue(left, right);
+ }
+
+ private boolean sameNullableValue(BigDecimal left, BigDecimal right) {
+ if (left == null || right == null) return left == right;
+ return sameValue(left, right);
+ }
+
+ private String nullableChangeValue(BigDecimal value) {
+ return value == null ? "绌�" : formatChangeValue(value);
+ }
+
+ private String formatChangeValue(BigDecimal value) {
+ return HolidayNumberUtils.changeText(value);
+ }
+
+ @Transactional
+ public void seedReferenceDataIfNeeded() {
+ File reference = resolveReferenceFile();
+ if (!reference.exists() || !reference.isFile()) {
+ log.warn("Holiday reference workbook not found: {}", reference.getAbsolutePath());
+ return;
+ }
+ try (InputStream in = new FileInputStream(reference); Workbook workbook = WorkbookFactory.create(in)) {
+ FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
+ seedNationalDayCalendar();
+ for (int year : REFERENCE_YEARS) {
+ if (passengerFlowMapper.selectCount(new LambdaQueryWrapper<HolidayPassengerFlow>()
+ .eq(HolidayPassengerFlow::getYear, year)) == 0) {
+ seedPassengerYear(workbook, evaluator, year);
+ }
+ if (vehicleFlowMapper.selectCount(new LambdaQueryWrapper<HolidayVehicleFlow>()
+ .eq(HolidayVehicleFlow::getYear, year)) == 0) {
+ seedVehicleYear(workbook, evaluator, year);
+ }
+ if (coefficientMapper.selectCount(new LambdaQueryWrapper<HolidayPassengerCoefficient>()
+ .eq(HolidayPassengerCoefficient::getYear, year)) == 0) {
+ seedCoefficientYear(workbook, evaluator, year);
+ }
+ }
+ } catch (Exception e) {
+ log.error("Failed to seed holiday reference data", e);
+ }
+ }
+
+ public byte[] exportWorkbook(int year, String holidayType) {
+ File reference = resolveReferenceFile();
+ if (!reference.exists() || !reference.isFile()) {
+ throw new IllegalArgumentException("鑺傚亣鏃ユā鏉挎枃浠朵笉瀛樺湪: " + reference.getAbsolutePath());
+ }
+ try (InputStream in = new FileInputStream(reference); XSSFWorkbook workbook = new XSSFWorkbook(in)) {
+ FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
+ fillPassengerSheets(workbook, evaluator, year, holidayType);
+ fillVehicleSheets(workbook, evaluator, year, holidayType);
+ fillCoefficients(workbook, evaluator, year, holidayType);
+ workbook.setForceFormulaRecalculation(true);
+ try {
+ evaluator.evaluateAll();
+ } catch (Exception e) {
+ log.warn("Workbook formula recalculation did not complete; Excel will recalculate on open", e);
+ }
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ workbook.write(out);
+ return out.toByteArray();
+ } catch (Exception e) {
+ throw new IllegalArgumentException("鑺傚亣鏃ユ暟鎹鍑哄け璐�: " + e.getMessage(), e);
+ }
+ }
+
+ private void importPassengerSheets(Workbook workbook, FormulaEvaluator evaluator, int year,
+ Map<Integer, LocalDate> dateByDay, ImportSummary summary) {
+ for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) {
+ Sheet sheet = workbook.getSheet(entry.getKey());
+ if (sheet == null) {
+ summary.addError(entry.getKey() + "锛氭壘涓嶅埌宸ヤ綔琛�");
+ continue;
+ }
+ Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7);
+ if (yearColumn == null) {
+ summary.addError(entry.getKey() + "锛氭壘涓嶅埌 " + year + " 骞存暟鎹垪");
+ continue;
+ }
+ for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) {
+ Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1);
+ if (row == null) {
+ summary.addError(entry.getKey() + " 绗�" + day.getKey() + "鏃ワ細宸ヤ綔琛ㄧ己灏戣琛�");
+ continue;
+ }
+ Integer rowDay = parseDayIndex(row.getCell(0), evaluator);
+ if (rowDay == null || !rowDay.equals(day.getKey())) {
+ summary.addError(entry.getKey() + " 绗�" + (row.getRowNum() + 1) + "琛岋細绗嚑鏃ヤ笌鑺傚亣鏃ラ厤缃笉涓�鑷�");
+ continue;
+ }
+ BigDecimal value;
+ try {
+ value = numberValue(row.getCell(yearColumn), evaluator);
+ } catch (Exception e) {
+ summary.addError(entry.getKey() + " 绗�" + (row.getRowNum() + 1) + "琛岋細" + e.getMessage());
+ continue;
+ }
+ if (value == null) {
+ summary.incrementSkipped();
+ continue;
+ }
+ upsertPassenger(year, day.getValue(), entry.getValue(), value);
+ summary.incrementPassenger();
+ }
+ }
+ }
+
+ private void importVehicleSheets(Workbook workbook, FormulaEvaluator evaluator, int year,
+ Map<Integer, LocalDate> dateByDay, ImportSummary summary) {
+ importVehicleSheet(workbook, evaluator, year, dateByDay, summary,
+ "楂橀�熷叕璺�", HolidayConstants.ROAD_EXPRESSWAY, HolidayConstants.VEHICLE_SMALL_PASSENGER, 2, 7, null);
+ importVehicleSheet(workbook, evaluator, year, dateByDay, summary,
+ "鏅�氬浗鐪侀亾", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER, 3, 8,
+ HolidayConstants.supportsComparisonBase(year) ? 2 : null);
+ }
+
+ private void importVehicleSheet(Workbook workbook, FormulaEvaluator evaluator, int year,
+ Map<Integer, LocalDate> dateByDay, ImportSummary summary,
+ String sheetName, String roadCategory, String vehicleType,
+ int startColumn, int endColumn, Integer comparisonBaseColumn) {
+ Sheet sheet = workbook.getSheet(sheetName);
+ if (sheet == null) {
+ summary.addError(sheetName + "锛氭壘涓嶅埌宸ヤ綔琛�");
+ return;
+ }
+ Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn);
+ if (yearColumn == null) {
+ summary.addError(sheetName + "锛氭壘涓嶅埌 " + year + " 骞存暟鎹垪");
+ return;
+ }
+ for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) {
+ Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1);
+ if (row == null) {
+ summary.addError(sheetName + " 绗�" + day.getKey() + "鏃ワ細宸ヤ綔琛ㄧ己灏戣琛�");
+ continue;
+ }
+ Integer rowDay = parseDayIndex(row.getCell(0), evaluator);
+ if (rowDay == null || !rowDay.equals(day.getKey())) {
+ summary.addError(sheetName + " 绗�" + (row.getRowNum() + 1) + "琛岋細绗嚑鏃ヤ笌鑺傚亣鏃ラ厤缃笉涓�鑷�");
+ continue;
+ }
+ BigDecimal value;
+ try {
+ value = numberValue(row.getCell(yearColumn), evaluator);
+ } catch (Exception e) {
+ summary.addError(sheetName + " 绗�" + (row.getRowNum() + 1) + "琛岋細" + e.getMessage());
+ continue;
+ }
+ if (value == null) {
+ summary.incrementSkipped();
+ continue;
+ }
+ BigDecimal comparisonBase = null;
+ if (comparisonBaseColumn != null) {
+ try {
+ comparisonBase = numberValue(row.getCell(comparisonBaseColumn), evaluator);
+ } catch (Exception e) {
+ summary.addError(sheetName + " 绗�" + (row.getRowNum() + 1) + "琛岀幆姣斿熀鏈燂細" + e.getMessage());
+ continue;
+ }
+ }
+ upsertVehicle(year, day.getValue(), roadCategory, vehicleType, value, comparisonBase);
+ summary.incrementVehicle();
+ }
+ }
+
+ private void fillPassengerSheets(XSSFWorkbook workbook, FormulaEvaluator evaluator, int selectedYear, String holidayType) {
+ for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) {
+ Sheet sheet = workbook.getSheet(entry.getKey());
+ if (sheet == null) continue;
+ for (int year : REFERENCE_YEARS) {
+ Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7);
+ if (yearColumn == null) continue;
+ Map<Integer, LocalDate> dayDates = dayDates(year, holidayType);
+ Map<LocalDate, BigDecimal> values = loadPassengerValues(year, entry.getValue());
+ for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) {
+ if (!values.containsKey(day.getValue())) continue;
+ setNumeric(sheet, FIRST_DATA_ROW_INDEX + day.getKey() - 1, yearColumn, values.get(day.getValue()));
+ }
+ }
+ }
+ }
+
+ private void fillVehicleSheets(XSSFWorkbook workbook, FormulaEvaluator evaluator, int selectedYear, String holidayType) {
+ fillVehicleSheet(workbook, evaluator, "楂橀�熷叕璺�", HolidayConstants.ROAD_EXPRESSWAY, 2, 7, holidayType, null);
+ fillVehicleSheet(workbook, evaluator, "鏅�氬浗鐪侀亾", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, 3, 8, holidayType,
+ HolidayConstants.supportsComparisonBase(selectedYear) ? 2 : null);
+ }
+
+ private void fillVehicleSheet(XSSFWorkbook workbook, FormulaEvaluator evaluator, String sheetName,
+ String roadCategory, int startColumn, int endColumn, String holidayType,
+ Integer comparisonBaseColumn) {
+ Sheet sheet = workbook.getSheet(sheetName);
+ if (sheet == null) return;
+ for (int year : REFERENCE_YEARS) {
+ Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn);
+ if (yearColumn == null) continue;
+ Map<Integer, LocalDate> dayDates = dayDates(year, holidayType);
+ Map<LocalDate, BigDecimal> values = loadVehicleValues(year, roadCategory);
+ for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) {
+ if (!values.containsKey(day.getValue())) continue;
+ setNumeric(sheet, FIRST_DATA_ROW_INDEX + day.getKey() - 1, yearColumn, values.get(day.getValue()));
+ }
+ if (comparisonBaseColumn != null && HolidayConstants.supportsComparisonBase(year)) {
+ Map<LocalDate, BigDecimal> bases = loadComparisonBaseValues(year);
+ for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) {
+ if (!bases.containsKey(day.getValue())) continue;
+ setNumeric(sheet, FIRST_DATA_ROW_INDEX + day.getKey() - 1, comparisonBaseColumn, bases.get(day.getValue()));
+ }
+ for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) {
+ if (!bases.containsKey(day.getValue())) continue;
+ int rowIndex = FIRST_DATA_ROW_INDEX + day.getKey() - 1;
+ int excelRow = rowIndex + 1;
+ setFormula(sheet, rowIndex, 4, "=(D" + excelRow + "-C" + excelRow + ")/C" + excelRow);
+ }
+ }
+ }
+ }
+
+ private void fillCoefficients(XSSFWorkbook workbook, FormulaEvaluator evaluator, int selectedYear, String holidayType) {
+ fillCoefficientSheet(workbook, evaluator, "楂橀�熷叕璺�", 8, 10, holidayType);
+ fillCoefficientSheet(workbook, evaluator, "鏅�氬浗鐪侀亾", 9, 11, holidayType);
+ }
+
+ private void fillCoefficientSheet(XSSFWorkbook workbook, FormulaEvaluator evaluator, String sheetName,
+ int startColumn, int endColumn, String holidayType) {
+ Sheet sheet = workbook.getSheet(sheetName);
+ if (sheet == null) return;
+ for (int year : REFERENCE_YEARS) {
+ Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn);
+ if (yearColumn == null) continue;
+ BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY);
+ if (coefficient == null) continue;
+ Map<Integer, LocalDate> dayDates = dayDates(year, holidayType);
+ for (Integer dayIndex : dayDates.keySet()) {
+ setNumeric(sheet, FIRST_DATA_ROW_INDEX + dayIndex - 1, yearColumn, coefficient);
+ }
+ }
+ }
+
+ private void seedNationalDayCalendar() {
+ // Calendar is admin-managed after the first bootstrap. Re-adding missing day indexes on
+ // every start would resurrect days the admin deleted, so only seed a fresh table.
+ if (!configService.listHolidays(null, null).isEmpty()) {
+ return;
+ }
+ int[] lengths = {7, 8, 8};
+ for (int i = 0; i < REFERENCE_YEARS.length; i++) {
+ int year = REFERENCE_YEARS[i];
+ List<HolidayCalendar> existing = configService.findHolidayDays(year, HolidayConstants.TYPE_NATIONAL_DAY);
+ Map<Integer, HolidayCalendar> byDay = new LinkedHashMap<>();
+ for (HolidayCalendar row : existing) {
+ byDay.put(row.getDayIndex(), row);
+ }
+ for (int day = 1; day <= lengths[i]; day++) {
+ if (byDay.containsKey(day)) continue;
+ HolidayCalendar calendar = new HolidayCalendar();
+ calendar.setYear(year);
+ calendar.setHolidayType(HolidayConstants.TYPE_NATIONAL_DAY);
+ calendar.setHolidayDate(LocalDate.of(year, 10, day));
+ calendar.setDayIndex(day);
+ calendar.setCreatedAt(LocalDateTime.now());
+ calendar.setUpdatedAt(LocalDateTime.now());
+ configService.saveHoliday(toMap(calendar));
+ }
+ }
+ }
+
+ private void seedPassengerYear(Workbook workbook, FormulaEvaluator evaluator, int year) {
+ Map<Integer, LocalDate> dayDates = dayDates(year, HolidayConstants.TYPE_NATIONAL_DAY);
+ if (dayDates.isEmpty()) return;
+ for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) {
+ Sheet sheet = workbook.getSheet(entry.getKey());
+ if (sheet == null) continue;
+ Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7);
+ if (yearColumn == null) continue;
+ for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) {
+ Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1);
+ if (row == null) continue;
+ BigDecimal value = numberValue(row.getCell(yearColumn), evaluator);
+ if (value != null) {
+ upsertPassenger(year, day.getValue(), entry.getValue(), value);
+ }
+ }
+ }
+ }
+
+ private void seedVehicleYear(Workbook workbook, FormulaEvaluator evaluator, int year) {
+ Map<Integer, LocalDate> dayDates = dayDates(year, HolidayConstants.TYPE_NATIONAL_DAY);
+ if (dayDates.isEmpty()) return;
+ seedVehicleSheet(workbook, evaluator, year, dayDates, "楂橀�熷叕璺�",
+ HolidayConstants.ROAD_EXPRESSWAY, HolidayConstants.VEHICLE_SMALL_PASSENGER, 2, 7);
+ seedVehicleSheet(workbook, evaluator, year, dayDates, "鏅�氬浗鐪侀亾",
+ HolidayConstants.ROAD_NATIONAL_PROVINCIAL, HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER, 3, 8);
+ }
+
+ private void seedVehicleSheet(Workbook workbook, FormulaEvaluator evaluator, int year,
+ Map<Integer, LocalDate> dayDates, String sheetName,
+ String roadCategory, String vehicleType, int startColumn, int endColumn) {
+ Sheet sheet = workbook.getSheet(sheetName);
+ if (sheet == null) return;
+ Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn);
+ if (yearColumn == null) return;
+ for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) {
+ Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1);
+ if (row == null) continue;
+ BigDecimal value = numberValue(row.getCell(yearColumn), evaluator);
+ if (value != null) {
+ upsertVehicle(year, day.getValue(), roadCategory, vehicleType, value, null);
+ }
+ }
+ }
+
+ private void seedCoefficientYear(Workbook workbook, FormulaEvaluator evaluator, int year) {
+ List<BigDecimal> values = new ArrayList<>();
+ collectCoefficientValues(workbook, evaluator, "楂橀�熷叕璺�", year, 8, 10, values);
+ collectCoefficientValues(workbook, evaluator, "鏅�氬浗鐪侀亾", year, 9, 11, values);
+ if (values.isEmpty()) return;
+ BigDecimal sum = BigDecimal.ZERO;
+ for (BigDecimal value : values) {
+ sum = HolidayNumberUtils.normalize(sum.add(value));
+ }
+ BigDecimal coefficient = HolidayNumberUtils.divide(sum, BigDecimal.valueOf(values.size()));
+ upsertCoefficient(year, HolidayConstants.CATEGORY_HOLIDAY, coefficient);
+ upsertCoefficient(year, HolidayConstants.CATEGORY_NON_HOLIDAY, coefficient);
+ }
+
+ private void collectCoefficientValues(Workbook workbook, FormulaEvaluator evaluator, String sheetName,
+ int year, int startColumn, int endColumn, List<BigDecimal> values) {
+ Sheet sheet = workbook.getSheet(sheetName);
+ if (sheet == null) return;
+ Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn);
+ if (yearColumn == null) return;
+ for (int rowIndex = FIRST_DATA_ROW_INDEX; rowIndex < FIRST_DATA_ROW_INDEX + 10; rowIndex++) {
+ Row row = sheet.getRow(rowIndex);
+ if (row == null) continue;
+ try {
+ BigDecimal value = numberValue(row.getCell(yearColumn), evaluator);
+ if (value != null && value.compareTo(BigDecimal.ZERO) > 0) values.add(value);
+ } catch (Exception ignored) {
+ // Skip cells that are not numeric coefficients.
+ }
+ }
+ }
+
+ private Map<Integer, LocalDate> dayDates(int year, String holidayType) {
+ Map<Integer, LocalDate> result = new LinkedHashMap<>();
+ for (HolidayCalendar row : configService.findHolidayDays(year, holidayType)) {
+ result.put(row.getDayIndex(), row.getHolidayDate());
+ }
+ return result;
+ }
+
+ private Map<LocalDate, BigDecimal> loadPassengerValues(int year, String transportMode) {
+ Map<LocalDate, BigDecimal> result = new LinkedHashMap<>();
+ List<HolidayPassengerFlow> rows = passengerFlowMapper.selectList(
+ new LambdaQueryWrapper<HolidayPassengerFlow>()
+ .eq(HolidayPassengerFlow::getYear, year)
+ .eq(HolidayPassengerFlow::getTransportMode, transportMode));
+ for (HolidayPassengerFlow row : rows) result.put(row.getFlowDate(), row.getPassengerCount());
+ return result;
+ }
+
+ private Map<LocalDate, BigDecimal> loadVehicleValues(int year, String roadCategory) {
+ Map<LocalDate, BigDecimal> result = new LinkedHashMap<>();
+ List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList(
+ new LambdaQueryWrapper<HolidayVehicleFlow>()
+ .eq(HolidayVehicleFlow::getYear, year)
+ .eq(HolidayVehicleFlow::getRoadCategory, roadCategory));
+ for (HolidayVehicleFlow row : rows) result.put(row.getFlowDate(), row.getVehicleCount());
+ return result;
+ }
+
+ private Map<LocalDate, BigDecimal> loadComparisonBaseValues(int year) {
+ Map<LocalDate, BigDecimal> result = new LinkedHashMap<>();
+ List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList(
+ new LambdaQueryWrapper<HolidayVehicleFlow>()
+ .eq(HolidayVehicleFlow::getYear, year)
+ .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL));
+ for (HolidayVehicleFlow row : rows) {
+ if (row.getComparisonBase() != null) result.put(row.getFlowDate(), row.getComparisonBase());
+ }
+ return result;
+ }
+
+ private void upsertPassenger(int year, LocalDate date, String mode, BigDecimal value) {
+ HolidayPassengerFlow existing = passengerFlowMapper.selectOne(new LambdaQueryWrapper<HolidayPassengerFlow>()
+ .eq(HolidayPassengerFlow::getYear, year)
+ .eq(HolidayPassengerFlow::getFlowDate, date)
+ .eq(HolidayPassengerFlow::getTransportMode, mode)
+ .last("limit 1"));
+ if (existing == null) {
+ existing = new HolidayPassengerFlow();
+ existing.setYear(year);
+ existing.setFlowDate(date);
+ existing.setTransportMode(mode);
+ existing.setPassengerCount(value);
+ existing.setCreatedAt(LocalDateTime.now());
+ existing.setUpdatedAt(LocalDateTime.now());
+ passengerFlowMapper.insert(existing);
+ } else {
+ existing.setPassengerCount(value);
+ existing.setUpdatedAt(LocalDateTime.now());
+ passengerFlowMapper.updateById(existing);
+ }
+ }
+
+ private void upsertVehicle(int year, LocalDate date, String roadCategory, String vehicleType,
+ BigDecimal value, BigDecimal comparisonBase) {
+ HolidayVehicleFlow existing = vehicleFlowMapper.selectOne(new LambdaQueryWrapper<HolidayVehicleFlow>()
+ .eq(HolidayVehicleFlow::getYear, year)
+ .eq(HolidayVehicleFlow::getFlowDate, date)
+ .eq(HolidayVehicleFlow::getRoadCategory, roadCategory)
+ .eq(HolidayVehicleFlow::getVehicleType, vehicleType)
+ .last("limit 1"));
+ if (existing == null) {
+ existing = new HolidayVehicleFlow();
+ existing.setYear(year);
+ existing.setFlowDate(date);
+ existing.setRoadCategory(roadCategory);
+ existing.setVehicleType(vehicleType);
+ existing.setVehicleCount(value);
+ existing.setComparisonBase(comparisonBase);
+ existing.setCreatedAt(LocalDateTime.now());
+ existing.setUpdatedAt(LocalDateTime.now());
+ vehicleFlowMapper.insert(existing);
+ } else {
+ existing.setVehicleCount(value);
+ existing.setComparisonBase(comparisonBase);
+ existing.setUpdatedAt(LocalDateTime.now());
+ vehicleFlowMapper.updateById(existing);
+ }
+ }
+
+ private void upsertCoefficient(int year, String dateCategory, BigDecimal value) {
+ HolidayPassengerCoefficient existing = coefficientMapper.selectOne(
+ new LambdaQueryWrapper<HolidayPassengerCoefficient>()
+ .eq(HolidayPassengerCoefficient::getYear, year)
+ .eq(HolidayPassengerCoefficient::getDateCategory, dateCategory)
+ .last("limit 1"));
+ if (existing == null) {
+ existing = new HolidayPassengerCoefficient();
+ existing.setYear(year);
+ existing.setDateCategory(dateCategory);
+ existing.setCoefficient(value);
+ existing.setCreatedAt(LocalDateTime.now());
+ existing.setUpdatedAt(LocalDateTime.now());
+ coefficientMapper.insert(existing);
+ } else {
+ existing.setCoefficient(value);
+ existing.setUpdatedAt(LocalDateTime.now());
+ coefficientMapper.updateById(existing);
+ }
+ }
+
+ private Integer findYearColumn(Sheet sheet, FormulaEvaluator evaluator, int year, int startColumn, int endColumn) {
+ Row header = sheet.getRow(1);
+ if (header == null) return null;
+ String wanted = year + "骞�";
+ DataFormatter formatter = new DataFormatter();
+ for (int column = startColumn; column <= endColumn; column++) {
+ Cell cell = header.getCell(column);
+ if (cell == null) continue;
+ String text = formatter.formatCellValue(cell, evaluator).replace("\n", "").trim();
+ if (wanted.equals(text) || text.startsWith(wanted + "绱")) return column;
+ }
+ return null;
+ }
+
+ private Integer parseDayIndex(Cell cell, FormulaEvaluator evaluator) {
+ if (cell == null) return null;
+ DataFormatter formatter = new DataFormatter();
+ String text = formatter.formatCellValue(cell, evaluator).trim();
+ java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("绗琝\s*(\\d+)\\s*鏃�").matcher(text);
+ return matcher.find() ? Integer.valueOf(matcher.group(1)) : null;
+ }
+
+ private BigDecimal numberValue(Cell cell, FormulaEvaluator evaluator) {
+ if (cell == null) return null;
+ CellType type = cell.getCellType();
+ if (type == CellType.FORMULA) {
+ type = evaluator.evaluateFormulaCell(cell);
+ }
+ if (type == CellType.BLANK) return null;
+ if (type == CellType.NUMERIC) return HolidayNumberUtils.excelValue(cell.getNumericCellValue());
+ if (type == CellType.STRING) {
+ String text = cell.getStringCellValue().trim().replace(",", "");
+ if (text.isEmpty() || "-".equals(text) || "鈥�".equals(text)) return null;
+ try {
+ return HolidayNumberUtils.decimalValue(text);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("鍗曞厓鏍间笉鏄湁鏁堟暟瀛�: " + text);
+ }
+ }
+ if (type == CellType.BOOLEAN) throw new IllegalArgumentException("鍗曞厓鏍间笉鑳芥槸甯冨皵鍊�");
+ throw new IllegalArgumentException("鍗曞厓鏍间笉鏄湁鏁堟暟瀛�");
+ }
+
+ private void setNumeric(Sheet sheet, int rowIndex, int columnIndex, BigDecimal value) {
+ if (value == null) return;
+ Row row = sheet.getRow(rowIndex);
+ if (row == null) row = sheet.createRow(rowIndex);
+ Cell cell = row.getCell(columnIndex);
+ if (cell == null) cell = row.createCell(columnIndex);
+ cell.setCellValue(value.doubleValue());
+ }
+
+ private void setFormula(Sheet sheet, int rowIndex, int columnIndex, String formula) {
+ Row row = sheet.getRow(rowIndex);
+ if (row == null) row = sheet.createRow(rowIndex);
+ Cell cell = row.getCell(columnIndex);
+ if (cell == null) cell = row.createCell(columnIndex);
+ cell.setCellFormula(formula);
+ }
+
+ private Map<String, Object> toMap(HolidayCalendar calendar) {
+ Map<String, Object> params = new LinkedHashMap<>();
+ params.put("year", calendar.getYear());
+ params.put("holidayType", calendar.getHolidayType());
+ params.put("holidayDate", calendar.getHolidayDate().toString());
+ params.put("dayIndex", calendar.getDayIndex());
+ return params;
+ }
+
+ @Data
+ public static class ImportSummary {
+ private int passengerRows;
+ private int vehicleRows;
+ private int skippedRows;
+ private boolean confirmationRequired;
+ private final List<String> errors = new ArrayList<>();
+ private final List<String> historicalChanges = new ArrayList<>();
+
+ public void incrementPassenger() {
+ passengerRows++;
+ }
+
+ public void incrementVehicle() {
+ vehicleRows++;
+ }
+
+ public void incrementSkipped() {
+ skippedRows++;
+ }
+
+ public void addError(String message) {
+ errors.add(message);
+ }
+
+ public void addHistoricalChange(String message) {
+ if (historicalChanges.size() < 200) {
+ historicalChanges.add(message);
+ }
+ }
+
+ public int getSuccessRows() {
+ return passengerRows + vehicleRows;
+ }
+
+ public int getFailRows() {
+ return errors.size();
+ }
+ }
+}
diff --git a/traffic-audit-web/src/main.js b/traffic-audit-web/src/main.js
index 7937fbf..0413f43 100644
--- a/traffic-audit-web/src/main.js
+++ b/traffic-audit-web/src/main.js
@@ -11,6 +11,11 @@
// 鍏ㄥ眬鐧诲嚭锛氭竻闄ょ櫥褰曟�佸苟鍥炵櫥褰曢〉锛堝悇椤甸潰渚ц竟鏍忋�岄��鍑虹櫥褰曘�嶈彍鍗曞叡鐢級
Vue.mixin({
+ computed: {
+ operatorIsAdmin() {
+ return sessionStorage.getItem('username') === 'admin'
+ }
+ },
methods: {
// el-menu 鍚岃矾鐢卞鑸紙NavigationDuplicated锛夋椂 Element UI 浼氭妸楂樹寒鍥炴粴涓虹偣鍑诲墠鏃у�硷紱
// 鐐瑰嚮鍚庡欢杩熶竴涓畯浠诲姟寮哄埗绾犳锛堝畯浠诲姟鏅氫簬 push 鐨勫井浠诲姟鍥炶皟锛屼繚璇佹渶缁堥珮浜纭級銆�
diff --git a/traffic-audit-web/src/router/index.js b/traffic-audit-web/src/router/index.js
index 6316dc6..93d08e7 100644
--- a/traffic-audit-web/src/router/index.js
+++ b/traffic-audit-web/src/router/index.js
@@ -14,7 +14,9 @@
{ path: '/audit', name: 'AuditResult', component: () => import('@/views/AuditResult.vue') },
{ path: '/report', name: 'ReportExport', component: () => import('@/views/ReportExport.vue') },
{ path: '/rules', name: 'RuleManage', component: () => import('@/views/RuleManage.vue') },
- { path: '/users', name: 'UserManage', component: () => import('@/views/UserManage.vue') }
+ { path: '/users', name: 'UserManage', component: () => import('@/views/UserManage.vue') },
+ { path: '/holiday-stat', name: 'HolidayStat', component: () => import('@/views/HolidayStat.vue') },
+ { path: '/system-config', name: 'SystemConfig', component: () => import('@/views/SystemConfig.vue'), meta: { requireAdmin: true } }
]
})
@@ -22,8 +24,12 @@
const token = sessionStorage.getItem('token')
if (to.path === '/login') {
next(token ? '/dashboard' : undefined)
+ } else if (!token) {
+ next('/login')
+ } else if (to.meta && to.meta.requireAdmin && sessionStorage.getItem('username') !== 'admin') {
+ next('/dashboard')
} else {
- next(token ? undefined : '/login')
+ next()
}
})
diff --git a/traffic-audit-web/src/views/AuditResult.vue b/traffic-audit-web/src/views/AuditResult.vue
index f1a2bfd..e3b218a 100644
--- a/traffic-audit-web/src/views/AuditResult.vue
+++ b/traffic-audit-web/src/views/AuditResult.vue
@@ -10,9 +10,14 @@
<el-menu-item index="/explain">浼佷笟瑙i噴</el-menu-item>
<el-menu-item index="/audit">瀹℃牳缁撴灉</el-menu-item>
<el-menu-item index="/report">鎶ヨ〃鐢熸垚</el-menu-item>
+ <el-submenu index="/special-stats">
+ <template slot="title">涓撻」缁熻</template>
+ <el-menu-item index="/holiday-stat">鑺傚亣鏃�</el-menu-item>
+ </el-submenu>
<el-menu-item index="/rules">瑙勫垯绠$悊</el-menu-item>
<el-menu-item index="/users">鐢ㄦ埛绠$悊</el-menu-item>
-</el-menu>
+ <el-menu-item v-if="operatorIsAdmin" index="/system-config">绯荤粺閰嶇疆</el-menu-item>
+ </el-menu>
</el-aside>
<el-main :class="{ 'main-focus': focusMode }" style="display:flex;flex-direction:column;min-height:0">
<h3 v-show="!focusMode" style="flex:none;margin:0 0 10px">瀹℃牳缁撴灉</h3>
diff --git a/traffic-audit-web/src/views/Dashboard.vue b/traffic-audit-web/src/views/Dashboard.vue
index ea88d0e..5753ac3 100644
--- a/traffic-audit-web/src/views/Dashboard.vue
+++ b/traffic-audit-web/src/views/Dashboard.vue
@@ -10,8 +10,14 @@
<el-menu-item index="/explain">浼佷笟瑙i噴</el-menu-item>
<el-menu-item index="/audit">瀹℃牳缁撴灉</el-menu-item>
<el-menu-item index="/report">鎶ヨ〃鐢熸垚</el-menu-item>
+ <el-submenu index="/special-stats">
+ <template slot="title">涓撻」缁熻</template>
+ <el-menu-item index="/holiday-stat">鑺傚亣鏃�</el-menu-item>
+ </el-submenu>
<el-menu-item index="/rules">瑙勫垯绠$悊</el-menu-item>
- <el-menu-item index="/users">鐢ㄦ埛绠$悊</el-menu-item></el-menu>
+ <el-menu-item index="/users">鐢ㄦ埛绠$悊</el-menu-item>
+ <el-menu-item v-if="operatorIsAdmin" index="/system-config">绯荤粺閰嶇疆</el-menu-item>
+ </el-menu>
</el-aside>
<el-main>
<div class="page-scroll">
diff --git a/traffic-audit-web/src/views/DataImport.vue b/traffic-audit-web/src/views/DataImport.vue
index deb64c6..82abc05 100644
--- a/traffic-audit-web/src/views/DataImport.vue
+++ b/traffic-audit-web/src/views/DataImport.vue
@@ -10,9 +10,14 @@
<el-menu-item index="/explain">浼佷笟瑙i噴</el-menu-item>
<el-menu-item index="/audit">瀹℃牳缁撴灉</el-menu-item>
<el-menu-item index="/report">鎶ヨ〃鐢熸垚</el-menu-item>
+ <el-submenu index="/special-stats">
+ <template slot="title">涓撻」缁熻</template>
+ <el-menu-item index="/holiday-stat">鑺傚亣鏃�</el-menu-item>
+ </el-submenu>
<el-menu-item index="/rules">瑙勫垯绠$悊</el-menu-item>
<el-menu-item index="/users">鐢ㄦ埛绠$悊</el-menu-item>
-</el-menu>
+ <el-menu-item v-if="operatorIsAdmin" index="/system-config">绯荤粺閰嶇疆</el-menu-item>
+ </el-menu>
</el-aside>
<el-main>
<div class="page-scroll">
diff --git a/traffic-audit-web/src/views/DataView.vue b/traffic-audit-web/src/views/DataView.vue
index d72dc3a..6eb9ba0 100644
--- a/traffic-audit-web/src/views/DataView.vue
+++ b/traffic-audit-web/src/views/DataView.vue
@@ -10,9 +10,14 @@
<el-menu-item index="/explain">浼佷笟瑙i噴</el-menu-item>
<el-menu-item index="/audit">瀹℃牳缁撴灉</el-menu-item>
<el-menu-item index="/report">鎶ヨ〃鐢熸垚</el-menu-item>
+ <el-submenu index="/special-stats">
+ <template slot="title">涓撻」缁熻</template>
+ <el-menu-item index="/holiday-stat">鑺傚亣鏃�</el-menu-item>
+ </el-submenu>
<el-menu-item index="/rules">瑙勫垯绠$悊</el-menu-item>
<el-menu-item index="/users">鐢ㄦ埛绠$悊</el-menu-item>
-</el-menu>
+ <el-menu-item v-if="operatorIsAdmin" index="/system-config">绯荤粺閰嶇疆</el-menu-item>
+ </el-menu>
</el-aside>
<el-main>
<h3>瀵煎叆鏁版嵁鏌ョ湅</h3>
diff --git a/traffic-audit-web/src/views/ExplainReview.vue b/traffic-audit-web/src/views/ExplainReview.vue
index f622ecc..130272b 100644
--- a/traffic-audit-web/src/views/ExplainReview.vue
+++ b/traffic-audit-web/src/views/ExplainReview.vue
@@ -10,9 +10,14 @@
<el-menu-item index="/explain">浼佷笟瑙i噴</el-menu-item>
<el-menu-item index="/audit">瀹℃牳缁撴灉</el-menu-item>
<el-menu-item index="/report">鎶ヨ〃鐢熸垚</el-menu-item>
+ <el-submenu index="/special-stats">
+ <template slot="title">涓撻」缁熻</template>
+ <el-menu-item index="/holiday-stat">鑺傚亣鏃�</el-menu-item>
+ </el-submenu>
<el-menu-item index="/rules">瑙勫垯绠$悊</el-menu-item>
<el-menu-item index="/users">鐢ㄦ埛绠$悊</el-menu-item>
-</el-menu>
+ <el-menu-item v-if="operatorIsAdmin" index="/system-config">绯荤粺閰嶇疆</el-menu-item>
+ </el-menu>
</el-aside>
<el-main>
<h3>浼佷笟瑙i噴</h3>
diff --git a/traffic-audit-web/src/views/HolidayStat.vue b/traffic-audit-web/src/views/HolidayStat.vue
new file mode 100644
index 0000000..6f7f266
--- /dev/null
+++ b/traffic-audit-web/src/views/HolidayStat.vue
@@ -0,0 +1,302 @@
+<template>
+ <el-container direction="vertical">
+ <app-header title="浜ら�氱粺璁℃姤琛ㄥ鏍哥郴缁� - 涓撻」缁熻"></app-header>
+ <el-container>
+ <el-aside width="200px">
+ <el-menu ref="sideMenu" router :default-active="$route.path" @select="onMenuSelect">
+ <el-menu-item index="/dashboard">宸ヤ綔鍙�</el-menu-item>
+ <el-menu-item index="/import">鏁版嵁瀵煎叆</el-menu-item>
+ <el-menu-item index="/data">鏁版嵁鏌ョ湅</el-menu-item>
+ <el-menu-item index="/explain">浼佷笟瑙i噴</el-menu-item>
+ <el-menu-item index="/audit">瀹℃牳缁撴灉</el-menu-item>
+ <el-menu-item index="/report">鎶ヨ〃鐢熸垚</el-menu-item>
+ <el-submenu index="/special-stats">
+ <template slot="title">涓撻」缁熻</template>
+ <el-menu-item index="/holiday-stat">鑺傚亣鏃�</el-menu-item>
+ </el-submenu>
+ <el-menu-item index="/rules">瑙勫垯绠$悊</el-menu-item>
+ <el-menu-item index="/users">鐢ㄦ埛绠$悊</el-menu-item>
+ <el-menu-item v-if="operatorIsAdmin" index="/system-config">绯荤粺閰嶇疆</el-menu-item>
+ </el-menu>
+ </el-aside>
+ <el-main>
+ <div class="page-scroll">
+ <h3>涓撻」缁熻 - 鑺傚亣鏃�</h3>
+ <div class="toolbar">
+ <el-select v-model="year" style="width:120px" @change="loadOverview">
+ <el-option v-for="item in years" :key="item" :label="item + '骞�'" :value="item"></el-option>
+ </el-select>
+ <el-select v-model="holidayType" style="width:150px" @change="loadOverview">
+ <el-option v-for="item in holidayTypeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
+ </el-select>
+ <el-button icon="el-icon-refresh" @click="loadOverview">鍒锋柊</el-button>
+ <el-button icon="el-icon-download" @click="downloadTemplate">涓嬭浇妯℃澘</el-button>
+ </div>
+
+ <el-card shadow="never" class="operation-card">
+ <div class="operation-row">
+ <div class="operation-item">
+ <span class="label">瀵煎叆鏁版嵁</span>
+ <el-upload action="#" :auto-upload="false" :show-file-list="true" :limit="1"
+ accept=".xlsx,.xls" :on-change="onFileChange" :on-remove="onFileRemove" :file-list="fileList">
+ <el-button type="primary" plain icon="el-icon-folder-opened">閫夋嫨鏂囦欢</el-button>
+ </el-upload>
+ <el-button type="primary" icon="el-icon-upload2" :loading="importing" :disabled="!selectedFile" @click="importData">瀵煎叆</el-button>
+ </div>
+ <div class="operation-item">
+ <span class="label">瀵煎嚭鏁版嵁</span>
+ <el-button type="success" icon="el-icon-download" :loading="exporting" @click="exportData">瀵煎嚭 Excel</el-button>
+ </div>
+ <div class="operation-item">
+ <span class="label">瀹㈡祦蹇姤</span>
+ <el-date-picker v-model="bulletinDate" type="date" value-format="yyyy-MM-dd" style="width:160px" placeholder="閫夋嫨鏃ユ湡"></el-date-picker>
+ <el-button type="warning" icon="el-icon-document" :loading="bulletinLoading" @click="downloadBulletin">涓嬭浇蹇姤</el-button>
+ </div>
+ </div>
+ </el-card>
+
+ <div v-if="importMessage" class="import-message">
+ <pre>{{ importMessage }}</pre>
+ </div>
+
+ <el-card shadow="never" style="margin-top:14px">
+ <div slot="header">{{ year }}骞磠{ holidayTypeLabel }}鑺傚亣鏃ユ暟鎹�</div>
+ <el-table :data="rows" border stripe size="small" v-loading="loading" style="width:100%">
+ <el-table-column prop="dayIndex" label="绗嚑鏃�" width="80" align="center"></el-table-column>
+ <el-table-column prop="date" label="鏃ユ湡" width="120" align="center"></el-table-column>
+ <el-table-column label="閾佽矾锛堜竾浜烘锛�" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.rail) }}</template></el-table-column>
+ <el-table-column label="姘戣埅锛堜竾浜烘锛�" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.civilAviation) }}</template></el-table-column>
+ <el-table-column label="閬撹矾锛堜竾浜烘锛�" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.road) }}</template></el-table-column>
+ <el-table-column label="姘磋矾锛堜竾浜烘锛�" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.water) }}</template></el-table-column>
+ <el-table-column label="楂橀�熻溅娴侊紙涓囪締娆★級" width="150" align="right"><template slot-scope="s">{{ formatNumber(s.row.expressway) }}</template></el-table-column>
+ <el-table-column label="鍥界渷閬撹溅娴侊紙涓囪締娆★級" width="160" align="right"><template slot-scope="s">{{ formatNumber(s.row.nationalProvincial) }}</template></el-table-column>
+ <el-table-column label="鍥界渷閬撶幆姣斿熀鏈燂紙涓囪締娆★級" width="190" align="right"><template slot-scope="s">{{ formatNumber(s.row.comparisonBase) }}</template></el-table-column>
+ <el-table-column label="鍥界渷閬撹溅娴佺幆姣�" width="140" align="right"><template slot-scope="s">{{ formatRatio(s.row.nationalProvincialRingRatio) }}</template></el-table-column>
+ <el-table-column label="瀹㈣繍绯绘暟" width="100" align="right"><template slot-scope="s">{{ formatCoefficient(s.row.coefficient) }}</template></el-table-column>
+ <el-table-column label="闈炶惀涓氭�т汉鍛樻祦鍔ㄩ噺锛堜竾浜烘锛�" min-width="200" align="right"><template slot-scope="s">{{ formatNumber(s.row.nonBusinessPersonFlow) }}</template></el-table-column>
+ <el-table-column label="鎬讳汉鍛樻祦鍔ㄩ噺锛堜竾浜烘锛�" min-width="180" align="right"><template slot-scope="s">{{ formatNumber(s.row.totalPersonFlow) }}</template></el-table-column>
+ </el-table>
+ </el-card>
+ </div>
+ </el-main>
+ </el-container>
+ </el-container>
+</template>
+
+<script>
+import api from '@/api'
+import AppHeader from '@/components/AppHeader.vue'
+
+export default {
+ components: { AppHeader },
+ name: 'HolidayStat',
+ data() {
+ return {
+ years: [2024, 2025, 2026, 2027, 2028, 2029, 2030],
+ year: 2026,
+ holidayType: 'NATIONAL_DAY',
+ holidayTypeOptions: [],
+ rows: [],
+ loading: false,
+ importing: false,
+ exporting: false,
+ bulletinLoading: false,
+ selectedFile: null,
+ fileList: [],
+ importMessage: '',
+ allowHistoricalOverwrite: false,
+ bulletinDate: this.previousDateString()
+ }
+ },
+ computed: {
+ holidayTypeLabel() {
+ const item = this.holidayTypeOptions.find(x => x.value === this.holidayType)
+ return item ? item.label : this.holidayType
+ }
+ },
+ mounted() {
+ this.loadOptions()
+ this.loadOverview()
+ },
+ methods: {
+ previousDateString() {
+ const date = new Date()
+ date.setDate(date.getDate() - 1)
+ const month = String(date.getMonth() + 1).padStart(2, '0')
+ const day = String(date.getDate()).padStart(2, '0')
+ return date.getFullYear() + '-' + month + '-' + day
+ },
+ loadOptions() {
+ api.get('/holiday/config/options').then(res => {
+ const map = (res.data && res.data.holidayTypes) || {}
+ this.holidayTypeOptions = Object.keys(map).map(key => ({ value: key, label: map[key] }))
+ })
+ },
+ loadOverview() {
+ this.loading = true
+ api.get('/holiday/overview', { params: { year: this.year, holidayType: this.holidayType } })
+ .then(res => {
+ if (res.code !== 200) throw new Error(res.message || '鍔犺浇澶辫触')
+ this.rows = (res.data && res.data.rows) || []
+ })
+ .catch(err => this.showError(err, '鑺傚亣鏃ユ暟鎹姞杞藉け璐�'))
+ .finally(() => { this.loading = false })
+ },
+ onFileChange(file) {
+ this.selectedFile = file.raw
+ this.fileList = [file]
+ this.importMessage = ''
+ },
+ onFileRemove() {
+ this.selectedFile = null
+ this.fileList = []
+ },
+ importData(allowHistoricalOverwrite) {
+ if (!this.selectedFile) return
+ const allowOverwrite = allowHistoricalOverwrite === true || this.allowHistoricalOverwrite === true
+ const form = new FormData()
+ form.append('file', this.selectedFile)
+ form.append('year', this.year)
+ form.append('holidayType', this.holidayType)
+ form.append('allowHistoricalOverwrite', allowOverwrite ? 'true' : 'false')
+ this.importing = true
+ this.importMessage = ''
+ api.post('/holiday/import', form, { timeout: 120000, headers: { 'Content-Type': 'multipart/form-data' } })
+ .then(res => {
+ if (res.code !== 200) throw new Error(res.message || '瀵煎叆澶辫触')
+ const data = res.data || {}
+ if (data.confirmationRequired) {
+ const changes = data.historicalChanges || []
+ this.importMessage = ['妫�娴嬪埌鍘嗗彶鏃ユ湡鐨勬暟鎹彂鐢熷彉鍖栵紝灏氭湭瀵煎叆銆傝纭鏄惁鍏佽瑕嗙洊锛�'].concat(changes).join('\n')
+ this.$message.warning('妫�娴嬪埌鍘嗗彶鏁版嵁鍙戠敓鍙樺寲锛岃纭鏄惁鍏佽瀵煎叆')
+ this.confirmHistoricalImport(changes)
+ return
+ }
+ const lines = [
+ '瀵煎叆瀹屾垚锛氬娴� ' + (data.passengerRows || 0) + ' 鏉★紝杞︽祦 ' + (data.vehicleRows || 0) + ' 鏉★紝璺宠繃绌虹櫧 ' + (data.skippedRows || 0) + ' 鏉°��'
+ ]
+ if (data.errors && data.errors.length) {
+ lines.push('澶辫触鏄庣粏锛�')
+ data.errors.forEach(item => lines.push(item))
+ }
+ this.importMessage = lines.join('\n')
+ this.allowHistoricalOverwrite = false
+ this.$message.success('瀵煎叆瀹屾垚')
+ this.loadOverview()
+ })
+ .catch(err => {
+ this.allowHistoricalOverwrite = false
+ this.showError(err, '瀵煎叆澶辫触')
+ })
+ .finally(() => { this.importing = false })
+ },
+ confirmHistoricalImport(changes) {
+ const rows = (changes && changes.length ? changes : ['鍘嗗彶鏁版嵁鍙戠敓鍙樺寲']).map(this.escapeHtml)
+ const html = '<div>妫�娴嬪埌宸插彂甯冩棩鏈熺殑鏁版嵁鍙戠敓鍙樺寲銆傚厑璁稿鍏ュ皢瑕嗙洊杩欎簺鍘嗗彶鏁版嵁銆�</div>'
+ + '<pre style="max-height:320px;overflow:auto;white-space:pre-wrap;margin-top:10px;background:#f7f7f7;padding:8px;border:1px solid #e4e7ed;">'
+ + rows.join('\n') + '</pre>'
+ this.$confirm(html, '鍘嗗彶鏁版嵁鍙戠敓鍙樺寲', {
+ dangerouslyUseHTMLString: true,
+ confirmButtonText: '鍏佽瀵煎叆',
+ cancelButtonText: '涓嶅厑璁�',
+ type: 'warning',
+ distinguishCancelAndClose: true
+ }).then(() => {
+ this.allowHistoricalOverwrite = true
+ this.importData(true)
+ }).catch(() => {
+ this.allowHistoricalOverwrite = false
+ })
+ },
+ escapeHtml(value) {
+ return String(value === null || value === undefined ? '' : value)
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+ },
+ exportData() {
+ this.exporting = true
+ api.get('/holiday/export', { params: { year: this.year, holidayType: this.holidayType }, responseType: 'blob', timeout: 120000 })
+ .then(res => { this.downloadBlob(res, this.year + '骞�' + this.holidayTypeLabel + '鏁版嵁.xlsx'); this.$message.success('瀵煎嚭鎴愬姛') })
+ .catch(err => this.handleBlobError(err, '瀵煎嚭澶辫触'))
+ .finally(() => { this.exporting = false })
+ },
+ downloadTemplate() {
+ const templatePath = '涓撻」缁熻/閲嶈鑺傚亣鏃�/2026骞村崄涓�鏁版嵁.xlsx'
+ const encodedPath = templatePath.split('/').map(encodeURIComponent).join('/')
+ api.get('/templates/' + encodedPath, { responseType: 'blob' })
+ .then(res => { this.downloadBlob(res, '2026骞村崄涓�鏁版嵁.xlsx'); this.$message.success('妯℃澘涓嬭浇鎴愬姛') })
+ .catch(err => this.handleBlobError(err, '妯℃澘涓嬭浇澶辫触'))
+ },
+ downloadBulletin() {
+ this.bulletinLoading = true
+ api.get('/holiday/bulletin', { params: { date: this.bulletinDate }, responseType: 'blob', timeout: 120000 })
+ .then(res => {
+ const day = Number((this.bulletinDate || '').slice(-2))
+ this.downloadBlob(res, '婀栧寳鐪佸浗搴嗗亣鏈熺患鍚堣繍杈撳娴佸揩鎶ワ紙10鏈�' + day + '鏃ワ級.docx')
+ this.$message.success('蹇姤涓嬭浇鎴愬姛')
+ })
+ .catch(err => this.handleBlobError(err, '蹇姤鐢熸垚澶辫触'))
+ .finally(() => { this.bulletinLoading = false })
+ },
+ downloadBlob(data, fileName) {
+ const blob = data instanceof Blob ? data : new Blob([data])
+ const url = window.URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = fileName
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ window.URL.revokeObjectURL(url)
+ },
+ handleBlobError(err, fallback) {
+ if (err && err.response && err.response.data instanceof Blob) {
+ err.response.data.text().then(text => {
+ try {
+ const parsed = JSON.parse(text)
+ this.$message.error(parsed.message || fallback)
+ } catch (e) {
+ this.$message.error(fallback)
+ }
+ })
+ return
+ }
+ this.showError(err, fallback)
+ },
+ showError(err, fallback) {
+ if (err && err._toast) return
+ const data = err && err.response && err.response.data
+ this.$message.error((data && data.message) || (err && err.message) || fallback)
+ },
+ formatNumber(value) {
+ if (value === null || value === undefined || value === '') return '鈥�'
+ return Number(value).toFixed(2)
+ },
+ formatCoefficient(value) {
+ if (value === null || value === undefined || value === '') return '鈥�'
+ const number = Number(value)
+ if (!Number.isFinite(number)) return value
+ return number.toFixed(15).replace(/0+$/, '').replace(/\.$/, '')
+ },
+ formatRatio(value) {
+ if (value === null || value === undefined || value === '') return '鈥�'
+ const number = Number(value)
+ if (!Number.isFinite(number)) return value
+ return (number * 100).toFixed(2) + '%'
+ }
+ }
+}
+</script>
+
+<style scoped>
+.toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }
+.operation-card { margin-top: 12px; }
+.operation-row { display: flex; gap: 36px; align-items: flex-start; flex-wrap: wrap; }
+.operation-item { display: flex; align-items: center; gap: 8px; }
+.operation-item .label { color: #606266; font-size: 14px; }
+.import-message { margin-top: 12px; padding: 10px 12px; background: #f5f7fa; border: 1px solid #e4e7ed; border-radius: 4px; }
+.import-message pre { margin: 0; white-space: pre-wrap; font-family: inherit; color: #606266; }
+</style>
diff --git a/traffic-audit-web/src/views/ReportExport.vue b/traffic-audit-web/src/views/ReportExport.vue
index d9e76d3..bb53610 100644
--- a/traffic-audit-web/src/views/ReportExport.vue
+++ b/traffic-audit-web/src/views/ReportExport.vue
@@ -10,8 +10,13 @@
<el-menu-item index="/explain">浼佷笟瑙i噴</el-menu-item>
<el-menu-item index="/audit">瀹℃牳缁撴灉</el-menu-item>
<el-menu-item index="/report">鎶ヨ〃鐢熸垚</el-menu-item>
+ <el-submenu index="/special-stats">
+ <template slot="title">涓撻」缁熻</template>
+ <el-menu-item index="/holiday-stat">鑺傚亣鏃�</el-menu-item>
+ </el-submenu>
<el-menu-item index="/rules">瑙勫垯绠$悊</el-menu-item>
<el-menu-item index="/users">鐢ㄦ埛绠$悊</el-menu-item>
+ <el-menu-item v-if="operatorIsAdmin" index="/system-config">绯荤粺閰嶇疆</el-menu-item>
</el-menu>
</el-aside>
<el-main>
diff --git a/traffic-audit-web/src/views/RuleManage.vue b/traffic-audit-web/src/views/RuleManage.vue
index 260f61c..2322a65 100644
--- a/traffic-audit-web/src/views/RuleManage.vue
+++ b/traffic-audit-web/src/views/RuleManage.vue
@@ -10,8 +10,13 @@
<el-menu-item index="/explain">浼佷笟瑙i噴</el-menu-item>
<el-menu-item index="/audit">瀹℃牳缁撴灉</el-menu-item>
<el-menu-item index="/report">鎶ヨ〃鐢熸垚</el-menu-item>
+ <el-submenu index="/special-stats">
+ <template slot="title">涓撻」缁熻</template>
+ <el-menu-item index="/holiday-stat">鑺傚亣鏃�</el-menu-item>
+ </el-submenu>
<el-menu-item index="/rules">瑙勫垯绠$悊</el-menu-item>
<el-menu-item index="/users">鐢ㄦ埛绠$悊</el-menu-item>
+ <el-menu-item v-if="operatorIsAdmin" index="/system-config">绯荤粺閰嶇疆</el-menu-item>
</el-menu>
</el-aside>
<el-main>
diff --git a/traffic-audit-web/src/views/SystemConfig.vue b/traffic-audit-web/src/views/SystemConfig.vue
new file mode 100644
index 0000000..f9df604
--- /dev/null
+++ b/traffic-audit-web/src/views/SystemConfig.vue
@@ -0,0 +1,295 @@
+<template>
+ <el-container direction="vertical">
+ <app-header title="浜ら�氱粺璁℃姤琛ㄥ鏍哥郴缁� - 绯荤粺閰嶇疆"></app-header>
+ <el-container>
+ <el-aside width="200px">
+ <el-menu ref="sideMenu" router :default-active="$route.path" @select="onMenuSelect">
+ <el-menu-item index="/dashboard">宸ヤ綔鍙�</el-menu-item>
+ <el-menu-item index="/import">鏁版嵁瀵煎叆</el-menu-item>
+ <el-menu-item index="/data">鏁版嵁鏌ョ湅</el-menu-item>
+ <el-menu-item index="/explain">浼佷笟瑙i噴</el-menu-item>
+ <el-menu-item index="/audit">瀹℃牳缁撴灉</el-menu-item>
+ <el-menu-item index="/report">鎶ヨ〃鐢熸垚</el-menu-item>
+ <el-submenu index="/special-stats">
+ <template slot="title">涓撻」缁熻</template>
+ <el-menu-item index="/holiday-stat">鑺傚亣鏃�</el-menu-item>
+ </el-submenu>
+ <el-menu-item index="/rules">瑙勫垯绠$悊</el-menu-item>
+ <el-menu-item index="/users">鐢ㄦ埛绠$悊</el-menu-item>
+ <el-menu-item v-if="operatorIsAdmin" index="/system-config">绯荤粺閰嶇疆</el-menu-item>
+ </el-menu>
+ </el-aside>
+ <el-main>
+ <div class="page-scroll">
+ <h3>绯荤粺閰嶇疆</h3>
+ <el-tabs v-model="activeTab" @tab-click="loadActiveTab">
+ <el-tab-pane label="鑺傚亣鏃ョ鐞�" name="holidays">
+ <div class="toolbar">
+ <el-select v-model="holidayQuery.year" clearable placeholder="骞翠唤" style="width:130px" @change="loadHolidays">
+ <el-option v-for="year in years" :key="year" :label="year + '骞�'" :value="year"></el-option>
+ </el-select>
+ <el-select v-model="holidayQuery.holidayType" clearable placeholder="鑺傚亣鏃ュ垎绫�" style="width:150px" @change="loadHolidays">
+ <el-option v-for="item in holidayTypeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
+ </el-select>
+ <el-button type="primary" icon="el-icon-search" @click="loadHolidays">鏌ヨ</el-button>
+ <el-button type="success" icon="el-icon-plus" @click="openHolidayDialog()">鏂板鑺傚亣鏃�</el-button>
+ </div>
+ <el-table :data="holidays" border stripe size="small" v-loading="loadingHolidays" style="width:100%">
+ <el-table-column prop="year" label="骞翠唤" width="100" align="center"></el-table-column>
+ <el-table-column label="鑺傚亣鏃ュ垎绫�" width="140">
+ <template slot-scope="scope">{{ labelOf(holidayTypeOptions, scope.row.holidayType) }}</template>
+ </el-table-column>
+ <el-table-column prop="holidayDate" label="鏃ユ湡" width="150" align="center"></el-table-column>
+ <el-table-column prop="dayIndex" label="绗嚑鏃�" width="100" align="center"></el-table-column>
+ <el-table-column label="鎿嶄綔" width="150" align="center">
+ <template slot-scope="scope">
+ <el-button type="text" size="mini" @click="openHolidayDialog(scope.row)">缂栬緫</el-button>
+ <el-button type="text" size="mini" class="danger-text" @click="removeHoliday(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-tab-pane>
+
+ <el-tab-pane label="瀹㈣繍绯绘暟绠$悊" name="coefficients">
+ <div class="toolbar">
+ <el-select v-model="coefficientQuery.year" clearable placeholder="骞翠唤" style="width:130px" @change="loadCoefficients">
+ <el-option v-for="year in years" :key="year" :label="year + '骞�'" :value="year"></el-option>
+ </el-select>
+ <el-button type="primary" icon="el-icon-search" @click="loadCoefficients">鏌ヨ</el-button>
+ <el-button type="success" icon="el-icon-plus" @click="openCoefficientDialog()">鏂板瀹㈣繍绯绘暟</el-button>
+ </div>
+ <el-table :data="coefficients" border stripe size="small" v-loading="loadingCoefficients" style="width:100%">
+ <el-table-column prop="year" label="骞翠唤" width="120" align="center"></el-table-column>
+ <el-table-column label="鏃ユ湡鍒嗙被" width="160">
+ <template slot-scope="scope">{{ labelOf(dateCategoryOptions, scope.row.dateCategory) }}</template>
+ </el-table-column>
+ <el-table-column label="瀹㈣繍绯绘暟" width="160" align="center">
+ <template slot-scope="scope">{{ formatCoefficient(scope.row.coefficient) }}</template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="150" align="center">
+ <template slot-scope="scope">
+ <el-button type="text" size="mini" @click="openCoefficientDialog(scope.row)">缂栬緫</el-button>
+ <el-button type="text" size="mini" class="danger-text" @click="removeCoefficient(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+
+ <el-dialog :title="holidayForm.id ? '缂栬緫鑺傚亣鏃�' : '鏂板鑺傚亣鏃�'" :visible.sync="holidayDialog" width="520px" v-dialog-drag>
+ <el-form ref="holidayForm" :model="holidayForm" :rules="holidayRules" label-width="110px">
+ <el-form-item label="骞翠唤" prop="year">
+ <el-select v-model="holidayForm.year" style="width:100%" @change="onHolidayYearChange">
+ <el-option v-for="year in years" :key="year" :label="year + '骞�'" :value="year"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鑺傚亣鏃ュ垎绫�" prop="holidayType">
+ <el-select v-model="holidayForm.holidayType" style="width:100%">
+ <el-option v-for="item in holidayTypeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏃ユ湡" prop="holidayDate">
+ <el-date-picker v-model="holidayForm.holidayDate" type="date" value-format="yyyy-MM-dd"
+ style="width:100%" placeholder="閫夋嫨鏃ユ湡"></el-date-picker>
+ </el-form-item>
+ <el-form-item label="绗嚑鏃�" prop="dayIndex">
+ <el-input-number v-model="holidayForm.dayIndex" :min="1" :max="31" style="width:100%"></el-input-number>
+ </el-form-item>
+ </el-form>
+ <span slot="footer">
+ <el-button @click="holidayDialog = false">鍙栨秷</el-button>
+ <el-button type="primary" :loading="saving" @click="saveHoliday">淇濆瓨</el-button>
+ </span>
+ </el-dialog>
+
+ <el-dialog :title="coefficientForm.id ? '缂栬緫瀹㈣繍绯绘暟' : '鏂板瀹㈣繍绯绘暟'" :visible.sync="coefficientDialog" width="520px" v-dialog-drag>
+ <el-form ref="coefficientForm" :model="coefficientForm" :rules="coefficientRules" label-width="110px">
+ <el-form-item label="骞翠唤" prop="year">
+ <el-select v-model="coefficientForm.year" style="width:100%">
+ <el-option v-for="year in years" :key="year" :label="year + '骞�'" :value="year"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏃ユ湡鍒嗙被" prop="dateCategory">
+ <el-select v-model="coefficientForm.dateCategory" style="width:100%">
+ <el-option v-for="item in dateCategoryOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹㈣繍绯绘暟" prop="coefficient">
+ <el-input-number v-model="coefficientForm.coefficient" :min="0.01" :precision="4" :step="0.1" style="width:100%"></el-input-number>
+ </el-form-item>
+ </el-form>
+ <span slot="footer">
+ <el-button @click="coefficientDialog = false">鍙栨秷</el-button>
+ <el-button type="primary" :loading="saving" @click="saveCoefficient">淇濆瓨</el-button>
+ </span>
+ </el-dialog>
+ </el-main>
+ </el-container>
+ </el-container>
+</template>
+
+<script>
+import api from '@/api'
+import AppHeader from '@/components/AppHeader.vue'
+
+export default {
+ components: { AppHeader },
+ name: 'SystemConfig',
+ data() {
+ return {
+ activeTab: 'holidays',
+ years: [2024, 2025, 2026, 2027, 2028, 2029, 2030],
+ holidayTypeOptions: [],
+ dateCategoryOptions: [],
+ holidayQuery: { year: 2026, holidayType: 'NATIONAL_DAY' },
+ coefficientQuery: { year: 2026 },
+ holidays: [],
+ coefficients: [],
+ loadingHolidays: false,
+ loadingCoefficients: false,
+ saving: false,
+ holidayDialog: false,
+ coefficientDialog: false,
+ holidayForm: { id: null, year: 2026, holidayType: 'NATIONAL_DAY', holidayDate: '', dayIndex: 1 },
+ coefficientForm: { id: null, year: 2026, dateCategory: 'HOLIDAY', coefficient: 2.4 },
+ holidayRules: {
+ year: [{ required: true, message: '璇烽�夋嫨骞翠唤', trigger: 'change' }],
+ holidayType: [{ required: true, message: '璇烽�夋嫨鑺傚亣鏃ュ垎绫�', trigger: 'change' }],
+ holidayDate: [{ required: true, message: '璇烽�夋嫨鏃ユ湡', trigger: 'change' }],
+ dayIndex: [{ required: true, message: '璇疯緭鍏ョ鍑犳棩', trigger: 'change' }]
+ },
+ coefficientRules: {
+ year: [{ required: true, message: '璇烽�夋嫨骞翠唤', trigger: 'change' }],
+ dateCategory: [{ required: true, message: '璇烽�夋嫨鏃ユ湡鍒嗙被', trigger: 'change' }],
+ coefficient: [{ required: true, message: '璇疯緭鍏ュ杩愮郴鏁�', trigger: 'change' }]
+ }
+ }
+ },
+ mounted() {
+ this.loadOptions()
+ this.loadHolidays()
+ this.loadCoefficients()
+ },
+ methods: {
+ loadOptions() {
+ api.get('/holiday/config/options').then(res => {
+ const data = res.data || {}
+ this.holidayTypeOptions = this.mapOptions(data.holidayTypes)
+ this.dateCategoryOptions = this.mapOptions(data.dateCategories)
+ })
+ },
+ mapOptions(map) {
+ if (!map) return []
+ return Object.keys(map).map(key => ({ value: key, label: map[key] }))
+ },
+ labelOf(options, value) {
+ const item = (options || []).find(x => x.value === value)
+ return item ? item.label : value
+ },
+ loadActiveTab() {
+ if (this.activeTab === 'holidays') this.loadHolidays()
+ else this.loadCoefficients()
+ },
+ loadHolidays() {
+ this.loadingHolidays = true
+ api.get('/holiday/config/holidays', { params: this.holidayQuery })
+ .then(res => { this.holidays = res.data || [] })
+ .catch(e => { if (!e || !e._toast) this.$message.error('鑺傚亣鏃ュ姞杞藉け璐�') })
+ .finally(() => { this.loadingHolidays = false })
+ },
+ loadCoefficients() {
+ this.loadingCoefficients = true
+ api.get('/holiday/config/coefficients', { params: this.coefficientQuery })
+ .then(res => { this.coefficients = res.data || [] })
+ .catch(e => { if (!e || !e._toast) this.$message.error('瀹㈣繍绯绘暟鍔犺浇澶辫触') })
+ .finally(() => { this.loadingCoefficients = false })
+ },
+ openHolidayDialog(row) {
+ this.holidayDialog = true
+ this.$nextTick(() => {
+ this.holidayForm = row ? Object.assign({}, row) : { id: null, year: 2026, holidayType: 'NATIONAL_DAY', holidayDate: '', dayIndex: 1 }
+ if (this.$refs.holidayForm) this.$refs.holidayForm.clearValidate()
+ })
+ },
+ onHolidayYearChange(year) {
+ if (this.holidayForm.holidayDate && Number(this.holidayForm.holidayDate.slice(0, 4)) !== year) {
+ this.holidayForm.holidayDate = ''
+ }
+ },
+ saveHoliday() {
+ this.$refs.holidayForm.validate(valid => {
+ if (!valid) return
+ this.saving = true
+ api.post('/holiday/config/holidays/save', this.holidayForm)
+ .then(res => {
+ if (res.code !== 200) throw new Error(res.message || '淇濆瓨澶辫触')
+ this.$message.success('淇濆瓨鎴愬姛')
+ this.holidayDialog = false
+ this.loadHolidays()
+ })
+ .catch(err => this.showError(err, '淇濆瓨澶辫触'))
+ .finally(() => { this.saving = false })
+ })
+ },
+ removeHoliday(row) {
+ this.$confirm('纭畾鍒犻櫎璇ヨ妭鍋囨棩鏃ユ湡鍚楋紵', '鍒犻櫎纭', { type: 'warning' })
+ .then(() => api.post('/holiday/config/holidays/delete', null, { params: { id: row.id } }))
+ .then(res => {
+ if (res.code !== 200) throw new Error(res.message || '鍒犻櫎澶辫触')
+ this.$message.success('宸插垹闄�')
+ this.loadHolidays()
+ })
+ .catch(err => { if (err !== 'cancel') this.showError(err, '鍒犻櫎澶辫触') })
+ },
+ openCoefficientDialog(row) {
+ this.coefficientDialog = true
+ this.$nextTick(() => {
+ this.coefficientForm = row ? Object.assign({}, row) : { id: null, year: 2026, dateCategory: 'HOLIDAY', coefficient: 2.4 }
+ if (this.$refs.coefficientForm) this.$refs.coefficientForm.clearValidate()
+ })
+ },
+ saveCoefficient() {
+ this.$refs.coefficientForm.validate(valid => {
+ if (!valid) return
+ this.saving = true
+ api.post('/holiday/config/coefficients/save', this.coefficientForm)
+ .then(res => {
+ if (res.code !== 200) throw new Error(res.message || '淇濆瓨澶辫触')
+ this.$message.success('淇濆瓨鎴愬姛')
+ this.coefficientDialog = false
+ this.loadCoefficients()
+ })
+ .catch(err => this.showError(err, '淇濆瓨澶辫触'))
+ .finally(() => { this.saving = false })
+ })
+ },
+ removeCoefficient(row) {
+ this.$confirm('纭畾鍒犻櫎璇ュ杩愮郴鏁板悧锛�', '鍒犻櫎纭', { type: 'warning' })
+ .then(() => api.post('/holiday/config/coefficients/delete', null, { params: { id: row.id } }))
+ .then(res => {
+ if (res.code !== 200) throw new Error(res.message || '鍒犻櫎澶辫触')
+ this.$message.success('宸插垹闄�')
+ this.loadCoefficients()
+ })
+ .catch(err => { if (err !== 'cancel') this.showError(err, '鍒犻櫎澶辫触') })
+ },
+ formatCoefficient(value) {
+ if (value === null || value === undefined || value === '') return '鈥�'
+ const number = Number(value)
+ if (!Number.isFinite(number)) return value
+ return number.toFixed(15).replace(/0+$/, '').replace(/\.$/, '')
+ },
+ showError(err, fallback) {
+ if (err && err._toast) return
+ const data = err && err.response && err.response.data
+ this.$message.error((data && data.message) || (err && err.message) || fallback)
+ }
+ }
+}
+</script>
+
+<style scoped>
+.toolbar { margin-bottom: 12px; display: flex; gap: 8px; align-items: center; }
+.danger-text { color: #f56c6c; }
+</style>
diff --git a/traffic-audit-web/src/views/UserManage.vue b/traffic-audit-web/src/views/UserManage.vue
index 4c2651a..b9b6cdc 100644
--- a/traffic-audit-web/src/views/UserManage.vue
+++ b/traffic-audit-web/src/views/UserManage.vue
@@ -10,8 +10,13 @@
<el-menu-item index="/explain">浼佷笟瑙i噴</el-menu-item>
<el-menu-item index="/audit">瀹℃牳缁撴灉</el-menu-item>
<el-menu-item index="/report">鎶ヨ〃鐢熸垚</el-menu-item>
+ <el-submenu index="/special-stats">
+ <template slot="title">涓撻」缁熻</template>
+ <el-menu-item index="/holiday-stat">鑺傚亣鏃�</el-menu-item>
+ </el-submenu>
<el-menu-item index="/rules">瑙勫垯绠$悊</el-menu-item>
<el-menu-item index="/users">鐢ㄦ埛绠$悊</el-menu-item>
+ <el-menu-item v-if="operatorIsAdmin" index="/system-config">绯荤粺閰嶇疆</el-menu-item>
</el-menu>
</el-aside>
<el-main>
--
Gitblit v1.9.1