| | |
| | | web/ |
| | | traffic-audit-server/web/ |
| | | |
| | | # ==== Windows æå¡å
è£
å¨ï¼WinSW äºè¿å¶ï¼17.4MBï¼ç± pack-deploy.ps1 ä¸è½½å¹¶æ ¡éª SHA-256ï¼ä¸å
¥åºï¼==== |
| | | deploy-templates/tools/ |
| | | |
| | | # ==== é¨ç½²äº§ç©ï¼ä¸å
¥åºï¼==== |
| | | deploy/ |
| | | traffic-audit-deploy-*.zip |
| | |
| | | > **æ¯å¤©ç»ææ¶**ï¼æå½å¤©è¿å±ã踩è¿çåãæ°éæ±ãæªå®æäºé¡¹æ´æ°å°æ¬æä»¶ï¼ç¶åå¯ä»¥æ¾å¿å¼æ°å¯¹è¯ã |
| | | > **åä½çº¦å®ï¼2026-08-31ï¼**ï¼è¯´ãæäº¤ã= æ¬å° commit + æ¨éå°è¿ç«¯ gitï¼origin mainï¼Gitblitï¼ï¼ä¸ååªåæ¬å° commitã |
| | | |
| | | ## ææ°ï¼2026-10-03ï¼ |
| | | |
| | | ### 10-03 æï¼éè¦èåæ¥æ°æ¨¡æ¿ï¼2019/å䏿¥ï¼éé
|
| | | - **模æ¿åå**ï¼`2026å¹´å䏿°æ®.xlsx` çé路表å¢å 2019 å¹´å忝2019ï¼éè·¯ãæ°èªå¢å âå䏿¥âè¡ï¼å¿«æ¥æ¨¡æ¿å¢å 忝2024 åãæ§æ¨¡æ¿å·²æ¾å¨ `docs/ä¸é¡¹ç»è®¡/éè¦è忥/å¤ä»½/`ã |
| | | - **æ°æ®åºåå§å**ï¼å¯å¨æ¶ç´æ¥åå
¥ 2019 éè·¯æ°æ®ï¼ä»¥å 2024/2025/2026 å¹´éè·¯ãæ°èªå䏿¥æ°æ®ï¼`holiday_calendar.day_index=0` 表示âå䏿¥âï¼æ§åºå¯å¨ä¼èªå¨è¡¥é½å·²æå¹´ä»½çå䏿¥é
ç½®è¡ï¼ä¸ä¸è¦ç管çå已维æ¤çæ¥æã |
| | | - **æ£å¸¸å¯¼å
¥å£å¾**ï¼åªè¯»åæé年份çå½å¹´åï¼2026 å¹´éè·¯/æ°èª/éè·¯/水路为 C åãé«é为 C åãæ®éå½çé为 D åï¼ï¼åæ¶åªé¢å¤è¯»åå½å年份éè·¯/æ°èªçâå䏿¥âè¡ã2019/2024/2025 åå²åä¸åä¸å¯¼å
¥ã |
| | | - **å¿«æ¥å£å¾**ï¼åæ¯åªä½¿ç¨ 2025ã2024ï¼åæç¬¬ 1 æ¥çéè·¯ãæ°èªç¯æ¯ä½¿ç¨âå䏿¥âæ°æ®ï¼å
¬è·¯ãæ°´è·¯ãæ»è®¡åå
¶ä»å£å¾ç¬¬ 1 æ¥ä»æ¾ç¤ºâââï¼ç¬¬ 2 æ¥èµ·æ£å¸¸è®¡ç®ç¯æ¯ã |
| | | - **è¿ç§»èæ¬**ï¼`docs/sql_holiday_previous_day_migration.sql`ï¼é¨ç½²å
åæ¥ä¸º `deploy-templates/db/sql_holiday_previous_day_migration.sql`ï¼èæ¬å¯é夿§è¡ã |
| | | - **2019 æ¾ç¤ºè¡¥é½ï¼10-03 æè¿½å ï¼**ï¼`holiday_calendar` å¢å 2019-10-01 è³ 2019-10-08 é
ç½®ï¼ç³»ç»é
ç½®-èåæ¥ç®¡çåä¸é¡¹ç»è®¡-è忥åå¢å 2019 年份ï¼åå²åºæ§è¡å¹çè¿ç§»åå³å¯æ¥ç 2019 å¹´ 8 天éè·¯æ°æ®ã |
| | | - **éªè¯ï¼æ¬æº 3305 åº + ç宿¨¡æ¿ï¼**ï¼æ¥å£å¯¼å
¥ç»æ `passengerRows=10, vehicleRows=4, successRows=14, failRows=0`ï¼æ°æ®åºæ½æ¥ 2026-09-30 éè·¯=82.1ãæ°èª=6.6994ï¼2026-10-01 éè·¯=113.8ãæ°èª=6.7482ãéè·¯=78.82ãæ°´è·¯=1.311ï¼2026-10-02 éè·¯=82.0ãæ°èª=6.3914ãéè·¯=75.61ãæ°´è·¯=3.3373ï¼2026-10-01 å¿«æ¥éè·¯ç¯æ¯=+38.6%ãæ°èªç¯æ¯=+0.7%ï¼æ»è¡ç¯æ¯ä¸ºâââï¼2026-10-02 éè·¯ç¯æ¯=-27.9%ãæ°èªç¯æ¯=-5.3%ã |
| | | - **è¿è¡ç¶æ**ï¼å端 8090ï¼éå¯åå·²å è½½æ°ä»£ç ï¼ãå端 8080 åå¨è¿è¡ï¼è®¿é® `http://localhost:8080`ã |
| | | |
| | | ### 10-03 æ·±å¤ï¼å¿«æ¥è¡¨æ ¼ç¾åæ¯ä¸æ¨¡æ¿æ ·å¼ä¿®å¤ï¼å·²éªæ¶ï¼ |
| | | - **é®é¢**ï¼çæå¿«æ¥è¡¨æ ¼ä½¿ç¨âå¢é¿/ä¸éâæåï¼æ°åæè¡å¹¶å¤åº 1 页ï¼å¤æ ¸åç°æ¨¡æ¿è·¯å¾æ£ç¡®ï¼æ ¹å æ¯ `HolidayBulletinService#setCellText` æ¿æ¢åå
æ ¼æ¶åªä¿ç段è½å±æ§ï¼æªä¿çæ¨¡æ¿ run çåä½/åå·å±æ§ã |
| | | - **å£å¾**ï¼è¡¨æ ¼ç¯æ¯/忝æ¹ä¸ºå¸¦ç¬¦å·ä¸ä½å°æ°ï¼æ£æ°ä¸å `+`ï¼è´æ°ä¿ç `-`ï¼ï¼é¶æä¸ä½å°æ°åä¸ºé¶æ¾ç¤º `0%`ï¼ç¼ºå¤±/åºæ°ä¸ºç©ºæä¸ºé¶æ¾ç¤º `â`ï¼æ£æç»§ç»ä½¿ç¨âå¢é¿/ä¸éâã |
| | | - **ä¿®å¤**ï¼åå
æ ¼æ¿æ¢æ¶ç»§æ¿æ¨¡æ¿ `rPr`ï¼æ°å¢ `HolidayBulletinServiceTest` è¦çæ£æ°ãè´æ°ãé¶ãåèäºå
¥ä¸ºé¶ã缺失åé¶åºæ°ã |
| | | - **éªè¯**ï¼Maven ç¼è¯åæµè¯éè¿ï¼éæ°çæ 2026-10-01 å¿«æ¥ï¼æ£æä»ä¸ºâå¢é¿2.3%âä¸ä¸å«âå¢é¿äºâï¼Word 导åºç± 2 页æ¢å¤ä¸º 1 é¡µï¼æ°åä¸åæè¡ãæç»æ ·æ¥è§æ¬å° `_tmp_holiday_verify/bulletin_final_signed_styled_2026-10-01.docx`ã |
| | | - **ç¶æ**ï¼2026-10-03 ç¨æ·éªæ¶éè¿ï¼æ¬æ¬¡æäº¤åªå
å«ä»£ç ãå彿µè¯åæ¬äº¤æ¥è®°å½ï¼ä¸å
å« `_tmp_*` è¿ç¨äº§ç©åç¨æ·æªæäº¤çæ°æ®æä»¶éå½åã |
| | | |
| | | ### 10-03 æ·±å¤ï¼2ï¼ï¼ç¼ºå½æ¥æ°èªæ°æ®æ¶çç´¯è®¡åæ¯å¯¹æ¯æé´ä¿®æ£ |
| | | - **å¿«æ¥æ°èªç´¯è®¡åæ¯å¯¹æ¯æé´ä¿®æ£ï¼2026-10-03ï¼ç¼ºå½æ¥æ°èªæ°æ®åºæ¯ï¼**ï¼å½æè¿è¾æ¹å¼å½å¹´ç´¯è®¡è¦ççæ¥æçäºæéæ¥æï¼å
¸å为 10 æ 2 æ¥æ°èªæ°æ®æªåºãæ°èªç´¯è®¡åªæ 10-01ï¼ï¼ç´¯è®¡åæ¯æ¹ä¸ºãåªåå¾å¹´å·²ç»è¦çå°çæ¥ææ¯è¾ãââå
æ¾å½å¹´æåä¸ä¸ªæè¯¥ææ æ°æ®çæ¥æï¼ååå¾å¹´åæ¥ç´¯è®¡ãä¿®å¤å缺失æ°èªç 10 æ 2 æ¥å¿«æ¥æ°èªç´¯è®¡åæ¯ç± `-50.5%/-49.9%` å为 `-10.0%/-0.9%`ï¼ä¸æå·¥ç¨¿ -10.03%/-0.87% ç 1 ä½å°æ°å¼ä¸è´ï¼ãå®ç°è§ `HolidayBulletinService.comparableCumulative`ï¼ä»
ä½ç¨äºæ°èªè¡ï¼ï¼å
¶ä½è¡ä¸æ»äººåæµå¨éå£å¾ä¸åã |
| | | - **ãç´¯è®¡åæ¯ -4.65% vs å¿«æ¥ -4.6%ãç»è®ºï¼2026-10-03ï¼æ 鿹代ç ï¼**ï¼Excel æ¾ç¤ºç -4.65% æ¯**两ä½å°æ°**ç»æï¼åºå±ç²¾ç¡®æ¯å¼æ¯ `-4.64633%`ï¼2026 累计 3082.23818ã2025 累计 3232.42742ï¼ï¼**䏿¬¡åèäºå
¥å° 1 ä½å°æ°å°±æ¯ -4.6%**ãæå·²ç»åå°ä¸¤ä½å°æ°ç -4.65 ååä¸ä½æå¾å° -4.7ï¼å±äºæ¬¡åæ´ï¼ä¸éç¨ãåç 2026 对 2024 为 `-1.7710%` â `-1.8%`ãå¿«æ¥ç¾åæ¯ç»ä¸ 1 ä½å°æ°å£å¾ä¸åã |
| | | - **ãæç¼ºæ°èªãæç¤º**ï¼ç¨æ·ç¡®è®¤ç±äººå·¥å¨çæçå¿«æ¥ç¨¿ä¸ä¿®æ¹ï¼ç³»ç»ä¸çæè¯¥æç¤ºè¡ï¼**ä¸åä»£ç æ¹å¨**ã |
| | | - **éªè¯**ï¼`HolidayBulletinServiceTest` 3 个ç¨ä¾éè¿ï¼æ°å¢ 2 个è¦çæ°èªç´¯è®¡å¯¹æ¯æé´ä¸æ æ°æ®è¿å nullï¼ï¼ç«¯å°ç«¯ç¨åºå
çå® 2026-10-02 æ°æ®ï¼éè·¯/éè·¯/æ°´è·¯/车æµé½å
¨ã缺æ°èªï¼éæ°çæå¿«æ¥ï¼æ°èªç´¯è®¡åæ¯ `-10.0%/-0.9%`ãæ°èªç´¯è®¡ `6.75`ãæ»è®¡ç´¯è®¡ `3082.24`ãæ»è®¡ç´¯è®¡åæ¯ `-4.6%/-1.8%`ï¼ä¸æå·¥ç¨¿å£å¾ä¸è´ã |
| | | - **Git**ï¼æ¬æ¬¡æ¹å¨å¾
ç¨æ·ãéªæ¶éè¿ãååæäº¤/æ¨éã |
| | | |
| | | ### 10-03 æ·±å¤ï¼3ï¼ï¼å¿«æ¥ç´¯è®¡æ£æåä½ç±ãäº¿äººæ¬¡ãæ¹ãä¸äººæ¬¡ãï¼å¾
éªæ¶ï¼ |
| | | |
| | | - **å£å¾ï¼ç¨æ·/é¢å¯¼ç¡®è®¤ï¼**ï¼åæç´¯è®¡æ°å¼ç®ååå°ï¼0.31 亿ï¼ï¼æ£æå¯è¯»æ§å·®ï¼æ¹ä¸ºä¸å½æ¥ä¸è´ç**ä¸äººæ¬¡**ï¼ä¾å¦ `åæä»¥æ¥ï¼å
¨çå
¨ç¤¾ä¼è·¨åºåäººåæµå¨é累计3088.63ä¸äººæ¬¡`ï¼ä¸åè¾åºã0.31亿人次ããè¡¨æ ¼ä¸å
¶ä½å£å¾ä¸åã |
| | | - **å®ç°**ï¼`HolidayBulletinService` æ½åº `cumulativeParagraph(...)`ï¼ç´æ¥æä¸äººæ¬¡ï¼2 ä½å°æ°ï¼è¾åºç´¯è®¡å¼ï¼åæ¯ä»ä¸ºãå¢é¿/ä¸é + ä¸ä½å°æ°ãï¼**ä¿æä¸æ¬¡åæ´ä¸å**ï¼æ»è®¡ç´¯è®¡åæ¯ 2025 精确 -4.4486% â `-4.4%`ï¼æå·¥ç¨¿ç -4.5% å±äºæ¬¡åæ´ï¼ä¸éç¨ï¼ã |
| | | - **éªè¯**ï¼`HolidayBulletinServiceTest` 4 个ç¨ä¾éè¿ï¼æ°å¢ `cumulativeParagraphUsesWanUnitAndSingleRounding` åºåãä¸äººæ¬¡ + 䏿¬¡åæ´ãï¼ï¼éå¯å端åéæ°çæ 2026-10-02 å¿«æ¥ï¼æ£æä¸º `累计3088.63ä¸äººæ¬¡ï¼æ¯2025å¹´åæä¸é4.4%ï¼æ¯2024å¹´åæä¸é1.6%`ï¼ä¸æå·¥ç¨¿ä»
å·®æ¢å®çåæ´å£å¾ï¼2026-10-01 å彿£å¸¸ï¼`累计1643.17ä¸äººæ¬¡`ï¼ã |
| | | - **ç¶æ**ï¼å¾
ç¨æ·ãéªæ¶éè¿ãåæäº¤/æ¨éã |
| | | |
| | | ### 10-03 æï¼4ï¼ï¼æ°å¢ãè§æµç«åæ¥è°æ¥æ°æ®ãåå§æ°æ®å¯¼å
¥ï¼å¾
éªæ¶ï¼ |
| | | - **èæ¯ï¼ç¨æ· 2026-10-03 æåºï¼**ï¼æ®éå½çé车æµéçæ¥æºä¹ä¸æ¯è§æµç«åæ¥è°æ¥æ°æ®ï¼æ¨¡æ¿ `docs/ä¸é¡¹ç»è®¡/éè¦è忥/è§æµç«åæ¥è°æ¥æ°æ® (2026å¹´10æ1æ¥).xls`ï¼å
¶ä¸å段ãä¸å°å®¢æµéã峿®éå½çé车æµéæ¥æºï¼ã**ç³»ç»å
ä¸åå
³è**ï¼è忥ç»è®¡ä»èµ°åæå¯¼å
¥ï¼æ¬åè½åªååå§æ°æ®çåï¼ä¸ºåç»åæææç§¯ç´¯æ°æ®åºç¡ã |
| | | - **æ°æ®è¡¨**ï¼æ°å¢ `observation_station_flow`ï¼DDL è§ `docs/sql_observation_station.sql`ï¼é¨ç½²å
åæ¥ä¸º `deploy-templates/db/sql_observation_station.sql`ãä¿ç模æ¿å
¨é¨ 26 åï¼è§æµç«ç¼å·/åç§°/è§æµéç¨/å°å¸/åºå¿/è¡é©¶æ¹å + åè½¦åæµé + 客车/货车/汽车/æºå¨è½¦æµé + åç±»å½é + æºå¨è½¦é度 + æ¥æ¤åº¦ + æ¥æºæä»¶åï¼ãå¯ä¸é® `(obs_date, station_code, direction)`ï¼**æ°å¼åä¸å¾ `DECIMAL(30,15)`**ï¼Java å®ä½å段ä¸å¾ `BigDecimal`ï¼ä¸èåæ¥è¡¨åå£å¾ï¼æ float/doubleã |
| | | - **导å
¥å®ç°**ï¼æ°å¢å
`com.trafficaudit.observation`ï¼entity / mapper / dto / service / controllerï¼ãç¨ POI `WorkbookFactory` åæ¶æ¯æ `.xls` å `.xlsx`ï¼**表头æååå¹é
**ï¼å顺åºååä¸å½±å导å
¥ï¼ï¼æ«æå 10 è¡å®ä½è¡¨å¤´ï¼æ¥æå
¼å®¹ Excel æ¥ææ ¼å¼ä¸ `yyyy-M-d`/`yyyy/M/d`/`yyyyMMdd`/ä¸æå¹´ææ¥ï¼å䏿¥æ**éå¤å¯¼å
¥å³æ´ä½è¦ç**ï¼å
å è¯¥æ¥æåæå
¥ï¼ï¼å¹çã |
| | | - **æ¥å£**ï¼`POST /api/observation/import`ã`GET /api/observation/dates`ï¼ææ¥ææ±æ»ç«æ°ãä¸å°å®¢æµéå计ãæè¿å¯¼å
¥æ¶é´ï¼ã`GET /api/observation/rows?date=&limit=`ã`POST /api/observation/delete?date=`ã |
| | | - **å端**ï¼`traffic-audit-web/src/views/HolidayStat.vue`ï¼ä¸é¡¹ç»è®¡ - èåæ¥ï¼æ°å¢ãè§æµç«åæ¥è°æ¥æ°æ®ãå¡çï¼éæ©æä»¶ â 导å
¥ â ææ¥ææ±æ»è¡¨ â æ¥çæç»ï¼å¼¹çªï¼â å é¤ã |
| | | - **宿µï¼ç宿¨¡æ¿ï¼æ¬æº 3305ï¼**ï¼å¯¼å
¥ 692 è¡ / 692 ç« / 0 è·³è¿ / 0 失败ï¼ä¸é¡¹åè®¡ä¸æºæä»¶é项ä¸è´ï¼ä¸å°å®¢æµé 3,523,873ãæºå¨è½¦æµé 5,487,965ãæºå¨è½¦å½é 7,140,575ï¼ï¼**692 è¡ Ã 25 åæ®µå
¨ééè¡æ¯å¯¹ 0 å¤ä¸ä¸è´**ï¼ä¸æï¼ç«å/å°å¸/åºå¿ï¼è½åºæ ä¹±ç ï¼éå¤å¯¼å
¥ä»ä¸º 692 è¡ï¼è¦çèéç´¯å ï¼ï¼ä¸ä¼ é Excel è¿åæç¡®é误ãæè¯ï¼692 ç«ä¸å°å®¢æµéå计 3,523,873 è¾ = 352.3873 ä¸è¾ï¼ä¸ 2026-10-01 èåæ¥ãæ®éå½çéãå
¥åºå¼å®å
¨ä¸è´ï¼å°è¯äºæ°æ®æ¥æºå
³ç³»ã |
| | | - **顺带修å¤**ï¼`pack-deploy.ps1` æ db èæ¬å¤å¶è¿å
å
docs çæ¸
åéæ¼äº `sql_holiday_previous_day_migration.sql`ï¼æ¬æ¬¡ä¸å¹¶è¡¥ä¸ `sql_observation_station.sql`ï¼`deploy-templates/README-é¨ç½²è¯´æ.txt` ç db æ¸
ååæ¥æ´æ°ã |
| | | - **10-03 æ·±å¤è¿½å ï¼è§æµç«çé¢å¾®è°ï¼**ï¼â ãæ¥çãæç»å¼¹çªææ¨¡æ¿è¡¨å¤´å±ç¤ºåå§æ°æ®å
¨é¨å段ï¼è§æµæ¥æ/ç«å·/ç«å/éç¨/å°å¸/åºå¿/æ¹å + å
¨é¨è½¦åæµéã客货汽车æºå¨è½¦æµéãåç±»å½éãéåº¦ãæ¥æ¤åº¦ï¼å¦éæ¥æºæä»¶åï¼å
± 27 åï¼æ¨ªåæ»å¨ï¼ï¼â¡æ±æ»è¡¨ãä¸å°å®¢æµéå计ãåä½ç±è¾æ¬¡æ¹ä¸º**ä¸è¾æ¬¡**ãä¿ç 4 ä½å°æ°ï¼`formatWanVehicles`ï¼å¦ 3,523,873 â 352.3873ï¼ãå端 dev server å·²çæ´æ°ç¼è¯éè¿ã |
| | | - **10-03 æ·±å¤è¿½å ï¼è§æµç«å表/æç»ä¿®æ£ï¼**ï¼â **æç»è¡åº**忥æ `stationCode` æåºï¼ä¸ Excel è¡åºå¯¹ä¸ä¸ï¼æ¹ä¸ºæå¯¼å
¥é¡ºåºï¼`id ASC`ï¼å³ Excel è¡åºï¼ï¼ä¾¿äºéè¡æ¯å¯¹ï¼â¡æç»**å顺åº**䏿¨¡æ¿ 26 åå®å
¨ä¸è´ï¼æ«å°¾éæ¥æºæä»¶åï¼ï¼â¢**è§æµéç¨/æºå¨è½¦é度/æ¥æ¤åº¦**æ¹ä¸ºæåå§å°æ°æ¾ç¤ºï¼`formatPlain`ï¼å»å°¾é¶ï¼å¦ 50.92 / 43.882 / 0.777 / 58ï¼ï¼ä¸ååºå® 2 ä½ï¼â£**17 个æµéä¸å½éå**æ¹ä¸ºæ´æ°æ¾ç¤ºï¼`formatInteger`ï¼å¦ 6413ï¼ï¼ä¸ Excel ä¸è´ï¼â¤æ±æ»å表ãè§æµæ¥æãæ¹ä¸º**æ£åº**ï¼mapper `ORDER BY obs_date ASC`ï¼ãå·²ç¨ 10-01/10-02 ç宿°æ®éªè¯è¡åºä¸ Excel å 5 è¡ä¸è´ï¼ä¸å¸®/ä¸åº/沼山ç«/é¾åç«/ç½äºï¼ã |
| | | - **ç¶æ**ï¼å¾
ç¨æ·ãéªæ¶éè¿ãåæäº¤/æ¨éã |
| | | |
| | | --- |
| | | |
| | | ### è¡¥å
ï¼10-03 ä¸åï¼ï¼æå¡å¨é¨ç½²æ¥ âAccess denied ... (using password: NO)â ââ æ ¹å ä¸ä¿®å¤ |
| | | - **ç°è±¡**ï¼æ `deploy/traffic-audit-release-20261002/` é¨ç½²å° `61.183.254.94` åï¼å端å¯å¨æ¥ `java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: NO)`ã |
| | | - **æ ¹å **ï¼jar å
ç½®é
ç½®éåºå£ä»¤æ¯å ä½ç¬¦ `${TRAFFIC_DB_PASSWORD:}`ï¼é»è®¤ç©ºä¸²ï¼ï¼è 10-02 é£ä¸ªå
**åªå« jar + web + dist**ï¼æ²¡æå¤ç½® `application.yml`ãæ²¡æ `docs/`ã`db/`ãå¯å¨èæ¬ãé¨ç½²æºæ²¡æä»»ä½å¤é¨é
ç½®è¦çï¼Spring å°±æ¿ç©ºå¯ç è¿æ¬æº MySQL â `using password: NO`ï¼æ³¨æï¼è¿æ¯âæ«ä¼ å¯ç âï¼ä¸æ¯å¯ç éï¼ã |
| | | - **æå¡å¨ä¾§ä¸æ¥ä¿®å¤ï¼æ ééæå
ï¼**ï¼å¨ jar åçº§æ¾ `application.yml`ï¼æ `spring.datasource.url/username/password` æ¹æç°åºçå®å¼åéå¯å³å¯ï¼ä¹å¯ç¨ç¯å¢åé `TRAFFIC_DB_PASSWORD` 代æ¿ãå¯å¨èæ¬å·²å 颿£æéï¼ç¼º `application.yml` / password 仿¯å 使åé½ä¼ WARNï¼ãä¿®å¤åè¥æ¥âæªæ¾å°æ°æ®ç®å½ docsâå说æå
å
缺 `backend/docs`ï¼ä»æ°å
ææºæºå¨æ·ã |
| | | - **æå
å·¥å
·å·²ä¿®ï¼å âæå
å·¥å
·å¾
ä¿®â 两项åå·²è½å®ï¼**ï¼`pack-deploy.ps1` éåä¸ºäºæ¥ï¼â ç¨æ°å¢æ¨¡æ¿ `deploy-templates/jar-application.yml`ï¼**å
¨å ä½ç¬¦**ï¼ä¸´æ¶æ¿æ¢æºç é
ç½®å `mvn clean package`ï¼æ 论æè´¥é½è¿åçå®é
ç½®ï¼â¡ è§£å jar å»å¯èªæ£ï¼â¢ å端æå»ºï¼â£ ç»è£
**宿´å
**ï¼`backend/{jar, application.yml, start-backend.cmd/.sh, docs/, web/}` + `frontend/dist` + `db/*.sql` + READMEï¼`backend/web` ä» dist èªå¨åæ¥ï¼éæä»¶ä¸è´ï¼ï¼â¤ æ´å
ææå¯é¥æ«æãæ°å¢ `-SkipBackend` å¯å¤ç¨ç°æ jar å¿«ééç»å
ã |
| | | - **模æ¿åæ¥ä¿®æ£**ï¼`deploy-templates/backend/application.yml` ç `server.port` ç± 8080 æ¹å 8090ï¼åå¼ä¼å¯¼è´åç«¯èµ·å¨ 8080ï¼ãè¡¥ `static-locations: file:./web/` ä¸ password é·é±æ³¨éï¼`README-é¨ç½²è¯´æ.txt` 常è§é®é¢æ°å¢è¯¥æ¥éå¤ç½®ï¼`docs/æå
é¨ç½²è¯´æ.md` æé表æ°å¢è¯¥è¡ï¼`application.yml.example` ç¯å¢åéåç»ä¸ä¸º `TRAFFIC_DB_USER/TRAFFIC_DB_PASSWORD/TRAFFIC_JWT_SECRET`ï¼åæ¥è¯¯ç¨ `MYSQL_PASSWORD`/`JWT_SECRET`ï¼ä¸ `start-dev.ps1` ä¸ä¸è´ï¼ã |
| | | - **æ°å
ï¼æ¬å°ï¼ä¸å
¥åºï¼**ï¼`deploy/traffic-audit-deploy-20261003/`ï¼388 æä»¶ / 93.07 MBï¼zip 76.37 MBï¼ï¼å·²éªè¯ï¼jar å
ç½®é
ç½®å
¨å ä½ç¬¦ï¼å»å¯èªæ£éè¿ï¼ã`backend/web` ä¸ `frontend/dist` éæä»¶ä¸è´ï¼å 44 个ï¼ã`backend/docs/çææ±æ»å¤§è¡¨` æ¯çåå¨ã`åè½æµè¯æ¥å/工使¥ç»/é®é¢æ±æ»/_cache` å·²æé¤ã |
| | | - **踩åï¼éè¦ï¼**ï¼æ¬æº PowerShell 5.1 ææ BOM ç `.ps1` å½ ANSI 读ï¼ä¸ææ³¨éä¼åæä¹±ç 并导è´èæ¬è§£æå¤±è´¥ã`pack-deploy.ps1` å¿
é¡»ä¿å为 **UTF-8 with BOM**ï¼ä»åºå
¶ä» `*.ps1` ä¹é½æ¯ BOMï¼ï¼ææ¥ä¸æ¾å®é
踩å°ã |
| | | |
| | | - **è¿è¡ç¶æï¼åäº¤æ¥æ¶ï¼**ï¼å端 8090ãå端 8080 åæªçå¬ï¼æ°å¯¹è¯å¦ç»§ç»å¼åææµè¯ï¼å
æ§è¡ `powershell -NoProfile -ExecutionPolicy Bypass -File start-dev.ps1`ãæ°æ®åºä»ä¸º `127.0.0.1:3305/traffic_audit`ï¼MySQL æå¡æªåã |
| | | - **åå¸æå»ºï¼å·²å®æï¼**ï¼åºâåå¸ç¼è¯âè¦æ±ï¼å·²å®æå端 `mvn clean package -DskipTests` åå端 `npm run build`ãjar å
å·²åå»å¯èªæ£ï¼åªä¿çç¯å¢åéå ä½ç¬¦ï¼æå»ºåæ¬æºçå® `src/main/resources/application.yml` å·²åæ ·æ¢å¤ã |
| | | - **åå¸äº§ç©ï¼æ¬å°ï¼æªå
¥åºï¼**ï¼ |
| | | - å端 jarï¼`traffic-audit-server/target/traffic-audit-server-1.0.0-SNAPSHOT.jar`ï¼çº¦ 54.6 MBï¼ |
| | | - å端ï¼`traffic-audit-web/dist`ï¼44 个æä»¶ã约 3.55 MBï¼ |
| | | - åå¸ç®å½ï¼`deploy/traffic-audit-release-20261002/`ï¼å« `backend/`ã`frontend/dist/`ï¼ |
| | | - åå¸å缩å
ï¼`deploy/traffic-audit-release-20261002.zip`ï¼çº¦ 51.11 MBï¼ |
| | | - **åå¸å
ç»æè¯´æ**ï¼`frontend/dist` æ¯æ åå端åå¸ç®å½ï¼`backend/web` æ¯åä¸ä»½ dist ç坿¬ï¼ç¨äºå端 8090 åç«¯å£æ¨¡å¼ï¼å½åéæèµæºè·¯å¾ä¸º `file:./web/,classpath:/static/`ï¼ã两å¤å·²éæä»¶æ¯å¯¹ï¼æä»¶åè¡¨ä¸ SHA-256 å®å
¨ä¸è´ã |
| | | - **å®å
¨ç¶æ**ï¼æ¬æºçå®é
置已ä¿åå¨è¢«å¿½ç¥ç `traffic-audit-server/config/application.yml`ï¼åå¸ jar ä¸å«ç宿°æ®åºå£ä»¤ãJWT secret æ DeepSeek keyãåå¸å
ä¸å
å«é¨ç½²æºççå®å¤é¨é
ç½®ï¼é¨ç½²æ¶éè¦ç»§ç»ä½¿ç¨ç°åº `application.yml` æææ¨¡æ¿å¡«åã |
| | | - **æå
å·¥å
·å¾
ä¿®ï¼éè¦ï¼**ï¼`pack-deploy.ps1` ç®åç´æ¥æ¶è´¹ `src/main/resources/application.yml`ï¼è¥è¯¥æä»¶è¢«æ¬æºçå®é
ç½®è¦çï¼ä¼å¨å»å¯èªæ£é¶æ®µä¸æ¢ï¼ä¸å®åªæ dist æ¾å° `frontend/dist`ï¼æ²¡æåæ¥å° `backend/web`ï¼é»è®¤å
æ æ³ç´æ¥ç¨äº 8090 åç«¯å£æ¨¡å¼ãæ¬æ¬¡ä½¿ç¨â临æ¶å ä½ç¬¦é
ç½® â æå»ºèªæ£ â æ¢å¤çå®é
ç½®âçæ¹å¼å®æã䏿¬¡åå¸å建议æ¹ä¸ºä» `application.yml.example` çæä¸´æ¶èµæºï¼å¹¶å¨ç»è£
é¶æ®µåæ¶å¤å¶ `backend/web`ã |
| | | - **Git/å·¥ä½åºï¼æ¬æ®µæ´æ°åï¼**ï¼`HEAD = origin/main = 37f5209`ï¼å½åæ å·²è·è¸ªæä»¶æ¹å¨ãæªè·è¸ªé¡¹æ `_tmp_holiday_verify/`ï¼åå²éªè¯äº§ç©ï¼å `docs.zip`ï¼çº¦ 31.6 MBï¼ç¨æ·æ¬å°äº§ç©ï¼ï¼äºè
åä¿çï¼æ°å¯¹è¯ä¸è¦è¯¯å æè¯¯æäº¤ã |
| | | - **ä¸ä¸æ¥**ï¼å¦éé¨ç½²ï¼ç´æ¥ä½¿ç¨ä¸é¢åå¸ç®å½æ zipï¼å¦éç»§ç»ä¿®å¤åå¸å·¥å
·ï¼å
å®ç°âå ä½ç¬¦æ¨¡æ¿èªå¨åæ¢ + `backend/web` èªå¨ç»è£
+ jar å»å¯èªæ£âååéæ°äº§åºåå¸å
ã |
| | | |
| | | ### Windows 弿ºèªå¯ï¼10-03 追å ï¼ç¨æ·éâæ¹æ¡ Bâï¼æ³¨åæ Windows æå¡ï¼ |
| | | - **å
åçè¿çé®é¢**ï¼`start-backend.cmd` **ä¸è½**ç´æ¥å弿ºèªå¯ï¼æ«å°¾æ `pause`ãæ æ¥å¿éå®åãä¾èµ PATHï¼ãç»åº Aï¼è®¡åä»»å¡ï¼/Bï¼Windows æå¡ï¼/C 䏿¹æ¡åï¼ç¨æ·é **B**ã |
| | | - **æ°å¢æ¨¡æ¿ï¼å·²å
¥åºï¼**ï¼ |
| | | - `deploy-templates/backend/trafficAudit.xml`ï¼WinSW æå¡é
ç½®ââid `TrafficAuditBackend`ã`startmode=Automatic` + `delayedAutoStart=true`ï¼ç MySQL å
就绪ï¼ã`onfailure restart 10s/30s/60s` + `resetfailure 1 hour`ã`workingdirectory=%BASE%`ãæ¥å¿ `logs\trafficAudit.out.log|err.log`ï¼10MB à 10 æ»å¨ï¼ã |
| | | - `deploy-templates/backend/install-service.ps1`ï¼ç®¡çåå®è£
/æ¥ç/éå¯/å é¤ï¼`-Status/-Restart/-Remove/-NoStart/-JavaPath`ï¼ï¼èªå¨å®ä½ javaï¼JAVA_HOME â PATH â JavaSoft 注å表 â Program Filesï¼å¹¶æç»å¯¹è·¯å¾åè¿ XML ç `executable` å
ç´ ï¼é夿§è¡å¹çï¼é管çåç´æ¥æç»ã |
| | | - `start-backend-service.cmd` + `install-autostart.ps1`ï¼è®¡å任塿¹å¼ï¼ä¿ç为**å¤é**ï¼æ³¨æ `New-ScheduledTask*` é Win8/Server 2012+ï¼ä¸**ä¸å¯ä¸æå¡æ¹å¼åæ¶å¯ç¨**ï¼ä¼æ¢ 8090ï¼ã |
| | | - `pack-deploy.ps1`ï¼æ°å¢ WinSW è·åï¼ç¼å `deploy-templates/tools/winsw.exe` å¹¶æ ¡ SHA-256ï¼ç¼ºåä» GitHub ä¸è½½ v2.12.0 x64ï¼ä¸ `-SkipServiceWrapper`ï¼å
å
è¾åº `backend\trafficAudit.exe`ï¼æ¹å以å¹é
XML æä»¶åï¼ã`.gitignore` æ°å¢ `deploy-templates/tools/`ï¼17.4MB äºè¿å¶ä¸å
¥åºï¼ã |
| | | - **WinSW = Windows Service Wrapper**ï¼ææ®éç¨åºå
æ Windows æå¡ãjava.exe èªå·±ä¸æ¯æå¡ç¨åºï¼ç´æ¥ `sc create` 伿¥âæå¡æ²¡æåæ¶ååºå¯å¨ææ§å¶è¯·æ±âï¼WinSW å®ç°äºæå¡æ§å¶åè®®åæèµ· `java -jar`ï¼å¹¶ç®¡éå¯ä¸æ¥å¿ã |
| | | - **宿µéªè¯ï¼æ¬æºï¼æªçæ£å®è£
æå¡ï¼**ï¼`trafficAudit.exe status` è½è¯»æå
XML å¹¶è¾åº `NonExistent`ï¼è§£æéè¿ï¼ï¼æ
æåå XML â `FATAL - The configuration file cound not be loaded`ï¼è¯æè¯¥æ ¡éªè·¯å¾ç宿æï¼ï¼`install-service.ps1 -Status` æ£å¸¸è¾åºâæªå®è£
âï¼æ åæ° / `-Remove` å¨é管çå䏿âå¿
须管çåâä¸ä¸äº§ç注å项ã |
| | | - **踩åï¼éè¦ï¼**ï¼XML æ¹åæåç¨æ£å `(?s)<executable>.*?</executable>`ï¼ä¼**å
å½ä¸æ³¨ééç `<executable>` åæ ·**ï¼ææ³¨éå¤´ä¸ service å¼å¤´æ´æ®µåæ â XML éæ³ãå·²æ¹ä¸º DOMï¼`SelectSingleNode('/service/executable')`ï¼å¹¶ææ³¨ééç裸æ ç¾æ¹æãå¦ï¼æ°å¢æå
ä»£ç æ¾å¤ä¸ä¸ª `}` 导è´è¯æ³éï¼å·²ç¨ `Parser::ParseFile` AST 夿 ¸ï¼ä¸¤ä¸ª ps1 ç°å¨è¯æ³ OK ä¸å为 UTF-8 with BOMã |
| | | - **å
¼å®¹æ§ï¼ç¨æ·é®âWin7/Win10/Win11/Server æ¯ä¸æ¯é½å¯ä»¥âï¼**ï¼æå¡æºå¶å®å
¨ä¸æ ·ï¼èªå¨å¯å¨/å»¶è¿å¯å¨/失败éå¯é½æ¯ç³»ç»è½åï¼ï¼å·®å«åªå¨å
è£
å¨ä¾èµä»ä¹è¿è¡æ¶ã宿µäºè¿å¶ï¼`WinSW-x64.exe` 2.12.0 æ¯**èªå
å« .NET 6 åæä»¶**ï¼å« `singlefilehost`/`System.Private.CoreLib`/`Microsoft.NETCore.App 6.0.13`/`net6.0`ï¼ï¼**ä¸éè¦è£
.NET Framework**ï¼èç³»ç»ï¼Win7 SP1/2008R2/2012R2ï¼è¥æ¥ `api-ms-win-*.dll` 缺失ï¼å¯æ¢å®æ¹ `WinSW.NET461.exe`ï¼0.6MBï¼é .NET 4.6.1+ï¼æ¹åæ `trafficAudit.exe` å³å¯ï¼XML ä¸å¨ï¼ï¼32 ä½ç³»ç»æ¢ `WinSW-x86.exe`ã |
| | | - **å®å
¨æç¤º**ï¼WinSW 宿¹äº§ç©**æªå Authenticode ç¾å**ï¼æ¬æº `Get-AuthenticodeSignature` = NotSignedï¼ï¼å æ¤ä»¥åºå® SHA-256 ä½å®æ´æ§æ ¡éªï¼`05B82D46AD331CC16BDC00DE5C6332C1EF818DF8CEEFCD49C726553209B3A0DA`ï¼ä¸ GitHub åå¸èµäº§ 18,243,033 åèä¸è´ï¼ã |
| | | - **æªéªè¯é¡¹ï¼éå¨ç®æ æºå䏿¬¡ï¼**ï¼æ¬æºå½å䏿¯ç®¡çåï¼**æ²¡çæ£ install è¿æå¡**ï¼å¼æºèªå¯ã失败éå¯ãæ¥å¿æ»å¨è¦å¨æå¡å¨ä¸æ README 3.1 æ§è¡ä¸æ¬¡ç¡®è®¤ã |
| | | - **æ°å
ï¼æ¬å°ï¼ä¸å
¥åºï¼**ï¼`deploy/traffic-audit-deploy-20261003-3/`ï¼393 æä»¶ / 110.49 MBï¼zip 83.77 MBï¼ãå·²éªè¯ï¼æå¡ä¸ä»¶å¥å¨ `backend\`ãå
å
`trafficAudit.exe` SHA-256 ä¸åºå®å¼ä¸è´ã`backend\web` ä¸ `frontend/dist` å 44 æä»¶ãjar å»å¯èªæ£éè¿ãæ ææ keyã**æ§ç `traffic-audit-deploy-20261003/` ä¸ `-2/` 已被å代**ï¼é¨ç½²è¯·ç¨ `-3`ï¼æ§å
å¾
ç¨æ·ç¡®è®¤ååå ï¼ã |
| | | - **è¿è¡ç¶æï¼åæ¬æ¡æ¶ï¼**ï¼å¼åç¨å端 8090ï¼PID 43040ï¼ãå端 8080ï¼PID 43152ï¼åå¨è·ï¼æ°æ®åº `127.0.0.1:3305/traffic_audit`ã |
| | | - **è¡¥å
坿使§ï¼ç¨æ·åé¦âbackend ä¸æä»¶å¤ªå¤ï¼ä¸ç¥éæä¹æä½âï¼**ï¼ |
| | | - æ°å¢åå»å
¥å£ `deploy-templates/backend/install-service.cmd`ï¼èªå¨ UAC ææ â è° `install-service.ps1`ï¼è¥æ¾ä¸å° `trafficAudit.exe` åæç¤ºâå¿
é¡»æ´ä¸ª backend ä¸èµ·æ·âã |
| | | - `install-service.ps1` è£
å®ä¼èªå·±æ¢æµç«¯å£ï¼TCP æ¢æµï¼å
¼å®¹ PowerShell 2.0ï¼ï¼æå° OK + æ¬æº/对å¤ç½åï¼èµ·ä¸æ¥æ¶ç´æ¥æå° `logs\*.log` æå 15 è¡ï¼è¥ `application.yml` å£ä»¤ä»åå ä½ç¬¦ï¼æååæä¸æ¬¡éç®æç¤ºã |
| | | - `README-é¨ç½²è¯´æ.txt` 顶鍿°å¢ãæå¡å¨ä¸åªè¦åè¿ 3 æ¥ã+âåªäºè¦æ·/åªäºä¸ç¨æ·âï¼`docs/æå
é¨ç½²è¯´æ.md` ç 5.4 æ¹ä¸ºâå³é® `install-service.cmd` â以管çåè¿è¡âã |
| | | - ä¸ä¸ª `.cmd` 模æ¿ç»ä¸ä¸º CRLFï¼æ°å¢çä¸¤ä¸ªåæ¥æ¯ LFï¼æ¹å¤çå¨ Windows ä¸åºç¨ CRLFï¼ã |
| | | - 宿µï¼å¼ºå¶èµ°âå·²ææâåæ¯æ¶ `cmd â install-service.ps1 -Status` é¾è·¯è·éã`%*` ä¼ åæ£å¸¸ï¼`Test-LocalPort` 对 8090 è¿å Trueã对 8099 è¿å Falseã |
| | | - æ°å
ï¼`deploy/traffic-audit-deploy-20261003-4/`ï¼394 æä»¶ / 110.50 MBï¼zip 83.78 MBï¼ââ**å½åå¯ä¸å¯ç¨å
**ã |
| | | - **åå¸å
æ¸
çï¼10-03 æï¼ç¨æ·ç¡®è®¤âåªç -4âï¼**ï¼å·²å é¤ `traffic-audit-release-20261002/`(+zip)ã`traffic-audit-deploy-20261003/`(+zip)ã`-2/`(+zip)ã`-3/`(+zip) å空ç `server-fix/`ï¼`deploy\` ç°å¨åªæ `traffic-audit-deploy-20261003-4/` åå®ç zipï¼éæ¾çº¦ 671MBï¼ã |
| | | > å æ¤æ¬æä»¶ä¸é¢æå°ç `deploy/traffic-audit-deploy-20261003/`ï¼388 æä»¶ï¼ä¸ `deploy/traffic-audit-release-20261002/` å为**åå²è®°å½**ï¼ç£çä¸å·²ä¸åå¨ï¼å«å廿¾ã |
| | | - **ç´§æ¥ä¿®å¤ï¼10-03 æï¼ï¼`install-service.ps1` å¯ç è§£æå´©æºï¼æå¡æ ¹æ¬æ²¡è£
ä¸** |
| | | - ç°è±¡ï¼ç¨æ·å¨æå¡å¨ï¼`D:\trafficAudit`ï¼åå» `install-service.cmd`ï¼æ¥ `æ æ³å°âTrimâçåæ°âtrimCharsâ转æ¢ä¸ºç±»åâSystem.Charâ`ï¼ä½ç½® `install-service.ps1:210`ã |
| | | - æ ¹å ï¼é£è¡æ¯ `.Trim(' ''', '"')`ï¼å
¶ä¸ `' '''` 被 PowerShell è§£ææä¸ä¸ª**é¿åº¦ 2 çå符串**ï¼ç©ºæ ¼ + åå¼å·ï¼ï¼`Trim(char[])` æ æ³æ 2 å符串转æå个 char â æ´è¡æå¼å¸¸ï¼`MethodArgumentConversionInvalidCastArgument`ï¼ã |
| | | - ä¿®å¤ï¼æè¿æ®µæ½æçº¯å½æ° `Test-AppConfigPassword`ï¼æ¹å为 `Trim()` â `Trim("'")` â `Trim('"')`ï¼å¹¶å èæ¬çº§ `trap`ï¼ä»»ä½å¼å¸¸åªæå°ä¸è¡è¯´æ + è¡å·ï¼ä¸åç©çº¢åå æ ã |
| | | - éªè¯ï¼8 ä¸ªæ ·æ¬ï¼ç¼ºæä»¶ / ç©ºå¼ / 䏿å ä½ç¬¦ / è£¸å¼ / åå¼å· / åå¼å· / å«ç©ºæ ¼ç¹æ®å符 / ç宿¨¡æ¿æä»¶ï¼å
¨é¨ç¬¦åé¢æï¼æå
å
èæ¬è¯æ³è§£æéè¿ã`-Status` å®è·æ£å¸¸ãé管çåæ¶ trap è¾åºå¹²åé误并 exit 1ã |
| | | - æè®ï¼è¿æ¡åæ¯ä¹åæ²¡è¢«ä»»ä½æµè¯è¦çï¼`-Status` æå exitãé管çåå¨ `Assert-Admin` å°±éåºï¼ï¼æä»¥è§£æç±»é»è¾è¦æ½æçº¯å½æ°å¹¶ç¨æ ·æ¬æµã |
| | | - å
åï¼ä¿®å¤çéæå
æ¶èæ¬å¤ç¨äºå·²éæ¾çåºç¡åï¼æç»å·²å ææ§åå
å¹¶æ¹åï¼`deploy\` ä»ç¶æ¯**å¯ä¸**ç `traffic-audit-deploy-20261003-4\`ï¼å«ä¿®å¤åç install-service.ps1ï¼ä¸åå zipã |
| | | - ç¨æ·ä¾§æå°ä¿®å¤ï¼åªéç¨æ°å
éç `backend\install-service.ps1` è¦çæå¡å¨ `D:\trafficAudit\install-service.ps1`ï¼åéè· `install-service.cmd`ï¼ä¸å¿
éæ· 110MBï¼ã |
| | | - **æå¡å¨é¨ç½²æåï¼10-03 æï¼ç¨æ·ç¡®è®¤ï¼**ï¼æå¡å¨ï¼é¨ç½²ç®å½ `D:\trafficAudit`ï¼æ backend éçå
å®¹ç´æ¥éºå¨è¯¥ç®å½ä¸ï¼ç¨ä¿®å¤åç `install-service.cmd` å®è£
æå¡ `TrafficAuditBackend` æåã |
| | | - **å·²éªè¯ï¼ç¨æ·ç¡®è®¤ï¼**ï¼â åå
¬çµèæµè§å¨è½æå¼æå¡å¨é¡µé¢ï¼è¯´æç½ç»/é²ç«å¢è¿ä¸å±éï¼æ éé¢å¤æ¾è¡ 8090ï¼ï¼â¡ **é坿å¡å¨åä»è½ä»åå
¬çµèæå¼é¡µé¢ â 弿ºèªå¯å®æµçæ**ï¼æå¡ä¸ä¾èµä»»ä½è´¦å·ç»å½ï¼ã |
| | | - **ä»å¾
å**ï¼ä¸å¡åè½éªæ¶ï¼ç»å½ / æ°æ®å¯¼å
¥ / æ¥è¡¨å¯¼åº / Word å¿«æ¥ï¼ï¼ç¡®è®¤ `application.yml` ç jwt.secret å·²æ¢ææ£å¼éæºå¼ãé¢ç½® admin/admin123 å·²æ¹å¯ã |
| | | - é²ç«å¢æ¾è¡ï¼èç³»ç»ä¹è½ç¨ï¼ï¼`netsh advfirewall firewall add rule name="TrafficAudit 8090" dir=in action=allow protocol=TCP localport=8090` |
| | | - è£
å®å常ç¨ï¼`install-service.ps1 -Status / -Restart / -Remove`ï¼æ¥å¿å¨ `D:\trafficAudit\logs\trafficAudit.out.log`ï¼é误ç `trafficAudit.err.log`ï¼ã |
| | | |
| | | ## ææ°ï¼2026-10-02ï¼ |
| | | |
| | | - **æ°æ¨¡åï¼ç¨æ·å·²éªæ¶ï¼æ¬æ¬¡æäº¤/æ¨éï¼**ï¼æ°å¢âä¸é¡¹ç»è®¡-éè¦èåæ¥âæ¨¡åï¼å«è忥é
ç½®ãè¥ä¸æ§å®¢æµãéè¥ä¸æ§è½¦æµã客è¿ç³»æ° 4 å¼ è¡¨ï¼ç³»ç»é
ç½®èåä»
admin å¯ç»´æ¤ï¼ä¸é¡¹ç»è®¡-èåæ¥æ¯æå¯¼å
¥ã导åºã模æ¿ä¸è½½å Word å¿«æ¥ä¸è½½ã |
| | |
| | | - **2026-08-28 夿 ¸ãç¹æ°æ®å¯¼å
¥æµè§å¨å´©æºãï¼æå
æµè¯ç¯å¢ï¼é¨åç¨æ·ï¼**ï¼éç¹ææ¥ `GET /api/import/batches`ââå端 limit ä¸é 200ï¼å
¨è¡¨ 81 æ¡ãerror_detail æé¿ 2.6KBãå
¨éååºçº¦ 17KBï¼å端åªå 50 æ¡ï¼ï¼åç«¯è¡¨æ ¼ä»
8 åçåæ®µï¼errorDetail 䏿¸²æï¼ãæ 轮询/宿¶å¨ã**ç»è®ºï¼æ¥å£ä¸æ¯å´©æºæ ¹å **ï¼è¥æ¥å£æé®é¢ï¼ææç¨æ·é½ä¼å´©ï¼èé"é¨åç¨æ·"ï¼ãç°è±¡ç¬¦å客æ·ç«¯ç¯å¢å·®å¼ï¼ç¬¬ä¸æ¹æ©å±æ³¨å
¥/æ§æ¾å¡é©±å¨/ä½é
å
åï¼ä¸ 08-27 è¿
é·æ©å±+Intel æ§é©±å¨ç»è®ºä¸è´ï¼ã建议对崩æºç¨æ·ä¾æ¬¡éªè¯ï¼æ çæ¨¡å¼âæ¢ Edgeâç¦æ©å±ï¼å¹¶ç chrome://crashesã |
| | | - ãæ°éæ±Â·ä»
è®°å½æªå®ç°Â·2026-08-28ãå¢å **ç»æµææ è¯ç®åè½**ï¼é¢å¯¼é常å
³æ³¨ç»æµææ ï¼**è´§è¿æ¯å¤§å¤´**ï¼å¸¸é©»ç°åºçææäººé½è¦ä¼è®¡ç®è´§è¿ç¸å
³æ°æ®ï¼è´§è¿é/å¨è½¬éçï¼ãé¢å¯¼ç»å¸¸è¦æ±ææä¸ªæ°æ®ãåä¸åãï¼ä»åèèå¨è½¯ä»¶éå¢å è¯ç®åè½ââ坿éè¦è°æ´/æµç®ææ æ°å¼ï¼å¦æ¨¡æä¸è°æä¼ä¸/æå£å¾çè´§è¿éåçæ±æ»ä¸è¶å¿å½±åï¼ï¼è¾
å©ç°åºäººåå¿«éè¯ç®ä¸åºå¯¹ã |
| | | |
| | | - ãå¾
å·2026-10-03ã**æ°å¼ç²¾åº¦ç»ä¸æ¹é **ï¼ææ¢æä¸å¡è¡¨çæ°å¼åç± `double` æ¹ä¸º `DECIMAL(30,15)`ï¼Java å®ä½åæ®µç± `Double/Float` æ¹ä¸º `BigDecimal`ï¼ä¸èåæ¥è¡¨ãè§æµç«è¡¨ç»ä¸å£å¾ï¼ç¨æ· 2026-10-03 æç¡®è¦æ±ï¼æ©æºå®æ½ï¼ã |
| | | - ç°ç¶çç¹ï¼traffic_auditï¼2026-10-03 æ¥è¯¢ï¼ï¼**16 å¼ è¡¨ãå
± 228 个 double å**ââ`freight_turnover_import` 48ã`city_bus_monthly` 34ã`h2032_enterprise_monthly` 33ã`city_taxi_monthly` 29ã`wyc_order_monthly` 22ã`h2031_enterprise_monthly` 18ã`h204_vehicle_quarterly` 12ã`scale_split_transport` 9ã`investment_monthly` 5ã`investment_system` 5ã`wyc_total_monthly` 4ã`h204_auth_vehicle` 2ã`investment_project` 2ã`passenger_individual_monthly` 2ã`transport_auth_vehicle` 2ã`vehicle_track_mileage` 1ã |
| | | - å·²æ¯ decimal å£å¾ãæ éæ¹å¨ï¼`holiday_calendar` / `holiday_passenger_coefficient` / `holiday_passenger_flow` / `holiday_vehicle_flow` / `observation_station_flow`ã |
| | | - æ¹é èå´ä¸åªæ¯æ¹åç±»åï¼å®ä½ç±»å段类åã导å
¥è§£æï¼ä¸è¦ç» double ä¸è½¬ï¼ã计ç®ä¸åæ´é»è¾ãæ¥è¡¨å¯¼åºæ ¼å¼åï¼ä»¥ååéæ°æ®çè¿ç§»èæ¬ï¼é½è¦ä¸èµ·æ¹ï¼æ¹å®è¦åé表æ°å¼æ½æ¥ã |
| | | |
| | | ## å
«ãæ¯æ¥äº¤æ¥çº¦å® |
| | | 1. æ°å¯¹è¯ç¬¬ä¸æ¥ï¼è¯» `AGENTS.md` + `HANDOFF.md`ï¼æ¶å AI åæ/å®¡æ ¸æ¶åè `docs/` 䏿°æ®åºè¡¨ç»æã |
| | | 2. æ¯å¤©ç»ææ¶ï¼æ´æ° `HANDOFF.md`ï¼è¿å±ãåãæ°éæ±ãå¾
åï¼ï¼åå
³å¯¹è¯ã |
| | |
| | | TrafficAudit 交éè´§è¿ç»è®¡å®¡æ ¸ç³»ç» ââ é¨ç½²å
ï¼{stamp}ï¼ |
| | | ======================================================== |
| | | |
| | | ãæå¡å¨ä¸åªè¦åè¿ 3 æ¥ãï¼å
¶å®æä»¶é½ä¸ç¨å¨ï¼ |
| | | 第 1 æ¥ æ¹é
ç½®ï¼ç¨è®°äºæ¬æå¼ backend\application.ymlï¼ææ°æ®åºå°å/è´¦å·/å¯ç |
| | | ï¼spring.datasource ä¸é¢ä¸é¡¹ï¼å jwt.secret æ¹æè¿å°æå¡å¨ççå®å¼ã |
| | | 第 2 æ¥ è£
æå¡ï¼è¿å
¥ backend æä»¶å¤¹ï¼å³é® install-service.cmd âã以管çå身份è¿è¡ã |
| | | ï¼ç´æ¥åå»ä¹è¡ï¼å®ä¼èªå·±å¼¹ç®¡çå确认ï¼ãèæ¬ä¼æ³¨å弿ºèªå¯æå¡å¹¶å¯å¨å®ã |
| | | 第 3 æ¥ çç»æï¼èæ¬æåä¼æå° OK åç½åï¼æµè§å¨æå¼ http://<æå¡å¨IP>:8090 |
| | | ï¼è´¦å· admin / admin123ï¼ï¼è½çå°ç»å½é¡µå°±ç®æåã |
| | | |
| | | åªäºè¦æ·ãåªäºä¸ç¨æ·ï¼ |
| | | backend\ å¿
é¡»æ´ä¸ªç®å½ä¸èµ·æ·ï¼jarãapplication.ymlãdocsãwebãæå¡æä»¶é½å¨éé¢ï¼ï¼ |
| | | ä¸è¦åªæå 个æä»¶æ·ã |
| | | db\ åªæéè¦åå§å/è¿åæ°æ®åºæ¶æç¨ï¼å¹³æ¶å¯ä¸æ·ã |
| | | frontend\ ä¸ç¨æ·ï¼å端已å
åµå¨ jar éï¼8090 端å£ç´æ¥è®¿é®ï¼ã |
| | | æ¬æä»¶ 坿·å¯ä¸æ·ï¼å°±æ¯è¿ä»½è¯´æä¹¦ã |
| | | |
| | | ãç®å½ç»æã |
| | | backend/ |
| | | traffic-audit-server-1.0.0-SNAPSHOT.jar å端ç¨åºï¼å«å
¨é¨ä¾èµï¼**å·²å
åµå端**ï¼ |
| | | docs/ æ¥è¡¨æ¨¡æ¿ä¸è¿è¡æ°æ®ç®å½ï¼å¿
éï¼å¿å ï¼ |
| | | application.yml å¤é¨é
ç½®ï¼æ¹è¿éå³å¯ï¼æ ééæ°æå
ï¼ |
| | | start-backend.cmd / start-backend.sh å¯å¨èæ¬ |
| | | start-backend.cmd / start-backend.sh åå°å¯å¨èæ¬ï¼äººå·¥è°è¯ç¨ï¼å
³äºçªå£å°±åï¼ |
| | | trafficAudit.exe WinSW æå¡å
è£
å¨ï¼æ java åæ Windows æå¡ï¼å¿å å¿æ¹åï¼ |
| | | trafficAudit.xml WinSW æå¡é
ç½®ï¼æå¡å/å¯å¨æ¹å¼/æ¥å¿/失败éå¯çç¥ï¼ |
| | | install-service.cmd åå»å
¥å£ï¼æ³¨å弿ºèªå¯æå¡ï¼ä¼èªå¨è¯·æ±ç®¡çåæéï¼ |
| | | install-service.ps1 ä¸é¢é£ä¸ª cmd å®é
è°ç¨çèæ¬ï¼æ³¨å/æ¥ç/éå¯/å é¤ï¼ |
| | | start-backend-service.cmd é交äºå¯å¨å¨ï¼æ pauseãå app.logï¼é
å计åä»»å¡ç¨ï¼ |
| | | install-autostart.ps1 å¤éï¼æ³¨åâ计åä»»å¡âæ¹å¼ç弿ºèªå¯ |
| | | frontend/ |
| | | dist/ å端æå»ºäº§ç©ï¼å¤ç¨ï¼æ³ç¨ nginx æç®¡æ¶ç¨ï¼ |
| | | nginx.conf.example nginx é
ç½®ç¤ºä¾ |
| | |
| | | sql_holiday.sql ä¸é¡¹ç»è®¡-éè¦èåæ¥è¡¨ï¼æ§åºç¼ºè¡¨æ¶æ§è¡ï¼ |
| | | sql_holiday_decimal_migration.sql è忥æ°å¼å精度è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | sql_holiday_comparison_base_migration.sql èåæ¥ç¯æ¯åºæå段è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | sql_holiday_previous_day_migration.sql è忥å䏿¥é
ç½®è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | sql_observation_station.sql è§æµç«åæ¥è°æ¥æ°æ®è¡¨ï¼æ°åè½ï¼æ§åºç¼ºè¡¨æ¶æ§è¡ï¼ |
| | | README-é¨ç½²è¯´æ.txt |
| | | |
| | | ãé¨ç½²æºç¯å¢è¦æ±ã |
| | |
| | | **ä¸éè¦ Redis**ï¼ä»£ç æªä½¿ç¨ï¼æ§ç说æéç Redis æ¯éçé
ç½®ï¼å¯å¿½ç¥ï¼ |
| | | **ä¸éè¦ nginx**ï¼å端已å
åµè¿ jarï¼è®¿é® 8090 å³å¯ï¼nginx ä»
为å¤éæ¹æ¡ï¼ |
| | | |
| | | ãæä½ç³»ç»å
¼å®¹æ§ã |
| | | 弿ºèªå¯ç¨çæ¯ Windows æå¡ï¼èâèªå¨å¯å¨ / å»¶è¿å¯å¨ / 失败èªå¨éå¯âæ¯ç³»ç»èªå¸¦è½åï¼ |
| | | ä» Win7 SP1 å° Win11ãServer 2008 R2 å° Server 2022/2025 æºå¶å®å
¨ä¸æ ·ãå·®å«åªå¨æå¡å
è£
å¨ |
| | | trafficAudit.exe ä¾èµä»ä¹è¿è¡æ¶ï¼ |
| | | - æ¬å
å
ç trafficAudit.exe æ¯ WinSW 2.12.0 ç x64 èªå
å«æå»ºï¼èªå¸¦ .NET 6 è¿è¡æ¶ï¼å·²æ ¡éª SHA-256ï¼ï¼ |
| | | **ä¸éè¦å¦å¤å®è£
.NET Framework**ï¼æ¨èç¨äº 64 ä½ Win10 / Win11 / Server 2016 å以ä¸ã |
| | | - Win7 SP1 / Server 2008 R2 / Server 2012(R2)ï¼èªå
å«çé常ä¹è½è·ï¼Win7 SP1 éå
æ KB2533623ï¼ï¼ |
| | | è¥æ¥ç¼ºå° api-ms-win-*.dll ä¹ç±»ï¼æ¢æå®æ¹ thin æå»ºï¼æ WinSW.NET461.exe ä¸è½½åæ¹åæ |
| | | trafficAudit.exe è¦çï¼é
ç½® trafficAudit.xml ä¸ç¨æ¹ï¼ï¼å®é .NET Framework 4.6.1+ï¼ |
| | | https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW.NET461.exe |
| | | - 32 ä½ç³»ç»ï¼æå
è£
卿¢æ WinSW-x86.exeï¼åä¸ä¸è½½é¡µï¼ã |
| | | - Win7 / Server 2012 æ¬èº«å·²è¿å¾®è½¯æ¯ææï¼æ°æå¡å¨å»ºè®®ç´æ¥ä¸ Server 2016 å以ä¸ã |
| | | - èæ¬è¦æ±ï¼install-service.ps1 å¨ Windows PowerShell 2.0+ ä¸å¯ç¨ï¼Win7 èªå¸¦ 2.0ï¼å·²åå
¼å®¹ï¼ï¼ |
| | | 3.2 ç计åä»»å¡èæ¬ç¨å°äº New-ScheduledTask* å½ä»¤ï¼éè¦ Win8 / Server 2012 å以ä¸ã |
| | | |
| | | ãé¨ç½²æ¥éª¤ã |
| | | 1. åå§åæ°æ®åºï¼4 ä¸ªèæ¬é½è¦æ§è¡ï¼é¡ºåºå¦ä¸ï¼ |
| | | mysql --default-character-set=utf8mb4 -u root -p < db/init.sql |
| | | 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 å·²å«è忥 4 å¼ è¡¨ï¼è¥æ¯å¨æ©æçæ¬åºä¸è¡¥èåæ¥æ¨¡åï¼åæ§è¡ db/sql_holiday.sqlï¼å¹¶æéæ§è¡ä¸ä¸ªè¿ç§»èæ¬ï¼æ°å¼ç²¾åº¦ãç¯æ¯åºæãå䏿¥é
ç½®ï¼ãå䏿¥è¿ç§»èæ¬å¯é夿§è¡ã |
| | | é¢ç½®è´¦å·ï¼admin / admin123ï¼é¦æ¬¡ç»å½å请æ¹å¯ç ï¼ |
| | | |
| | | 2. æ¹é
ç½® backend/application.yml |
| | |
| | | - deepseek.api-key ï¼éè¦ AI åææ¶å¡«ï¼ä¸éè¦å¯ç空 |
| | | 注ï¼application.yml ä¸ jar å级å³çæï¼ä¸ç¨éæ°æå
ã |
| | | |
| | | 3. å¯å¨å端ï¼å¨ backend ç®å½ä¸ï¼ |
| | | 3. å¯å¨å端ï¼å¨ backend ç®å½ä¸ï¼è¿æ¥åªæ¯æå·¥è¯è·ï¼å
³çªå£å°±åï¼è¦å¼æºèªå¯è¯·ç 3.1ï¼ |
| | | Windowsï¼åå» start-backend.cmd |
| | | Linuxï¼ sh start-backend.sh |
| | | æåæ å¿ï¼åºç° "Started TrafficAuditApplication" ä¸ "Tomcat started on port(s): 8090" |
| | | |
| | | 3.1 Windows 弿ºèªå¯ï¼æ¨èï¼æ³¨åæ Windows æå¡ï¼ä¸æ¬¡é
ç½®é¿æçæï¼ |
| | | åæï¼backend ç®å½ä¸ç trafficAudit.exe / trafficAudit.xml / install-service.cmd / install-service.ps1 |
| | | é½å·²éæ¬é¨ç½²å
æä¾ï¼ä¸éè¦å¦å¤ä¸è½½ WinSWï¼ä¹ä¸éè¦ç» java è£
å«çä¸è¥¿ã |
| | | æçäºï¼å¨ backend ç®å½å³é® install-service.cmd âã以管çå身份è¿è¡ã |
| | | ï¼ç´æ¥åå»ä¹è¡ï¼å®ä¼èªå·±å¼¹ UAC 管çå确认ï¼ã |
| | | çä»·çæå·¥æ¹å¼ï¼å¨ backend ç®å½ä¸ï¼ç¨ã管çåãPowerShellï¼ï¼ |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 |
| | | ææï¼æ³¨å Windows æå¡ TrafficAuditBackendï¼å¼æºèªå¨å¯å¨ï¼å»¶è¿å¯å¨ï¼å
ç MySQL 就绪ï¼ï¼ |
| | | ä¸éè¦ä»»ä½ç¨æ·ç»å½ï¼java è¿ç¨å¼å¸¸éåºç±æå¡ç®¡çå¨èªå¨éå¯ï¼10 ç§ / 30 ç§ / 60 ç§éå¢éè¯ï¼ |
| | | 1 å°æ¶åéè¯è®¡æ°æ¸
é¶ï¼ãæå¡ä»¥ LocalSystem 身份è¿è¡ã |
| | | èæ¬ä¼èªå¨å®ä½ java.exeï¼JAVA_HOME â PATH â JavaSoft 注å表 â Program Filesï¼ï¼ |
| | | å¹¶æç»å¯¹è·¯å¾åè¿ trafficAudit.xmlï¼SYSTEM æå¡ç PATH ä¸ç»å½ç¨æ·ä¸åï¼åç»å¯¹è·¯å¾æ´ç¨³ï¼ã |
| | | 常ç¨åæ°ï¼ |
| | | -Status æ¥çæå¡ç¶æï¼ä¸éè¦ç®¡çåï¼ |
| | | -Restart éå¯æå¡ -Remove 忢并å é¤æå¡ |
| | | -NoStart åªæ³¨åä¸å¯å¨ -JavaPath "D:\jdk8\bin\java.exe" æå¨æå® JDK |
| | | æ¥å¿ï¼backend\logs\trafficAudit.out.logï¼è¿è¡è¾åºï¼ä¸ trafficAudit.err.logï¼é误è¾åºï¼ï¼ |
| | | å个æä»¶ 10MBï¼ä¿ç 10 个æ»å¨æä»¶ã |
| | | éªè¯ï¼èæ¬è£
å®ä¼èªå¨æ¢æµç«¯å£ï¼æå° OK + æ¬æº/对å¤ç½åï¼èµ·ä¸æ¥æ¶ç´æ¥æå° logs\*.log æå 15 è¡ï¼ |
| | | ææç¤ºæ¹ application.yml åæ§è¡ install-service.ps1 -Restart å³å¯ãä¹å¯æå·¥ç¨æµè§å¨æå¼ |
| | | http://<é¨ç½²æºIP>:8090 确认ã |
| | | 注æï¼ |
| | | - 弿ºèªå¯é æå¡çâèªå¨âå¯å¨ç±»åï¼å¦ææå®æ¹æâæå¨âï¼å°±ä¸ä¼åéæºå¨å¯å¨ã |
| | | - é夿§è¡ install-service.ps1 æ¯å®å
¨çï¼ä¼å
åæå¡åéæ°æ³¨åå¹¶å¯å¨ã |
| | | - åä¸å°æºä¸è¦åæ¶å¯ç¨ä¸é¢ç计å任塿¹å¼ï¼ä¸¤ä¸ªè¿ç¨ä¼æ¢ 8090 端å£ã |
| | | - 建议æé¨ç½²å
æ¾å¨ D:\trafficAudit è¿ç±»ç®åè·¯å¾ï¼æ¾å¨æäºç¨æ·ç®å½ä¸æ¶ LocalSystem |
| | | å¯è½æ²¡æåæéï¼æ¥å¿ä¼æ¥æéé误ã |
| | | |
| | | 3.2 å¤éï¼è®¡å任塿¹å¼ï¼ä¸æ³æå端è£
ææå¡æ¶ç¨ï¼ä¸ 3.1 äºéä¸ï¼ |
| | | å¨ backend ç®å½ä¸ï¼ç¨ã管çåãPowerShell æ§è¡ï¼ |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-autostart.ps1 |
| | | ææï¼æ³¨å计åä»»å¡ TrafficAuditBackendï¼å¼æºå»¶è¿ 60 ç§å以 SYSTEM 身份è¿è¡ start-backend-service.cmdï¼ |
| | | ä¸éè¦ç¨æ·ç»å½ï¼å¤±è´¥èªå¨éè¯ 3 次ï¼è¿è¡æ¥å¿åå¨ backend\app.logã |
| | | 常ç¨åæ°ï¼-DelaySeconds 90ï¼æ¹å»¶è¿ç§æ°ï¼ -StartNowï¼æ³¨ååç«å³å¯å¨ä¸æ¬¡ï¼ |
| | | -Statusï¼æ¥çç¶æï¼ -Removeï¼å é¤å¼æºèªå¯ï¼ |
| | | 注ï¼å¯å¨å¨ä¼å
ç¨ JAVA_HOME\bin\java.exeï¼æ¾ä¸å°æ¶åé PATHï¼æ£æµå° 8090 å·²å¨çå¬åç´æ¥éåºã |
| | | |
| | | 4. 访é®ç³»ç» |
| | | æµè§å¨æå¼ http://<é¨ç½²æºIP>:8090 ï¼å端已å
åµï¼é¡µé¢ä¸æ¥å£åæºï¼ |
| | |
| | | 注æï¼å
¨é dump ä¼å¸¦ä¸æ¬æºçè´¦å·å¯ç ï¼è¦ç init.sql é¢ç½®ç admin è´¦å·ã |
| | | |
| | | ã常è§é®é¢ã |
| | | 0. å¯å¨æ¥ java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: NO)ï¼ |
| | | âusing password: NOâ = å端æ¿å°çæ°æ®åºå¯ç æ¯ç©ºä¸²ï¼ä¸æ¯å¯ç éï¼ã |
| | | æ£æ¥ backend/application.yml æ¯å¦ä¸ jar åçº§ä¸ spring.datasource.password 已填çå®å¯ç ï¼ |
| | | è¥æªæ¹ application.ymlï¼ä¹å¯ç¨ç¯å¢åé TRAFFIC_DB_PASSWORD 代æ¿ãæ¹å®éå¯å³å¯ï¼æ ééæ°æå
ã |
| | | 1. å¯å¨æ¥ãæªæ¾å°æ°æ®ç®å½ docsã/ å¯¼åºæ¥ãæªæ¾å°æ¨¡æ¿æä»¶ãï¼ |
| | | å¿
é¡»å¨ backend ç®å½ä¸å¯å¨ï¼jar ä¸ docs å级ï¼ï¼ä¸è¦åªæ· jarã |
| | | 2. æ¥è¡¨å¯¼åºæ¥ãæªæ¾å°æ±æ»è¡¨æ¯çãYYYYå¹´Mæéè·¯è¿è¾éæ±æ»è¡¨.xlsxããï¼ |
| | |
| | | 3. 8090 端å£è¢«å ç¨ï¼æ¹ application.yml ç server.port åéå¯ã |
| | | 4. 页颿弿¯ç©ºç½ï¼ç¡®è®¤è®¿é®çæ¯ http://<IP>:8090 æ ¹è·¯å¾ï¼ä¸æµè§å¨å¼ºå¶å·æ°ï¼Ctrl+F5ï¼ã |
| | | 5. ä¸ä¼ 大æä»¶å¤±è´¥ï¼å端éå¶ 100MBï¼å·²é
ç½®ï¼å¦åé¢è¿æäº nginxï¼é忥æ¾å¤§ client_max_body_sizeã |
| | | 6. æå¡å·²æ³¨åä½ 8090 æä¸å¼ï¼ç backend\logs\trafficAudit.err.log çæ¥éå
å®¹ï¼ |
| | | æå¡æ¹å¼ä¸ç¨çæ¯ backend\application.ymlï¼password å¿
é¡»æ¯çå® MySQL å¯ç ï¼å 使åä¼å¯¼è´ using password: NOï¼ã |
| | | 7. æå¡æ¥âæç»è®¿é®âæåä¸äºæ¥å¿ï¼é¨ç½²ç®å½æéä¸è¶³ã |
| | | æé¨ç½²å
æ¾å° D:\trafficAudit è¿ç±»ç®åè·¯å¾ï¼æç» LocalSystem æäºè¯¥ç®å½ç读åæéã |
| | | 8. æ³ä¸´æ¶åæ¶å¼æºèªå¯ä½ä¿çæå¡ï¼Set-Service TrafficAuditBackend -StartupType Manual |
| | | |
| | | ã忢æå¡ã |
| | | Windowsï¼å
³é start-backend.cmd çªå£ |
| | | Linuxï¼ Ctrl+Cï¼æ kill å¯¹åº java è¿ç¨ï¼ps -ef | grep javaï¼ |
| | | æå¡æ¹å¼ï¼ Stop-Service TrafficAuditBackend ï¼éæ°å¯å¨ï¼Start-Service TrafficAuditBackendï¼ |
| | | å½»åºå é¤å¼æºèªå¯ï¼install-service.ps1 -Remove |
| | | åå°å¯å¨ï¼ å
³é start-backend.cmd çªå£ |
| | | 计åä»»å¡ï¼ Stop-ScheduledTask -TaskName TrafficAuditBackend |
| | | Linuxï¼ Ctrl+Cï¼æ kill å¯¹åº java è¿ç¨ï¼ps -ef | grep javaï¼ |
| | |
| | | # é¨ç½²å°æ°æºå¨åï¼è¯·**è³å°**ä¿®æ¹ä¸é¢æ 注 TODO çä¸å¤æ°æ®åºé
ç½®ï¼ |
| | | # ============================================================ |
| | | server: |
| | | port: 8080 |
| | | port: 8090 # åç«¯å£æ¨¡å¼ï¼å端 8090 åæ¶æç®¡å端页é¢ï¼ä¸è¦æ¹æ 8080ï¼ |
| | | app: |
| | | captcha: |
| | | required: false # ç产ç¯å¢å¦éå¾å½¢éªè¯ç ï¼æ¹ä¸º true |
| | | spring: |
| | | web: |
| | | resources: |
| | | # åç«¯å£æ¨¡å¼ï¼ä¼å
读å jar å级 web\ ç®å½ï¼æå
æ¶ç±å端 dist èªå¨çæï¼ |
| | | static-locations: file:./web/,classpath:/static/ |
| | | datasource: |
| | | url: jdbc:mysql://127.0.0.1:3306/traffic_audit?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true |
| | | username: root # TODO æ¹æä½ ç MySQL è´¦å· |
| | | password: è¯·æ¹æä½ çæ°æ®åºå¯ç # TODO æ¹æä½ ç MySQL å¯ç |
| | | password: è¯·æ¹æä½ çæ°æ®åºå¯ç # TODO æ¹æä½ ç MySQL å¯ç ï¼çç©ºä¼æ¥ Access denied ... (using password: NO) |
| | | driver-class-name: com.mysql.cj.jdbc.Driver |
| | | servlet: |
| | | multipart: |
| New file |
| | |
| | | <# |
| | | Register / remove / inspect a Windows Scheduled Task that starts the TrafficAudit |
| | | backend at machine boot, with no user session required. |
| | | |
| | | Run from an ELEVATED PowerShell in the backend folder (jar + application.yml + |
| | | this script + start-backend-service.cmd): |
| | | |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-autostart.ps1 |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-autostart.ps1 -DelaySeconds 90 |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-autostart.ps1 -StartNow |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-autostart.ps1 -Status |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-autostart.ps1 -Remove |
| | | |
| | | What it does: |
| | | - trigger : AtStartup, delayed by -DelaySeconds (default 60s, so MySQL is up first) |
| | | - principal: SYSTEM, highest privileges, "run whether user is logged on or not" |
| | | - settings : no execution time limit, start when available, restart 3x / 1 min on failure |
| | | - action : cmd.exe /c start-backend-service.cmd (logs to app.log next to the jar) |
| | | |
| | | Notes: |
| | | - Re-running without -Remove is idempotent (-Force overwrites the task). |
| | | - Start/stop manually with: Start-ScheduledTask / Stop-ScheduledTask -TaskName <name> |
| | | - Keep this file ASCII-only: Windows PowerShell 5.1 reads a BOM-less .ps1 as ANSI. |
| | | #> |
| | | param( |
| | | [string]$TaskName = 'TrafficAuditBackend', |
| | | [int]$DelaySeconds = 60, |
| | | [switch]$Remove, |
| | | [switch]$Status, |
| | | [switch]$StartNow |
| | | ) |
| | | |
| | | $ErrorActionPreference = 'Stop' |
| | | $root = Split-Path -Parent $MyInvocation.MyCommand.Path |
| | | $launcher = Join-Path $root 'start-backend-service.cmd' |
| | | $logFile = Join-Path $root 'app.log' |
| | | |
| | | function Assert-Admin { |
| | | $identity = [Security.Principal.WindowsIdentity]::GetCurrent() |
| | | $principal = New-Object Security.Principal.WindowsPrincipal($identity) |
| | | if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { |
| | | throw 'This script must run from an elevated (Run as Administrator) PowerShell.' |
| | | } |
| | | } |
| | | |
| | | if ($Status) { |
| | | $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue |
| | | if (-not $task) { Write-Host "Scheduled task '$TaskName' is not registered."; exit 1 } |
| | | $task | Select-Object TaskName, TaskPath, State | Format-List |
| | | Get-ScheduledTaskInfo -TaskName $TaskName | |
| | | Select-Object LastRunTime, LastTaskResult, NextRunTime, NumberOfMissedRuns | Format-List |
| | | exit 0 |
| | | } |
| | | |
| | | if ($Remove) { |
| | | Assert-Admin |
| | | if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) { |
| | | Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false |
| | | Write-Host "Removed scheduled task '$TaskName'." |
| | | } else { |
| | | Write-Host "Scheduled task '$TaskName' was not registered; nothing to remove." |
| | | } |
| | | exit 0 |
| | | } |
| | | |
| | | Assert-Admin |
| | | if (-not (Test-Path $launcher)) { throw "Missing launcher: $launcher" } |
| | | if (-not (Test-Path (Join-Path $root 'traffic-audit-server-1.0.0-SNAPSHOT.jar'))) { |
| | | Write-Host "WARN: jar not found in $root - the task will be registered, but startup will fail until the jar is there." |
| | | } |
| | | |
| | | $action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument ('/c "' + $launcher + '"') -WorkingDirectory $root |
| | | |
| | | $trigger = New-ScheduledTaskTrigger -AtStartup |
| | | if ($DelaySeconds -gt 0) { $trigger.Delay = 'PT' + $DelaySeconds + 'S' } |
| | | |
| | | $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest |
| | | |
| | | $settings = New-ScheduledTaskSettingsSet ` |
| | | -AllowStartIfOnBatteries ` |
| | | -DontStopIfGoingOnBatteries ` |
| | | -StartWhenAvailable ` |
| | | -MultipleInstances IgnoreNew ` |
| | | -RestartCount 3 ` |
| | | -RestartInterval (New-TimeSpan -Minutes 1) ` |
| | | -ExecutionTimeLimit ([TimeSpan]::Zero) |
| | | |
| | | Register-ScheduledTask ` |
| | | -TaskName $TaskName ` |
| | | -Action $action ` |
| | | -Trigger $trigger ` |
| | | -Principal $principal ` |
| | | -Settings $settings ` |
| | | -Description 'TrafficAudit backend auto-start at machine boot (script-managed).' ` |
| | | -Force | Out-Null |
| | | |
| | | Write-Host "Registered scheduled task '$TaskName'." |
| | | Write-Host " Trigger : At startup, +${DelaySeconds}s" |
| | | Write-Host " Principal: SYSTEM (highest privileges, no logon required)" |
| | | Write-Host " Launcher : $launcher" |
| | | Write-Host " Log : $logFile" |
| | | Write-Host '' |
| | | Write-Host "Service address after boot: http://<server-ip>:8090" |
| | | Write-Host "Manage it with: -Status | -StartNow | -Remove" |
| | | |
| | | if ($StartNow) { |
| | | Start-ScheduledTask -TaskName $TaskName |
| | | Write-Host "Started task '$TaskName' now." |
| | | } |
| New file |
| | |
| | | @echo off |
| | | rem ============================================================ |
| | | rem Double-click entry point for a fresh Windows server: |
| | | rem installs the TrafficAudit backend as a Windows service |
| | | rem (auto-start at boot) and then verifies it comes up. |
| | | rem |
| | | rem It asks for Administrator rights (UAC) and then runs |
| | | rem install-service.ps1 from this folder. |
| | | rem |
| | | rem If double-clicking does nothing, right-click this file and |
| | | rem choose "Run as administrator". |
| | | rem |
| | | rem ASCII-only on purpose: cmd.exe reads this file with the OEM |
| | | rem code page. |
| | | rem ============================================================ |
| | | setlocal |
| | | cd /d "%~dp0" |
| | | |
| | | if not exist "%~dp0trafficAudit.exe" ( |
| | | echo [ERROR] trafficAudit.exe was not found next to this script. |
| | | echo Copy the WHOLE backend folder to the server, do not |
| | | echo pick single files out of it. |
| | | echo. |
| | | pause |
| | | exit /b 1 |
| | | ) |
| | | |
| | | rem S-1-16-12288 = High Mandatory Level, i.e. this window is already elevated. |
| | | whoami /groups | findstr /C:"S-1-16-12288" >nul 2>&1 |
| | | if errorlevel 1 ( |
| | | echo Administrator rights are required. A UAC prompt will appear now. |
| | | powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -FilePath '%~f0' -Verb RunAs" |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Automatic elevation failed. |
| | | echo Right-click install-service.cmd and choose |
| | | echo "Run as administrator" instead. |
| | | echo. |
| | | pause |
| | | ) |
| | | exit /b |
| | | ) |
| | | |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0install-service.ps1" %* |
| | | |
| | | echo. |
| | | echo ------------------------------------------------------------ |
| | | echo Finished. Press any key to close this window. |
| | | pause >nul |
| New file |
| | |
| | | <# |
| | | Install / update / remove the TrafficAudit backend as a Windows service. |
| | | |
| | | The service is a WinSW (Windows Service Wrapper) host: trafficAudit.exe reads |
| | | trafficAudit.xml and runs "java -jar traffic-audit-server-<version>.jar". |
| | | Because it is a real Windows service it starts at boot with no user logged on, |
| | | is listed in services.msc / Get-Service, and is restarted by the Service |
| | | Control Manager when the java process dies. |
| | | |
| | | Run from an ELEVATED PowerShell in the backend folder |
| | | (the folder that contains the jar, application.yml, docs, web and trafficAudit.exe): |
| | | |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 -Status |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 -Restart |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 -Remove |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 -NoStart |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 -JavaPath 'D:\jdk8\bin\java.exe' |
| | | |
| | | What it does: |
| | | 1. locates java.exe (JAVA_HOME -> PATH -> JavaSoft registry -> Program Files) |
| | | 2. writes that absolute path into trafficAudit.xml (<executable>) |
| | | 3. installs or reinstalls the service and starts it (unless -NoStart) |
| | | |
| | | Notes: |
| | | - Re-running is idempotent: an existing service is stopped and reinstalled. |
| | | - Logs: logs\trafficAudit.out.log / logs\trafficAudit.err.log (10 MB x 10 roll). |
| | | - Do NOT enable the scheduled-task autostart (install-autostart.ps1) at the |
| | | same time: two hosts would race for the same jar and port 8090. |
| | | - Keep this file ASCII-only; Windows PowerShell 5.1 reads a BOM-less .ps1 as ANSI. |
| | | #> |
| | | param( |
| | | [switch]$Remove, |
| | | [switch]$Status, |
| | | [switch]$Restart, |
| | | [switch]$NoStart, |
| | | [string]$JavaPath |
| | | ) |
| | | |
| | | $ErrorActionPreference = 'Stop' |
| | | |
| | | $root = Split-Path -Parent $MyInvocation.MyCommand.Path |
| | | $wrapper = Join-Path $root 'trafficAudit.exe' |
| | | $config = Join-Path $root 'trafficAudit.xml' |
| | | $appConfig = Join-Path $root 'application.yml' |
| | | $serviceId = 'TrafficAuditBackend' |
| | | |
| | | # Any terminating error below lands here instead of dumping a raw stack trace |
| | | # at the operator. The line number is kept so a copy of this window is enough |
| | | # to diagnose a report. |
| | | trap { |
| | | Write-Host '' |
| | | Write-Host '==============================================================' |
| | | Write-Host ' ERROR: the installer stopped before finishing.' |
| | | Write-Host (' ' + $_.Exception.Message) |
| | | Write-Host (' at line ' + $_.InvocationInfo.ScriptLineNumber + ' of install-service.ps1') |
| | | Write-Host ' The service may not be installed. Send this window text to the' |
| | | Write-Host ' developer if the reason is not obvious.' |
| | | Write-Host '==============================================================' |
| | | exit 1 |
| | | } |
| | | |
| | | function Assert-Admin { |
| | | $identity = [Security.Principal.WindowsIdentity]::GetCurrent() |
| | | $principal = New-Object Security.Principal.WindowsPrincipal($identity) |
| | | if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { |
| | | throw 'This script must run from an elevated (Run as Administrator) PowerShell.' |
| | | } |
| | | } |
| | | |
| | | function Resolve-JavaPath { |
| | | param([string]$Explicit) |
| | | |
| | | if ($Explicit) { |
| | | if (-not (Test-Path $Explicit)) { throw "JavaPath does not exist: $Explicit" } |
| | | return (Resolve-Path $Explicit).Path |
| | | } |
| | | |
| | | if ($env:JAVA_HOME) { |
| | | $candidate = Join-Path $env:JAVA_HOME 'bin\java.exe' |
| | | if (Test-Path $candidate) { return (Resolve-Path $candidate).Path } |
| | | } |
| | | |
| | | $onPath = Get-Command java.exe -ErrorAction SilentlyContinue |
| | | if ($onPath -and $onPath.Source) { return $onPath.Source } |
| | | |
| | | # A SYSTEM service inherits the machine PATH, which often lacks the JDK. |
| | | # Fall back to the install locations used by Oracle / OpenJDK installers. |
| | | foreach ($reg in @('HKLM:\SOFTWARE\JavaSoft\JDK', 'HKLM:\SOFTWARE\JavaSoft\Java Development Kit')) { |
| | | if (-not (Test-Path $reg)) { continue } |
| | | foreach ($ver in (Get-ChildItem $reg -ErrorAction SilentlyContinue | Sort-Object Name -Descending)) { |
| | | $home = (Get-ItemProperty -Path $ver.PSPath -Name JavaHome -ErrorAction SilentlyContinue).JavaHome |
| | | if ($home) { |
| | | $candidate = Join-Path $home 'bin\java.exe' |
| | | if (Test-Path $candidate) { return (Resolve-Path $candidate).Path } |
| | | } |
| | | } |
| | | } |
| | | |
| | | $roots = @($env:ProgramFiles) |
| | | if (${env:ProgramFiles(x86)}) { $roots += ${env:ProgramFiles(x86)} } |
| | | foreach ($base in $roots) { |
| | | foreach ($vendor in @('Java', 'Eclipse Adoptium', 'AdoptOpenJDK', 'Microsoft')) { |
| | | $hit = Get-ChildItem -Path (Join-Path $base ($vendor + '\*\bin\java.exe')) -ErrorAction SilentlyContinue | |
| | | Sort-Object FullName -Descending | Select-Object -First 1 |
| | | if ($hit) { return $hit.FullName } |
| | | } |
| | | } |
| | | |
| | | throw 'java.exe not found. Install a JDK 8 and re-run, or pass -JavaPath "D:\path\to\bin\java.exe".' |
| | | } |
| | | |
| | | function Set-WrapperJavaPath { |
| | | param([string]$JavaExe) |
| | | |
| | | if (-not (Test-Path $config)) { throw "Missing config file: $config" } |
| | | |
| | | # Edit through the DOM: a regex would also hit the executable mention inside |
| | | # the header comment and would mangle the whole document. |
| | | $doc = New-Object System.Xml.XmlDocument |
| | | $doc.PreserveWhitespace = $true |
| | | $doc.Load($config) |
| | | |
| | | $node = $doc.SelectSingleNode('/service/executable') |
| | | if (-not $node) { throw "No /service/executable element found in $config" } |
| | | |
| | | if ($node.InnerText -eq $JavaExe) { |
| | | Write-Host "Config already points at $JavaExe" |
| | | return |
| | | } |
| | | |
| | | $node.InnerText = $JavaExe |
| | | |
| | | $settings = New-Object System.Xml.XmlWriterSettings |
| | | $settings.Encoding = New-Object System.Text.UTF8Encoding $false |
| | | $settings.Indent = $true |
| | | $settings.IndentChars = ' ' |
| | | $writer = [System.Xml.XmlWriter]::Create($config, $settings) |
| | | try { $doc.Save($writer) } finally { $writer.Close() } |
| | | Write-Host "Updated $config -> $JavaExe" |
| | | } |
| | | |
| | | function Get-ServiceState { |
| | | Get-Service -Name $serviceId -ErrorAction SilentlyContinue |
| | | } |
| | | |
| | | function Test-LocalPort { |
| | | param([int]$Port, [string]$TargetHost = '127.0.0.1') |
| | | |
| | | # Probe TCP instead of HTTP: Invoke-WebRequest does not exist on Windows |
| | | # PowerShell 2 (Windows 7), and this works on every PowerShell version. |
| | | $client = New-Object System.Net.Sockets.TcpClient |
| | | try { |
| | | $async = $client.BeginConnect($TargetHost, $Port, $null, $null) |
| | | if ($async.AsyncWaitHandle.WaitOne(1000, $false) -and $client.Connected) { return $true } |
| | | return $false |
| | | } catch { |
| | | return $false |
| | | } finally { |
| | | $client.Close() |
| | | } |
| | | } |
| | | |
| | | function Test-AppConfigPassword { |
| | | param([string]$Path) |
| | | |
| | | # Returns $true when the datasource password looks usable. Prints a WARN and |
| | | # returns $false for the states that break startup with |
| | | # "Access denied ... (using password: NO)": missing file / empty value / placeholder. |
| | | if (-not (Test-Path -LiteralPath $Path)) { |
| | | Write-Host 'WARN: application.yml is missing next to the jar.' |
| | | Write-Host ' The built-in password placeholder resolves to an empty string and startup will fail with' |
| | | Write-Host " Java SQLException: Access denied for user 'root'@'localhost' (using password: NO)." |
| | | return $false |
| | | } |
| | | |
| | | $line = Select-String -LiteralPath $Path -Pattern '^\s*password\s*:' | Select-Object -First 1 |
| | | if (-not $line) { |
| | | Write-Host "WARN: no 'password:' entry found in application.yml; check the datasource block." |
| | | return $false |
| | | } |
| | | |
| | | # Trim() takes a single char, so strip whitespace, then one quote kind at a |
| | | # time. Passing a 2-char string here throws |
| | | # "cannot convert value ... to type System.Char" and aborts the script. |
| | | $value = $line.Line -replace '^\s*password\s*:\s*', '' |
| | | $value = $value.Trim() |
| | | $value = $value.Trim("'") |
| | | $value = $value.Trim('"') |
| | | |
| | | if ($value -eq '') { |
| | | Write-Host "WARN: spring.datasource.password is empty in application.yml; startup may fail with '(using password: NO)'." |
| | | return $false |
| | | } |
| | | if ($value -match '[^\x00-\x7F]') { |
| | | Write-Host 'WARN: the datasource password contains non-ASCII characters.' |
| | | Write-Host ' If it is still the Chinese placeholder text, replace it with the real MySQL password first.' |
| | | return $false |
| | | } |
| | | return $true |
| | | } |
| | | |
| | | # ---------- read-only status: no elevation needed ---------- |
| | | if ($Status) { |
| | | $service = Get-ServiceState |
| | | if (-not $service) { |
| | | Write-Host "Service '$serviceId' is NOT installed." |
| | | exit 1 |
| | | } |
| | | $service | Select-Object Name, DisplayName, Status, StartType | Format-List |
| | | # Get-CimInstance needs PowerShell 3+; fall back to WMI for Windows 7 / PowerShell 2. |
| | | $detail = $null |
| | | try { |
| | | $detail = Get-CimInstance Win32_Service -Filter "Name='$serviceId'" -ErrorAction Stop |
| | | } catch { |
| | | $detail = Get-WmiObject Win32_Service -Filter "Name='$serviceId'" -ErrorAction SilentlyContinue |
| | | } |
| | | if ($detail) { $detail | Select-Object ProcessId, StartMode, DelayedAutoStart, PathName | Format-List } |
| | | if (Test-Path $wrapper) { & $wrapper status } |
| | | exit 0 |
| | | } |
| | | |
| | | if ($Restart) { |
| | | Assert-Admin |
| | | if (-not (Get-ServiceState)) { throw "Service '$serviceId' is not installed; run this script without -Restart first." } |
| | | Write-Host "Restarting service '$serviceId'..." |
| | | Restart-Service -Name $serviceId -Force |
| | | Start-Sleep -Seconds 3 |
| | | Get-ServiceState | Select-Object Name, Status | Format-List |
| | | exit 0 |
| | | } |
| | | |
| | | if ($Remove) { |
| | | Assert-Admin |
| | | if (-not (Test-Path $wrapper)) { throw "Missing wrapper executable: $wrapper" } |
| | | if (Get-ServiceState) { |
| | | Write-Host "Stopping and removing service '$serviceId'..." |
| | | & $wrapper stop | Out-Null |
| | | Start-Sleep -Seconds 3 |
| | | & $wrapper uninstall | Out-Null |
| | | Write-Host "Removed service '$serviceId'. Files in $root were left untouched." |
| | | } else { |
| | | Write-Host "Service '$serviceId' is not installed; nothing to remove." |
| | | } |
| | | exit 0 |
| | | } |
| | | |
| | | # ---------- install / reinstall ---------- |
| | | Assert-Admin |
| | | |
| | | if (-not (Test-Path $wrapper)) { |
| | | throw "Missing wrapper executable: $wrapper`nRebuild the deploy package with pack-deploy.ps1 (it provides WinSW), or copy WinSW-x64.exe here renamed to trafficAudit.exe." |
| | | } |
| | | $passwordOk = Test-AppConfigPassword -Path $appConfig |
| | | |
| | | $java = Resolve-JavaPath -Explicit $JavaPath |
| | | Set-WrapperJavaPath -JavaExe $java |
| | | |
| | | $existing = Get-ServiceState |
| | | if ($existing) { |
| | | Write-Host "Service '$serviceId' already exists (status: $($existing.Status)); reinstalling..." |
| | | if ($existing.Status -ne 'Stopped') { |
| | | & $wrapper stop | Out-Null |
| | | Start-Sleep -Seconds 3 |
| | | } |
| | | & $wrapper uninstall | Out-Null |
| | | Start-Sleep -Seconds 2 |
| | | } |
| | | |
| | | Write-Host "Installing service '$serviceId'..." |
| | | & $wrapper install |
| | | if ($LASTEXITCODE -ne 0) { throw "WinSW install failed with exit code $LASTEXITCODE" } |
| | | |
| | | if (-not $NoStart) { |
| | | Write-Host "Starting service '$serviceId' (auto-start at boot is enabled)..." |
| | | & $wrapper start |
| | | Start-Sleep -Seconds 5 |
| | | $service = Get-ServiceState |
| | | $service | Select-Object Name, DisplayName, Status, StartType | Format-List |
| | | |
| | | # Read the port from application.yml so the check matches the real config. |
| | | $port = 8090 |
| | | if (Test-Path $appConfig) { |
| | | $portLine = Select-String -LiteralPath $appConfig -Pattern '^\s*port\s*:' | Select-Object -First 1 |
| | | if ($portLine -and $portLine.Line -match '(\d{2,5})') { $port = [int]$matches[1] } |
| | | } |
| | | |
| | | $ready = $false |
| | | for ($i = 1; $i -le 30; $i++) { |
| | | if (Test-LocalPort -Port $port) { $ready = $true; break } |
| | | Start-Sleep -Seconds 2 |
| | | $state = Get-ServiceState |
| | | if ($state -and $state.Status -ne 'Running') { break } |
| | | } |
| | | |
| | | $logDir = Join-Path $root 'logs' |
| | | Write-Host '' |
| | | if ($ready) { |
| | | Write-Host "OK : the backend is listening on port $port." |
| | | Write-Host " On this server : http://127.0.0.1:$port" |
| | | Write-Host " For other people : http://<server-ip>:$port" |
| | | } else { |
| | | Write-Host "WARN: nothing is listening on port $port yet - startup is still running, or it failed." |
| | | Write-Host " Fix application.yml / MySQL, then run: .\install-service.ps1 -Restart" |
| | | foreach ($name in @('trafficAudit.err.log', 'trafficAudit.out.log')) { |
| | | $logFile = Join-Path $logDir $name |
| | | if (Test-Path -LiteralPath $logFile) { |
| | | Write-Host '' |
| | | Write-Host "---- last 15 lines of $name ----" |
| | | Get-Content -LiteralPath $logFile -Tail 15 -ErrorAction SilentlyContinue | ForEach-Object { Write-Host " $_" } |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (-not $passwordOk) { |
| | | Write-Host '' |
| | | Write-Host '==============================================================' |
| | | Write-Host ' IMPORTANT: backend\application.yml still looks unset.' |
| | | Write-Host ' Put the real MySQL url / username / password in it, then run' |
| | | Write-Host ' .\install-service.ps1 -Restart' |
| | | Write-Host '==============================================================' |
| | | } |
| | | |
| | | Write-Host '' |
| | | Write-Host "Logs : $logDir\trafficAudit.out.log / trafficAudit.err.log" |
| | | Write-Host "Manage: .\install-service.ps1 -Status | -Restart | -Remove" |
| | | Write-Host " Stop-Service $serviceId | Start-Service $serviceId" |
| | | Write-Host '' |
| | | Write-Host 'This service starts automatically on every boot (Automatic + delayed start).' |
| | | } |
| New file |
| | |
| | | @echo off |
| | | rem ============================================================ |
| | | rem Non-interactive launcher for Task Scheduler / boot auto-start. |
| | | rem Differences from start-backend.cmd (which is meant for a human): |
| | | rem - no "pause" at the end (nobody can press a key during boot) |
| | | rem - all output goes to app.log next to this file |
| | | rem - resolves java via JAVA_HOME first, then PATH |
| | | rem - exits 0 immediately if port 8090 is already listening |
| | | rem ASCII-only on purpose: cmd.exe reads this file with the OEM |
| | | rem code page, so non-ASCII text here would be garbled. |
| | | rem Registered by install-autostart.ps1; run under the account that |
| | | rem owns the deployment folder (SYSTEM by default). |
| | | rem ============================================================ |
| | | setlocal |
| | | cd /d "%~dp0" |
| | | |
| | | set "APP_JAR=traffic-audit-server-1.0.0-SNAPSHOT.jar" |
| | | set "LOG=%~dp0app.log" |
| | | |
| | | netstat -ano | findstr /C:":8090" | findstr /I "LISTENING" >nul 2>nul |
| | | if not errorlevel 1 ( |
| | | echo [%date% %time%] port 8090 is already listening; nothing to do. >> "%LOG%" |
| | | exit /b 0 |
| | | ) |
| | | |
| | | if not exist "%~dp0%APP_JAR%" ( |
| | | echo [%date% %time%] ERROR: %APP_JAR% not found in "%CD%". >> "%LOG%" |
| | | exit /b 1 |
| | | ) |
| | | |
| | | set "JAVA_EXE=" |
| | | if defined JAVA_HOME if exist "%JAVA_HOME%\bin\java.exe" set "JAVA_EXE=%JAVA_HOME%\bin\java.exe" |
| | | if not defined JAVA_EXE for %%J in (java.exe) do set "JAVA_EXE=%%~$PATH:J" |
| | | if not defined JAVA_EXE ( |
| | | echo [%date% %time%] ERROR: java.exe not found. Install JDK 8 or set JAVA_HOME / PATH. >> "%LOG%" |
| | | exit /b 1 |
| | | ) |
| | | |
| | | if not exist "%~dp0application.yml" ( |
| | | echo [%date% %time%] WARN: application.yml is missing next to the jar. >> "%LOG%" |
| | | echo [%date% %time%] The DB password placeholder will resolve to empty and startup will fail with >> "%LOG%" |
| | | echo [%date% %time%] "Access denied ... (using password: NO)". >> "%LOG%" |
| | | ) |
| | | |
| | | echo [%date% %time%] starting backend: "%JAVA_EXE%" -jar "%APP_JAR%" (cwd "%CD%") >> "%LOG%" |
| | | "%JAVA_EXE%" -jar "%~dp0%APP_JAR%" >> "%LOG%" 2>&1 |
| | | set "RC=%ERRORLEVEL%" |
| | | echo [%date% %time%] backend exited with code %RC%. >> "%LOG%" |
| | | endlocal & exit /b %RC% |
| | |
| | | pause |
| | | exit /b 1 |
| | | ) |
| | | if not exist "%~dp0application.yml" ( |
| | | echo [WARN] æªæ¾å° %~dp0application.yml |
| | | echo 没æå¤ç½®é
ç½®æ¶ï¼jar å
ç½®å¯ç å ä½ç¬¦ä¼è§£æä¸ºç©ºï¼ |
| | | echo å¯å¨å°æ¥ java.sql.SQLException: Access denied ... ^(using password: NO^) |
| | | ) |
| | | findstr /C:"è¯·æ¹æä½ çæ°æ®åºå¯ç " "%~dp0application.yml" >nul 2>nul |
| | | if not errorlevel 1 ( |
| | | echo [WARN] application.yml é spring.datasource.password è¿æ¯å 使åï¼ |
| | | echo 请å
æ¹æé¨ç½²æºçå®ç MySQL å¯ç åå¯å¨ã |
| | | ) |
| | | echo ============================================ |
| | | echo TrafficAudit å¯å¨ä¸ï¼http://localhost:8090 |
| | | echo å端已å
åµï¼ç´æ¥è®¿é® 8090 å³å¯ï¼ä¸éè¦ nginxï¼ |
| | |
| | | #!/bin/sh |
| | | cd "$(dirname "$0")" || exit 1 |
| | | if [ ! -f application.yml ]; then |
| | | echo "[WARN] æªæ¾å° application.ymlï¼jar å级ï¼ï¼æ°æ®åºå¯ç ä¼è§£æä¸ºç©ºï¼å¯è½æ¥ Access denied ... (using password: NO)" |
| | | fi |
| | | if grep -q "è¯·æ¹æä½ çæ°æ®åºå¯ç " application.yml 2>/dev/null; then |
| | | echo "[WARN] application.yml é spring.datasource.password è¿æ¯å 使åï¼è¯·å
æ¹æçå® MySQL å¯ç " |
| | | fi |
| | | echo "TrafficAudit å¯å¨ä¸ï¼http://localhost:8090 ï¼Ctrl+C 忢ï¼" |
| | | java -jar traffic-audit-server-1.0.0-SNAPSHOT.jar 2>&1 | tee app.log |
| New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <!-- |
| | | WinSW (Windows Service Wrapper) configuration for the TrafficAudit backend. |
| | | |
| | | Layout requirement: this file MUST stay in the same folder as the wrapper |
| | | executable named trafficAudit.exe, the application jar, application.yml, |
| | | docs\ and web\. WinSW looks for a config file whose name matches the |
| | | executable (trafficAudit.exe -> trafficAudit.xml). |
| | | |
| | | %BASE% is expanded by WinSW to the folder that holds this file, so the |
| | | service always runs with that folder as its working directory. |
| | | |
| | | The executable element below is rewritten with an absolute java.exe path by |
| | | install-service.ps1 (a SYSTEM service does not inherit the interactive |
| | | user PATH, so relying on PATH alone is fragile). |
| | | --> |
| | | <service> |
| | | <id>TrafficAuditBackend</id> |
| | | <name>TrafficAudit Backend</name> |
| | | <description>TrafficAudit freight statistics audit backend (Spring Boot, HTTP port 8090).</description> |
| | | |
| | | <executable>java.exe</executable> |
| | | <arguments>-Dfile.encoding=UTF-8 -jar "%BASE%\traffic-audit-server-1.0.0-SNAPSHOT.jar"</arguments> |
| | | <workingdirectory>%BASE%</workingdirectory> |
| | | |
| | | <!-- Start at boot, but delayed so a local MySQL service is ready first. --> |
| | | <startmode>Automatic</startmode> |
| | | <delayedAutoStart>true</delayedAutoStart> |
| | | |
| | | <!-- Restart on crash: quickly at first, then back off. Counter resets after 1 hour. --> |
| | | <onfailure action="restart" delay="10 sec"/> |
| | | <onfailure action="restart" delay="30 sec"/> |
| | | <onfailure action="restart" delay="60 sec"/> |
| | | <resetfailure>1 hour</resetfailure> |
| | | |
| | | <stoptimeout>30 sec</stoptimeout> |
| | | |
| | | <logpath>%BASE%\logs</logpath> |
| | | <log mode="roll-by-size"> |
| | | <sizeThreshold>10240</sizeThreshold> |
| | | <keepFiles>10</keepFiles> |
| | | </log> |
| | | <outfile>trafficAudit.out.log</outfile> |
| | | <errfile>trafficAudit.err.log</errfile> |
| | | </service> |
| | |
| | | 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 å¼å§', |
| | | day_index INT NOT NULL COMMENT '0=å䏿¥ï¼1..N=åæç¬¬Næ¥', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | |
| | | year INT NOT NULL, |
| | | holiday_type VARCHAR(32) NOT NULL, |
| | | holiday_date DATE NOT NULL, |
| | | -- 0 is the configured previous day; normal holiday days are 1..N. |
| | | day_index INT NOT NULL, |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| New file |
| | |
| | | -- Backfill the 2019 National Day calendar and previous-day rows used by rail and civil-aviation ring-ratio calculations. |
| | | -- day_index = 0 identifies the configured previous day; 2019 uses normal days 1..8. |
| | | INSERT IGNORE INTO holiday_calendar |
| | | (year, holiday_type, holiday_date, day_index, created_at, updated_at) |
| | | VALUES |
| | | (2019, 'NATIONAL_DAY', '2019-10-01', 1, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-02', 2, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-03', 3, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-04', 4, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-05', 5, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-06', 6, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-07', 7, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-08', 8, NOW(), NOW()), |
| | | (2024, 'NATIONAL_DAY', '2024-09-30', 0, NOW(), NOW()), |
| | | (2025, 'NATIONAL_DAY', '2025-09-30', 0, NOW(), NOW()), |
| | | (2026, 'NATIONAL_DAY', '2026-09-30', 0, NOW(), NOW()); |
| New file |
| | |
| | | -- Observation station daily survey data (raw retention for future analysis). |
| | | -- Run against the traffic_audit database. |
| | | CREATE TABLE IF NOT EXISTS observation_station_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | obs_date DATE NOT NULL COMMENT 'Observation date', |
| | | station_code VARCHAR(64) NOT NULL COMMENT 'Station code', |
| | | station_name VARCHAR(128) DEFAULT NULL COMMENT 'Station name', |
| | | mileage DECIMAL(30,15) DEFAULT NULL COMMENT 'Observed mileage (km)', |
| | | city_name VARCHAR(64) DEFAULT NULL COMMENT 'City name', |
| | | county_name VARCHAR(64) DEFAULT NULL COMMENT 'County name', |
| | | direction VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'Travel direction', |
| | | small_medium_passenger DECIMAL(30,15) DEFAULT NULL COMMENT 'Small/medium passenger cars', |
| | | large_passenger DECIMAL(30,15) DEFAULT NULL COMMENT 'Large passenger cars', |
| | | small_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Small trucks', |
| | | medium_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Medium trucks', |
| | | large_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Large trucks', |
| | | extra_large_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Extra large trucks', |
| | | container_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Container trucks', |
| | | motorcycle DECIMAL(30,15) DEFAULT NULL COMMENT 'Motorcycles', |
| | | tractor DECIMAL(30,15) DEFAULT NULL COMMENT 'Tractors', |
| | | passenger_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total passenger vehicles', |
| | | truck_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total trucks', |
| | | car_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total automobiles', |
| | | motor_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total motor vehicles', |
| | | passenger_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Passenger car equivalent', |
| | | truck_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Truck equivalent', |
| | | car_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Automobile equivalent', |
| | | motor_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Motor vehicle equivalent', |
| | | vehicle_speed DECIMAL(30,15) DEFAULT NULL COMMENT 'Motor vehicle speed', |
| | | congestion_degree DECIMAL(30,15) DEFAULT NULL COMMENT 'Congestion degree', |
| | | source_file VARCHAR(255) DEFAULT NULL COMMENT 'Source file name', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_observation_station (obs_date, station_code, direction), |
| | | KEY idx_observation_station_date (obs_date), |
| | | KEY idx_observation_station_city (city_name), |
| | | KEY idx_observation_station_code (station_code) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| New file |
| | |
| | | # ============================================================ |
| | | # æå
ç¨ãè¿ jar çé»è®¤é
ç½®ãââåªå
许å ä½ç¬¦ï¼ç¦æ¢çå®å¯é¥/å£ä»¤ï¼ |
| | | # |
| | | # pack-deploy.ps1 æå»ºåä¼ç¨æ¬æä»¶ä¸´æ¶æ¿æ¢ |
| | | # traffic-audit-server/src/main/resources/application.yml |
| | | # æå»ºå®æï¼å«å»å¯èªæ£ï¼åèªå¨è¿åå¼åæºççå®é
ç½®ã |
| | | # |
| | | # é¨ç½²æºçå®å¼ç±ä¸é¢ä¸¤ç§æ¹å¼è¦çï¼ä¼å
级 ç¯å¢åé > jar å级 application.yml > æ¬æä»¶ï¼ |
| | | # 1) jar å级ï¼å¯å¨å·¥ä½ç®å½ï¼æ¾ application.ymlï¼è§ deploy-templates/backend/application.yml |
| | | # 2) ç¯å¢åé TRAFFIC_DB_URL / TRAFFIC_DB_USER / TRAFFIC_DB_PASSWORD / |
| | | # TRAFFIC_JWT_SECRET / DEEPSEEK_API_KEY |
| | | # |
| | | # 注æï¼password é»è®¤å¼ä¸ºç©ºä¸²ï¼è¥é¨ç½²æºæ¢æ²¡æå¤ç½® application.yml 乿²¡è®¾ |
| | | # TRAFFIC_DB_PASSWORDï¼å¯å¨ä¼æ¥ java.sql.SQLException: Access denied ... (using password: NO) |
| | | # ============================================================ |
| | | server: |
| | | port: 8090 |
| | | tomcat: |
| | | basedir: . |
| | | threads: |
| | | max: 500 |
| | | accesslog: |
| | | enabled: true |
| | | directory: logs |
| | | prefix: access |
| | | suffix: .log |
| | | pattern: '%h %l %u %t "%r" %s %b %D' |
| | | |
| | | spring: |
| | | web: |
| | | resources: |
| | | static-locations: file:./web/,classpath:/static/ |
| | | datasource: |
| | | url: ${TRAFFIC_DB_URL:jdbc:mysql://127.0.0.1:3306/traffic_audit?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true} |
| | | username: ${TRAFFIC_DB_USER:root} |
| | | password: ${TRAFFIC_DB_PASSWORD:} |
| | | driver-class-name: com.mysql.cj.jdbc.Driver |
| | | servlet: |
| | | multipart: |
| | | max-file-size: 100MB |
| | | max-request-size: 100MB |
| | | jackson: |
| | | date-format: yyyy-MM-dd HH:mm:ss |
| | | time-zone: Asia/Shanghai |
| | | |
| | | mybatis-plus: |
| | | mapper-locations: classpath*:/mapper/**/*.xml |
| | | type-aliases-package: com.trafficaudit |
| | | configuration: |
| | | map-underscore-to-camel-case: true |
| | | # æ¾å¼æå® Slf4jï¼MyBatis-Plus é»è®¤ StdOutImpl 伿æ´è¡æ°æ®ï¼å«ä¼ä¸å/çµè¯ï¼æè¿æ¥å¿ |
| | | log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl |
| | | global-config: |
| | | db-config: |
| | | id-type: auto |
| | | logic-delete-field: deleted |
| | | logic-delete-value: 1 |
| | | logic-not-delete-value: 0 |
| | | |
| | | jwt: |
| | | secret: ${TRAFFIC_JWT_SECRET:8f2c1d64a7b34e09c5d81f6a2b0e7c93} |
| | | expiration: 86400000 |
| | | |
| | | docs: |
| | | template-root: docs |
| | | |
| | | city-passenger: |
| | | template-dir: docs/åå¸å®¢è¿ |
| | | |
| | | passenger: |
| | | template-dir: docs/å
¬è·¯æ
客 |
| | | |
| | | freight: |
| | | template-dir: docs/è´§è¿ |
| | | |
| | | energy: |
| | | template-dir: docs/è½è |
| | | |
| | | investment: |
| | | template-dir: docs/æèµ/æ¨¡æ¿ |
| | | |
| | | summary: |
| | | template-dir: docs/çææ±æ»å¤§è¡¨ |
| | | |
| | | deepseek: |
| | | api-key: ${DEEPSEEK_API_KEY:} |
| | | api-url: https://api.deepseek.com/v1/chat/completions |
| | | model: deepseek-chat |
| | | |
| | | app: |
| | | cors: |
| | | allowed-origins: http://localhost:*,http://127.0.0.1:*,http://192.168.*:*,http://10.*:*,http://172.*:* |
| | | captcha: |
| | | required: false |
| | |
| | | |
| | | ## ä¹ãä¸é¡¹ç»è®¡-éè¦èåæ¥æ¨¡å |
| | | |
| | | > æ°æ®æ¥æºä¸æ¨¡æ¿ï¼`docs/ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´å䏿°æ®.xlsx`ï¼å¿«æ¥æ¨¡æ¿ï¼`docs/ä¸é¡¹ç»è®¡/éè¦è忥/æ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥æ¨¡æ¿.docx`ã馿¬¡å¯å¨ä¼ä»å·¥ä½ç°¿åå§å 2024/2025 å¹´åä¸å®¢æµãè½¦æµæ°æ®ï¼å¹¶åå§å 2024-2026 年客è¿ç³»æ°ã |
| | | > æ°æ®æ¥æºä¸æ¨¡æ¿ï¼`docs/ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´å䏿°æ®.xlsx`ï¼å¿«æ¥æ¨¡æ¿ï¼`docs/ä¸é¡¹ç»è®¡/éè¦è忥/æ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥æ¨¡æ¿.docx`ã馿¬¡å¯å¨ä¼ä»å·¥ä½ç°¿åå§å 2024/2025 å¹´åä¸å®¢æµãè½¦æµæ°æ®ã2024-2026 å¹´éè·¯/æ°èªå䏿¥æ°æ®ã2019 å¹´éè·¯æ°æ®ï¼å¹¶åå§å 2024-2026 年客è¿ç³»æ°ã |
| | | |
| | | ### holiday_calendar èåæ¥æ¥æé
ç½® |
| | | | åæ®µ | ç±»å | 说æ | |
| | |
| | | | year | int | 年份 | |
| | | | holiday_type | varchar(32) | æ¥è SPRING_FESTIVAL / äºä¸ LABOR_DAY / åä¸ NATIONAL_DAY | |
| | | | holiday_date | date | è忥å
·ä½æ¥æ | |
| | | | day_index | int | ç¬¬å æ¥ï¼ä» 1 å¼å§ | |
| | | | day_index | int | 0=å䏿¥ï¼1..N=åæç¬¬ N æ¥ | |
| | | | created_at / updated_at | datetime | å建/æ´æ°æ¶é´ | |
| | | |
| | | å¯ä¸é®ï¼`(year, holiday_type, day_index)`ã`(year, holiday_type, holiday_date)`ãä»
admin å¯éè¿ç³»ç»é
ç½®èåç»´æ¤ã |
| | | å¯ä¸é®ï¼`(year, holiday_type, day_index)`ã`(year, holiday_type, holiday_date)`ãä»
admin å¯éè¿ç³»ç»é
ç½®èåç»´æ¤ï¼`day_index=0` ç¨äºéè·¯ãæ°èªçå䏿¥ç¯æ¯åºæ°ã |
| | | |
| | | ### holiday_passenger_flow èåæ¥ç»¼åè¿è¾è¥ä¸æ§å®¢æµ |
| | | | åæ®µ | ç±»å | 说æ | |
| | |
| | | - 2026 å¹´æ®éå½çéå¦å¡«åç¯æ¯åºæï¼ååªå½±åæ®éå½çé车æµéèªèº«ç¯æ¯ï¼å¡«åæ¶æâ彿¥ / ç¯æ¯åºæ - 1âï¼æªå¡«åæ¶æâ彿¥ / å䏿¥ - 1âï¼æç®äººåæµå¨éãæ»äººåæµå¨éçç»§ç»æ Excel åå
¬å¼è®¡ç®ã2024ã2025 å岿°æ®ä¸ä½¿ç¨è¯¥å段ã |
| | | - å
¬è·¯äººåæµå¨é = éè·¯è¥ä¸æ§å®¢è¿é + éè¥ä¸æ§äººåæµå¨éã |
| | | - æ»äººåæµå¨é = å
¬è·¯ + éè·¯ + æ°èª + æ°´è·¯ã |
| | | - 导å
¥æâ年份 + è忥åç±»â读å `holiday_calendar` çæ¥æï¼å°å·¥ä½ç°¿â第Næ¥âè¡è¦çåå
¥å½å¹´å®¢æµ/车æµï¼æ¥å¸¸åªè¯»åæé年份çå½å¹´åï¼åå²å¹´ä»½ä»
馿¬¡åå§åã |
| | | - 导å
¥æâ年份 + è忥åç±»â读å `holiday_calendar` ä¸ `day_index>=1` çæ¥æï¼å°å·¥ä½ç°¿â第Næ¥âè¡è¦çåå
¥å½å¹´å®¢æµ/车æµï¼åæ¶è¯»åå½å年份éè·¯ãæ°èªçâå䏿¥âè¡ï¼åå
¥ `day_index=0` 对åºçæ¥æã |
| | | - æ¥å¸¸å¯¼å
¥åªè¯»åæé年份çå½å¹´åï¼é路表ç 2019/2024/2025 ååä¸è¯»åï¼ï¼2019 å¹´éè·¯æ°æ®å 2024/2025/2026 å¹´å䏿¥éè·¯ãæ°èªæ°æ®ç±å¯å¨åå§åç´æ¥åå
¥æ°æ®åºã2019 å¹´ 10 æ 1-8 æ¥ç `holiday_calendar` é
ç½®ç¨äºå¨ç³»ç»é
ç½®åè忥ç»è®¡é¡µæ¥ç该åå²åºçº¿ã |
| | | - 导å
¥åæ£æ¥æ¯æ¬æ¬¡ææ°å¯¼å
¥æ¥æ´æ©çå岿¥ææ¯å¦åçååï¼å¦æååå
ä¸ååºï¼è¿å差弿¸
åï¼ç¨æ·ç¡®è®¤åæå
许è¦çã |
| | | - 导åºä»¥æºå·¥ä½ç°¿ä¸ºåºç¨¿ï¼åå¡«åºå
æ°æ®ä¸å®¢è¿ç³»æ°ï¼å¿«æ¥ææ¥æçæ Word å¹¶æ¿æ¢æ¨¡æ¿ä¸ç彿¥ã累计ãç¯æ¯å忝å
容ã |
| | | - è®°å½âå䏿¥âçéè·¯ãæ°èªæ°æ®åï¼åæç¬¬ 1 æ¥çéè·¯ãæ°èªç¯æ¯æè¯¥æ°æ®è®¡ç®ï¼å
¬è·¯ãæ°´è·¯ãæ»äººåæµå¨éå¨ç¬¬ 1 æ¥ä»ä¸è®¡ç®ç¯æ¯ã |
| | | - 导åºä»¥æºå·¥ä½ç°¿ä¸ºåºç¨¿ï¼åå¡«åºå
æ°æ®ä¸å®¢è¿ç³»æ°ï¼å¿«æ¥ææ¥æçæ Wordï¼åæ¯åºå®ä½¿ç¨ 2025ã2024 两年ï¼ä¸ä½¿ç¨ 2019 æ°æ®ã |
| | |
| | | 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 å¼å§', |
| | | day_index INT NOT NULL COMMENT '0=å䏿¥ï¼1..N=åæç¬¬Næ¥', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| New file |
| | |
| | | -- Backfill the 2019 National Day calendar and previous-day rows used by rail and civil-aviation ring-ratio calculations. |
| | | -- day_index = 0 identifies the configured previous day; 2019 uses normal days 1..8. |
| | | INSERT IGNORE INTO holiday_calendar |
| | | (year, holiday_type, holiday_date, day_index, created_at, updated_at) |
| | | VALUES |
| | | (2019, 'NATIONAL_DAY', '2019-10-01', 1, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-02', 2, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-03', 3, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-04', 4, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-05', 5, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-06', 6, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-07', 7, NOW(), NOW()), |
| | | (2019, 'NATIONAL_DAY', '2019-10-08', 8, NOW(), NOW()), |
| | | (2024, 'NATIONAL_DAY', '2024-09-30', 0, NOW(), NOW()), |
| | | (2025, 'NATIONAL_DAY', '2025-09-30', 0, NOW(), NOW()), |
| | | (2026, 'NATIONAL_DAY', '2026-09-30', 0, NOW(), NOW()); |
| New file |
| | |
| | | -- Observation station daily survey data (raw retention for future analysis). |
| | | -- Run against the traffic_audit database. |
| | | CREATE TABLE IF NOT EXISTS observation_station_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | obs_date DATE NOT NULL COMMENT 'Observation date', |
| | | station_code VARCHAR(64) NOT NULL COMMENT 'Station code', |
| | | station_name VARCHAR(128) DEFAULT NULL COMMENT 'Station name', |
| | | mileage DECIMAL(30,15) DEFAULT NULL COMMENT 'Observed mileage (km)', |
| | | city_name VARCHAR(64) DEFAULT NULL COMMENT 'City name', |
| | | county_name VARCHAR(64) DEFAULT NULL COMMENT 'County name', |
| | | direction VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'Travel direction', |
| | | small_medium_passenger DECIMAL(30,15) DEFAULT NULL COMMENT 'Small/medium passenger cars', |
| | | large_passenger DECIMAL(30,15) DEFAULT NULL COMMENT 'Large passenger cars', |
| | | small_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Small trucks', |
| | | medium_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Medium trucks', |
| | | large_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Large trucks', |
| | | extra_large_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Extra large trucks', |
| | | container_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Container trucks', |
| | | motorcycle DECIMAL(30,15) DEFAULT NULL COMMENT 'Motorcycles', |
| | | tractor DECIMAL(30,15) DEFAULT NULL COMMENT 'Tractors', |
| | | passenger_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total passenger vehicles', |
| | | truck_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total trucks', |
| | | car_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total automobiles', |
| | | motor_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total motor vehicles', |
| | | passenger_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Passenger car equivalent', |
| | | truck_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Truck equivalent', |
| | | car_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Automobile equivalent', |
| | | motor_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Motor vehicle equivalent', |
| | | vehicle_speed DECIMAL(30,15) DEFAULT NULL COMMENT 'Motor vehicle speed', |
| | | congestion_degree DECIMAL(30,15) DEFAULT NULL COMMENT 'Congestion degree', |
| | | source_file VARCHAR(255) DEFAULT NULL COMMENT 'Source file name', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_observation_station (obs_date, station_code, direction), |
| | | KEY idx_observation_station_date (obs_date), |
| | | KEY idx_observation_station_city (city_name), |
| | | KEY idx_observation_station_code (station_code) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| | |
| | | â ââ traffic-audit-server-1.0.0-SNAPSHOT.jar å端 + å
åµå端ï¼çº¦ 55 MBï¼ |
| | | â ââ docs/ 模æ¿ä¸è¿è¡æ°æ®ï¼å¿
éï¼å¿å ï¼ |
| | | â ââ application.yml å¤é¨é
ç½®ï¼æ¹è¿éå³å¯ï¼æ ééæå
ï¼ |
| | | â ââ start-backend.cmd / start-backend.sh å¯å¨èæ¬ |
| | | â ââ start-backend.cmd / start-backend.sh å¯å¨èæ¬ï¼äººå·¥åå»/å½ä»¤è¡ï¼ |
| | | â ââ trafficAudit.exe WinSW æå¡å
è£
å¨ï¼2.12.0 x64ï¼èªå
å« .NET 6ï¼ |
| | | â ââ trafficAudit.xml WinSW æå¡é
ç½®ï¼èªå¨/å»¶è¿å¯å¨ã失败éå¯ãæ¥å¿æ»å¨ï¼ |
| | | â ââ install-service.cmd åå»å
¥å£ï¼æ³¨å弿ºèªå¯æå¡ï¼èªå¨è¯·æ±ç®¡çåæéï¼ |
| | | â ââ install-service.ps1 ä¸é¢é£ä¸ª cmd å®é
è°ç¨çèæ¬ï¼æ³¨å/æ¥ç/éå¯/å é¤ï¼ |
| | | â ââ start-backend-service.cmd å¤éï¼é交äºå¯å¨å¨ï¼æ pauseãå app.logï¼ |
| | | â ââ install-autostart.ps1 å¤éï¼æ³¨å/æ¥ç/å é¤ Windows 弿ºèªå¯è®¡åä»»å¡ |
| | | â ââ web/ åç«¯éææä»¶ï¼ç± dist èªå¨åæ¥ï¼8090 åç«¯å£æ¨¡å¼å¿
éï¼ |
| | | ââ frontend/ |
| | | â ââ dist/ å端产ç©ï¼ä»
nginx æ¹æ¡éè¦ï¼ |
| | | â ââ nginx.conf.example nginx é
ç½®ç¤ºä¾ |
| | |
| | | â ââ sql_wyc.sql ç½çº¦è½¦å
¨çæ»é/订ååå¸å·è¡¨ï¼å¿
é¡»æ§è¡ï¼ |
| | | â ââ sql_holiday.sql ä¸é¡¹ç»è®¡-éè¦èåæ¥è¡¨ï¼æ§åºè¡¥å»ºæ¶æ§è¡ï¼ |
| | | â ââ sql_holiday_decimal_migration.sql è忥æ°å¼å精度è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | â ââ sql_holiday_comparison_base_migration.sql èåæ¥ç¯æ¯åºæå段è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | â ââ sql_holiday_comparison_base_migration.sql èåæ¥ç¯æ¯åºæå段è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | â ââ sql_holiday_previous_day_migration.sql è忥å䏿¥é
ç½®è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | ââ README-é¨ç½²è¯´æ.txt |
| | | ``` |
| | | |
| | |
| | | ``` |
| | | |
| | | > â ï¸ **`init.sql` æ¯ç ´åæ§ç**ï¼éé¢çåºç¡è¡¨é½å¸¦ `DROP TABLE IF EXISTS`ã**å·²ææ°æ®çåºä¸åä¸ä¸è¦åè· init.sql**ï¼å¦åè¡¨æ°æ®ä¼è¢«æ¸
空é建ã |
| | | > æ©æçæ¬åºè¥æ²¡æè忥 4 å¼ è¡¨ï¼æ§è¡ `sql_holiday.sql`ï¼è¥å·²æèåæ¥è¡¨ä½ç¼ºç²¾åº¦æç¯æ¯åºæåæ®µï¼æéæ§è¡ä¸¤ä¸ªè¿ç§»èæ¬ã |
| | | > æ©æçæ¬åºè¥æ²¡æè忥 4 å¼ è¡¨ï¼æ§è¡ `sql_holiday.sql`ï¼è¥å·²æèåæ¥è¡¨ä½ç¼ºç²¾åº¦ãç¯æ¯åºæå段æå䏿¥é
ç½®ï¼æéæ§è¡ `sql_holiday_decimal_migration.sql`ã`sql_holiday_comparison_base_migration.sql`ã`sql_holiday_previous_day_migration.sql`ã |
| | | > 4 ä¸ªèæ¬å
±è¦ç 33 å¼ è¡¨ï¼init.sql 28 å¼ ï¼å
¶ä¸å«è忥 4 å¼ + åå¸å®¢è¿ 3 å¼ + ç½çº¦è½¦ 2 å¼ ï¼ã |
| | | |
| | | #### 路线 Bï¼æç°ç½/å¼åæºçæ°æ®æ´å¥æ¬è¿å»ï¼æ¨èï¼ä¸ç¨è·èæ¬ï¼ |
| | |
| | | |
| | | æåæ å¿ï¼æ¥å¿åºç° `Started TrafficAuditApplication` ä¸ `Tomcat started on port(s): 8090`ã |
| | | |
| | | ### 5.4 è®¿é® |
| | | ### 5.4 Windows 弿ºèªå¯ï¼æ¨èï¼æ³¨åæ Windows æå¡ï¼ |
| | | |
| | | å¨ `backend` ç®å½é**å³é® `install-service.cmd` âã以管çå身份è¿è¡ã**å³å¯ï¼ç´æ¥åå»ä¹è¡ï¼å®ä¼èªå·±å¼¹ UACï¼ãå®å®é
è°ç¨çå°±æ¯åç®å½ç `install-service.ps1`ï¼çä»·å½ä»¤ï¼ |
| | | |
| | | ```powershell |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File install-service.ps1 |
| | | ``` |
| | | |
| | | èæ¬ä¼èªå¨å®ä½ javaãæ³¨åæå¡ãå¯å¨ï¼ç¶åæ¢æµç«¯å£æå°ç»æï¼èµ·ä¸æ¥æ¶ç´æ¥æå° `logs\*.log` çæå 15 è¡ã |
| | | |
| | | | 项 | å¼ | |
| | | |----|----| |
| | | | æå¡å | `TrafficAuditBackend`ï¼services.msc éæ¾ç¤ºä¸º TrafficAudit Backendï¼ | |
| | | | å¯å¨ç±»å | èªå¨ + å»¶è¿å¯å¨ï¼å
ç MySQL 就绪ï¼ï¼ä¸éè¦ä»»ä½ç¨æ·ç»å½ | |
| | | | è¿è¡èº«ä»½ | LocalSystem | |
| | | | 失败éå¯ | 10s / 30s / 60s éå¢ï¼1 å°æ¶åè®¡æ°æ¸
é¶ï¼ç±æå¡æ§å¶ç®¡ç卿§è¡ï¼ | |
| | | | æ¥å¿ | `backend\logs\trafficAudit.out.log`ã`trafficAudit.err.log`ï¼10MB à 10 æ»å¨ï¼ | |
| | | |
| | | 常ç¨åæ°ï¼`-Status`ï¼æ é管çåï¼ã`-Restart`ã`-Remove`ã`-NoStart`ã`-JavaPath "D:\jdk8\bin\java.exe"`ã |
| | | èæ¬ä¼èªå¨å®ä½ `java.exe`ï¼JAVA_HOME â PATH â JavaSoft 注å表 â Program Filesï¼å¹¶æ**ç»å¯¹è·¯å¾**åè¿ `trafficAudit.xml`ï¼SYSTEM æå¡ç PATH ä¸ç»å½ç¨æ·ä¸åï¼åç»å¯¹è·¯å¾æ´ç¨³ï¼ãé夿§è¡æ¯å¹ççï¼å
åæå¡åéæ°æ³¨åå¹¶å¯å¨ã |
| | | |
| | | > åä¸å°æº**ä¸è¦**åæ¶å¯ç¨è®¡å任塿¹å¼ï¼`install-autostart.ps1`ï¼ï¼ä¸¤ä¸ªå®¿ä¸»ä¼æ¢ 8090 端å£ã |
| | | |
| | | #### æä½ç³»ç»å
¼å®¹æ§ |
| | | |
| | | æå¡æ¬èº«ï¼èªå¨å¯å¨ / å»¶è¿å¯å¨ / 失败éå¯ï¼ä» Win7 SP1 å° Win11ãServer 2008 R2 å° Server 2022/2025 æºå¶ä¸è´ï¼å·®å«åªå¨å
è£
å¨ä¾èµçè¿è¡æ¶ï¼ |
| | | |
| | | | ç®æ ç³»ç» | ç¨åªä¸ªå
è£
å¨ | é¢å¤ä¾èµ | |
| | | |----------|--------------|----------| |
| | | | Win10 / Win11 / Server 2016 å以ä¸ï¼æ¨èï¼ | å
å
`trafficAudit.exe`ï¼WinSW 2.12.0 x64 èªå
å«ï¼ | æ ï¼èªå¸¦ .NET 6 è¿è¡æ¶ï¼ | |
| | | | Win7 SP1 / Server 2008 R2 / Server 2012(R2) | å
è¯å
å
èªå
å«çï¼Win7 é KB2533623ï¼ï¼æ¥ `api-ms-win-*.dll` ç¼ºå¤±æ¶æ¢ `WinSW.NET461.exe` æ¹åæ `trafficAudit.exe` | .NET Framework 4.6.1+ | |
| | | | 32 ä½ç³»ç» | `WinSW-x86.exe` æ¹åæ `trafficAudit.exe` | æ ï¼èªå¸¦ .NET 6 è¿è¡æ¶ï¼ | |
| | | |
| | | ä¸è½½é¡µï¼<https://github.com/winsw/winsw/releases/tag/v2.12.0> ââ ä»»ä½æå»ºé½ç¨åä¸ä»½ `trafficAudit.xml`ï¼æ¹çåªæ¯å
è£
卿件åã |
| | | `install-service.ps1` å¨ Windows PowerShell 2.0+ å¯ç¨ï¼`install-autostart.ps1` ç¨å° `New-ScheduledTask*`ï¼é Win8 / Server 2012 å以ä¸ã |
| | | |
| | | ### 5.5 è®¿é® |
| | | |
| | | æµè§å¨æå¼ `http://<é¨ç½²æºIP>:8090`ï¼å端已å
åµï¼é¡µé¢ä¸æ¥å£åæºï¼æ é nginxï¼ï¼è´¦å· `admin / admin123`ã |
| | | |
| | |
| | | | ç»å½æ¥å£ 500 | `jwt.secret` å¡«äºéæ³å符ï¼å¦ä¸æï¼ï¼æ¹æ 32 ä½åå
è¿å¶ä¸² | |
| | | | 页é¢è½å¼ä½æ¥å£ 404 | èµ° nginx æ¹æ¡æ¶æ²¡é
`/api` åä»£ï¼æ¹ç¨ jar å
åµè®¿é® 8090 | |
| | | | æ°æ®åºè¿æ¥å¤±è´¥ | æ£æ¥ `application.yml`ï¼ç¡®è®¤ 5 个 SQL èæ¬å·²æ§è¡ | |
| | | | å¯å¨æ¥ `Access denied ... (using password: NO)` | å端æ¿å°çåºå£ä»¤æ¯**空串**ï¼jar å
ç½® `${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` å¿
é¡»æ¯çå®å£ä»¤ | |
| | | | æå¡æ¥âæç»è®¿é®â/åä¸äºæ¥å¿ | é¨ç½²ç®å½æéä¸è¶³ï¼æ¢å° `D:\trafficAudit` è¿ç±»ç®åè·¯å¾ï¼æç» LocalSystem æåæé | |
| | | | 䏿³å¼æºèªå¯ä½ä¿çæå¡ | `Set-Service TrafficAuditBackend -StartupType Manual` | |
| | | | Win7/2008R2 䏿å¡å¯ä¸äºï¼æ¥ `api-ms-win-*.dll`ï¼ | è§ 5.4ãæä½ç³»ç»å
¼å®¹æ§ãï¼æ `WinSW.NET461.exe` æ¹åæ `trafficAudit.exe` æ¿æ¢ | |
| | | |
| | | ## åãå¼åç¯å¢æ¥å¸¸å¯å¨ |
| | | |
| | |
| | | |
| | | ## åä¸ãåæ´è®°å½ |
| | | |
| | | - **2026-10-03**ï¼`pack-deploy.ps1` è¡¥é½å¤ç½® `application.yml`/`docs`/`db`/å¯å¨èæ¬å¹¶åæ¥ `backend\web`ï¼ä¿® `Access denied ... (using password: NO)`ï¼ï¼æ¨¡æ¿ç«¯å£ 8080â8090ï¼æ°å¢ Windows æå¡æ¹å¼å¼æºèªå¯ï¼WinSW 2.12.0 `trafficAudit.exe` + `trafficAudit.xml` + `install-service.ps1`ï¼èªå¨/å»¶è¿å¯å¨ã失败éå¯ãæ¥å¿æ»å¨ï¼ï¼è®¡å任塿¹å¼é为å¤éï¼è¡¥æä½ç³»ç»å
¼å®¹æ§è¯´æã |
| | | - **2026-09-13**ï¼æ°å¢ä¸é®æå
èæ¬ `package-deploy.py`ï¼jar æ¹ä¸ºå
åµå端ï¼é¨ç½²æºä¸åéè¦ nginxï¼ï¼å é¤ Redis ç¸å
³è¯´æï¼ä»£ç æªä½¿ç¨ï¼ï¼è¡¥ `sql_wyc.sql` ä¸ `summary.template-dir`ï¼è¡¥ `jwt.secret` åå¼è§åï¼è¡¥é¨ç½²åéªè¯æ¸
åã |
| | | - 2026-08-27ï¼é¦çï¼zip é¨ç½²å
+ nginx æ¹æ¡ï¼ã |
| | | |
| | |
| | | ``` |
| | | |
| | | èæ¬ååä»¶äºï¼ |
| | | 1. `mvn clean package -DskipTests` æå»ºå端 jarï¼ |
| | | 1. ç¨ `deploy-templates/jar-application.yml`ï¼å
¨å ä½ç¬¦ï¼ä¸´æ¶æ¿æ¢æºç `application.yml`ï¼å `mvn clean package -DskipTests`ï¼æå»ºåèªå¨è¿åå¼åæºçå®é
ç½®ï¼ |
| | | 2. **è§£å jar èªæ£**ï¼æ£æ¥ `BOOT-INF/classes/application.yml` æ¯å¦å
¨ä¸ºå ä½ç¬¦ââ䏿¦åç° `sk-` æææå£ä»¤**ç´æ¥ä¸æ¢æå
**ï¼ |
| | | 3. `npm run build` é建å端 `dist`ï¼ |
| | | 4. æ `deploy-templates/` ç»è£
宿´é¨ç½²å
ï¼backend / frontend / db / README-é¨ç½²è¯´æ.txtï¼ï¼è¾åºå° `<OutRoot>\traffic-audit-deploy-<yyyyMMdd>`ã |
| | | 4. æ `deploy-templates/` ç»è£
宿´é¨ç½²å
ï¼backend / frontend / db / README-é¨ç½²è¯´æ.txtï¼ââ`backend/` å« jarã`application.yml`ãå¯å¨èæ¬ã`docs/`ã`web/`ï¼ç± dist èªå¨åæ¥ï¼ä¾ 8090 åç«¯å£æ¨¡å¼ï¼ï¼ä»¥å Windows æå¡ä¸ä»¶å¥ `trafficAudit.exe` + `trafficAudit.xml` + `install-service.ps1`ï¼è¾åºå° `<OutRoot>\traffic-audit-deploy-<yyyyMMdd>`ï¼ |
| | | WinSW ä¼å
ç¨ `deploy-templates\tools\winsw.exe` ç¼åï¼æ ¡éª SHA-256ï¼ï¼ç¼å缺失æåå¸ä¸ç¬¦æ¶èªå¨ä» GitHub 宿¹åå¸é¡µä¸è½½ v2.12.0 x64ï¼è¯¥äº§ç©æªå Authenticode ç¾åï¼å æ¤ä»¥åºå® SHA-256 ä½å®æ´æ§æ ¡éªï¼ï¼ |
| | | 5. 对ç»è£
åçæ´å
åæ«ä¸éææå¯é¥ã |
| | | |
| | | 模æ¿ä»¶ç»ä¸æ¾å¨ **`deploy-templates/`**ï¼å·²å
¥åºï¼ï¼å¤é¨ `application.yml`ã`start-backend.cmd/.sh`ã`nginx.conf.example`ã`db\{init.sql,sql_city_bus,sql_city_taxi,sql_wyc}.sql`ã`README-é¨ç½²è¯´æ.txt`ãæ¹æ¨¡æ¿å³æ¹æå
产ç©ã |
| | | å¯éåæ°ï¼`-OutRoot` æå®è¾åºç®å½ï¼é»è®¤ä»åº `deploy\`ï¼ã`-SkipFrontend` å¤ç¨ç°æ distã`-SkipBackend` å¤ç¨ç°æ jarï¼åªéç»å
ãä¸éç¼è¯ï¼ç¨äºå¿«éè¡¥å
ï¼ã`-SkipServiceWrapper` 䏿 WinSW æå¡å
è£
卿è¿å
ï¼å³ä¸è¦âæ³¨åæ Windows æå¡âç弿ºèªå¯ï¼ã |
| | | |
| | | > è®°å½ï¼2026-10-03ï¼ï¼æ¤åæå·¥äº§åºç 10-02 å
åªæ jar+web+distï¼ç¼º `application.yml`/`docs`/`db`/å¯å¨èæ¬ï¼é¨ç½²æºå¯å¨æ¥ `Access denied ... (using password: NO)`ãç°å·²ç±èæ¬èªå¨è¡¥é½è¿äºæä»¶å¹¶å¨æå
åæ¿æ¢å ä½ç¬¦é
ç½®ã |
| | | |
| | | 模æ¿ä»¶ç»ä¸æ¾å¨ **`deploy-templates/`**ï¼å·²å
¥åºï¼ï¼å¤é¨ `application.yml`ã`start-backend.cmd/.sh`ã`trafficAudit.xml`ã`install-service.ps1`ã`start-backend-service.cmd`ã`install-autostart.ps1`ã`nginx.conf.example`ã`db\{init.sql,sql_city_bus,sql_city_taxi,sql_wyc}.sql`ã`README-é¨ç½²è¯´æ.txt`ãæ¹æ¨¡æ¿å³æ¹æå
产ç©ã |
| | | |
| | | > `deploy-templates\tools\`ï¼WinSW äºè¿å¶ï¼17.4MBï¼**ä¸å
¥åº**ï¼å·²å `.gitignore`ï¼ï¼ç±æå
èæ¬æéä¸è½½å¹¶æ ¡éª SHA-256ã |
| | | |
| | | ### è½®æ¢å¯é¥ååæ¥æ¬æº |
| | | |
| | |
| | | <# |
| | | ä¸é®æå
é¨ç½²å
ï¼å«"å»å¯èªæ£"ï¼ââ 2026-09-27 æ°å¢ |
| | | ä¸é®æå
é¨ç½²å
ï¼å«"å»å¯èªæ£"ï¼ââ 2026-09-27 æ°å¢ï¼2026-10-03 修订 |
| | | ç¨æ³ï¼ |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File pack-deploy.ps1 |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File pack-deploy.ps1 -OutRoot "E:\è¯éª" -SkipFrontend |
| | | ååä»¶äºï¼ |
| | | 1) mvn clean package æå»ºå端 jar |
| | | |
| | | åäºä»¶äºï¼ |
| | | 1) ç¨ deploy-templates\jar-application.ymlï¼å
¨å ä½ç¬¦ï¼ä¸´æ¶æ¿æ¢æºç application.ymlï¼ |
| | | æ§è¡ mvn clean packageï¼æ 论æè´¥é½è¿åå¼åæºçå®é
ç½® |
| | | 2) è§£å jar æ£æ¥å
ç½® application.ymlï¼å¿
é¡»å
¨æ¯å ä½ç¬¦ï¼ç¦æ¢ sk- å¯é¥ / ææå£ä»¤ï¼ï¼ä¸åæ ¼ç´æ¥ä¸æ¢ |
| | | 3) npm run build æå»ºå端 distï¼å¯ -SkipFrontendï¼ |
| | | 4) ç»è£
deploy ç®å½ç»æï¼backend / frontend / db / README-é¨ç½²è¯´æ.txtï¼ï¼è¾åºå° <OutRoot>\traffic-audit-deploy-<yyyyMMdd> |
| | | 3) npm run build é建å端 distï¼å¯ -SkipFrontendï¼ |
| | | 4) ç»è£
宿´é¨ç½²å
ï¼backend(jar + application.yml + å¯å¨èæ¬ + æå¡å
è£
å¨ + docs + web) / frontend/dist / db / README |
| | | Windows æå¡å
è£
å¨ WinSWï¼ä¼å
ç¨ deploy-templates\tools\winsw.exe ç¼åï¼æ ¡éª SHA-256ï¼ï¼ |
| | | 缺失æ¶èªå¨ä¸è½½ v2.12.0 x64 å°è¯¥ç¼åç®å½ï¼éå
è¾åºä¸º backend\trafficAudit.exe |
| | | 5) 对ç»è£
åçæ´å
åæ«ä¸éææå¯é¥ |
| | | |
| | | ä¿®è®¢èæ¯ï¼2026-10-03ï¼ï¼2026-10-02 æå·¥äº§åºçå
缺 application.yml / docs / db / å¯å¨èæ¬ï¼ |
| | | 䏿²¡ææ dist åæ¥å° backend\webï¼é¨ç½²æºå¯å¨ç´æ¥æ¥ |
| | | "java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: NO)"ã |
| | | æ¬çæä¸è¿°æä»¶å
¨é¨è¡¥é½ï¼å¹¶æ jar å
ç½®å ä½ç¬¦é
ç½®éæ°æå»ºï¼é¿å
忬¡åºç°ã |
| | | #> |
| | | param( |
| | | [string]$OutRoot = 'E:\è¯éª', |
| | | [switch]$SkipFrontend |
| | | [string]$OutRoot = '', |
| | | [switch]$SkipFrontend, |
| | | [switch]$SkipBackend, # å¤ç¨å·²åå¨ç jarï¼ä¸éç¼è¯ãä¸å¨æºç é
ç½®ï¼ï¼ä»
ç¨äºè¡¥å
¨/éç»åå¸å
|
| | | [switch]$SkipServiceWrapper # 䏿 WinSW æå¡å
è£
卿è¿å
ï¼å³ä¸æä¾âæ³¨åæ Windows æå¡âç弿ºèªå¯ï¼ |
| | | ) |
| | | |
| | | $ErrorActionPreference = 'Stop' |
| | | $root = Split-Path -Parent $MyInvocation.MyCommand.Path |
| | | $tplDir = Join-Path $root 'deploy-templates' # æå
模æ¿ä»¶ï¼å·²å
¥åºï¼2026-09-27 èµ·ï¼ |
| | | $jarRel = 'traffic-audit-server\target\traffic-audit-server-1.0.0-SNAPSHOT.jar' |
| | | $jar = Join-Path $root $jarRel |
| | | if (-not $OutRoot) { $OutRoot = Join-Path $root 'deploy' } # é»è®¤è¾åºå°ä»åº deploy\ï¼å·² gitignoreï¼ |
| | | $tplDir = Join-Path $root 'deploy-templates' # æå
模æ¿ä»¶ï¼å·²å
¥åºï¼ |
| | | $tplJarCfg = Join-Path $tplDir 'jar-application.yml' # è¿ jar çå ä½ç¬¦é
ç½® |
| | | $srcCfg = Join-Path $root 'traffic-audit-server\src\main\resources\application.yml' |
| | | $jarRel = 'traffic-audit-server\target\traffic-audit-server-1.0.0-SNAPSHOT.jar' |
| | | $jar = Join-Path $root $jarRel |
| | | $webDist = Join-Path $root 'traffic-audit-web\dist' |
| | | |
| | | Write-Host '[1/5] æå»ºå端ï¼mvn clean package -DskipTestsï¼...' |
| | | Push-Location $root |
| | | cmd /c "mvn.cmd -q clean package -DskipTests -f traffic-audit-server\pom.xml > pack-build.log 2>&1" |
| | | $rc = $LASTEXITCODE |
| | | Pop-Location |
| | | if ($rc -ne 0) { throw 'å端æå»ºå¤±è´¥ï¼è¯¦è§ pack-build.log' } |
| | | if (-not (Test-Path $tplJarCfg)) { throw "ç¼ºå°æå
模æ¿ï¼$tplJarCfg" } |
| | | |
| | | # ---------- [1/5] ç¨å ä½ç¬¦é
ç½®æå»º jarï¼æå»ºåè¿åå¼åæºçå®é
ç½® ---------- |
| | | if ($SkipBackend) { |
| | | Write-Host '[1/5] å·²æå® -SkipBackendï¼è·³è¿å端æå»ºï¼å¤ç¨ç°æ jar' |
| | | if (-not (Test-Path $jar)) { throw "æªæ¾å°ç°æ jarï¼$jarï¼å»æ -SkipBackend éæ°æå»ºï¼" } |
| | | } else { |
| | | Write-Host '[1/5] æå»ºå端ï¼mvn clean package -DskipTestsï¼æå»ºæä½¿ç¨å ä½ç¬¦é
ç½®ï¼...' |
| | | $backupCfg = $null |
| | | $hadCfg = Test-Path $srcCfg |
| | | try { |
| | | if ($hadCfg) { |
| | | $backupCfg = [System.IO.Path]::GetTempFileName() |
| | | Copy-Item $srcCfg $backupCfg -Force |
| | | } |
| | | Copy-Item $tplJarCfg $srcCfg -Force |
| | | Push-Location $root |
| | | try { |
| | | cmd /c "mvn.cmd -q clean package -DskipTests -f traffic-audit-server\pom.xml > pack-build.log 2>&1" |
| | | $rc = $LASTEXITCODE |
| | | } finally { Pop-Location } |
| | | if ($rc -ne 0) { throw 'å端æå»ºå¤±è´¥ï¼è¯¦è§ pack-build.log' } |
| | | } finally { |
| | | if ($backupCfg) { |
| | | Copy-Item $backupCfg $srcCfg -Force |
| | | Remove-Item $backupCfg -Force -ErrorAction SilentlyContinue |
| | | Write-Host ' å·²è¿åå¼åæº src\main\resources\application.yml' |
| | | } elseif (-not $hadCfg) { |
| | | Remove-Item $srcCfg -Force -ErrorAction SilentlyContinue |
| | | Write-Host ' 注æï¼æå»ºåæºç ç®å½æ¬æ¥æ²¡æ application.ymlï¼å·²æ¸
æä¸´æ¶å ä½ç¬¦æä»¶' |
| | | } |
| | | } |
| | | if (-not (Test-Path $jar)) { throw "æªæ¾å°æå»ºäº§ç©ï¼$jar" } |
| | | } |
| | | Write-Host (" 产ç©ï¼{0:N1} MB" -f ((Get-Item $jar).Length / 1MB)) |
| | | |
| | | # ---------- [2/5] å»å¯èªæ£ï¼jar å
ç½®é
ç½®å¿
é¡»å
¨æ¯å ä½ç¬¦ ---------- |
| | | Write-Host '[2/5] å»å¯èªæ£ï¼è§£å jar æ£æ¥ BOOT-INF/classes/application.yml ...' |
| | | Add-Type -AssemblyName System.IO.Compression.FileSystem |
| | | $zip = [System.IO.Compression.ZipFile]::OpenRead($jar) |
| | |
| | | $sr.Close() |
| | | } finally { $zip.Dispose() } |
| | | |
| | | $bad = @() |
| | | if ($text -match 'sk-[A-Za-z0-9]{16,}') { $bad += 'çä¼¼ææ API keyï¼sk-â¦ï¼' } |
| | | foreach ($line in ($text -split "`n")) { |
| | | if ($line -match '^\s*(password|api-key|secret)\s*:\s*(.+)$') { |
| | | $v = $matches[2].Trim().Trim('"') |
| | | if ($v -and -not $v.StartsWith('${')) { $bad += ("$($matches[1]) 䏿¯å ä½ç¬¦") } |
| | | function Get-SecretHits([string]$ymlText) { |
| | | $hits = @() |
| | | if ($ymlText -match 'sk-[A-Za-z0-9]{16,}') { $hits += 'çä¼¼ææ API key(sk-...)' } |
| | | foreach ($line in ($ymlText -split "`n")) { |
| | | if ($line -match '^\s*(password|api-key|secret)\s*:\s*(.+)$') { |
| | | $v = $matches[2].Trim().Trim('"') |
| | | if ($v -and -not $v.StartsWith('${')) { $hits += ("$($matches[1]) 䏿¯å ä½ç¬¦") } |
| | | } |
| | | } |
| | | return $hits |
| | | } |
| | | |
| | | $bad = Get-SecretHits $text |
| | | if ($bad.Count -gt 0) { |
| | | throw ('å»å¯èªæ£æªéè¿ï¼' + ($bad -join 'ï¼') + 'ãè¯·æ£æ¥ traffic-audit-server\src\main\resources\application.yml') |
| | | throw ('å»å¯èªæ£æªéè¿ï¼' + ($bad -join 'ï¼') + "ãè¯·æ£æ¥ $tplJarCfg") |
| | | } |
| | | if ($text -notmatch 'password:\s*\$\{TRAFFIC_DB_PASSWORD') { |
| | | Write-Host ' è¦åï¼jar å
ç½® password 䏿¯ TRAFFIC_DB_PASSWORD å ä½ç¬¦å½¢å¼ï¼è¯·ç¡®è®¤æ¨¡æ¿ã' |
| | | } |
| | | Write-Host ' éè¿ï¼jar å
ç½®é
ç½®å
¨é¨ä¸ºå ä½ç¬¦ï¼æ ææå¯é¥/å£ä»¤ã' |
| | | |
| | | # ---------- [3/5] å端æå»º ---------- |
| | | Write-Host '[3/5] æå»ºå端ï¼npm run buildï¼å¯ -SkipFrontend è·³è¿ï¼...' |
| | | if (-not $SkipFrontend) { |
| | | Push-Location (Join-Path $root 'traffic-audit-web') |
| | | cmd /c "npm.cmd run build > ..\pack-front.log 2>&1" |
| | | $rc2 = $LASTEXITCODE |
| | | Pop-Location |
| | | try { |
| | | cmd /c "npm.cmd run build > ..\pack-front.log 2>&1" |
| | | $rc2 = $LASTEXITCODE |
| | | } finally { Pop-Location } |
| | | if ($rc2 -ne 0) { throw 'å端æå»ºå¤±è´¥ï¼è¯¦è§ pack-front.log' } |
| | | Write-Host ' å端 dist å·²é建' |
| | | } else { |
| | | Write-Host ' å·²æ -SkipFrontend è·³è¿ï¼ä½¿ç¨ç°æ dist' |
| | | } |
| | | if (-not (Test-Path $webDist)) { throw "æªæ¾å°å端产ç©ï¼$webDistï¼å»æ -SkipFrontend éæ°æå»ºï¼" } |
| | | |
| | | # ---------- WinSWï¼Windows æå¡å
è£
å¨ï¼åå¤ï¼æ¬å°ç¼åä¼å
ï¼ç¼ºå¤±æ¶ä»å®æ¹åå¸é¡µä¸è½½å¹¶æ ¡éª SHA-256 ---------- |
| | | # WinSW v2.12.0 宿¹äº§ç©æªå Authenticode ç¾åï¼æä»¥ä»¥åºå® SHA-256 ä½ä¸ºå®æ´æ§æ ¡éªã |
| | | $winswUrl = 'https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW-x64.exe' |
| | | $winswSha256 = '05B82D46AD331CC16BDC00DE5C6332C1EF818DF8CEEFCD49C726553209B3A0DA' |
| | | $winswSrc = Join-Path $tplDir 'tools\winsw.exe' |
| | | |
| | | function Test-WinswCache { |
| | | param([string]$Path) |
| | | if (-not (Test-Path -LiteralPath $Path)) { return $false } |
| | | $hash = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash |
| | | if ($hash -ne $winswSha256) { |
| | | Write-Host (' è¦åï¼WinSW ç¼å SHA-256 ä¸å¹é
ï¼å°éæ°ä¸è½½ï¼ææ {0}...ï¼å®é
{1}...ï¼' -f $winswSha256.Substring(0, 12), $hash.Substring(0, 12)) |
| | | return $false |
| | | } |
| | | return $true |
| | | } |
| | | |
| | | if ($SkipServiceWrapper) { |
| | | Write-Host ' å·²æ -SkipServiceWrapper è·³è¿ï¼å
å
ä¸å« Windows æå¡æ¹å¼å¼æºèªå¯' |
| | | } else { |
| | | if (-not (Test-WinswCache $winswSrc)) { |
| | | New-Item -ItemType Directory -Force -Path (Split-Path -Parent $winswSrc) | Out-Null |
| | | Write-Host ' ä¸è½½ WinSW v2.12.0 x64 ...' |
| | | try { |
| | | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 |
| | | Invoke-WebRequest -Uri $winswUrl -OutFile $winswSrc -UseBasicParsing |
| | | } catch { |
| | | throw ("ä¸è½½ WinSW 失败ï¼{0}ã`n请æå¨ä¸è½½ {1} å为 {2} åéè¯ï¼æç¨ -SkipServiceWrapper è·³è¿ã" -f $_.Exception.Message, $winswUrl, $winswSrc) |
| | | } |
| | | if (-not (Test-WinswCache $winswSrc)) { throw "WinSW æ ¡éªå¤±è´¥ï¼å·²ä¸æ¢æå
ï¼$winswSrc" } |
| | | } |
| | | Write-Host (' WinSW 就绪ï¼SHA-256 å·²æ ¡éªï¼ï¼{0:N1} MB' -f ((Get-Item -LiteralPath $winswSrc).Length / 1MB)) |
| | | } |
| | | |
| | | # ---------- [4/5] ç»è£
宿´é¨ç½²ç®å½ ---------- |
| | | Write-Host '[4/5] ç»è£
é¨ç½²ç®å½...' |
| | | $stamp = Get-Date -Format 'yyyyMMdd' |
| | | $dest = Join-Path $OutRoot ("traffic-audit-deploy-" + $stamp) |
| | |
| | | Copy-Item (Join-Path $tplDir 'backend\application.yml') (Join-Path $dest 'backend') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\start-backend.cmd') (Join-Path $dest 'backend') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\start-backend.sh') (Join-Path $dest 'backend') -Force |
| | | Copy-Item (Join-Path $root 'traffic-audit-web\dist') (Join-Path $dest 'frontend') -Recurse -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\start-backend-service.cmd') (Join-Path $dest 'backend') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\install-autostart.ps1') (Join-Path $dest 'backend') -Force |
| | | if (-not $SkipServiceWrapper) { |
| | | # WinSW è¦æ±é
ç½®æä»¶åä¸ exe ååï¼trafficAudit.exe 读åç®å½ç trafficAudit.xml |
| | | Copy-Item -LiteralPath $winswSrc (Join-Path $dest 'backend\trafficAudit.exe') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\trafficAudit.xml') (Join-Path $dest 'backend') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\install-service.ps1') (Join-Path $dest 'backend') -Force |
| | | Copy-Item (Join-Path $tplDir 'backend\install-service.cmd') (Join-Path $dest 'backend') -Force |
| | | } |
| | | Copy-Item $webDist (Join-Path $dest 'frontend') -Recurse -Force |
| | | Copy-Item (Join-Path $tplDir 'frontend\nginx.conf.example') (Join-Path $dest 'frontend') -Force |
| | | Copy-Item (Join-Path $tplDir 'db\*.sql') (Join-Path $dest 'db') -Force |
| | | |
| | | # åç«¯å£ 8090 模å¼ï¼åç«¯ä» jar å级 web\ 读åç«¯éææä»¶ï¼å¿
é¡»å dist å
容ä¸è´ |
| | | Copy-Item $webDist (Join-Path $dest 'backend\web') -Recurse -Force |
| | | |
| | | # docsï¼è¿è¡æ¶æ¨¡æ¿ + ç¨æ·ææ¡£ï¼æé¤å¼åè¿ç¨ææ¡£ä¸ç¼å/å¤ä»½ï¼åå°ä½ç§¯ãé¿å
æå
鍿¸
å带åºå»ï¼ |
| | | $docsDest = Join-Path $dest 'backend\docs' |
| | |
| | | cmd /c "robocopy `"$root\docs`" `"$docsDest`" /E /NFL /NDL /NJH /NJS /NP /XD _cache 工使¥ç» åè½æµè¯æ¥å é®é¢æ±æ» /XF çæ_*.xlsx _bak_* >nul" |
| | | if ($LASTEXITCODE -ge 8) { throw "docs å¤å¶å¤±è´¥ï¼robocopy è¿å $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') $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_observation_station.sql') $docsDest -Force -ErrorAction SilentlyContinue |
| | | |
| | | # READMEï¼ç¨æ§å
模æ¿å¹¶æ¿æ¢æ¥æå ä½ |
| | | # READMEï¼ç¨æ¨¡æ¿å¹¶æ¿æ¢æ¥æå ä½ |
| | | $readme = Get-Content (Join-Path $tplDir 'README-é¨ç½²è¯´æ.txt') -Raw -Encoding UTF8 |
| | | $readme = $readme.Replace('{stamp}', $stamp) |
| | | Set-Content -LiteralPath (Join-Path $dest 'README-é¨ç½²è¯´æ.txt') -Value $readme -Encoding UTF8 |
| | | |
| | | Write-Host '[5/5] æ±æ»...' |
| | | # ---------- [5/5] æ´å
ææå¯é¥æ«æ + æ±æ» ---------- |
| | | Write-Host '[5/5] æ´å
æ«æ + æ±æ»...' |
| | | $leak = @() |
| | | foreach ($f in (Get-ChildItem (Join-Path $dest 'backend') -File | Where-Object { $_.Extension -in '.yml', '.yaml', '.cmd', '.sh', '.txt' })) { |
| | | $t = Get-Content $f.FullName -Raw -Encoding UTF8 -ErrorAction SilentlyContinue |
| | | if ($t -and $t -match 'sk-[A-Za-z0-9]{16,}') { $leak += $f.Name + '(çä¼¼ææ API key)' } |
| | | } |
| | | if ($leak.Count -gt 0) { throw ('æ´å
æ«ææªéè¿ï¼' + ($leak -join 'ï¼')) } |
| | | |
| | | $files = Get-ChildItem $dest -Recurse -File |
| | | $mb = (($files | Measure-Object Length -Sum).Sum / 1MB) |
| | | Write-Host (" è¾åºç®å½ï¼{0}" -f $dest) |
| | | Write-Host (" æä»¶æ°ï¼{0}ãå计ï¼{1:N2} MB" -f $files.Count, (($files | Measure-Object Length -Sum).Sum / 1MB)) |
| | | Write-Host ' æéï¼æå
æ·å°é¨ç½²æºåï¼åªéæ¹ backend\application.yml éçæ°æ®åºä¸å¤ + éè¦æ¶å¡« deepseek.api-keyã' |
| | | Write-Host (" æä»¶æ°ï¼{0} å计ï¼{1:N2} MB" -f $files.Count, $mb) |
| | | |
| | | $zipPath = $dest + '.zip' |
| | | if (Get-Command Compress-Archive -ErrorAction SilentlyContinue) { |
| | | if (Test-Path $zipPath) { Remove-Item $zipPath -Force } |
| | | Compress-Archive -Path (Join-Path $dest '*') -DestinationPath $zipPath -CompressionLevel Optimal |
| | | Write-Host (" å缩å
ï¼{0}ï¼{1:N2} MBï¼" -f $zipPath, ((Get-Item $zipPath).Length / 1MB)) |
| | | } |
| | | Write-Host ' æéï¼æ·å°é¨ç½²æºåï¼å¡å¿
å
æ¹ backend\application.yml ç spring.datasourceï¼url/username/passwordï¼åå¯å¨ã' |
| | |
| | | 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)); |
| | | options.put("years", Arrays.asList(2019, 2024, 2025, 2026, 2027, 2028, 2029, 2030)); |
| | | return options; |
| | | } |
| | | } |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.trafficaudit.holiday.HolidayNumberUtils; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.service.HolidayStatService.DailyMetrics; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | |
| | | import org.w3c.dom.Node; |
| | | import org.w3c.dom.NodeList; |
| | | |
| | | import javax.annotation.Resource; |
| | | import javax.xml.parsers.DocumentBuilderFactory; |
| | | import javax.xml.transform.OutputKeys; |
| | | import javax.xml.transform.Transformer; |
| | |
| | | import java.io.FileInputStream; |
| | | import java.io.InputStream; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | 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.function.Function; |
| | | import java.util.zip.ZipEntry; |
| | | import java.util.zip.ZipInputStream; |
| | | import java.util.zip.ZipOutputStream; |
| | |
| | | private String bulletinTemplatePath; |
| | | |
| | | private final HolidayStatService statService; |
| | | @Resource |
| | | private HolidayConfigService configService; |
| | | private static final ThreadLocal<Document> CURRENT_DOCUMENT = new ThreadLocal<>(); |
| | | |
| | | public HolidayBulletinService(HolidayStatService statService) { |
| | |
| | | } |
| | | DailyMetrics previousDay = current.getDayIndex() > 1 |
| | | ? statService.dailyMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex() - 1) : null; |
| | | HolidayCalendar previousDayConfig = configService.findPreviousDay(current.getYear(), current.getHolidayType()); |
| | | LocalDate previousDayDate = previousDayConfig == null || previousDayConfig.getHolidayDate() == null |
| | | ? current.getDate().minusDays(1) : previousDayConfig.getHolidayDate(); |
| | | DailyMetrics previousCalendarDay = previousDay != null |
| | | ? previousDay : statService.dailyMetricsByDate(previousDayDate); |
| | | DailyMetrics previousYear = statService.dailyMetrics(current.getYear() - 1, current.getHolidayType(), current.getDayIndex()); |
| | | DailyMetrics twoYearsAgo = statService.dailyMetrics(current.getYear() - 2, 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()); |
| | | DailyMetrics twoYearsAgoCumulative = statService.cumulativeMetrics(current.getYear() - 2, 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ã", |
| | | String paragraph1 = String.format("%dæ%dæ¥ï¼åæç¬¬%d天ï¼ï¼å
¨çå
¨ç¤¾ä¼è·¨åºåäººåæµå¨é%sä¸äººæ¬¡%sï¼æ¯2025å¹´åæ%sï¼æ¯2024å¹´åæ%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())); |
| | | ratioPhrase(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow()), |
| | | ratioPhrase(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow())); |
| | | String paragraph2 = cumulativeParagraph(currentCumulative, previousYearCumulative, twoYearsAgoCumulative); |
| | | |
| | | File template = resolveTemplateFile(); |
| | | if (!template.exists() || !template.isFile()) { |
| | |
| | | try { |
| | | replaceParagraph(document, text -> text.startsWith("10æ") && text.contains("åæç¬¬"), paragraph1); |
| | | replaceParagraph(document, text -> text.startsWith("åæä»¥æ¥"), paragraph2); |
| | | fillTable(document, current, previousDay, previousYear, currentCumulative, previousYearCumulative); |
| | | fillTable(document, current, previousDay, previousCalendarDay, previousYear, twoYearsAgo, |
| | | currentCumulative, previousYearCumulative, twoYearsAgoCumulative); |
| | | byte[] replacedXml = serializeDocument(document); |
| | | return replaceZipEntry(templateBytes, "word/document.xml", replacedXml); |
| | | } finally { |
| | |
| | | } |
| | | |
| | | private void fillTable(Document document, DailyMetrics current, DailyMetrics previousDay, |
| | | DailyMetrics previousYear, DailyMetrics currentCumulative, |
| | | DailyMetrics previousYearCumulative) { |
| | | DailyMetrics previousCalendarDay, DailyMetrics previousYear, DailyMetrics twoYearsAgo, |
| | | DailyMetrics currentCumulative, DailyMetrics previousYearCumulative, |
| | | DailyMetrics twoYearsAgoCumulative) { |
| | | DailyMetrics firstDayRailBase = previousDay == null ? previousCalendarDay : previousDay; |
| | | DailyMetrics firstDayCivilAviationBase = previousDay == null ? previousCalendarDay : previousDay; |
| | | Element table = firstElement(document, "tbl"); |
| | | if (table == null) { |
| | | throw new IllegalArgumentException("å¿«æ¥æ¨¡æ¿ä¸æ¾ä¸å°æ°æ®è¡¨"); |
| | | } |
| | | DailyMetrics previousYearCivilCumulative = comparableCumulative( |
| | | current, previousYear, DailyMetrics::getCivilAviation); |
| | | DailyMetrics twoYearsAgoCivilCumulative = comparableCumulative( |
| | | current, twoYearsAgo, DailyMetrics::getCivilAviation); |
| | | writeTableRow(table, 2, new String[]{ |
| | | number(current.getNonBusinessPersonFlow()), |
| | | ratio(current.getNonBusinessPersonFlow(), previousDay == null ? null : previousDay.getNonBusinessPersonFlow()), |
| | | ratio(current.getNonBusinessPersonFlow(), previousYear == null ? null : previousYear.getNonBusinessPersonFlow()), |
| | | "â", |
| | | ratio(current.getNonBusinessPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getNonBusinessPersonFlow()), |
| | | number(currentCumulative.getNonBusinessPersonFlow()), |
| | | ratio(currentCumulative.getNonBusinessPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getNonBusinessPersonFlow()), |
| | | "â" |
| | | ratio(currentCumulative.getNonBusinessPersonFlow(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.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()), |
| | | "â", |
| | | ratio(current.getRoad(), twoYearsAgo == null ? null : twoYearsAgo.getRoad()), |
| | | number(currentCumulative.getRoad()), |
| | | ratio(currentCumulative.getRoad(), previousYearCumulative == null ? null : previousYearCumulative.getRoad()), |
| | | "â" |
| | | ratio(currentCumulative.getRoad(), twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.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()), |
| | | "â", |
| | | ratio(current.getRoadPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getRoadPersonFlow()), |
| | | number(currentCumulative.getRoadPersonFlow()), |
| | | ratio(currentCumulative.getRoadPersonFlow(), previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow()), |
| | | "â" |
| | | ratio(currentCumulative.getRoadPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow()), |
| | | ratio(currentCumulative.getRoadPersonFlow(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.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())); |
| | | writeTableRow(table, 5, transportRow(current.getRail(), |
| | | firstDayRailBase == null ? null : firstDayRailBase.getRail(), |
| | | previousYear == null ? null : previousYear.getRail(), |
| | | twoYearsAgo == null ? null : twoYearsAgo.getRail(), |
| | | currentCumulative.getRail(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getRail(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRail())); |
| | | writeTableRow(table, 6, transportRow(current.getCivilAviation(), |
| | | firstDayCivilAviationBase == null ? null : firstDayCivilAviationBase.getCivilAviation(), |
| | | previousYear == null ? null : previousYear.getCivilAviation(), |
| | | twoYearsAgo == null ? null : twoYearsAgo.getCivilAviation(), |
| | | currentCumulative.getCivilAviation(), |
| | | previousYearCivilCumulative == null ? null : previousYearCivilCumulative.getCivilAviation(), |
| | | twoYearsAgoCivilCumulative == null ? null : twoYearsAgoCivilCumulative.getCivilAviation())); |
| | | writeTableRow(table, 7, transportRow(current.getWater(), |
| | | previousDay == null ? null : previousDay.getWater(), |
| | | previousYear == null ? null : previousYear.getWater(), |
| | | twoYearsAgo == null ? null : twoYearsAgo.getWater(), |
| | | currentCumulative.getWater(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getWater(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getWater())); |
| | | writeTableRow(table, 8, transportRow(current.getTotalPersonFlow(), |
| | | previousDay == null ? null : previousDay.getTotalPersonFlow(), |
| | | previousYear == null ? null : previousYear.getTotalPersonFlow(), |
| | | twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(), |
| | | currentCumulative.getTotalPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow())); |
| | | } |
| | | |
| | | private String[] transportRow(BigDecimal current, BigDecimal previousDay, BigDecimal previousYear, |
| | | BigDecimal currentCumulative, BigDecimal previousYearCumulative) { |
| | | BigDecimal twoYearsAgo, BigDecimal currentCumulative, |
| | | BigDecimal previousYearCumulative, BigDecimal twoYearsAgoCumulative) { |
| | | return new String[]{ |
| | | number(current), |
| | | ratio(current, previousDay), |
| | | ratio(current, previousYear), |
| | | "â", |
| | | ratio(current, twoYearsAgo), |
| | | number(currentCumulative), |
| | | ratio(currentCumulative, previousYearCumulative), |
| | | "â" |
| | | ratio(currentCumulative, twoYearsAgoCumulative) |
| | | }; |
| | | } |
| | | |
| | |
| | | int logicalColumn = 0; |
| | | for (Element cell : cells) { |
| | | int span = gridSpan(cell); |
| | | if (logicalColumn >= 2 && logicalColumn < 9) { |
| | | if (logicalColumn >= 2) { |
| | | int valueIndex = logicalColumn - 2; |
| | | if (valueIndex < values.length) { |
| | | setCellText(cell, values[valueIndex]); |
| | |
| | | Element pPr = firstChildElement(templateParagraph, "pPr"); |
| | | if (pPr != null) paragraph.appendChild(pPr.cloneNode(true)); |
| | | } |
| | | Element templateRun = templateParagraph == null ? null : firstChildElement(templateParagraph, "r"); |
| | | Element rPr = templateRun == null ? null : firstChildElement(templateRun, "rPr"); |
| | | 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 == null ? "" : text); |
| | |
| | | return (percent.signum() > 0 ? "å¢é¿" : "ä¸é") + HolidayNumberUtils.format1(percent.abs()) + "%"; |
| | | } |
| | | |
| | | private DailyMetrics comparableCumulative(DailyMetrics current, DailyMetrics comparisonYear, |
| | | Function<DailyMetrics, BigDecimal> selector) { |
| | | if (current == null || comparisonYear == null) return null; |
| | | int coveredDay = 0; |
| | | for (int day = current.getDayIndex(); day >= 1; day--) { |
| | | DailyMetrics metrics = statService.dailyMetrics(current.getYear(), current.getHolidayType(), day); |
| | | if (metrics != null && selector.apply(metrics) != null) { |
| | | coveredDay = day; |
| | | break; |
| | | } |
| | | } |
| | | if (coveredDay == 0) return null; |
| | | return statService.cumulativeMetrics(comparisonYear.getYear(), current.getHolidayType(), coveredDay); |
| | | } |
| | | |
| | | private String cumulativeParagraph(DailyMetrics currentCumulative, |
| | | DailyMetrics previousYearCumulative, |
| | | DailyMetrics twoYearsAgoCumulative) { |
| | | BigDecimal total = currentCumulative == null ? null : currentCumulative.getTotalPersonFlow(); |
| | | return String.format("åæä»¥æ¥ï¼å
¨çå
¨ç¤¾ä¼è·¨åºåäººåæµå¨é累计%sä¸äººæ¬¡ï¼æ¯2025å¹´åæ%sï¼æ¯2024å¹´åæ%sã", |
| | | formatNumber(total), |
| | | ratioPhrase(total, previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow()), |
| | | ratioPhrase(total, twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow())); |
| | | } |
| | | |
| | | private String ratio(BigDecimal current, BigDecimal base) { |
| | | if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return "â"; |
| | | return ratioPhrase(current, base); |
| | | BigDecimal ratio = HolidayNumberUtils.divide(current, base); |
| | | if (ratio == null) return "â"; |
| | | BigDecimal percent = HolidayNumberUtils.multiply(ratio.subtract(BigDecimal.ONE), BigDecimal.valueOf(100)); |
| | | if (percent == null) return "â"; |
| | | BigDecimal rounded = percent.setScale(1, RoundingMode.HALF_UP); |
| | | return rounded.signum() == 0 ? "0%" : rounded.toPlainString() + "%"; |
| | | } |
| | | |
| | | private String number(BigDecimal value) { |
| | |
| | | return holidayCalendarMapper.selectList(new LambdaQueryWrapper<HolidayCalendar>() |
| | | .eq(HolidayCalendar::getYear, year) |
| | | .eq(HolidayCalendar::getHolidayType, holidayType) |
| | | .ge(HolidayCalendar::getDayIndex, 1) |
| | | .orderByAsc(HolidayCalendar::getDayIndex)); |
| | | } |
| | | |
| | | public HolidayCalendar findPreviousDay(int year, String holidayType) { |
| | | return holidayCalendarMapper.selectOne(new LambdaQueryWrapper<HolidayCalendar>() |
| | | .eq(HolidayCalendar::getYear, year) |
| | | .eq(HolidayCalendar::getHolidayType, holidayType) |
| | | .eq(HolidayCalendar::getDayIndex, 0) |
| | | .last("limit 1")); |
| | | } |
| | | |
| | | public HolidayCalendar findHolidayByDate(LocalDate date) { |
| | |
| | | if (holidayDate == null || holidayDate.getYear() != year) { |
| | | throw new IllegalArgumentException("æ¥æå¿
é¡»å±äºæé年份"); |
| | | } |
| | | if (dayIndex == null || dayIndex < 1) { |
| | | throw new IllegalArgumentException("ç¬¬å æ¥å¿
é¡»ä» 1 å¼å§"); |
| | | if (dayIndex == null || dayIndex < 0) { |
| | | throw new IllegalArgumentException("æ¥æåºå·ä¸è½å°äº 0"); |
| | | } |
| | | LambdaQueryWrapper<HolidayCalendar> duplicate = new LambdaQueryWrapper<HolidayCalendar>() |
| | | .eq(HolidayCalendar::getYear, year) |
| | |
| | | } |
| | | } |
| | | if (target == null) return null; |
| | | return dailyMetrics(year, holidayType, dayIndex, target.getHolidayDate()); |
| | | } |
| | | |
| | | public DailyMetrics dailyMetricsByDate(LocalDate date) { |
| | | if (date == null) return null; |
| | | return dailyMetrics(date.getYear(), null, 0, date); |
| | | } |
| | | |
| | | private DailyMetrics dailyMetrics(int year, String holidayType, int dayIndex, LocalDate date) { |
| | | 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()); |
| | | 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(target.getHolidayDate()); |
| | | BigDecimal comparisonBase = comparisonBaseByDate.get(date); |
| | | BigDecimal nonBusiness = null; |
| | | if (coefficient != null && (expressway != null || nationalProvincial != null)) { |
| | | nonBusiness = HolidayNumberUtils.multiply( |
| | |
| | | metrics.setYear(year); |
| | | metrics.setHolidayType(holidayType); |
| | | metrics.setDayIndex(dayIndex); |
| | | metrics.setDate(target.getHolidayDate()); |
| | | metrics.setDate(date); |
| | | metrics.setRail(passenger.get(HolidayConstants.MODE_RAIL)); |
| | | metrics.setCivilAviation(passenger.get(HolidayConstants.MODE_CIVIL_AVIATION)); |
| | | metrics.setRoad(passenger.get(HolidayConstants.MODE_ROAD)); |
| | |
| | | |
| | | private static final Map<String, String> PASSENGER_SHEET_MODE = new LinkedHashMap<>(); |
| | | private static final int FIRST_DATA_ROW_INDEX = 2; |
| | | private static final int DAY_ROW_SCAN_LIMIT = 30; |
| | | private static final int[] REFERENCE_YEARS = {2024, 2025, 2026}; |
| | | |
| | | static { |
| | |
| | | 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); |
| | | importPassengerSheets(workbook, evaluator, year, holidayType, dateByDay, summary); |
| | | importVehicleSheets(workbook, evaluator, year, dateByDay, summary); |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("å·¥ä½ç°¿è§£æå¤±è´¥: " + e.getMessage(), e); |
| | |
| | | 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); |
| | | Row row = findDayRow(sheet, evaluator, dayIndex); |
| | | if (row == null) continue; |
| | | BigDecimal value = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (value != null) return true; |
| | |
| | | if (expressway != null) { |
| | | Integer yearColumn = findYearColumn(expressway, evaluator, year, 2, 7); |
| | | if (yearColumn != null) { |
| | | Row row = expressway.getRow(FIRST_DATA_ROW_INDEX + dayIndex - 1); |
| | | Row row = findDayRow(expressway, evaluator, dayIndex); |
| | | if (row != null && numberValue(row.getCell(yearColumn), evaluator) != null) return true; |
| | | } |
| | | } |
| | |
| | | if (nationalProvincial != null) { |
| | | Integer yearColumn = findYearColumn(nationalProvincial, evaluator, year, 3, 8); |
| | | if (yearColumn != null) { |
| | | Row row = nationalProvincial.getRow(FIRST_DATA_ROW_INDEX + dayIndex - 1); |
| | | Row row = findDayRow(nationalProvincial, evaluator, dayIndex); |
| | | if (row != null && numberValue(row.getCell(yearColumn), evaluator) != null) return true; |
| | | } |
| | | } |
| | |
| | | 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); |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | BigDecimal incoming = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (incoming == null) continue; |
| | |
| | | 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); |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | BigDecimal incoming = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (incoming == null) continue; |
| | |
| | | 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); |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | BigDecimal incoming; |
| | | try { |
| | |
| | | } |
| | | } |
| | | |
| | | Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>(); |
| | | 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) { |
| | |
| | | try (InputStream in = new FileInputStream(reference); Workbook workbook = WorkbookFactory.create(in)) { |
| | | FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); |
| | | seedNationalDayCalendar(); |
| | | ensurePreviousDayCalendarIfMissing(); |
| | | for (int year : REFERENCE_YEARS) { |
| | | if (passengerFlowMapper.selectCount(new LambdaQueryWrapper<HolidayPassengerFlow>() |
| | | .eq(HolidayPassengerFlow::getYear, year)) == 0) { |
| | |
| | | seedCoefficientYear(workbook, evaluator, year); |
| | | } |
| | | } |
| | | seedPreviousDayPassengerIfMissing(workbook, evaluator); |
| | | seedRoad2019IfMissing(workbook, evaluator); |
| | | } catch (Exception e) { |
| | | log.error("Failed to seed holiday reference data", e); |
| | | } |
| | |
| | | } |
| | | |
| | | private void importPassengerSheets(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, ImportSummary summary) { |
| | | String holidayType, 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) { |
| | |
| | | continue; |
| | | } |
| | | for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) { |
| | | Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1); |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) { |
| | | summary.addError(entry.getKey() + " 第" + day.getKey() + "æ¥ï¼å·¥ä½è¡¨ç¼ºå°è¯¥è¡"); |
| | | continue; |
| | |
| | | upsertPassenger(year, day.getValue(), entry.getValue(), value); |
| | | summary.incrementPassenger(); |
| | | } |
| | | } |
| | | importPreviousDayPassengerRows(workbook, evaluator, year, holidayType, dateByDay, summary); |
| | | } |
| | | |
| | | private void importPreviousDayPassengerRows(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | String holidayType, Map<Integer, LocalDate> dateByDay, |
| | | ImportSummary summary) { |
| | | LocalDate previousDate = previousDayDate(year, holidayType, dateByDay); |
| | | if (previousDate == null) return; |
| | | for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) { |
| | | if (!HolidayConstants.MODE_RAIL.equals(entry.getValue()) |
| | | && !HolidayConstants.MODE_CIVIL_AVIATION.equals(entry.getValue())) { |
| | | continue; |
| | | } |
| | | Sheet sheet = workbook.getSheet(entry.getKey()); |
| | | if (sheet == null) continue; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7); |
| | | if (yearColumn == null) continue; |
| | | Row row = findPreviousDayRow(sheet, evaluator); |
| | | if (row == null) continue; |
| | | BigDecimal value; |
| | | try { |
| | | value = numberValue(row.getCell(yearColumn), evaluator); |
| | | } catch (Exception e) { |
| | | summary.addError(entry.getKey() + " å䏿¥è¡ï¼" + e.getMessage()); |
| | | continue; |
| | | } |
| | | if (value == null) { |
| | | summary.incrementSkipped(); |
| | | continue; |
| | | } |
| | | upsertPassenger(year, previousDate, entry.getValue(), value); |
| | | summary.incrementPassenger(); |
| | | } |
| | | } |
| | | |
| | |
| | | return; |
| | | } |
| | | for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) { |
| | | Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1); |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) { |
| | | summary.addError(sheetName + " 第" + day.getKey() + "æ¥ï¼å·¥ä½è¡¨ç¼ºå°è¯¥è¡"); |
| | | continue; |
| | |
| | | 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())); |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | setNumeric(sheet, row.getRowNum(), yearColumn, values.get(day.getValue())); |
| | | } |
| | | LocalDate previousDate = previousDayDate(year, holidayType, dayDates); |
| | | Row previousRow = findPreviousDayRow(sheet, evaluator); |
| | | if (previousDate != null && previousRow != null) { |
| | | BigDecimal previousValue = values.get(previousDate); |
| | | if (previousValue != null) { |
| | | setNumeric(sheet, previousRow.getRowNum(), yearColumn, previousValue); |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | 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())); |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | setNumeric(sheet, row.getRowNum(), 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())); |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | setNumeric(sheet, row.getRowNum(), 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; |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | int rowIndex = row.getRowNum(); |
| | | int excelRow = rowIndex + 1; |
| | | setFormula(sheet, rowIndex, 4, "=(D" + excelRow + "-C" + excelRow + ")/C" + excelRow); |
| | | } |
| | |
| | | if (!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]; |
| | | int[] calendarYears = {2019, 2024, 2025, 2026}; |
| | | int[] lengths = {8, 7, 8, 8}; |
| | | for (int i = 0; i < calendarYears.length; i++) { |
| | | int year = calendarYears[i]; |
| | | List<HolidayCalendar> existing = configService.findHolidayDays(year, HolidayConstants.TYPE_NATIONAL_DAY); |
| | | Map<Integer, HolidayCalendar> byDay = new LinkedHashMap<>(); |
| | | for (HolidayCalendar row : existing) { |
| | |
| | | calendar.setUpdatedAt(LocalDateTime.now()); |
| | | configService.saveHoliday(toMap(calendar)); |
| | | } |
| | | if (year == 2019) { |
| | | continue; |
| | | } |
| | | HolidayCalendar previousDay = new HolidayCalendar(); |
| | | previousDay.setYear(year); |
| | | previousDay.setHolidayType(HolidayConstants.TYPE_NATIONAL_DAY); |
| | | previousDay.setHolidayDate(LocalDate.of(year, 10, 1).minusDays(1)); |
| | | previousDay.setDayIndex(0); |
| | | previousDay.setCreatedAt(LocalDateTime.now()); |
| | | previousDay.setUpdatedAt(LocalDateTime.now()); |
| | | configService.saveHoliday(toMap(previousDay)); |
| | | } |
| | | } |
| | | |
| | | private void ensurePreviousDayCalendarIfMissing() { |
| | | for (int year : REFERENCE_YEARS) { |
| | | if (configService.findPreviousDay(year, HolidayConstants.TYPE_NATIONAL_DAY) != null) { |
| | | continue; |
| | | } |
| | | LocalDate firstHolidayDate = firstDate(dayDates(year, HolidayConstants.TYPE_NATIONAL_DAY)); |
| | | if (firstHolidayDate == null) { |
| | | continue; |
| | | } |
| | | HolidayCalendar previousDay = new HolidayCalendar(); |
| | | previousDay.setYear(year); |
| | | previousDay.setHolidayType(HolidayConstants.TYPE_NATIONAL_DAY); |
| | | previousDay.setHolidayDate(firstHolidayDate.minusDays(1)); |
| | | previousDay.setDayIndex(0); |
| | | previousDay.setCreatedAt(LocalDateTime.now()); |
| | | previousDay.setUpdatedAt(LocalDateTime.now()); |
| | | configService.saveHoliday(toMap(previousDay)); |
| | | } |
| | | } |
| | | |
| | | private void seedPreviousDayPassengerIfMissing(Workbook workbook, FormulaEvaluator evaluator) { |
| | | for (int year : REFERENCE_YEARS) { |
| | | Map<Integer, LocalDate> dayDates = dayDates(year, HolidayConstants.TYPE_NATIONAL_DAY); |
| | | LocalDate previousDate = previousDayDate(year, HolidayConstants.TYPE_NATIONAL_DAY, dayDates); |
| | | if (previousDate == null) continue; |
| | | for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) { |
| | | if (!HolidayConstants.MODE_RAIL.equals(entry.getValue()) |
| | | && !HolidayConstants.MODE_CIVIL_AVIATION.equals(entry.getValue())) { |
| | | continue; |
| | | } |
| | | if (passengerFlowMapper.selectCount(new LambdaQueryWrapper<HolidayPassengerFlow>() |
| | | .eq(HolidayPassengerFlow::getYear, year) |
| | | .eq(HolidayPassengerFlow::getFlowDate, previousDate) |
| | | .eq(HolidayPassengerFlow::getTransportMode, entry.getValue())) > 0) { |
| | | continue; |
| | | } |
| | | Sheet sheet = workbook.getSheet(entry.getKey()); |
| | | if (sheet == null) continue; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7); |
| | | if (yearColumn == null) continue; |
| | | Row row = findPreviousDayRow(sheet, evaluator); |
| | | if (row == null) continue; |
| | | BigDecimal value = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (value != null) upsertPassenger(year, previousDate, entry.getValue(), value); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void seedRoad2019IfMissing(Workbook workbook, FormulaEvaluator evaluator) { |
| | | if (passengerFlowMapper.selectCount(new LambdaQueryWrapper<HolidayPassengerFlow>() |
| | | .eq(HolidayPassengerFlow::getYear, 2019) |
| | | .eq(HolidayPassengerFlow::getTransportMode, HolidayConstants.MODE_ROAD)) > 0) { |
| | | return; |
| | | } |
| | | Sheet sheet = workbook.getSheet("éè·¯"); |
| | | if (sheet == null) return; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, 2019, 2, 20); |
| | | if (yearColumn == null) return; |
| | | for (int rowIndex = 0; rowIndex <= Math.min(sheet.getLastRowNum(), DAY_ROW_SCAN_LIMIT); rowIndex++) { |
| | | Row row = sheet.getRow(rowIndex); |
| | | if (row == null) continue; |
| | | Integer dayIndex = parseDayIndex(row.getCell(0), evaluator); |
| | | if (dayIndex == null) continue; |
| | | BigDecimal value = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (value != null) { |
| | | upsertPassenger(2019, LocalDate.of(2019, 10, dayIndex), HolidayConstants.MODE_ROAD, value); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | 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); |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | BigDecimal value = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (value != null) { |
| | | upsertPassenger(year, day.getValue(), entry.getValue(), value); |
| | | } |
| | | } |
| | | if (HolidayConstants.MODE_RAIL.equals(entry.getValue()) |
| | | || HolidayConstants.MODE_CIVIL_AVIATION.equals(entry.getValue())) { |
| | | Row previousRow = findPreviousDayRow(sheet, evaluator); |
| | | LocalDate previousDate = previousDayDate(year, HolidayConstants.TYPE_NATIONAL_DAY, dayDates); |
| | | if (previousRow != null && previousDate != null) { |
| | | BigDecimal value = numberValue(previousRow.getCell(yearColumn), evaluator); |
| | | if (value != null) { |
| | | upsertPassenger(year, previousDate, entry.getValue(), value); |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | 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); |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | BigDecimal value = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (value != null) { |
| | |
| | | } |
| | | } |
| | | |
| | | private LocalDate previousDayDate(int year, String holidayType, Map<Integer, LocalDate> dateByDay) { |
| | | HolidayCalendar configured = configService.findPreviousDay(year, holidayType); |
| | | if (configured != null && configured.getHolidayDate() != null) { |
| | | return configured.getHolidayDate(); |
| | | } |
| | | LocalDate firstDate = firstDate(dateByDay); |
| | | return firstDate == null ? null : firstDate.minusDays(1); |
| | | } |
| | | |
| | | private LocalDate firstDate(Map<Integer, LocalDate> dateByDay) { |
| | | LocalDate result = null; |
| | | for (LocalDate date : dateByDay.values()) { |
| | | if (date != null && (result == null || date.isBefore(result))) result = date; |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Row findDayRow(Sheet sheet, FormulaEvaluator evaluator, int dayIndex) { |
| | | int lastRow = Math.min(sheet.getLastRowNum(), DAY_ROW_SCAN_LIMIT); |
| | | for (int rowIndex = 0; rowIndex <= lastRow; rowIndex++) { |
| | | Row row = sheet.getRow(rowIndex); |
| | | if (row == null) continue; |
| | | Integer rowDay = parseDayIndex(row.getCell(0), evaluator); |
| | | if (rowDay != null && rowDay.intValue() == dayIndex) return row; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private Row findPreviousDayRow(Sheet sheet, FormulaEvaluator evaluator) { |
| | | int lastRow = Math.min(sheet.getLastRowNum(), DAY_ROW_SCAN_LIMIT); |
| | | DataFormatter formatter = new DataFormatter(); |
| | | for (int rowIndex = 0; rowIndex <= lastRow; rowIndex++) { |
| | | Row row = sheet.getRow(rowIndex); |
| | | if (row == null || row.getCell(0) == null) continue; |
| | | String text = formatter.formatCellValue(row.getCell(0), evaluator).trim(); |
| | | if (text.contains("å䏿¥")) return row; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private Map<Integer, LocalDate> dayDates(int year, String holidayType) { |
| | | Map<Integer, LocalDate> result = new LinkedHashMap<>(); |
| | | for (HolidayCalendar row : configService.findHolidayDays(year, holidayType)) { |
| New file |
| | |
| | | package com.trafficaudit.observation.controller; |
| | | |
| | | import com.trafficaudit.common.Result; |
| | | import com.trafficaudit.observation.dto.ObservationDateSummary; |
| | | import com.trafficaudit.observation.entity.ObservationStationFlow; |
| | | import com.trafficaudit.observation.service.ObservationStationImportService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | 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.time.LocalDate; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * Observation station raw data. Collected and stored only; not linked to holiday flow data. |
| | | */ |
| | | @Slf4j |
| | | @RestController |
| | | @RequestMapping("/api/observation") |
| | | public class ObservationStationController { |
| | | |
| | | @Resource |
| | | private ObservationStationImportService importService; |
| | | |
| | | @PostMapping("/import") |
| | | public Result<Map<String, Object>> importWorkbook(@RequestParam("file") MultipartFile file) { |
| | | try { |
| | | return Result.ok(importService.importWorkbook(file)); |
| | | } catch (Exception e) { |
| | | log.error("observation station import error", e); |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @GetMapping("/dates") |
| | | public Result<List<ObservationDateSummary>> dates() { |
| | | return Result.ok(importService.listDateSummaries()); |
| | | } |
| | | |
| | | @GetMapping("/rows") |
| | | public Result<List<ObservationStationFlow>> rows(@RequestParam(value = "date", required = false) String date, |
| | | @RequestParam(value = "limit", defaultValue = "2000") int limit) { |
| | | try { |
| | | LocalDate target = (date == null || date.trim().isEmpty()) ? null : LocalDate.parse(date); |
| | | return Result.ok(importService.listRows(target, limit)); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @PostMapping("/delete") |
| | | public Result<Integer> delete(@RequestParam("date") String date) { |
| | | try { |
| | | return Result.ok(importService.deleteDate(LocalDate.parse(date))); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.observation.dto; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | |
| | | /** Per-date rollup shown on the import page. */ |
| | | @Data |
| | | public class ObservationDateSummary { |
| | | private LocalDate obsDate; |
| | | private Integer stationCount; |
| | | private BigDecimal smallMediumPassengerTotal; |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | private LocalDateTime importedAt; |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.observation.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; |
| | | |
| | | /** |
| | | * Observation station daily survey data. |
| | | * Stored raw for later analysis; it is deliberately NOT linked to the holiday flow tables, |
| | | * even though ä¸å°å®¢æµé is one of the sources of the ordinary national/provincial road flow. |
| | | */ |
| | | @Data |
| | | @TableName("observation_station_flow") |
| | | public class ObservationStationFlow { |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | private LocalDate obsDate; |
| | | private String stationCode; |
| | | private String stationName; |
| | | private BigDecimal mileage; |
| | | private String cityName; |
| | | private String countyName; |
| | | private String direction; |
| | | private BigDecimal smallMediumPassenger; |
| | | private BigDecimal largePassenger; |
| | | private BigDecimal smallTruck; |
| | | private BigDecimal mediumTruck; |
| | | private BigDecimal largeTruck; |
| | | private BigDecimal extraLargeTruck; |
| | | private BigDecimal containerTruck; |
| | | private BigDecimal motorcycle; |
| | | private BigDecimal tractor; |
| | | private BigDecimal passengerTotal; |
| | | private BigDecimal truckTotal; |
| | | private BigDecimal carTotal; |
| | | private BigDecimal motorTotal; |
| | | private BigDecimal passengerEquivalent; |
| | | private BigDecimal truckEquivalent; |
| | | private BigDecimal carEquivalent; |
| | | private BigDecimal motorEquivalent; |
| | | private BigDecimal vehicleSpeed; |
| | | private BigDecimal congestionDegree; |
| | | private String sourceFile; |
| | | private LocalDateTime createdAt; |
| | | private LocalDateTime updatedAt; |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.observation.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.trafficaudit.observation.dto.ObservationDateSummary; |
| | | import com.trafficaudit.observation.entity.ObservationStationFlow; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.apache.ibatis.annotations.Select; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Mapper |
| | | public interface ObservationStationFlowMapper extends BaseMapper<ObservationStationFlow> { |
| | | |
| | | @Select("SELECT obs_date, COUNT(*) AS station_count, " |
| | | + "SUM(small_medium_passenger) AS small_medium_passenger_total, " |
| | | + "MAX(updated_at) AS imported_at " |
| | | + "FROM observation_station_flow GROUP BY obs_date ORDER BY obs_date ASC") |
| | | List<ObservationDateSummary> selectDateSummary(); |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.observation.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.trafficaudit.observation.dto.ObservationDateSummary; |
| | | import com.trafficaudit.observation.entity.ObservationStationFlow; |
| | | import com.trafficaudit.observation.mapper.ObservationStationFlowMapper; |
| | | 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.DateUtil; |
| | | 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.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.io.InputStream; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * Imports the "observation station daily survey" workbook (.xls/.xlsx). |
| | | * Header names are matched by text, so column order may change between releases. |
| | | * Re-importing the same date replaces that date's rows. |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class ObservationStationImportService { |
| | | |
| | | private static final String H_DATE = "è§æµæ¥æ"; |
| | | private static final String H_CODE = "è§æµç«ç¼å·"; |
| | | private static final String H_NAME = "è§æµç«åç§°"; |
| | | private static final String H_MILEAGE = "è§æµéç¨"; |
| | | private static final String H_CITY = "å°å¸åç§°"; |
| | | private static final String H_COUNTY = "åºå¿åç§°"; |
| | | private static final String H_DIRECTION = "è¡é©¶æ¹å"; |
| | | private static final String H_SMALL_MEDIUM_PASSENGER = "ä¸å°å®¢æµé"; |
| | | private static final String H_LARGE_PASSENGER = "大客车æµé"; |
| | | private static final String H_SMALL_TRUCK = "å°è´§è½¦æµé"; |
| | | private static final String H_MEDIUM_TRUCK = "ä¸è´§è½¦æµé"; |
| | | private static final String H_LARGE_TRUCK = "大货车æµé"; |
| | | private static final String H_EXTRA_LARGE_TRUCK = "ç¹å¤§è´§æµé"; |
| | | private static final String H_CONTAINER = "éè£
ç®±æµé"; |
| | | private static final String H_MOTORCYCLE = "æ©æè½¦æµé"; |
| | | private static final String H_TRACTOR = "æææºæµé"; |
| | | private static final String H_PASSENGER_TOTAL = "客车æµé"; |
| | | private static final String H_TRUCK_TOTAL = "货车æµé"; |
| | | private static final String H_CAR_TOTAL = "汽车æµé"; |
| | | private static final String H_MOTOR_TOTAL = "æºå¨è½¦æµé"; |
| | | private static final String H_PASSENGER_EQUIVALENT = "客车å½é"; |
| | | private static final String H_TRUCK_EQUIVALENT = "货车å½é"; |
| | | private static final String H_CAR_EQUIVALENT = "汽车å½é"; |
| | | private static final String H_MOTOR_EQUIVALENT = "æºå¨è½¦å½é"; |
| | | private static final String H_SPEED = "æºå¨è½¦é度"; |
| | | private static final String H_CONGESTION = "æ¥æ¤åº¦"; |
| | | |
| | | private static final int HEADER_SCAN_ROWS = 10; |
| | | private static final int MAX_REPORTED_ERRORS = 50; |
| | | |
| | | private static final DateTimeFormatter[] DATE_FORMATS = { |
| | | DateTimeFormatter.ofPattern("yyyy-M-d"), |
| | | DateTimeFormatter.ofPattern("yyyy/M/d"), |
| | | DateTimeFormatter.ofPattern("yyyy.M.d"), |
| | | DateTimeFormatter.ofPattern("yyyyMMdd") |
| | | }; |
| | | |
| | | @Resource |
| | | private ObservationStationFlowMapper stationFlowMapper; |
| | | |
| | | @Transactional |
| | | public Map<String, Object> importWorkbook(MultipartFile file) { |
| | | if (file == null || file.isEmpty()) { |
| | | throw new IllegalArgumentException("è¯·éæ©è¦å¯¼å
¥ç Excel æä»¶"); |
| | | } |
| | | String sourceFile = file.getOriginalFilename(); |
| | | List<ObservationStationFlow> rows = new ArrayList<>(); |
| | | Set<LocalDate> dates = new LinkedHashSet<>(); |
| | | Set<String> stationCodes = new LinkedHashSet<>(); |
| | | List<String> errors = new ArrayList<>(); |
| | | int skipped = 0; |
| | | |
| | | DataFormatter formatter = new DataFormatter(); |
| | | try (InputStream in = file.getInputStream(); Workbook workbook = WorkbookFactory.create(in)) { |
| | | Sheet sheet = workbook.getNumberOfSheets() > 0 ? workbook.getSheetAt(0) : null; |
| | | if (sheet == null) { |
| | | throw new IllegalArgumentException("å·¥ä½ç°¿ä¸æ²¡æå¯è¯»åçå·¥ä½è¡¨"); |
| | | } |
| | | int headerRowIndex = findHeaderRow(sheet, formatter); |
| | | if (headerRowIndex < 0) { |
| | | throw new IllegalArgumentException("æªæ¾å°è¡¨å¤´ï¼è¯·ç¡®è®¤æä»¶å
å«\"è§æµæ¥æ\"å\"è§æµç«ç¼å·\"两å"); |
| | | } |
| | | Map<String, Integer> headerIndex = buildHeaderIndex(sheet.getRow(headerRowIndex), formatter); |
| | | |
| | | for (int r = headerRowIndex + 1; r <= sheet.getLastRowNum(); r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) { |
| | | skipped++; |
| | | continue; |
| | | } |
| | | String code = stringCell(row, headerIndex, H_CODE, formatter); |
| | | LocalDate obsDate = dateCell(row, headerIndex, H_DATE, formatter); |
| | | if (isBlank(code) && obsDate == null) { |
| | | skipped++; |
| | | continue; |
| | | } |
| | | int rowNo = r + 1; |
| | | if (obsDate == null) { |
| | | addError(errors, "第" + rowNo + "è¡ï¼è§æµæ¥ææ æ³è¯å«"); |
| | | continue; |
| | | } |
| | | if (isBlank(code)) { |
| | | addError(errors, "第" + rowNo + "è¡ï¼è§æµç«ç¼å·ä¸ºç©º"); |
| | | continue; |
| | | } |
| | | ObservationStationFlow entity = new ObservationStationFlow(); |
| | | entity.setObsDate(obsDate); |
| | | entity.setStationCode(code); |
| | | entity.setStationName(stringCell(row, headerIndex, H_NAME, formatter)); |
| | | entity.setMileage(decimalCell(row, headerIndex, H_MILEAGE, formatter)); |
| | | entity.setCityName(stringCell(row, headerIndex, H_CITY, formatter)); |
| | | entity.setCountyName(stringCell(row, headerIndex, H_COUNTY, formatter)); |
| | | String direction = stringCell(row, headerIndex, H_DIRECTION, formatter); |
| | | entity.setDirection(direction == null ? "" : direction); |
| | | entity.setSmallMediumPassenger(decimalCell(row, headerIndex, H_SMALL_MEDIUM_PASSENGER, formatter)); |
| | | entity.setLargePassenger(decimalCell(row, headerIndex, H_LARGE_PASSENGER, formatter)); |
| | | entity.setSmallTruck(decimalCell(row, headerIndex, H_SMALL_TRUCK, formatter)); |
| | | entity.setMediumTruck(decimalCell(row, headerIndex, H_MEDIUM_TRUCK, formatter)); |
| | | entity.setLargeTruck(decimalCell(row, headerIndex, H_LARGE_TRUCK, formatter)); |
| | | entity.setExtraLargeTruck(decimalCell(row, headerIndex, H_EXTRA_LARGE_TRUCK, formatter)); |
| | | entity.setContainerTruck(decimalCell(row, headerIndex, H_CONTAINER, formatter)); |
| | | entity.setMotorcycle(decimalCell(row, headerIndex, H_MOTORCYCLE, formatter)); |
| | | entity.setTractor(decimalCell(row, headerIndex, H_TRACTOR, formatter)); |
| | | entity.setPassengerTotal(decimalCell(row, headerIndex, H_PASSENGER_TOTAL, formatter)); |
| | | entity.setTruckTotal(decimalCell(row, headerIndex, H_TRUCK_TOTAL, formatter)); |
| | | entity.setCarTotal(decimalCell(row, headerIndex, H_CAR_TOTAL, formatter)); |
| | | entity.setMotorTotal(decimalCell(row, headerIndex, H_MOTOR_TOTAL, formatter)); |
| | | entity.setPassengerEquivalent(decimalCell(row, headerIndex, H_PASSENGER_EQUIVALENT, formatter)); |
| | | entity.setTruckEquivalent(decimalCell(row, headerIndex, H_TRUCK_EQUIVALENT, formatter)); |
| | | entity.setCarEquivalent(decimalCell(row, headerIndex, H_CAR_EQUIVALENT, formatter)); |
| | | entity.setMotorEquivalent(decimalCell(row, headerIndex, H_MOTOR_EQUIVALENT, formatter)); |
| | | entity.setVehicleSpeed(decimalCell(row, headerIndex, H_SPEED, formatter)); |
| | | entity.setCongestionDegree(decimalCell(row, headerIndex, H_CONGESTION, formatter)); |
| | | entity.setSourceFile(sourceFile); |
| | | rows.add(entity); |
| | | dates.add(obsDate); |
| | | stationCodes.add(code); |
| | | } |
| | | } catch (IllegalArgumentException e) { |
| | | throw e; |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("å·¥ä½ç°¿è§£æå¤±è´¥: " + e.getMessage(), e); |
| | | } |
| | | |
| | | if (!rows.isEmpty()) { |
| | | stationFlowMapper.delete(new LambdaQueryWrapper<ObservationStationFlow>() |
| | | .in(ObservationStationFlow::getObsDate, dates)); |
| | | for (ObservationStationFlow row : rows) { |
| | | stationFlowMapper.insert(row); |
| | | } |
| | | } |
| | | |
| | | List<String> dateTexts = new ArrayList<>(); |
| | | for (LocalDate date : dates) { |
| | | dateTexts.add(date.toString()); |
| | | } |
| | | BigDecimal smallMediumTotal = BigDecimal.ZERO; |
| | | for (ObservationStationFlow row : rows) { |
| | | if (row.getSmallMediumPassenger() != null) { |
| | | smallMediumTotal = smallMediumTotal.add(row.getSmallMediumPassenger()); |
| | | } |
| | | } |
| | | |
| | | Map<String, Object> summary = new LinkedHashMap<>(); |
| | | summary.put("totalRows", rows.size()); |
| | | summary.put("stationCount", stationCodes.size()); |
| | | summary.put("dates", dateTexts); |
| | | summary.put("skippedRows", skipped); |
| | | summary.put("smallMediumPassengerTotal", smallMediumTotal); |
| | | summary.put("errors", errors); |
| | | log.info("Observation station import done: file={} rows={} dates={} skipped={} failed={}", |
| | | sourceFile, rows.size(), dateTexts, skipped, errors.size()); |
| | | return summary; |
| | | } |
| | | |
| | | public List<ObservationDateSummary> listDateSummaries() { |
| | | return stationFlowMapper.selectDateSummary(); |
| | | } |
| | | |
| | | public List<ObservationStationFlow> listRows(LocalDate date, int limit) { |
| | | int effectiveLimit = limit <= 0 ? 2000 : Math.min(limit, 5000); |
| | | return stationFlowMapper.selectList(new LambdaQueryWrapper<ObservationStationFlow>() |
| | | .eq(date != null, ObservationStationFlow::getObsDate, date) |
| | | .orderByAsc(ObservationStationFlow::getId) |
| | | .last("LIMIT " + effectiveLimit)); |
| | | } |
| | | |
| | | public int deleteDate(LocalDate date) { |
| | | if (date == null) { |
| | | throw new IllegalArgumentException("è¯·éæ©è¦å é¤çè§æµæ¥æ"); |
| | | } |
| | | return stationFlowMapper.delete(new LambdaQueryWrapper<ObservationStationFlow>() |
| | | .eq(ObservationStationFlow::getObsDate, date)); |
| | | } |
| | | |
| | | private void addError(List<String> errors, String message) { |
| | | if (errors.size() < MAX_REPORTED_ERRORS) { |
| | | errors.add(message); |
| | | } |
| | | } |
| | | |
| | | private int findHeaderRow(Sheet sheet, DataFormatter formatter) { |
| | | int lastRow = Math.min(sheet.getLastRowNum(), HEADER_SCAN_ROWS); |
| | | for (int r = 0; r <= lastRow; r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) { |
| | | continue; |
| | | } |
| | | Map<String, Integer> index = buildHeaderIndex(row, formatter); |
| | | if (index.containsKey(normalize(H_DATE)) && index.containsKey(normalize(H_CODE))) { |
| | | return r; |
| | | } |
| | | } |
| | | return -1; |
| | | } |
| | | |
| | | private Map<String, Integer> buildHeaderIndex(Row row, DataFormatter formatter) { |
| | | Map<String, Integer> index = new LinkedHashMap<>(); |
| | | if (row == null) { |
| | | return index; |
| | | } |
| | | for (int c = row.getFirstCellNum(); c < row.getLastCellNum(); c++) { |
| | | Cell cell = row.getCell(c); |
| | | String header = normalize(formatter.formatCellValue(cell)); |
| | | if (!header.isEmpty() && !index.containsKey(header)) { |
| | | index.put(header, c); |
| | | } |
| | | } |
| | | return index; |
| | | } |
| | | |
| | | private String normalize(String value) { |
| | | if (value == null) { |
| | | return ""; |
| | | } |
| | | StringBuilder builder = new StringBuilder(value.length()); |
| | | for (int i = 0; i < value.length(); i++) { |
| | | char ch = value.charAt(i); |
| | | if (ch == 0xFEFF || Character.isWhitespace(ch)) { |
| | | continue; |
| | | } |
| | | builder.append(ch); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private String stringCell(Row row, Map<String, Integer> headerIndex, String header, DataFormatter formatter) { |
| | | Integer column = headerIndex.get(normalize(header)); |
| | | if (column == null) { |
| | | return null; |
| | | } |
| | | Cell cell = row.getCell(column); |
| | | if (cell == null) { |
| | | return null; |
| | | } |
| | | String value = formatter.formatCellValue(cell).trim(); |
| | | return value.isEmpty() ? null : value; |
| | | } |
| | | |
| | | private LocalDate dateCell(Row row, Map<String, Integer> headerIndex, String header, DataFormatter formatter) { |
| | | Integer column = headerIndex.get(normalize(header)); |
| | | if (column == null) { |
| | | return null; |
| | | } |
| | | return readDate(row.getCell(column), formatter); |
| | | } |
| | | |
| | | private BigDecimal decimalCell(Row row, Map<String, Integer> headerIndex, String header, DataFormatter formatter) { |
| | | Integer column = headerIndex.get(normalize(header)); |
| | | if (column == null) { |
| | | return null; |
| | | } |
| | | return readDecimal(row.getCell(column), formatter); |
| | | } |
| | | |
| | | private LocalDate readDate(Cell cell, DataFormatter formatter) { |
| | | if (cell == null) { |
| | | return null; |
| | | } |
| | | CellType type = cell.getCellType(); |
| | | if (type == CellType.FORMULA) { |
| | | type = cell.getCachedFormulaResultType(); |
| | | } |
| | | if (type == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) { |
| | | return cell.getLocalDateTimeCellValue().toLocalDate(); |
| | | } |
| | | String text = formatter.formatCellValue(cell).trim(); |
| | | if (text.isEmpty()) { |
| | | return null; |
| | | } |
| | | text = text.replace("å¹´", "-").replace("æ", "-").replace("æ¥", "").trim(); |
| | | for (DateTimeFormatter format : DATE_FORMATS) { |
| | | try { |
| | | return LocalDate.parse(text, format); |
| | | } catch (Exception ignored) { |
| | | // Try the next supported pattern. |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private BigDecimal readDecimal(Cell cell, DataFormatter formatter) { |
| | | if (cell == null) { |
| | | return null; |
| | | } |
| | | CellType type = cell.getCellType(); |
| | | if (type == CellType.FORMULA) { |
| | | type = cell.getCachedFormulaResultType(); |
| | | } |
| | | if (type == CellType.NUMERIC) { |
| | | return BigDecimal.valueOf(cell.getNumericCellValue()); |
| | | } |
| | | if (type == CellType.BLANK) { |
| | | return null; |
| | | } |
| | | String text = formatter.formatCellValue(cell).trim().replace(",", "").replace(" ", ""); |
| | | if (text.isEmpty() || "-".equals(text) || "â".equals(text) || "/".equals(text)) { |
| | | return null; |
| | | } |
| | | try { |
| | | return new BigDecimal(text); |
| | | } catch (NumberFormatException e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private boolean isBlank(String value) { |
| | | return value == null || value.trim().isEmpty(); |
| | | } |
| | | } |
| | |
| | | spring: |
| | | datasource: |
| | | url: jdbc:mysql://localhost:3305/traffic_audit?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true |
| | | username: root |
| | | password: ${MYSQL_PASSWORD:root} |
| | | username: ${TRAFFIC_DB_USER:root} |
| | | password: ${TRAFFIC_DB_PASSWORD:root} |
| | | driver-class-name: com.mysql.cj.jdbc.Driver |
| | | servlet: |
| | | multipart: |
| | |
| | | logic-not-delete-value: 0 |
| | | |
| | | jwt: |
| | | secret: ${JWT_SECRET:change-me-jwt-secret} |
| | | # å¿
é¡»æ¯ãBase64 åæ³å符 + é¿åº¦æ¯ 4 çåæ°ãï¼ä¸é¢è¿ä¸² 32 ä½åå
è¿å¶å¯ç´æ¥ç¨ |
| | | secret: ${TRAFFIC_JWT_SECRET:8f2c1d64a7b34e09c5d81f6a2b0e7c93} |
| | | expiration: 86400000 |
| | | |
| | | docs: |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.trafficaudit.holiday.service.HolidayStatService.DailyMetrics; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import java.lang.reflect.Method; |
| | | import java.math.BigDecimal; |
| | | import java.util.function.Function; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNull; |
| | | import static org.junit.jupiter.api.Assertions.assertSame; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.never; |
| | | import static org.mockito.Mockito.verify; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | class HolidayBulletinServiceTest { |
| | | |
| | | @Test |
| | | void formatsTableRatiosAsSignedPercentages() throws Exception { |
| | | HolidayBulletinService service = new HolidayBulletinService(null); |
| | | |
| | | assertEquals("-4.7%", ratio(service, "95.3", "100")); |
| | | assertEquals("1.0%", ratio(service, "101", "100")); |
| | | assertEquals("0%", ratio(service, "100", "100")); |
| | | assertEquals("0%", ratio(service, "10004", "10000")); |
| | | assertEquals("â", ratio(service, "100", null)); |
| | | assertEquals("â", ratio(service, "100", "0")); |
| | | } |
| | | |
| | | @Test |
| | | void comparesCivilAviationOnlyThroughCoveredDay() throws Exception { |
| | | HolidayStatService stat = mock(HolidayStatService.class); |
| | | HolidayBulletinService service = new HolidayBulletinService(stat); |
| | | |
| | | DailyMetrics current = new DailyMetrics(); |
| | | current.setYear(2026); |
| | | current.setHolidayType("NATIONAL_DAY"); |
| | | current.setDayIndex(2); |
| | | |
| | | DailyMetrics coveredDay = new DailyMetrics(); |
| | | coveredDay.setCivilAviation(new BigDecimal("6.7482")); |
| | | DailyMetrics missingDay = new DailyMetrics(); |
| | | |
| | | when(stat.dailyMetrics(2026, "NATIONAL_DAY", 2)).thenReturn(missingDay); |
| | | when(stat.dailyMetrics(2026, "NATIONAL_DAY", 1)).thenReturn(coveredDay); |
| | | |
| | | DailyMetrics previousYear = new DailyMetrics(); |
| | | previousYear.setYear(2025); |
| | | DailyMetrics previousYearThroughDay1 = new DailyMetrics(); |
| | | previousYearThroughDay1.setCivilAviation(new BigDecimal("7.5003")); |
| | | when(stat.cumulativeMetrics(2025, "NATIONAL_DAY", 1)).thenReturn(previousYearThroughDay1); |
| | | |
| | | Object result = comparableCumulative(service, current, previousYear, DailyMetrics::getCivilAviation); |
| | | |
| | | assertSame(previousYearThroughDay1, result); |
| | | verify(stat).cumulativeMetrics(2025, "NATIONAL_DAY", 1); |
| | | verify(stat, never()).cumulativeMetrics(2025, "NATIONAL_DAY", 2); |
| | | } |
| | | |
| | | @Test |
| | | void comparableCumulativeIsNullWhenCurrentYearHasNoMetricData() throws Exception { |
| | | HolidayStatService stat = mock(HolidayStatService.class); |
| | | HolidayBulletinService service = new HolidayBulletinService(stat); |
| | | |
| | | DailyMetrics current = new DailyMetrics(); |
| | | current.setYear(2026); |
| | | current.setHolidayType("NATIONAL_DAY"); |
| | | current.setDayIndex(2); |
| | | when(stat.dailyMetrics(2026, "NATIONAL_DAY", 2)).thenReturn(new DailyMetrics()); |
| | | when(stat.dailyMetrics(2026, "NATIONAL_DAY", 1)).thenReturn(new DailyMetrics()); |
| | | |
| | | Object result = comparableCumulative(service, current, new DailyMetrics(), DailyMetrics::getCivilAviation); |
| | | |
| | | assertNull(result); |
| | | verify(stat, never()).cumulativeMetrics(0, "NATIONAL_DAY", 1); |
| | | } |
| | | |
| | | @Test |
| | | void cumulativeParagraphUsesWanUnitAndSingleRounding() throws Exception { |
| | | HolidayBulletinService service = new HolidayBulletinService(null); |
| | | |
| | | DailyMetrics current = new DailyMetrics(); |
| | | current.setTotalPersonFlow(new BigDecimal("3088.62958")); |
| | | DailyMetrics previousYear = new DailyMetrics(); |
| | | previousYear.setTotalPersonFlow(new BigDecimal("3232.42742")); |
| | | DailyMetrics twoYearsAgo = new DailyMetrics(); |
| | | twoYearsAgo.setTotalPersonFlow(new BigDecimal("3137.80860")); |
| | | |
| | | Method method = HolidayBulletinService.class.getDeclaredMethod( |
| | | "cumulativeParagraph", DailyMetrics.class, DailyMetrics.class, DailyMetrics.class); |
| | | method.setAccessible(true); |
| | | String text = (String) method.invoke(service, current, previousYear, twoYearsAgo); |
| | | |
| | | assertEquals( |
| | | "åæä»¥æ¥ï¼å
¨çå
¨ç¤¾ä¼è·¨åºåäººåæµå¨é累计3088.63ä¸äººæ¬¡ï¼æ¯2025å¹´åæä¸é4.4%ï¼æ¯2024å¹´åæä¸é1.6%ã", |
| | | text); |
| | | } |
| | | |
| | | private Object comparableCumulative(HolidayBulletinService service, DailyMetrics current, |
| | | DailyMetrics comparisonYear, |
| | | Function<DailyMetrics, BigDecimal> selector) throws Exception { |
| | | Method method = HolidayBulletinService.class.getDeclaredMethod( |
| | | "comparableCumulative", DailyMetrics.class, DailyMetrics.class, Function.class); |
| | | method.setAccessible(true); |
| | | return method.invoke(service, current, comparisonYear, selector); |
| | | } |
| | | |
| | | private String ratio(HolidayBulletinService service, String current, String base) throws Exception { |
| | | Method method = HolidayBulletinService.class.getDeclaredMethod("ratio", BigDecimal.class, BigDecimal.class); |
| | | method.setAccessible(true); |
| | | BigDecimal currentValue = current == null ? null : new BigDecimal(current); |
| | | BigDecimal baseValue = base == null ? null : new BigDecimal(base); |
| | | return (String) method.invoke(service, currentValue, baseValue); |
| | | } |
| | | } |
| | |
| | | <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> |
| | | |
| | | <el-card shadow="never" style="margin-top:14px"> |
| | | <div slot="header"> |
| | | <span>è§æµç«åæ¥è°æ¥æ°æ®</span> |
| | | <span class="hint">ç¨äºåå§æ°æ®çåï¼ä¸ä¸ä¸æ¹è忥ç»è®¡æ°æ®å
³è</span> |
| | | </div> |
| | | <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=".xls,.xlsx" :on-change="onObservationFileChange" :on-remove="onObservationFileRemove" |
| | | :file-list="observationFileList"> |
| | | <el-button type="primary" plain icon="el-icon-folder-opened">éæ©æä»¶</el-button> |
| | | </el-upload> |
| | | <el-button type="primary" icon="el-icon-upload2" :loading="observationImporting" |
| | | :disabled="!observationFile" @click="importObservation">导å
¥</el-button> |
| | | </div> |
| | | </div> |
| | | <div v-if="observationMessage" class="import-message"> |
| | | <pre>{{ observationMessage }}</pre> |
| | | </div> |
| | | <el-table :data="observationDates" border stripe size="small" style="margin-top:12px" v-loading="observationLoading"> |
| | | <el-table-column prop="obsDate" label="è§æµæ¥æ" width="130" align="center"></el-table-column> |
| | | <el-table-column prop="stationCount" label="è§æµç«æ°" width="110" align="right"></el-table-column> |
| | | <el-table-column label="ä¸å°å®¢æµéå计ï¼ä¸è¾æ¬¡ï¼" width="210" align="right"> |
| | | <template slot-scope="s">{{ formatWanVehicles(s.row.smallMediumPassengerTotal) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="importedAt" label="æè¿å¯¼å
¥æ¶é´" min-width="180"></el-table-column> |
| | | <el-table-column label="æä½" width="150" align="center"> |
| | | <template slot-scope="s"> |
| | | <el-button type="text" @click="viewObservation(s.row.obsDate)">æ¥ç</el-button> |
| | | <el-button type="text" class="danger-text" @click="deleteObservation(s.row.obsDate)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <el-dialog :title="'è§æµç«æç» - ' + observationDialogDate" :visible.sync="observationDialogVisible" width="92%" top="5vh"> |
| | | <el-table :data="observationRows" border stripe size="mini" height="540" v-loading="observationRowsLoading"> |
| | | <el-table-column prop="obsDate" label="è§æµæ¥æ" width="110" fixed="left" align="center"></el-table-column> |
| | | <el-table-column prop="stationCode" label="è§æµç«ç¼å·" width="130" fixed="left"></el-table-column> |
| | | <el-table-column prop="stationName" label="è§æµç«åç§°" width="120"></el-table-column> |
| | | <el-table-column label="è§æµéç¨" width="100" align="right"><template slot-scope="s">{{ formatPlain(s.row.mileage) }}</template></el-table-column> |
| | | <el-table-column prop="cityName" label="å°å¸åç§°" width="140"></el-table-column> |
| | | <el-table-column prop="countyName" label="åºå¿åç§°" width="110"></el-table-column> |
| | | <el-table-column prop="direction" label="è¡é©¶æ¹å" width="90"></el-table-column> |
| | | <el-table-column label="ä¸å°å®¢æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.smallMediumPassenger) }}</template></el-table-column> |
| | | <el-table-column label="大客车æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.largePassenger) }}</template></el-table-column> |
| | | <el-table-column label="å°è´§è½¦æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.smallTruck) }}</template></el-table-column> |
| | | <el-table-column label="ä¸è´§è½¦æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.mediumTruck) }}</template></el-table-column> |
| | | <el-table-column label="大货车æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.largeTruck) }}</template></el-table-column> |
| | | <el-table-column label="ç¹å¤§è´§æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.extraLargeTruck) }}</template></el-table-column> |
| | | <el-table-column label="éè£
ç®±æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.containerTruck) }}</template></el-table-column> |
| | | <el-table-column label="æ©æè½¦æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.motorcycle) }}</template></el-table-column> |
| | | <el-table-column label="æææºæµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.tractor) }}</template></el-table-column> |
| | | <el-table-column label="客车æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.passengerTotal) }}</template></el-table-column> |
| | | <el-table-column label="货车æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.truckTotal) }}</template></el-table-column> |
| | | <el-table-column label="汽车æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.carTotal) }}</template></el-table-column> |
| | | <el-table-column label="æºå¨è½¦æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.motorTotal) }}</template></el-table-column> |
| | | <el-table-column label="客车å½é" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.passengerEquivalent) }}</template></el-table-column> |
| | | <el-table-column label="货车å½é" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.truckEquivalent) }}</template></el-table-column> |
| | | <el-table-column label="汽车å½é" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.carEquivalent) }}</template></el-table-column> |
| | | <el-table-column label="æºå¨è½¦å½é" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.motorEquivalent) }}</template></el-table-column> |
| | | <el-table-column label="æºå¨è½¦é度" width="110" align="right"><template slot-scope="s">{{ formatPlain(s.row.vehicleSpeed) }}</template></el-table-column> |
| | | <el-table-column label="æ¥æ¤åº¦" width="100" align="right"><template slot-scope="s">{{ formatPlain(s.row.congestionDegree) }}</template></el-table-column> |
| | | <el-table-column prop="sourceFile" label="æ¥æºæä»¶å" min-width="220" show-overflow-tooltip></el-table-column> |
| | | </el-table> |
| | | </el-dialog> |
| | | </div> |
| | | </el-main> |
| | | </el-container> |
| | |
| | | name: 'HolidayStat', |
| | | data() { |
| | | return { |
| | | years: [2024, 2025, 2026, 2027, 2028, 2029, 2030], |
| | | years: [2019, 2024, 2025, 2026, 2027, 2028, 2029, 2030], |
| | | year: 2026, |
| | | holidayType: 'NATIONAL_DAY', |
| | | holidayTypeOptions: [], |
| | |
| | | fileList: [], |
| | | importMessage: '', |
| | | allowHistoricalOverwrite: false, |
| | | bulletinDate: this.previousDateString() |
| | | bulletinDate: this.previousDateString(), |
| | | observationFile: null, |
| | | observationFileList: [], |
| | | observationImporting: false, |
| | | observationLoading: false, |
| | | observationMessage: '', |
| | | observationDates: [], |
| | | observationDialogVisible: false, |
| | | observationDialogDate: '', |
| | | observationRows: [], |
| | | observationRowsLoading: false |
| | | } |
| | | }, |
| | | computed: { |
| | |
| | | mounted() { |
| | | this.loadOptions() |
| | | this.loadOverview() |
| | | this.loadObservationDates() |
| | | }, |
| | | methods: { |
| | | previousDateString() { |
| | |
| | | const data = err && err.response && err.response.data |
| | | this.$message.error((data && data.message) || (err && err.message) || fallback) |
| | | }, |
| | | onObservationFileChange(file) { |
| | | this.observationFile = file.raw |
| | | this.observationFileList = [file] |
| | | this.observationMessage = '' |
| | | }, |
| | | onObservationFileRemove() { |
| | | this.observationFile = null |
| | | this.observationFileList = [] |
| | | }, |
| | | loadObservationDates() { |
| | | this.observationLoading = true |
| | | api.get('/observation/dates') |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'å 载失败') |
| | | this.observationDates = res.data || [] |
| | | }) |
| | | .catch(err => this.showError(err, 'è§æµç«æ°æ®å 载失败')) |
| | | .finally(() => { this.observationLoading = false }) |
| | | }, |
| | | importObservation() { |
| | | if (!this.observationFile) return |
| | | const form = new FormData() |
| | | form.append('file', this.observationFile) |
| | | this.observationImporting = true |
| | | this.observationMessage = '' |
| | | api.post('/observation/import', form, { timeout: 300000, headers: { 'Content-Type': 'multipart/form-data' } }) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || '导å
¥å¤±è´¥') |
| | | const data = res.data || {} |
| | | const dates = (data.dates && data.dates.length) ? data.dates.join('ã') : 'â' |
| | | const lines = ['导å
¥å®æï¼' + (data.totalRows || 0) + ' æ¡ï¼è§æµç« ' + (data.stationCount || 0) |
| | | + ' ä¸ªï¼æ¥æ ' + dates + 'ï¼è·³è¿ç©ºç½ ' + (data.skippedRows || 0) + ' æ¡ã'] |
| | | if (data.errors && data.errors.length) { |
| | | lines.push('失败æç»ï¼') |
| | | data.errors.forEach(item => lines.push(item)) |
| | | } |
| | | this.observationMessage = lines.join('\n') |
| | | this.$message.success('è§æµç«æ°æ®å¯¼å
¥å®æ') |
| | | this.observationFile = null |
| | | this.observationFileList = [] |
| | | this.loadObservationDates() |
| | | }) |
| | | .catch(err => this.showError(err, 'è§æµç«æ°æ®å¯¼å
¥å¤±è´¥')) |
| | | .finally(() => { this.observationImporting = false }) |
| | | }, |
| | | viewObservation(date) { |
| | | this.observationDialogDate = date |
| | | this.observationDialogVisible = true |
| | | this.observationRowsLoading = true |
| | | this.observationRows = [] |
| | | api.get('/observation/rows', { params: { date: date, limit: 2000 } }) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'å 载失败') |
| | | this.observationRows = res.data || [] |
| | | }) |
| | | .catch(err => this.showError(err, 'è§æµç«æç»å 载失败')) |
| | | .finally(() => { this.observationRowsLoading = false }) |
| | | }, |
| | | deleteObservation(date) { |
| | | this.$confirm('ç¡®å®å é¤ ' + date + ' çè§æµç«æ°æ®åï¼', 'å é¤ç¡®è®¤', { type: 'warning' }) |
| | | .then(() => api.post('/observation/delete', null, { params: { date: date } })) |
| | | .then(res => { |
| | | if (res && res.code !== 200) throw new Error(res.message || 'å é¤å¤±è´¥') |
| | | this.$message.success('å·²å é¤') |
| | | this.loadObservationDates() |
| | | }) |
| | | .catch(err => { if (err !== 'cancel' && err !== 'close') this.showError(err, 'å é¤å¤±è´¥') }) |
| | | }, |
| | | formatNumber(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | return Number(value).toFixed(2) |
| | | }, |
| | | formatWanVehicles(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return (number / 10000).toFixed(4) |
| | | }, |
| | | formatInteger(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return String(Math.round(number)) |
| | | }, |
| | | formatPlain(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return String(number) |
| | | }, |
| | | formatCoefficient(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | |
| | | .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; } |
| | | .hint { margin-left: 10px; color: #909399; font-size: 12px; } |
| | | .danger-text { color: #f56c6c; } |
| | | </style> |
| | |
| | | <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="120" align="center"> |
| | | <template slot-scope="scope">{{ dayIndexLabel(scope.row.dayIndex) }}</template> |
| | | </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-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 label="æ¥æç±»å" prop="dayIndex"> |
| | | <el-select v-model="holidayForm.dayIndex" style="width:100%"> |
| | | <el-option label="å䏿¥" :value="0"></el-option> |
| | | <el-option v-for="day in 31" :key="day" :label="'第' + day + 'æ¥'" :value="day"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | <span slot="footer"> |
| | |
| | | data() { |
| | | return { |
| | | activeTab: 'holidays', |
| | | years: [2024, 2025, 2026, 2027, 2028, 2029, 2030], |
| | | years: [2019, 2024, 2025, 2026, 2027, 2028, 2029, 2030], |
| | | holidayTypeOptions: [], |
| | | dateCategoryOptions: [], |
| | | holidayQuery: { year: 2026, holidayType: 'NATIONAL_DAY' }, |
| | |
| | | year: [{ required: true, message: 'è¯·éæ©å¹´ä»½', trigger: 'change' }], |
| | | holidayType: [{ required: true, message: 'è¯·éæ©è忥åç±»', trigger: 'change' }], |
| | | holidayDate: [{ required: true, message: 'è¯·éæ©æ¥æ', trigger: 'change' }], |
| | | dayIndex: [{ required: true, message: '请è¾å
¥ç¬¬å æ¥', trigger: 'change' }] |
| | | dayIndex: [{ required: true, message: 'è¯·éæ©æ¥æç±»å', trigger: 'change' }] |
| | | }, |
| | | coefficientRules: { |
| | | year: [{ required: true, message: 'è¯·éæ©å¹´ä»½', trigger: 'change' }], |
| | |
| | | this.loadCoefficients() |
| | | }, |
| | | methods: { |
| | | dayIndexLabel(value) { |
| | | if (Number(value) === 0) return 'å䏿¥' |
| | | return value === null || value === undefined || value === '' ? 'â' : '第' + value + 'æ¥' |
| | | }, |
| | | loadOptions() { |
| | | api.get('/holiday/config/options').then(res => { |
| | | const data = res.data || {} |