From e92968f85531df814268b6afbe62f6e062f5a07f Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期二, 06 十月 2026 02:02:48 +0800
Subject: [PATCH] feat(holiday): 新增往年国省道调整并按前一日环比基期重算汇总
---
docs/sql_holiday.sql | 13 +
deploy-templates/db/init.sql | 15 +
docs/init.sql | 15 +
traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayVehicleAdjustmentMapper.java | 9
traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayComparisonBaseTest.java | 136 ++++++++--
traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java | 155 ++++++++---
HANDOFF.md | 19 +
deploy-templates/README-部署说明.txt | 3
deploy-templates/db/sql_holiday_vehicle_adjustment_migration.sql | 14 +
docs/打包部署说明.md | 8
traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayVehicleAdjustment.java | 23 +
traffic-audit-web/src/views/HolidayStat.vue | 155 +++++++++++
traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayVehicleAdjustmentController.java | 59 ++++
deploy-templates/db/sql_holiday.sql | 13 +
traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayVehicleAdjustmentService.java | 124 +++++++++
docs/sql_holiday_vehicle_adjustment_migration.sql | 14 +
pack-deploy.ps1 | 2
traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayBulletinService.java | 2
18 files changed, 698 insertions(+), 81 deletions(-)
diff --git a/HANDOFF.md b/HANDOFF.md
index 2cf8844..4cb31da 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -6,7 +6,22 @@
## 鏈�鏂帮紙2026-10-06锛�
-### 10-06锛氭櫘閫氬浗鐪侀亾銆岀幆姣斿熀鏈熴�嶆敼涓轰汉宸ュ鐢ㄥ�硷紙涓嶅啀鍙備笌璁$畻锛�
+### 10-06锛氭柟妗� D 宸插疄鐜帮紙寰�骞村浗鐪侀亾璋冩暣琛� + 鐜瘮鍩烘湡鍙綔鐢ㄤ簬鍓嶄竴鏃ュ垎姣嶏級
+
+- **鏈�缁堝彛寰�**锛歚HolidayStatService.loadVehicleByDate` 鍏堢敤 `holiday_vehicle_adjustment` 瑕嗙洊瀵瑰簲鍘嗗彶骞翠唤鐨勨�滃綋鏃ュ�尖�濓紝渚涜蛋鍔垮浘銆佸悓姣斻�佺疮璁″拰蹇姤浣跨敤锛涢殢鍚庢瀯閫犫�滃墠涓�鏃ョ幆姣斿垎姣嶁�濓紝鑻ュ墠涓�鏃ユ櫘閫氬浗鐪侀亾 C 鍒楁湁鍊硷紝鍒欑敤 C 鏇挎崲璇ユ棩鍓嶄竴鏃ュ浗鐪侀亾鍊硷紝骞舵嵁姝ら噸绠楅潪钀ヤ笟鎬т汉鍛樻祦鍔ㄩ噺銆佸叕璺汉鍛樻祦鍔ㄩ噺鍜屾�昏銆傛墍鏈夌幆姣旇鍙户鎵胯繖浠藉垎姣嶃��
+- **鍏抽敭杈圭晫**锛欳 鍙奖鍝嶁�滀笅涓�鏃ョ殑鐜瘮鍒嗘瘝鈥濓紝涓嶅奖鍝� C 鎵�鍦ㄦ棩鐨勫綋鏃ュ�笺�佺疮璁℃垨璧板娍鍥撅紱璋冩暣琛ㄥ彧缁存姢鍘嗗彶骞翠唤锛坄year < 褰撳墠骞翠唤`锛夛紝2026 骞磋皟鏁翠粛鐢卞鍏� Excel 鐨� D 鍒楄繘鍏ュ綋鏃ュ�笺��
+- **鏈疆瀹炵幇**锛氭柊澧� `holiday_vehicle_adjustment` 琛ㄥ強 mapper/service/controller锛涘墠绔湪涓撻」缁熻-鑺傚亣鏃ラ〉鐨勨�滆妭鍋囨棩鏁版嵁鈥濆拰鈥滆娴嬬珯鍒嗘棩璋冩煡鏁版嵁鈥濅箣闂存柊澧炩�滃線骞村浗鐪侀亾鏁版嵁璋冩暣鈥濆崱鐗囷紙鍚庣画鎸夌敤鎴锋彁璁繚鎸佽浣嶇疆锛夛紝鏀寔骞翠唤绛涢�夈�佹柊澧炪�佺紪杈戙�佸垹闄ゅ拰鍘熷洜鐣欑棔锛涢儴缃茶剼鏈笌 migration 娓呭崟宸插悓姝ャ��
+- **10-04 瀵煎叆棰勬锛堝彧璇伙紝涓嶅啓搴擄級**锛氱敤 `C:\Users\jcxiong\Downloads\2026骞村崄涓�鏁版嵁 (2).xlsx` 浠� `allowHistoricalOverwrite=false` 璋冨鍏ユ帴鍙o紝杩斿洖纭椤癸細
+ - `绗�4鏃� 2026-10-04 鏅�氬浗鐪侀亾锛氬師鍊� 323.6551锛屽鍏ュ�� 322.7695`
+ - `绗�3鏃� 2026-10-03 鏅�氬浗鐪侀亾鐜瘮鍩烘湡锛氬師鍊� 绌猴紝瀵煎叆鍊� 320.2418`
+ 鍥犳 C5 鐢辩┖鍙樻湁鍊间笉浼氱‖鎷掔粷瀵煎叆锛屽彧浼氳姹傚巻鍙插彉鏇寸‘璁わ紱鐢ㄦ埛纭鍚庡嵆鍙殢璋冩暣鍚庣殑 Excel 閲嶅鍐欏叆銆�
+- **褰撳墠搴撶姸鎬�**锛氭湰鍦� `traffic_audit.holiday_vehicle_adjustment` 宸叉墽琛� DDL锛�0 琛岋級锛�2026-10-04 涓氬姟鍊间粛涓烘棫鍊� `323.6551`锛岀瓑寰呯敤鎴疯嚜琛岄噸瀵艰皟鏁村悗鐨� Excel銆傚姪鎵嬫湭鎵ц浠讳綍涓氬姟鏁版嵁瀵煎叆銆佽鐩栨垨閲嶅銆�
+- **楠岃瘉**锛氬悗绔� `mvn -o test` 9 涓祴璇曞叏缁匡紙鍚� C5 鍙敼 10-04 鐜瘮鍒嗘瘝銆佸巻鍙茶皟鏁磋繘鍏� 2025 褰撴棩鍊硷級銆佸墠绔� `npm run build` 閫氳繃锛涢〉闈㈠疄娴嬧�滃線骞村浗鐪侀亾鏁版嵁璋冩暣鈥濆崱鐗囦笌鏂板寮圭獥姝e父銆�
+- **宸茬煡鍙栨暣杈圭晫锛堥渶娌跨敤鐜版湁鍙e緞鎴栧彟浣滃喅绛栵級**锛氬揩鎶ョ櫨鍒嗘瘮鐩墠鎸夋棦瀹氳鍒欏厛澶嶇幇婧� Excel 鍗曞厓鏍兼牸寮忥紝鍐嶅彇 1 浣嶅皬鏁般��10-04 鎬昏鐜瘮鍘熷鍊肩害 `4.5481%`锛屾簮鏍兼樉绀� `4.55%`锛屽啀鍙� 1 浣嶅緱鍒� `4.6%`锛涜嫢鐩存帴浠庡師濮嬪�间竴娆″彇 1 浣嶅垯鏄� `4.5%`銆傚綋鍓嶄唬鐮佷繚鐣欌�滃厛 Excel 鏄剧ず銆佸啀蹇姤 1 浣嶁�濈殑鏃㈡湁鍙e緞锛涜嫢棰嗗瑕佹眰鏀逛负涓�娆″彇鏁达紝闇�瑕佸崟鐙敼蹇姤鐧惧垎姣旇矾寰勩��
+
+> 璇存槑锛氭湰鏂囦欢鏃╁墠鈥滅幆姣斿熀鏈熷彧浣滃鐢ㄣ�佷笉鍙備笌璁$畻鈥濈殑娈佃惤宸蹭綔搴熴�傝鐗堟湰閽堝鐨勬槸鏃у疄鐜拌鎶� C 鏀惧埌 C 鎵�鍦ㄦ棩鏈韩璁$畻鐨勯棶棰橈紱鑷柟妗� D 璧凤紝C 鐨勬纭涔夋槸**鍓嶄竴鏃ョ幆姣斿垎姣嶇殑璋冩暣鍊�**銆�
+
+### 10-06锛堝凡浣滃簾锛岃涓婃柟鏂规 D锛夛細鏅�氬浗鐪侀亾銆岀幆姣斿熀鏈熴�嶆敼涓轰汉宸ュ鐢ㄥ�硷紙涓嶅啀鍙備笌璁$畻锛�
- **鑳屾櫙**锛�10-04 閯傚窞妯婂搴勭珯涓皬瀹㈡祦閲忔柇宕栦笅璺岋紙35419 -> 8856锛夛紝棰嗗瑕佹眰鍓旈櫎銆傛墜宸ョ蹇姤鐨勫仛娉曟槸锛氬厛鐢ㄥ墧绔欏悗鐨勬暟鍊肩畻鍑� 10-04 鐜瘮锛屾妸 10-03 鍥界渷閬撳�兼尓鍒般�岀幆姣斿熀鏈熴�岰 鍒楀鐢紝鍐嶆妸 10-03 鎭㈠鍘熷�间互鍏嶅奖鍝嶇疮璁°�傜敤鎴锋槑纭彛寰勶細**銆岀幆姣斿熀鏈熴�嶅彧鐢ㄤ簬鏁版嵁澶囩敤锛屼笉鍙備笌浠讳綍璁$畻**銆�
- **鏃ц涓猴紙閿欙級**锛歚HolidayStatService.nationalProvincialRingRatio` 鍦� 2026 骞村彧瑕� `comparison_base` 鏈夊�煎氨鎸夈�屽綋鏃� / 鐜瘮鍩烘湡 - 1銆嶇畻鍥界渷閬撶幆姣旓紱瀵煎嚭鏃� E 鍒楀啓 `=(D{琛寎-C{琛寎)/C{琛寎`銆傛寜鎵嬪伐绋跨殑 C 鍒楁憜娉曪紙鍩烘湡鏀惧湪琚瘮鏃ラ偅琛岋級锛岃繖鏍风畻杩樹細閿欎綅锛�10-03 鐜瘮琚畻鎴� +1.11%锛夈��
@@ -306,7 +321,7 @@
- **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 涓嶄娇鐢ㄣ��
+- **鏅�氬浗鐪侀亾鐜瘮鍩烘湡锛�2026-10-02 杩藉姞锛�2026-10-06 鏂规 D 宸蹭慨姝o級**锛欳 鍒楄涔夋渶缁堢‘瀹氫负鈥滀笅涓�鏃ョ幆姣斿垎姣嶇殑璋冩暣鍊尖�濓紱濉啓鍚庡彧褰卞搷涓嬩竴鏃ョ殑鐜瘮鍒嗘瘝锛屼笉褰卞搷 C 鎵�鍦ㄦ棩鍙婄疮璁°�佽蛋鍔垮浘銆俙holiday_vehicle_flow.comparison_base` 宸插叆搴擄紝瀵煎叆瀵煎嚭涓庡巻鍙插彉鏇寸‘璁ゅ潎宸叉帴鍏ャ��
- **蹇姤棣栨棩鍙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)` 鍒楀瀷銆佸巻鍙插彉鏇翠袱闃舵瀵煎叆鍥炲綊鍧囧凡瀹炴祴锛涘姛鑳芥祴璇曟姤鍛婁笌宸ヤ綔鏃ョ粨鎸� 2026-09-27 鎻愪氦瑙勮寖浠呮湰鍦扮暀瀛橈紙涓嶅叆搴擄級锛歚docs/鍔熻兘娴嬭瘯鎶ュ憡/鍔熻兘娴嬭瘯鎶ュ憡_閲嶈鑺傚亣鏃ヤ笓椤圭粺璁2026-10-02.md`銆乣docs/宸ヤ綔鏃ョ粨/宸ヤ綔鏃ョ粨_2026-10-02.md`銆�
diff --git "a/deploy-templates/README-\351\203\250\347\275\262\350\257\264\346\230\216.txt" "b/deploy-templates/README-\351\203\250\347\275\262\350\257\264\346\230\216.txt"
index 81fa0a9..2a8fb0b 100644
--- "a/deploy-templates/README-\351\203\250\347\275\262\350\257\264\346\230\216.txt"
+++ "b/deploy-templates/README-\351\203\250\347\275\262\350\257\264\346\230\216.txt"
@@ -40,6 +40,7 @@
sql_holiday_decimal_migration.sql 鑺傚亣鏃ユ暟鍊煎垪绮惧害杩佺Щ锛堟棫搴撳崌绾э級
sql_holiday_comparison_base_migration.sql 鑺傚亣鏃ョ幆姣斿熀鏈熷瓧娈佃縼绉伙紙鏃у簱鍗囩骇锛�
sql_holiday_previous_day_migration.sql 鑺傚亣鏃ュ墠涓�鏃ラ厤缃縼绉伙紙鏃у簱鍗囩骇锛�
+ sql_holiday_vehicle_adjustment_migration.sql 寰�骞村浗鐪侀亾鏁版嵁璋冩暣琛紙鏃у簱鍗囩骇锛�
sql_observation_station.sql 瑙傛祴绔欏垎鏃ヨ皟鏌ユ暟鎹〃锛堟柊鍔熻兘锛屾棫搴撶己琛ㄦ椂鎵ц锛�
README-閮ㄧ讲璇存槑.txt
@@ -80,7 +81,7 @@
mysql --default-character-set=utf8mb4 -u root -p traffic_audit < db/sql_city_bus.sql
mysql --default-character-set=utf8mb4 -u root -p traffic_audit < db/sql_city_taxi.sql
mysql --default-character-set=utf8mb4 -u root -p traffic_audit < db/sql_wyc.sql
- 娉細鍏ㄦ柊搴撶殑 init.sql 宸插惈鑺傚亣鏃� 4 寮犺〃锛涜嫢鏄湪鏃╂湡鐗堟湰搴撲笂琛ヨ妭鍋囨棩妯″潡锛屽啀鎵ц db/sql_holiday.sql锛屽苟鎸夐渶鎵ц涓変釜杩佺Щ鑴氭湰锛堟暟鍊肩簿搴︺�佺幆姣斿熀鏈熴�佸墠涓�鏃ラ厤缃級銆傚墠涓�鏃ヨ縼绉昏剼鏈彲閲嶅鎵ц銆�
+ 娉細鍏ㄦ柊搴撶殑 init.sql 宸插惈鑺傚亣鏃ュ強鐩稿叧琛紱鑻ユ槸鍦ㄦ棭鏈熺増鏈簱涓婅ˉ鑺傚亣鏃ユā鍧楋紝鍐嶆墽琛� db/sql_holiday.sql锛屽苟鎸夐渶鎵ц杩佺Щ鑴氭湰锛堟暟鍊肩簿搴︺�佺幆姣斿熀鏈熴�佸墠涓�鏃ラ厤缃�佸線骞村浗鐪侀亾鏁版嵁璋冩暣銆佽娴嬬珯鍒嗘棩璋冩煡鏁版嵁锛夈�傝縼绉昏剼鏈潎鍙噸澶嶆墽琛屻��
棰勭疆璐﹀彿锛歛dmin / admin123锛堥娆$櫥褰曞悗璇锋敼瀵嗙爜锛�
2. 鏀归厤缃� backend/application.yml
diff --git a/deploy-templates/db/init.sql b/deploy-templates/db/init.sql
index 70df0b6..7d79383 100644
--- a/deploy-templates/db/init.sql
+++ b/deploy-templates/db/init.sql
@@ -673,6 +673,21 @@
KEY idx_holiday_vehicle_date (flow_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鑺傚亣鏃ョ患鍚堣繍杈撻潪钀ヤ笟鎬ц溅娴佹儏鍐�';
+-- 寰�骞村浗鐪侀亾杞︽祦閲忚皟鏁�
+DROP TABLE IF EXISTS holiday_vehicle_adjustment;
+CREATE TABLE holiday_vehicle_adjustment (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL COMMENT '骞翠唤',
+ flow_date DATE NOT NULL COMMENT '鏃ユ湡',
+ adjusted_value DECIMAL(30,15) NOT NULL COMMENT '璋冩暣鍚庢櫘閫氬浗鐪侀亾杞︽祦閲忥紙涓囪締娆★級',
+ reason VARCHAR(500) 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_vehicle_adjustment (year, flow_date),
+ KEY idx_holiday_vehicle_adjustment_date (flow_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='寰�骞村浗鐪侀亾杞︽祦閲忚皟鏁�';
+
-- 瀹㈣繍绯绘暟閰嶇疆
DROP TABLE IF EXISTS holiday_passenger_coefficient;
CREATE TABLE holiday_passenger_coefficient (
diff --git a/deploy-templates/db/sql_holiday.sql b/deploy-templates/db/sql_holiday.sql
index a2b6b59..6d80576 100644
--- a/deploy-templates/db/sql_holiday.sql
+++ b/deploy-templates/db/sql_holiday.sql
@@ -42,6 +42,19 @@
KEY idx_holiday_vehicle_date (flow_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE IF NOT EXISTS holiday_vehicle_adjustment (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL,
+ flow_date DATE NOT NULL,
+ adjusted_value DECIMAL(30,15) NOT NULL,
+ reason VARCHAR(500) 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_vehicle_adjustment (year, flow_date),
+ KEY idx_holiday_vehicle_adjustment_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,
diff --git a/deploy-templates/db/sql_holiday_vehicle_adjustment_migration.sql b/deploy-templates/db/sql_holiday_vehicle_adjustment_migration.sql
new file mode 100644
index 0000000..3c98eb6
--- /dev/null
+++ b/deploy-templates/db/sql_holiday_vehicle_adjustment_migration.sql
@@ -0,0 +1,14 @@
+-- Add the historical national/provincial road adjustment table.
+-- Run against the traffic_audit database on upgrade.
+CREATE TABLE IF NOT EXISTS holiday_vehicle_adjustment (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL COMMENT 'year',
+ flow_date DATE NOT NULL COMMENT 'holiday date',
+ adjusted_value DECIMAL(30,15) NOT NULL COMMENT 'adjusted national/provincial vehicle flow (10k vehicle trips)',
+ reason VARCHAR(500) NOT NULL COMMENT 'adjustment reason',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_holiday_vehicle_adjustment (year, flow_date),
+ KEY idx_holiday_vehicle_adjustment_date (flow_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='historical national/provincial vehicle flow adjustments';
diff --git a/docs/init.sql b/docs/init.sql
index 70df0b6..7d79383 100644
--- a/docs/init.sql
+++ b/docs/init.sql
@@ -673,6 +673,21 @@
KEY idx_holiday_vehicle_date (flow_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='鑺傚亣鏃ョ患鍚堣繍杈撻潪钀ヤ笟鎬ц溅娴佹儏鍐�';
+-- 寰�骞村浗鐪侀亾杞︽祦閲忚皟鏁�
+DROP TABLE IF EXISTS holiday_vehicle_adjustment;
+CREATE TABLE holiday_vehicle_adjustment (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL COMMENT '骞翠唤',
+ flow_date DATE NOT NULL COMMENT '鏃ユ湡',
+ adjusted_value DECIMAL(30,15) NOT NULL COMMENT '璋冩暣鍚庢櫘閫氬浗鐪侀亾杞︽祦閲忥紙涓囪締娆★級',
+ reason VARCHAR(500) 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_vehicle_adjustment (year, flow_date),
+ KEY idx_holiday_vehicle_adjustment_date (flow_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='寰�骞村浗鐪侀亾杞︽祦閲忚皟鏁�';
+
-- 瀹㈣繍绯绘暟閰嶇疆
DROP TABLE IF EXISTS holiday_passenger_coefficient;
CREATE TABLE holiday_passenger_coefficient (
diff --git a/docs/sql_holiday.sql b/docs/sql_holiday.sql
index ca39066..da2ccc1 100644
--- a/docs/sql_holiday.sql
+++ b/docs/sql_holiday.sql
@@ -41,6 +41,19 @@
KEY idx_holiday_vehicle_date (flow_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE IF NOT EXISTS holiday_vehicle_adjustment (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL,
+ flow_date DATE NOT NULL,
+ adjusted_value DECIMAL(30,15) NOT NULL,
+ reason VARCHAR(500) 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_vehicle_adjustment (year, flow_date),
+ KEY idx_holiday_vehicle_adjustment_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,
diff --git a/docs/sql_holiday_vehicle_adjustment_migration.sql b/docs/sql_holiday_vehicle_adjustment_migration.sql
new file mode 100644
index 0000000..3c98eb6
--- /dev/null
+++ b/docs/sql_holiday_vehicle_adjustment_migration.sql
@@ -0,0 +1,14 @@
+-- Add the historical national/provincial road adjustment table.
+-- Run against the traffic_audit database on upgrade.
+CREATE TABLE IF NOT EXISTS holiday_vehicle_adjustment (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ year INT NOT NULL COMMENT 'year',
+ flow_date DATE NOT NULL COMMENT 'holiday date',
+ adjusted_value DECIMAL(30,15) NOT NULL COMMENT 'adjusted national/provincial vehicle flow (10k vehicle trips)',
+ reason VARCHAR(500) NOT NULL COMMENT 'adjustment reason',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_holiday_vehicle_adjustment (year, flow_date),
+ KEY idx_holiday_vehicle_adjustment_date (flow_date)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='historical national/provincial vehicle flow adjustments';
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 c166e81..bf2e978 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"
@@ -103,7 +103,9 @@
鈹� 鈹溾攢 sql_holiday.sql 涓撻」缁熻-閲嶈鑺傚亣鏃ヨ〃锛堟棫搴撹ˉ寤烘椂鎵ц锛�
鈹� 鈹溾攢 sql_holiday_decimal_migration.sql 鑺傚亣鏃ユ暟鍊煎垪绮惧害杩佺Щ锛堟棫搴撳崌绾э級
鈹� 鈹溾攢 sql_holiday_comparison_base_migration.sql 鑺傚亣鏃ョ幆姣斿熀鏈熷瓧娈佃縼绉伙紙鏃у簱鍗囩骇锛�
-鈹� 鈹斺攢 sql_holiday_previous_day_migration.sql 鑺傚亣鏃ュ墠涓�鏃ラ厤缃縼绉伙紙鏃у簱鍗囩骇锛�
+鈹� 鈹溾攢 sql_holiday_previous_day_migration.sql 鑺傚亣鏃ュ墠涓�鏃ラ厤缃縼绉伙紙鏃у簱鍗囩骇锛�
+鈹� 鈹溾攢 sql_holiday_vehicle_adjustment_migration.sql 寰�骞村浗鐪侀亾鏁版嵁璋冩暣琛紙鏃у簱鍗囩骇锛�
+鈹� 鈹斺攢 sql_observation_station.sql 瑙傛祴绔欏垎鏃ヨ皟鏌ユ暟鎹〃锛堟棫搴撹ˉ寤烘椂鎵ц锛�
鈹斺攢 README-閮ㄧ讲璇存槑.txt
```
@@ -124,7 +126,7 @@
```
> 鈿狅笍 **`init.sql` 鏄牬鍧忔�х殑**锛氶噷闈㈢殑鍩虹琛ㄩ兘甯� `DROP TABLE IF EXISTS`銆�**宸叉湁鏁版嵁鐨勫簱涓婂崈涓囦笉瑕佸啀璺� init.sql**锛屽惁鍒欒〃鏁版嵁浼氳娓呯┖閲嶅缓銆�
-> 鏃╂湡鐗堟湰搴撹嫢娌℃湁鑺傚亣鏃� 4 寮犺〃锛屾墽琛� `sql_holiday.sql`锛涜嫢宸叉湁鑺傚亣鏃ヨ〃浣嗙己绮惧害銆佺幆姣斿熀鏈熷瓧娈垫垨鍓嶄竴鏃ラ厤缃紝鎸夐渶鎵ц `sql_holiday_decimal_migration.sql`銆乣sql_holiday_comparison_base_migration.sql`銆乣sql_holiday_previous_day_migration.sql`銆�
+> 鏃╂湡鐗堟湰搴撹嫢娌℃湁鑺傚亣鏃ョ浉鍏宠〃锛屾墽琛� `sql_holiday.sql`锛涜嫢宸叉湁鑺傚亣鏃ヨ〃浣嗙己绮惧害銆佺幆姣斿熀鏈熷瓧娈点�佸墠涓�鏃ラ厤缃�佸線骞村浗鐪侀亾璋冩暣琛ㄦ垨瑙傛祴绔欒〃锛屾寜闇�鎵ц瀵瑰簲鐨勮縼绉昏剼鏈�傝縼绉昏剼鏈潎鍙噸澶嶆墽琛屻��
> 4 涓剼鏈叡瑕嗙洊 33 寮犺〃锛坕nit.sql 28 寮狅紝鍏朵腑鍚妭鍋囨棩 4 寮� + 鍩庡競瀹㈣繍 3 寮� + 缃戠害杞� 2 寮狅級銆�
#### 璺嚎 B锛氭妸鐜扮綉/寮�鍙戞満鐨勬暟鎹暣濂楁惉杩囧幓锛堟帹鑽愶紝涓嶇敤璺戣剼鏈級
@@ -281,7 +283,7 @@
| 瀵煎嚭鎶ャ�屾湭鎵惧埌姹囨�昏〃姣嶇増銆奩YYY骞碝鏈堚�︺�嬨�� | `docs/鐢熸垚姹囨�诲ぇ琛╜ 缂鸿鏈堟瘝鐗� |
| 鐧诲綍鎺ュ彛 500 | `jwt.secret` 濉簡闈炴硶瀛楃锛堝涓枃锛夛紱鏀规垚 32 浣嶅崄鍏繘鍒朵覆 |
| 椤甸潰鑳藉紑浣嗘帴鍙� 404 | 璧� nginx 鏂规鏃舵病閰� `/api` 鍙嶄唬锛涙敼鐢� jar 鍐呭祵璁块棶 8090 |
-| 鏁版嵁搴撹繛鎺ュけ璐� | 妫�鏌� `application.yml`锛涚‘璁� 5 涓� SQL 鑴氭湰宸叉墽琛� |
+| 鏁版嵁搴撹繛鎺ュけ璐� | 妫�鏌� `application.yml`锛涚‘璁ゅ垵濮嬪寲鑴氭湰鍜屽搴斿崌绾ц縼绉昏剼鏈凡鎵ц |
| 鍚姩鎶� `Access denied ... (using password: NO)` | 鍚庣鎷垮埌鐨勫簱鍙d护鏄�**绌轰覆**锛歫ar 鍐呯疆 `${TRAFFIC_DB_PASSWORD:}` 鏈澶栫疆閰嶇疆瑕嗙洊銆傛妸 `backend/application.yml` 鏀惧湪 jar 鍚岀骇骞跺~ `spring.datasource.password`锛屾垨璁剧幆澧冨彉閲� `TRAFFIC_DB_PASSWORD`锛涙敼瀹岄噸鍚紝鏃犻渶閲嶆墦鍖� |
| 绔彛鍗犵敤 | 鏀� `server.port`锛涘墠绔紑鍙戠鍙� 8080 |
| 鏈嶅姟宸叉敞鍐屼絾 8090 鎵撲笉寮� | 鐪� `backend\logs\trafficAudit.err.log`锛涙湇鍔℃柟寮忚鐨勬槸 `backend\application.yml`锛宍password` 蹇呴』鏄湡瀹炲彛浠� |
diff --git a/pack-deploy.ps1 b/pack-deploy.ps1
index 98f78f9..68074de 100644
--- a/pack-deploy.ps1
+++ b/pack-deploy.ps1
@@ -231,7 +231,7 @@
cmd /c "robocopy `"$root\docs`" `"$docsDest`" /E /NFL /NDL /NJH /NJS /NP /XD _cache 宸ヤ綔鏃ョ粨 鍔熻兘娴嬭瘯鎶ュ憡 闂姹囨�� /XF 鐢熸垚_*.xlsx _bak_* >nul"
if ($LASTEXITCODE -ge 8) { throw "docs 澶嶅埗澶辫触锛坮obocopy 杩斿洖 $LASTEXITCODE锛�" }
# db 鑴氭湰涔熸斁涓�浠藉埌 docs锛堜笌鏃у寘涓�鑷达細docs/init.sql 绛夛級
-Copy-Item (Join-Path $tplDir 'db\init.sql'), (Join-Path $tplDir 'db\sql_city_bus.sql'), (Join-Path $tplDir 'db\sql_city_taxi.sql'), (Join-Path $tplDir 'db\sql_wyc.sql'), (Join-Path $tplDir 'db\sql_holiday.sql'), (Join-Path $tplDir 'db\sql_holiday_decimal_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_comparison_base_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_previous_day_migration.sql'), (Join-Path $tplDir 'db\sql_observation_station.sql') $docsDest -Force -ErrorAction SilentlyContinue
+Copy-Item (Join-Path $tplDir 'db\init.sql'), (Join-Path $tplDir 'db\sql_city_bus.sql'), (Join-Path $tplDir 'db\sql_city_taxi.sql'), (Join-Path $tplDir 'db\sql_wyc.sql'), (Join-Path $tplDir 'db\sql_holiday.sql'), (Join-Path $tplDir 'db\sql_holiday_decimal_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_comparison_base_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_previous_day_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_vehicle_adjustment_migration.sql'), (Join-Path $tplDir 'db\sql_observation_station.sql') $docsDest -Force -ErrorAction SilentlyContinue
# README锛氱敤妯℃澘骞舵浛鎹㈡棩鏈熷崰浣�
$readme = Get-Content (Join-Path $tplDir 'README-閮ㄧ讲璇存槑.txt') -Raw -Encoding UTF8
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayVehicleAdjustmentController.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayVehicleAdjustmentController.java
new file mode 100644
index 0000000..cedc9f2
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/controller/HolidayVehicleAdjustmentController.java
@@ -0,0 +1,59 @@
+package com.trafficaudit.holiday.controller;
+
+import com.trafficaudit.common.Result;
+import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment;
+import com.trafficaudit.holiday.service.HolidayVehicleAdjustmentService;
+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/adjustments")
+public class HolidayVehicleAdjustmentController {
+
+ @Resource
+ private HolidayVehicleAdjustmentService adjustmentService;
+ @Resource
+ private JwtUtils jwtUtils;
+
+ @GetMapping
+ public Result<List<HolidayVehicleAdjustment>> list(@RequestParam(value = "year", required = false) Integer year) {
+ return Result.ok(adjustmentService.list(year));
+ }
+
+ @PostMapping("/save")
+ public Result<HolidayVehicleAdjustment> save(@RequestHeader(value = "Authorization", required = false) String auth,
+ @RequestBody Map<String, Object> params) {
+ if (!isAdmin(auth)) return Result.error(403, "浠� admin 鍙互缁存姢寰�骞村浗鐪侀亾鏁版嵁璋冩暣");
+ try {
+ return Result.ok(adjustmentService.save(params));
+ } catch (Exception e) {
+ return Result.error(400, e.getMessage());
+ }
+ }
+
+ @PostMapping("/delete")
+ public Result<Void> delete(@RequestHeader(value = "Authorization", required = false) String auth,
+ @RequestParam("id") Long id) {
+ if (!isAdmin(auth)) return Result.error(403, "浠� admin 鍙互缁存姢寰�骞村浗鐪侀亾鏁版嵁璋冩暣");
+ try {
+ adjustmentService.delete(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/entity/HolidayVehicleAdjustment.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayVehicleAdjustment.java
new file mode 100644
index 0000000..8b9e561
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/entity/HolidayVehicleAdjustment.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_vehicle_adjustment")
+public class HolidayVehicleAdjustment {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private Integer year;
+ private LocalDate flowDate;
+ private BigDecimal adjustedValue;
+ private String reason;
+ private LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+}
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayVehicleAdjustmentMapper.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayVehicleAdjustmentMapper.java
new file mode 100644
index 0000000..986090a
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/mapper/HolidayVehicleAdjustmentMapper.java
@@ -0,0 +1,9 @@
+package com.trafficaudit.holiday.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface HolidayVehicleAdjustmentMapper extends BaseMapper<HolidayVehicleAdjustment> {
+}
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
index ebd3ca9..77f85f9 100644
--- 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
@@ -65,7 +65,7 @@
throw new IllegalArgumentException("鎵�閫夋棩鏈熶笉鍦ㄥ凡閰嶇疆鐨勮妭鍋囨棩鑼冨洿鍐�");
}
DailyMetrics previousDay = current.getDayIndex() > 1
- ? statService.dailyMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex() - 1) : null;
+ ? statService.ringRatioBaseMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex()) : null;
HolidayCalendar previousDayConfig = configService.findPreviousDay(current.getYear(), current.getHolidayType());
LocalDate previousDayDate = previousDayConfig == null || previousDayConfig.getHolidayDate() == null
? current.getDate().minusDays(1) : previousDayConfig.getHolidayDate();
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java
index a663821..2309466 100644
--- a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java
@@ -8,7 +8,9 @@
import com.trafficaudit.holiday.entity.HolidayCalendar;
import com.trafficaudit.holiday.entity.HolidayPassengerFlow;
import com.trafficaudit.holiday.entity.HolidayVehicleFlow;
+import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment;
import com.trafficaudit.holiday.mapper.HolidayPassengerFlowMapper;
+import com.trafficaudit.holiday.mapper.HolidayVehicleAdjustmentMapper;
import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper;
import lombok.Data;
import org.springframework.stereotype.Service;
@@ -36,6 +38,8 @@
private HolidayPassengerFlowMapper passengerFlowMapper;
@Resource
private HolidayVehicleFlowMapper vehicleFlowMapper;
+ @Resource
+ private HolidayVehicleAdjustmentMapper vehicleAdjustmentMapper;
@Resource
private ImportBatchMapper importBatchMapper;
@@ -93,37 +97,33 @@
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;
+ Map<Integer, HolidayCalendar> calendarByDay = new LinkedHashMap<>();
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(
- nationalProvincial, previousNationalProvincial);
- BigDecimal nonBusinessPersonFlow = null;
- if (coefficient != null && (expressway != null || nationalProvincial != null)) {
- nonBusinessPersonFlow = HolidayNumberUtils.multiply(
- HolidayNumberUtils.sumNullable(expressway, nationalProvincial), coefficient);
- }
+ if (day.getDayIndex() != null) calendarByDay.put(day.getDayIndex(), day);
+ }
+ List<Map<String, Object>> rows = new ArrayList<>();
+ for (HolidayCalendar day : calendar) {
+ DailyMetrics current = buildDailyMetrics(year, holidayType, day.getDayIndex(), day.getHolidayDate(),
+ passengerByDate, vehicleByDate, comparisonBaseByDate, coefficient);
+ DailyMetrics ringRatioBase = ringRatioBaseFromMaps(year, holidayType, day.getDayIndex(), calendarByDay,
+ passengerByDate, vehicleByDate, comparisonBaseByDate, coefficient);
+ BigDecimal nationalProvincialRingRatio = ratio(current.getNationalProvincial(),
+ ringRatioBase == null ? null : ringRatioBase.getNationalProvincial());
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("rail", current.getRail());
+ row.put("civilAviation", current.getCivilAviation());
+ row.put("road", current.getRoad());
+ row.put("water", current.getWater());
+ row.put("expressway", current.getExpressway());
+ row.put("nationalProvincial", current.getNationalProvincial());
+ row.put("comparisonBase", current.getComparisonBase());
row.put("nationalProvincialRingRatio", nationalProvincialRingRatio);
- row.put("coefficient", coefficient);
- row.put("nonBusinessPersonFlow", nonBusinessPersonFlow);
- row.put("totalPersonFlow", totalPersonFlow(passenger, nonBusinessPersonFlow));
+ row.put("coefficient", current.getCoefficient());
+ row.put("nonBusinessPersonFlow", current.getNonBusinessPersonFlow());
+ row.put("totalPersonFlow", current.getTotalPersonFlow());
rows.add(row);
- previousNationalProvincial = nationalProvincial;
}
return rows;
}
@@ -150,17 +150,37 @@
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);
+ return buildDailyMetrics(year, holidayType, dayIndex, date, passengerByDate, vehicleByDate,
+ comparisonBaseByDate, coefficient);
+ }
+
+ public DailyMetrics ringRatioBaseMetrics(int year, String holidayType, int dayIndex) {
+ if (dayIndex <= 1) return null;
+ List<HolidayCalendar> calendar = configService.findHolidayDays(year, holidayType);
+ Map<Integer, HolidayCalendar> calendarByDay = new LinkedHashMap<>();
+ for (HolidayCalendar day : calendar) {
+ if (day.getDayIndex() != null) calendarByDay.put(day.getDayIndex(), day);
+ }
+ 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);
+ return ringRatioBaseFromMaps(year, holidayType, dayIndex, calendarByDay, passengerByDate,
+ vehicleByDate, comparisonBaseByDate, coefficient);
+ }
+
+ private DailyMetrics buildDailyMetrics(int year, String holidayType, int dayIndex, LocalDate date,
+ Map<LocalDate, Map<String, BigDecimal>> passengerByDate,
+ Map<LocalDate, Map<String, BigDecimal>> vehicleByDate,
+ Map<LocalDate, BigDecimal> comparisonBaseByDate,
+ BigDecimal coefficient) {
Map<String, BigDecimal> passenger = passengerByDate.getOrDefault(date, Collections.emptyMap());
Map<String, BigDecimal> vehicle = vehicleByDate.getOrDefault(date, 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(date);
- BigDecimal nonBusiness = null;
- if (coefficient != null && (expressway != null || nationalProvincial != null)) {
- nonBusiness = HolidayNumberUtils.multiply(
- HolidayNumberUtils.sumNullable(expressway, nationalProvincial), coefficient);
- }
+ BigDecimal nonBusiness = nonBusinessPersonFlow(expressway, nationalProvincial, coefficient);
DailyMetrics metrics = new DailyMetrics();
metrics.setYear(year);
@@ -176,9 +196,43 @@
metrics.setComparisonBase(comparisonBase);
metrics.setCoefficient(coefficient);
metrics.setNonBusinessPersonFlow(nonBusiness);
- metrics.setRoadPersonFlow(sumNullable(metrics.getRoad(), nonBusiness));
- metrics.setTotalPersonFlow(totalPersonFlow(passenger, nonBusiness));
+ recalculateDerived(metrics);
return metrics;
+ }
+
+ private DailyMetrics ringRatioBaseFromMaps(int year, String holidayType, int dayIndex,
+ Map<Integer, HolidayCalendar> calendarByDay,
+ Map<LocalDate, Map<String, BigDecimal>> passengerByDate,
+ Map<LocalDate, Map<String, BigDecimal>> vehicleByDate,
+ Map<LocalDate, BigDecimal> comparisonBaseByDate,
+ BigDecimal coefficient) {
+ if (dayIndex <= 1) return null;
+ HolidayCalendar previous = calendarByDay.get(dayIndex - 1);
+ if (previous == null || previous.getHolidayDate() == null) return null;
+ DailyMetrics base = buildDailyMetrics(year, holidayType, previous.getDayIndex(), previous.getHolidayDate(),
+ passengerByDate, vehicleByDate, comparisonBaseByDate, coefficient);
+ BigDecimal comparisonBase = comparisonBaseByDate.get(previous.getHolidayDate());
+ if (comparisonBase != null) {
+ base.setNationalProvincial(comparisonBase);
+ recalculateDerived(base);
+ }
+ return base;
+ }
+
+ private void recalculateDerived(DailyMetrics metrics) {
+ BigDecimal nonBusiness = nonBusinessPersonFlow(metrics.getExpressway(), metrics.getNationalProvincial(),
+ metrics.getCoefficient());
+ metrics.setNonBusinessPersonFlow(nonBusiness);
+ metrics.setRoadPersonFlow(sumNullable(metrics.getRoad(), nonBusiness));
+ metrics.setTotalPersonFlow(totalPersonFlow(nonBusiness, metrics.getRoad(), metrics.getRail(),
+ metrics.getCivilAviation(), metrics.getWater()));
+ }
+
+ private BigDecimal nonBusinessPersonFlow(BigDecimal expressway, BigDecimal nationalProvincial,
+ BigDecimal coefficient) {
+ if (coefficient == null || (expressway == null && nationalProvincial == null)) return null;
+ return HolidayNumberUtils.multiply(
+ HolidayNumberUtils.sumNullable(expressway, nationalProvincial), coefficient);
}
public DailyMetrics cumulativeMetrics(int year, String holidayType, int dayIndex) {
@@ -228,6 +282,23 @@
result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>())
.put(row.getRoadCategory(), row.getVehicleCount());
}
+ Map<LocalDate, BigDecimal> adjustments = loadNationalProvincialAdjustmentsByDate(year);
+ for (Map.Entry<LocalDate, BigDecimal> entry : adjustments.entrySet()) {
+ result.computeIfAbsent(entry.getKey(), key -> new LinkedHashMap<>())
+ .put(HolidayConstants.ROAD_NATIONAL_PROVINCIAL, entry.getValue());
+ }
+ return result;
+ }
+
+ private Map<LocalDate, BigDecimal> loadNationalProvincialAdjustmentsByDate(int year) {
+ Map<LocalDate, BigDecimal> result = new LinkedHashMap<>();
+ List<HolidayVehicleAdjustment> rows = vehicleAdjustmentMapper.selectList(
+ new LambdaQueryWrapper<HolidayVehicleAdjustment>().eq(HolidayVehicleAdjustment::getYear, year));
+ for (HolidayVehicleAdjustment row : rows) {
+ if (row.getFlowDate() != null && row.getAdjustedValue() != null) {
+ result.put(row.getFlowDate(), row.getAdjustedValue());
+ }
+ }
return result;
}
@@ -243,22 +314,16 @@
return result;
}
- private BigDecimal totalPersonFlow(Map<String, BigDecimal> passenger, BigDecimal nonBusinessPersonFlow) {
+ private BigDecimal totalPersonFlow(BigDecimal nonBusinessPersonFlow, BigDecimal road, BigDecimal rail,
+ BigDecimal civilAviation, BigDecimal water) {
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));
+ total = sumNullable(total, road);
+ total = sumNullable(total, rail);
+ total = sumNullable(total, civilAviation);
+ total = sumNullable(total, water);
return total;
}
- // The national/provincial ring ratio always compares the current day with the previous day of
- // the same year. The stored comparison base (Excel column C) is kept as an operator backup
- // value only and never feeds this calculation.
- private BigDecimal nationalProvincialRingRatio(BigDecimal current, BigDecimal previous) {
- if (current == null) return null;
- return previous == null ? null : ratio(current, previous);
- }
private BigDecimal ratio(BigDecimal current, BigDecimal base) {
BigDecimal quotient = HolidayNumberUtils.divide(current, base);
diff --git a/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayVehicleAdjustmentService.java b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayVehicleAdjustmentService.java
new file mode 100644
index 0000000..d04d192
--- /dev/null
+++ b/traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayVehicleAdjustmentService.java
@@ -0,0 +1,124 @@
+package com.trafficaudit.holiday.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.trafficaudit.holiday.HolidayNumberUtils;
+import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment;
+import com.trafficaudit.holiday.mapper.HolidayVehicleAdjustmentMapper;
+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 HolidayVehicleAdjustmentService {
+
+ private static final int REASON_MAX_LENGTH = 500;
+
+ @Resource
+ private HolidayVehicleAdjustmentMapper adjustmentMapper;
+
+ public List<HolidayVehicleAdjustment> list(Integer year) {
+ return adjustmentMapper.selectList(new LambdaQueryWrapper<HolidayVehicleAdjustment>()
+ .eq(year != null, HolidayVehicleAdjustment::getYear, year)
+ .orderByDesc(HolidayVehicleAdjustment::getYear)
+ .orderByAsc(HolidayVehicleAdjustment::getFlowDate));
+ }
+
+ @Transactional
+ public HolidayVehicleAdjustment save(Map<String, Object> params) {
+ Long id = longValue(params.get("id"));
+ Integer year = intValue(params.get("year"));
+ LocalDate flowDate = dateValue(params.get("flowDate"));
+ BigDecimal adjustedValue = decimalValue(params.get("adjustedValue"));
+ String reason = textValue(params.get("reason"));
+
+ if (year == null || year < 2000 || year > 2100) {
+ throw new IllegalArgumentException("骞翠唤涓嶆纭�");
+ }
+ if (year >= LocalDate.now().getYear()) {
+ throw new IllegalArgumentException("寰�骞村浗鐪侀亾鏁版嵁璋冩暣浠呯淮鎶ゅ巻鍙插勾浠�");
+ }
+ if (flowDate == null || flowDate.getYear() != year) {
+ throw new IllegalArgumentException("鏃ユ湡蹇呴』灞炰簬鎵�閫夊勾浠�");
+ }
+ if (adjustedValue == null || adjustedValue.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("璋冩暣鍚庣殑鍥界渷閬撹溅娴侀噺涓嶈兘灏忎簬 0");
+ }
+ if (reason.isEmpty()) {
+ throw new IllegalArgumentException("璋冩暣鍘熷洜涓嶈兘涓虹┖");
+ }
+ if (reason.length() > REASON_MAX_LENGTH) {
+ throw new IllegalArgumentException("璋冩暣鍘熷洜涓嶈兘瓒呰繃 " + REASON_MAX_LENGTH + " 涓瓧绗�");
+ }
+
+ HolidayVehicleAdjustment sameKey = adjustmentMapper.selectOne(
+ new LambdaQueryWrapper<HolidayVehicleAdjustment>()
+ .eq(HolidayVehicleAdjustment::getYear, year)
+ .eq(HolidayVehicleAdjustment::getFlowDate, flowDate)
+ .last("limit 1"));
+ HolidayVehicleAdjustment entity;
+ if (id == null) {
+ entity = sameKey == null ? new HolidayVehicleAdjustment() : sameKey;
+ } else {
+ entity = adjustmentMapper.selectById(id);
+ if (entity == null) {
+ throw new IllegalArgumentException("璋冩暣璁板綍涓嶅瓨鍦�");
+ }
+ if (sameKey != null && !sameKey.getId().equals(id)) {
+ throw new IllegalArgumentException("璇ュ勾浠藉拰鏃ユ湡宸插瓨鍦ㄨ皟鏁磋褰�");
+ }
+ }
+
+ if (entity.getId() == null) {
+ entity.setCreatedAt(LocalDateTime.now());
+ }
+ entity.setYear(year);
+ entity.setFlowDate(flowDate);
+ entity.setAdjustedValue(HolidayNumberUtils.normalize(adjustedValue));
+ entity.setReason(reason);
+ entity.setUpdatedAt(LocalDateTime.now());
+ if (entity.getId() == null) {
+ adjustmentMapper.insert(entity);
+ } else {
+ adjustmentMapper.updateById(entity);
+ }
+ return entity;
+ }
+
+ @Transactional
+ public void delete(Long id) {
+ if (id == null) {
+ throw new IllegalArgumentException("缂哄皯璁板綍 id");
+ }
+ adjustmentMapper.deleteById(id);
+ }
+
+ private Long longValue(Object value) {
+ if (value == null || value.toString().trim().isEmpty()) return null;
+ return Long.valueOf(value.toString().trim());
+ }
+
+ private Integer intValue(Object value) {
+ if (value == null || value.toString().trim().isEmpty()) return null;
+ return Integer.valueOf(value.toString().trim());
+ }
+
+ private BigDecimal decimalValue(Object value) {
+ if (value == null || value.toString().trim().isEmpty()) return null;
+ return HolidayNumberUtils.decimalValue(value);
+ }
+
+ private LocalDate dateValue(Object value) {
+ if (value == null || value.toString().trim().isEmpty()) return null;
+ return LocalDate.parse(value.toString().trim());
+ }
+
+ private String textValue(Object value) {
+ return value == null ? "" : value.toString().trim();
+ }
+}
diff --git a/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayComparisonBaseTest.java b/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayComparisonBaseTest.java
index 62c7366..8dc3531 100644
--- a/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayComparisonBaseTest.java
+++ b/traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayComparisonBaseTest.java
@@ -4,8 +4,10 @@
import com.trafficaudit.holiday.HolidayNumberUtils;
import com.trafficaudit.holiday.entity.HolidayCalendar;
import com.trafficaudit.holiday.entity.HolidayPassengerFlow;
+import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment;
import com.trafficaudit.holiday.entity.HolidayVehicleFlow;
import com.trafficaudit.holiday.mapper.HolidayPassengerFlowMapper;
+import com.trafficaudit.holiday.mapper.HolidayVehicleAdjustmentMapper;
import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper;
import org.junit.jupiter.api.Test;
@@ -13,79 +15,159 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
- * The Excel comparison base column (C) is an operator backup value only: it must never change the
- * national/provincial ring ratio. This locks in that behaviour.
+ * Ring-ratio denominators are built one day at a time. The Excel comparison base (column C) is
+ * consumed on the source day and inherited by the next day's denominator, while historical
+ * adjustment records change only that historical day's same-day value.
*/
class HolidayComparisonBaseTest {
@Test
- void ringRatioUsesPreviousDayEvenWhenComparisonBaseIsStored() throws Exception {
+ void comparisonBaseChangesOnlyTheNextDayRingRatioDenominator() throws Exception {
+ HolidayStatService service = serviceWithData(
+ 2026,
+ passengerRows(2026, "50", "10", "6", "3"),
+ vehicleRows(2026),
+ Collections.<HolidayVehicleAdjustment>emptyList());
+
+ List<Map<String, Object>> rows = service.buildOverviewRows(
+ 2026, HolidayConstants.TYPE_NATIONAL_DAY, calendar(2026));
+
+ assertEquals(new BigDecimal("323.7837"), rows.get(2).get("nationalProvincial"));
+ assertEquals(new BigDecimal("320.2418"), rows.get(2).get("comparisonBase"));
+ assertEquals(new BigDecimal("322.7695"), rows.get(3).get("nationalProvincial"));
+
+ BigDecimal nationalRatio = (BigDecimal) rows.get(3).get("nationalProvincialRingRatio");
+ assertEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("320.2418")), nationalRatio);
+ assertNotEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("323.7837")), nationalRatio);
+
+ HolidayStatService.DailyMetrics base = service.ringRatioBaseMetrics(
+ 2026, HolidayConstants.TYPE_NATIONAL_DAY, 4);
+ assertNotNull(base);
+ assertEquals(new BigDecimal("320.2418"), base.getNationalProvincial());
+ assertDecimalEquals("1008.58032", base.getNonBusinessPersonFlow());
+ assertDecimalEquals("1058.58032", base.getRoadPersonFlow());
+ assertDecimalEquals("1077.58032", base.getTotalPersonFlow());
+ }
+
+ @Test
+ void historicalAdjustmentChangesTheHistoricalSameDayValue() throws Exception {
+ HolidayVehicleAdjustment adjustment = new HolidayVehicleAdjustment();
+ adjustment.setYear(2025);
+ adjustment.setFlowDate(LocalDate.of(2025, 10, 4));
+ adjustment.setAdjustedValue(new BigDecimal("337.5009"));
+
+ HolidayStatService service = serviceWithData(
+ 2025,
+ passengerRows(2025, "50", "10", "6", "3"),
+ Arrays.asList(
+ vehicle("2025-10-03", HolidayConstants.ROAD_EXPRESSWAY, "100", null),
+ vehicle("2025-10-03", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "362.0035", null),
+ vehicle("2025-10-04", HolidayConstants.ROAD_EXPRESSWAY, "110", null),
+ vehicle("2025-10-04", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "350.0000", null)),
+ Collections.singletonList(adjustment));
+
+ List<Map<String, Object>> rows = service.buildOverviewRows(
+ 2025, HolidayConstants.TYPE_NATIONAL_DAY, calendar(2025));
+
+ assertEquals(new BigDecimal("337.5009"), rows.get(3).get("nationalProvincial"));
+ }
+
+ private static HolidayStatService serviceWithData(int year,
+ List<HolidayPassengerFlow> passengerRows,
+ List<HolidayVehicleFlow> vehicleRows,
+ List<HolidayVehicleAdjustment> adjustments) throws Exception {
HolidayConfigService configService = mock(HolidayConfigService.class);
HolidayPassengerFlowMapper passengerMapper = mock(HolidayPassengerFlowMapper.class);
HolidayVehicleFlowMapper vehicleMapper = mock(HolidayVehicleFlowMapper.class);
+ HolidayVehicleAdjustmentMapper adjustmentMapper = mock(HolidayVehicleAdjustmentMapper.class);
- when(configService.coefficientOf(2026, HolidayConstants.CATEGORY_HOLIDAY)).thenReturn(new BigDecimal("2.4"));
- when(passengerMapper.selectList(any())).thenReturn(new ArrayList<HolidayPassengerFlow>());
- when(vehicleMapper.selectList(any())).thenReturn(vehicleRows());
+ when(configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY)).thenReturn(new BigDecimal("2.4"));
+ when(configService.findHolidayDays(year, HolidayConstants.TYPE_NATIONAL_DAY)).thenReturn(calendar(year));
+ when(passengerMapper.selectList(any())).thenReturn(passengerRows);
+ when(vehicleMapper.selectList(any())).thenReturn(vehicleRows);
+ when(adjustmentMapper.selectList(any())).thenReturn(adjustments);
HolidayStatService service = new HolidayStatService();
inject(service, "configService", configService);
inject(service, "passengerFlowMapper", passengerMapper);
inject(service, "vehicleFlowMapper", vehicleMapper);
-
- List<Map<String, Object>> rows = service.buildOverviewRows(2026, HolidayConstants.TYPE_NATIONAL_DAY, calendar());
-
- BigDecimal ringRatio = (BigDecimal) rows.get(3).get("nationalProvincialRingRatio");
- assertEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("323.7837")), ringRatio);
- assertNotEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("320.2418")), ringRatio);
- assertEquals(new BigDecimal("320.2418"), rows.get(2).get("comparisonBase"));
- }
-
- @Test
- void exportFormulaComparesPreviousRowInsteadOfComparisonBaseColumn() {
- assertEquals("=(D6-D5)/D5", HolidayWorkbookService.ringRatioFormula(6, 5));
+ inject(service, "vehicleAdjustmentMapper", adjustmentMapper);
+ return service;
}
private static BigDecimal ratio(BigDecimal current, BigDecimal base) {
return HolidayNumberUtils.normalize(HolidayNumberUtils.divide(current, base).subtract(BigDecimal.ONE));
}
- private static List<HolidayVehicleFlow> vehicleRows() {
+ private static void assertDecimalEquals(String expected, BigDecimal actual) {
+ assertNotNull(actual);
+ assertEquals(0, new BigDecimal(expected).compareTo(actual));
+ }
+
+ private static List<HolidayVehicleFlow> vehicleRows(int year) {
List<HolidayVehicleFlow> rows = new ArrayList<>();
- rows.add(vehicle("2026-10-03", "323.7837", "320.2418"));
- rows.add(vehicle("2026-10-04", "322.7695", null));
+ rows.add(vehicle(year + "-10-03", HolidayConstants.ROAD_EXPRESSWAY, "100", null));
+ rows.add(vehicle(year + "-10-03", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "323.7837", "320.2418"));
+ rows.add(vehicle(year + "-10-04", HolidayConstants.ROAD_EXPRESSWAY, "110", null));
+ rows.add(vehicle(year + "-10-04", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "322.7695", null));
return rows;
}
- private static HolidayVehicleFlow vehicle(String date, String value, String base) {
+ private static HolidayVehicleFlow vehicle(String date, String roadCategory, String value, String base) {
HolidayVehicleFlow row = new HolidayVehicleFlow();
- row.setYear(2026);
+ row.setYear(LocalDate.parse(date).getYear());
row.setFlowDate(LocalDate.parse(date));
- row.setRoadCategory(HolidayConstants.ROAD_NATIONAL_PROVINCIAL);
+ row.setRoadCategory(roadCategory);
row.setVehicleType(HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER);
row.setVehicleCount(new BigDecimal(value));
row.setComparisonBase(base == null ? null : new BigDecimal(base));
return row;
}
- private static List<HolidayCalendar> calendar() {
+ private static List<HolidayPassengerFlow> passengerRows(int year, String road, String rail,
+ String civilAviation, String water) {
+ List<HolidayPassengerFlow> rows = new ArrayList<>();
+ rows.add(passenger(year + "-10-03", HolidayConstants.MODE_ROAD, road));
+ rows.add(passenger(year + "-10-03", HolidayConstants.MODE_RAIL, rail));
+ rows.add(passenger(year + "-10-03", HolidayConstants.MODE_CIVIL_AVIATION, civilAviation));
+ rows.add(passenger(year + "-10-03", HolidayConstants.MODE_WATER, water));
+ rows.add(passenger(year + "-10-04", HolidayConstants.MODE_ROAD, road));
+ rows.add(passenger(year + "-10-04", HolidayConstants.MODE_RAIL, rail));
+ rows.add(passenger(year + "-10-04", HolidayConstants.MODE_CIVIL_AVIATION, civilAviation));
+ rows.add(passenger(year + "-10-04", HolidayConstants.MODE_WATER, water));
+ return rows;
+ }
+
+ private static HolidayPassengerFlow passenger(String date, String mode, String value) {
+ HolidayPassengerFlow row = new HolidayPassengerFlow();
+ row.setYear(LocalDate.parse(date).getYear());
+ row.setFlowDate(LocalDate.parse(date));
+ row.setTransportMode(mode);
+ row.setPassengerCount(new BigDecimal(value));
+ return row;
+ }
+
+ private static List<HolidayCalendar> calendar(int year) {
List<HolidayCalendar> days = new ArrayList<>();
for (int i = 1; i <= 4; i++) {
HolidayCalendar day = new HolidayCalendar();
- day.setYear(2026);
+ day.setYear(year);
day.setHolidayType(HolidayConstants.TYPE_NATIONAL_DAY);
day.setDayIndex(i);
- day.setHolidayDate(LocalDate.of(2026, 10, i));
+ day.setHolidayDate(LocalDate.of(year, 10, i));
days.add(day);
}
return days;
diff --git a/traffic-audit-web/src/views/HolidayStat.vue b/traffic-audit-web/src/views/HolidayStat.vue
index 799267a..230ff0e 100644
--- a/traffic-audit-web/src/views/HolidayStat.vue
+++ b/traffic-audit-web/src/views/HolidayStat.vue
@@ -84,6 +84,61 @@
<el-card shadow="never" style="margin-top:14px">
<div slot="header">
+ <span>寰�骞村浗鐪侀亾鏁版嵁璋冩暣</span>
+ </div>
+ <div class="operation-row">
+ <div class="operation-item">
+ <span class="label">骞翠唤</span>
+ <el-select v-model="adjustmentYear" style="width:120px" @change="loadAdjustments">
+ <el-option v-for="item in availableAdjustmentYears" :key="item" :label="item + '骞�'" :value="item"></el-option>
+ </el-select>
+ </div>
+ <div v-if="operatorIsAdmin" class="operation-item">
+ <el-button type="primary" icon="el-icon-plus" @click="openAdjustmentDialog()">鏂板璋冩暣</el-button>
+ </div>
+ </div>
+ <el-table :data="adjustments" border stripe size="small" style="margin-top:12px" v-loading="adjustmentLoading">
+ <el-table-column prop="year" label="骞翠唤" width="90" align="center"></el-table-column>
+ <el-table-column prop="flowDate" label="鏃ユ湡" width="130" align="center"></el-table-column>
+ <el-table-column label="璋冩暣鍚庡浗鐪侀亾杞︽祦锛堜竾杈嗘锛�" width="230" align="right">
+ <template slot-scope="s">{{ formatAdjustmentValue(s.row.adjustedValue) }}</template>
+ </el-table-column>
+ <el-table-column prop="reason" label="璋冩暣鍘熷洜" min-width="280" show-overflow-tooltip></el-table-column>
+ <el-table-column label="鏇存柊鏃堕棿" min-width="170"><template slot-scope="s">{{ formatDateTime(s.row.updatedAt) }}</template></el-table-column>
+ <el-table-column v-if="operatorIsAdmin" label="鎿嶄綔" width="130" align="center">
+ <template slot-scope="s">
+ <el-button type="text" @click="openAdjustmentDialog(s.row)">缂栬緫</el-button>
+ <el-button type="text" class="danger-text" @click="deleteAdjustment(s.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <el-dialog :title="adjustmentForm.id ? '缂栬緫璋冩暣' : '鏂板璋冩暣'" :visible.sync="adjustmentDialogVisible" width="560px" v-dialog-drag>
+ <el-form label-width="120px">
+ <el-form-item label="骞翠唤">
+ <el-select v-model="adjustmentForm.year" style="width:100%" @change="adjustmentForm.flowDate = ''">
+ <el-option v-for="item in availableAdjustmentYears" :key="item" :label="item + '骞�'" :value="item"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏃ユ湡">
+ <el-date-picker v-model="adjustmentForm.flowDate" type="date" value-format="yyyy-MM-dd"
+ style="width:100%" :picker-options="adjustmentDatePickerOptions" placeholder="閫夋嫨鏃ユ湡"></el-date-picker>
+ </el-form-item>
+ <el-form-item label="璋冩暣鍚庤溅娴侀噺">
+ <el-input-number v-model="adjustmentForm.adjustedValue" :min="0" :precision="4" :step="0.0001" controls-position="right" style="width:100%"></el-input-number>
+ </el-form-item>
+ <el-form-item label="璋冩暣鍘熷洜">
+ <el-input v-model="adjustmentForm.reason" type="textarea" :rows="3" maxlength="500" show-word-limit></el-input>
+ </el-form-item>
+ </el-form>
+ <div slot="footer">
+ <el-button @click="adjustmentDialogVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" :loading="adjustmentSaving" @click="saveAdjustment">淇濆瓨</el-button>
+ </div>
+ </el-dialog>
+ </el-card>
+
+ <el-card shadow="never" style="margin-top:14px">
+ <div slot="header">
<span>瑙傛祴绔欏垎鏃ヨ皟鏌ユ暟鎹�</span>
<span class="hint">鐢ㄤ簬鍘熷鏁版嵁鐣欏瓨锛屼笉涓庝笂鏂硅妭鍋囨棩缁熻鏁版嵁鍏宠仈</span>
</div>
@@ -188,19 +243,41 @@
observationDialogVisible: false,
observationDialogDate: '',
observationRows: [],
- observationRowsLoading: false
+ observationRowsLoading: false,
+ adjustmentYear: new Date().getFullYear() - 1,
+ adjustments: [],
+ adjustmentLoading: false,
+ adjustmentDialogVisible: false,
+ adjustmentSaving: false,
+ adjustmentForm: {
+ id: null,
+ year: new Date().getFullYear() - 1,
+ flowDate: '',
+ adjustedValue: null,
+ reason: ''
+ }
}
},
computed: {
holidayTypeLabel() {
const item = this.holidayTypeOptions.find(x => x.value === this.holidayType)
return item ? item.label : this.holidayType
+ },
+ availableAdjustmentYears() {
+ const currentYear = new Date().getFullYear()
+ return this.years.filter(item => item < currentYear)
+ },
+ adjustmentDatePickerOptions() {
+ return {
+ disabledDate: date => date.getFullYear() !== Number(this.adjustmentForm.year)
+ }
}
},
mounted() {
this.loadOptions()
this.loadOverview()
this.loadObservationDates()
+ this.loadAdjustments()
},
methods: {
previousDateString() {
@@ -225,6 +302,72 @@
})
.catch(err => this.showError(err, '鑺傚亣鏃ユ暟鎹姞杞藉け璐�'))
.finally(() => { this.loading = false })
+ },
+ loadAdjustments() {
+ this.adjustmentLoading = true
+ api.get('/holiday/adjustments', { params: { year: this.adjustmentYear } })
+ .then(res => {
+ if (res.code !== 200) throw new Error(res.message || '鍔犺浇澶辫触')
+ this.adjustments = res.data || []
+ })
+ .catch(err => this.showError(err, '寰�骞村浗鐪侀亾鏁版嵁璋冩暣鍔犺浇澶辫触'))
+ .finally(() => { this.adjustmentLoading = false })
+ },
+ openAdjustmentDialog(row) {
+ const source = row || {}
+ this.adjustmentForm = {
+ id: source.id || null,
+ year: source.year || this.adjustmentYear,
+ flowDate: source.flowDate || '',
+ adjustedValue: source.adjustedValue === undefined || source.adjustedValue === null
+ ? null : Number(source.adjustedValue),
+ reason: source.reason || ''
+ }
+ this.adjustmentDialogVisible = true
+ },
+ saveAdjustment() {
+ const form = this.adjustmentForm
+ if (!form.year || !form.flowDate) {
+ this.$message.warning('璇烽�夋嫨骞翠唤鍜屾棩鏈�')
+ return
+ }
+ if (form.adjustedValue === null || form.adjustedValue === undefined || Number(form.adjustedValue) < 0) {
+ this.$message.warning('璇疯緭鍏ヤ笉浣庝簬 0 鐨勮皟鏁村悗杞︽祦閲�')
+ return
+ }
+ if (!form.reason || !form.reason.trim()) {
+ this.$message.warning('璇峰~鍐欒皟鏁村師鍥�')
+ return
+ }
+ this.adjustmentSaving = true
+ api.post('/holiday/adjustments/save', {
+ id: form.id,
+ year: form.year,
+ flowDate: form.flowDate,
+ adjustedValue: form.adjustedValue,
+ reason: form.reason
+ })
+ .then(res => {
+ if (res.code !== 200) throw new Error(res.message || '淇濆瓨澶辫触')
+ this.adjustmentDialogVisible = false
+ this.adjustmentYear = form.year
+ this.loadAdjustments()
+ this.loadOverview()
+ this.$message.success('璋冩暣宸蹭繚瀛�')
+ })
+ .catch(err => this.showError(err, '淇濆瓨澶辫触'))
+ .finally(() => { this.adjustmentSaving = false })
+ },
+ deleteAdjustment(row) {
+ this.$confirm('纭畾鍒犻櫎 ' + row.flowDate + ' 鐨勫浗鐪侀亾鏁版嵁璋冩暣鍚楋紵', '鍒犻櫎纭', { type: 'warning' })
+ .then(() => api.post('/holiday/adjustments/delete', null, { params: { id: row.id } }))
+ .then(res => {
+ if (res && res.code !== 200) throw new Error(res.message || '鍒犻櫎澶辫触')
+ this.loadAdjustments()
+ this.loadOverview()
+ this.$message.success('璋冩暣宸插垹闄�')
+ })
+ .catch(err => { if (err !== 'cancel' && err !== 'close') this.showError(err, '鍒犻櫎澶辫触') })
},
onFileChange(file) {
this.selectedFile = file.raw
@@ -433,6 +576,16 @@
})
.catch(err => { if (err !== 'cancel' && err !== 'close') this.showError(err, '鍒犻櫎澶辫触') })
},
+ formatAdjustmentValue(value) {
+ if (value === null || value === undefined || value === '') return '鈥�'
+ const number = Number(value)
+ if (!Number.isFinite(number)) return value
+ return number.toFixed(4)
+ },
+ formatDateTime(value) {
+ if (!value) return '鈥�'
+ return String(value).replace('T', ' ').slice(0, 19)
+ },
formatNumber(value) {
if (value === null || value === undefined || value === '') return '鈥�'
return Number(value).toFixed(2)
--
Gitblit v1.9.1