Merge remote-tracking branch 'origin/main'
| | |
| | | web/ |
| | | traffic-audit-server/web/ |
| | | |
| | | # ==== Windows æå¡å
è£
å¨ï¼WinSW äºè¿å¶ï¼17.4MBï¼ç± pack-deploy.ps1 ä¸è½½å¹¶æ ¡éª SHA-256ï¼ä¸å
¥åºï¼==== |
| | | deploy-templates/tools/ |
| | | |
| | | # ==== é¨ç½²äº§ç©ï¼ä¸å
¥åºï¼==== |
| | | deploy/ |
| | | traffic-audit-deploy-*.zip |
| | |
| | | ## éªè¯æ³¨æ |
| | | - æ²ç®±å
`Get-NetTCPConnection` ä¼å æéâæç»è®¿é®âèéé»å¤±è´¥ï¼**ä¸è¦**æ®æ¤å¤å®ç«¯å£æªçå¬ï¼å¤ææå¡ç¶æä»¥ backend.log / frontend.log 为åã |
| | | - ç¨æ·è¦æ±ï¼ä¸´æ¶èæ¬/æä»¶æªç»åæä¸å ï¼å¯¼å
¥å¤±è´¥è¦å¨é¡µé¢æ¾ç¤ºè¡å·ï¼AI åæç»æè¦ç»ç»æåæç + éåè¾åºå¨ç»ã |
| | | - **æ°æ®å¯¼å
¥æéï¼2026-10-04 ç¨æ·æç¡®ï¼**ï¼é¤éç¨æ·æç¡®è¦æ±å©æå¯¼å
¥ï¼å¦åä¸è¦æ§è¡ä»»ä½æ°æ®å¯¼å
¥ãè¦çæé导æä½ï¼ç¨æ·èªè¡å¯¼å
¥çæ°æ®é»è®¤è§ä¸ºå·²å®æï¼å©æåªååªè¯»æ ¸å¯¹ã |
| | | |
| | | ## éè¦èåæ¥å¿«æ¥ä¸åææ¥åå£å¾ï¼2026-10-04 èµ·ï¼ |
| | | - å¿«æ¥ç¾åæ¯å¿
é¡»å
æå¯¼å
¥ç `2026å¹´å䏿°æ®.xlsx` 对åºåå
æ ¼çæ°åæ ¼å¼çææ¾ç¤ºå¼ï¼åæå¿«æ¥ç»ä¸ 1 ä½å°æ°åæ´ãå½åæºè¡¨ä¸æ¯å
¨å±åä¸ç²¾åº¦ï¼å¤æ°åå
æ ¼æ¯ `0.0%`ï¼`äººåæµå¨éæ±æ»` ç彿¥ç¯æ¯ D/P/Jãç´¯è®¡åæ¯ V/W/Y/Z/AB/ACï¼éè·¯ `H`ï¼åæ¯2024å¹´ï¼ï¼ä»¥åæ°´è·¯å½æ¥ç¯æ¯ D æ¯ `0.00%`ã |
| | | - `äººåæµå¨éæ±æ»` çç´¯è®¡å¼ U/X/AA åçæ¯äº¿åä½ï¼åå
æ ¼æ ¼å¼ä¸º `0.0000000_`ï¼å¿«æ¥å¿
须卿¢ç®ä¸äººæ¬¡åå
æäº¿åä½ 7 ä½å°æ°çææ¾ç¤ºå¼ï¼åä¹ 10000 å¹¶æå¿«æ¥ 2 ä½è¾åºãå¯¹åºæ»è®¡ãå
¬è·¯å计ãéè¥ä¸æ§å°å®¢è½¦ä¸é¡¹ã |
| | | - æ ¸å¯¹é¢å¯¼åææ¥åæ¶ï¼æè¯¥åææ¥åèªèº«æ¾ç¤ºçå°æ°ä½æ ¸å¯¹ï¼ä¸å¥ç¨å¿«æ¥ç 1 ä½å°æ°å£å¾ï¼ç®æ æ¯å¤ç°äººå·¥å¨ææ¡£/Excel ä¸çå°çå¼ã |
| | | |
| | | ## å·¥ä½è§èï¼2026-09-02 èµ·ï¼2026-09-27 æåäºæäº¤æ¹å¼è°æ´ï¼ |
| | | |
| | |
| | | > **æ¯å¤©ç»ææ¶**ï¼æå½å¤©è¿å±ã踩è¿çåãæ°éæ±ãæªå®æäºé¡¹æ´æ°å°æ¬æä»¶ï¼ç¶åå¯ä»¥æ¾å¿å¼æ°å¯¹è¯ã |
| | | > **åä½çº¦å®ï¼2026-08-31ï¼**ï¼è¯´ãæäº¤ã= æ¬å° commit + æ¨éå°è¿ç«¯ gitï¼origin mainï¼Gitblitï¼ï¼ä¸ååªåæ¬å° commitã |
| | | |
| | | ## ææ°ï¼2026-10-06ï¼ |
| | | |
| | | ### 10-06ï¼æçº¿å¾çºµåé´è·å é«ï¼éªæ¶éè¿ï¼ |
| | | |
| | | - **ç¨æ·èæ¯**ï¼å®¢è¿éå¾çº¿é´è·è¾å¥½ãæ ç¾ææ¾åçï¼å
¶ä»å¤åºåæçº¿å¾å 线è·åè¿ï¼æ ç¾ä»å¯è½å线ãç¨æ·åæéå½å¢å å¾é«ï¼Word åªæ¯å徿¥æºï¼ä¸è¦æ±å½åå页为æç»çï¼ä½å¾ä¸è½è¿é«ã |
| | | - **å®ç°**ï¼ |
| | | - `HolidayTrendChartService`ï¼æçº¿çºµè½´çç½æ¶ç´§ä¸º `lowerMargin=0.04 / upperMargin=0.07`ï¼`LINE_CHART_HEIGHT_SCALE=1.20`ï¼æææçº¿ PNG é«åº¦æ¾å¤§çº¦ 20%ã |
| | | - `HolidayTrendService`ï¼12 å¼ æçº¿å¾ç Word å
åµå¾çé«åº¦åæ¥æ¾å¤§çº¦ 20%ï¼ä¿æ PNG ä¸ Word å¾çæ¡æ¯ä¾ä¸è´ï¼é¥¼å¾ãå å 徿ªæ¹ã |
| | | - `HolidayTrendChartServiceTest`ï¼æ°å¢æçº¿å¾é«åº¦ç¼©æ¾æè¨ã |
| | | - **éªæ¶**ï¼ç¨æ·äº 2026-10-06 æ¥ç `C:\Users\jcxiong\Downloads\2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾ï¼æçº¿å é«20å®éªï¼.docx` åéªæ¶éè¿ã |
| | | - **éªè¯ç»æ**ï¼ |
| | | 1. 15% å é«çå¨é路客è¿éå¾ `image5` æ£åº OCR æ ç¾æ¡éå ï¼20% ç 12 å¼ æçº¿å¾ï¼`image1/2/4/5/6/7/8/9/10/11/14/15`ï¼æªæ£åºæ ç¾æ¡éå ã |
| | | 2. PNG 宽髿¯ä¸ Word `wp:extent` 宽髿¯æå¤§è¯¯å·®çº¦ `0.162%`ï¼æªåçææ¾æä¼¸ã |
| | | 3. Word ä»ä¸º 8 é¡µãæ¯é¡µ 2 å¼ å¾ï¼é¦é¡µ 1 å¼ ï¼ï¼ä¸ 15% å é«å®éªç¸åã |
| | | 4. å端å
¨é `mvn -q test -f traffic-audit-server\pom.xml` éè¿ï¼`HolidayTrendChartServiceTest` èç¦æµè¯éè¿ã |
| | | 5. æ ç¾äº²ååº¦æ£æ¥è®°å½å° 3 夿 ç¾æ¡æ´é è¿é»è¿çº¿ï¼`image1` ç `1,518.02`ã`image4` ç `1,346.17`ã`image9` ç `248.65`ï¼ï¼ç½æè¾¹ä¿è¯å¯è¯»ï¼ç¨æ·æ´ä½éªæ¶éè¿å¹¶ä¿ç该æççç¥ã |
| | | 6. `image13` å å 徿 4 å¤ OCR æ ç¾éå ï¼ä½è¯¥å¾æªå䏿¬å®éªä¸ä»£ç æªæ¹ã |
| | | - **æ°æ®è¾¹ç**ï¼æªå¯¼å
¥ãè¦çæé导任ä½ä¸å¡æ°æ®ï¼ä»
æ¬å°åªè¯»çæèµ°å¿å¾åå®éªã |
| | | |
| | | ### 10-06ï¼å¹³åæ¥æ¤åº¦æ ç¾æ¢å¤ä¸º 2 ä½å°æ° |
| | | |
| | | - **è°æ´**ï¼æç¨æ·è¦æ±ï¼âæ®éå½çéè§æµç«å¹³åæ¥æ¤åº¦âä¸ååç¬ä½¿ç¨ 3 ä½å°æ°ï¼æ¢å¤ä¸ºæçº¿å¾é»è®¤ 2 ä½ã |
| | | - **ä¸è´æ§**ï¼ç±äºæçº¿å¾ç°å·²ç»ä¸ææ¾ç¤ºæ ç¾ç²¾åº¦ç»å¶ï¼10-04ã10-05 çæ¥æ¤åº¦é½ä¼æ¾ç¤ºå¹¶ç»å¶ä¸º `0.49`ï¼ä¸ä¼ååºç°åæ ç¾ä¸åé«åº¦ã |
| | | - **éªè¯**ï¼å端å
¨é `mvn -q test -f traffic-audit-server\pom.xml` éè¿ã |
| | | |
| | | ### 10-06ï¼æçº¿å¾ç»ä¸ææ¾ç¤ºæ ç¾ç²¾åº¦ç»å¶ |
| | | |
| | | - **å£å¾ï¼ç¨æ· 2026-10-06 确认ï¼**ï¼åä¸å¼ æçº¿å¾ä¸ï¼åä¸ä¸ªæ¾ç¤ºæ ç¾å¿
须对åºåä¸ä¸ªç¹ä½é«åº¦ï¼åææåä¸å¾å¼ç¨æ¯è¯¥å¾æ¾ç¤ºç²¾åº¦æ´ç»çå·®å¼ã |
| | | - **å®ç°**ï¼`HolidayTrendChartService.lineChart` 卿æåå§ç»è®¡è®¡ç®å®æåï¼äºæç»å
¥å¾è¾¹çè°ç¨ `displayValue`ï¼æ®éå¾æè¯¥å¾ `valueDecimals` å `HALF_UP` èå
¥ï¼ç¾åæ¯ç¹æ ç¾æ 1 ä½ç¾åæ¯ç²¾åº¦ï¼å³åå§å°æ° 3 ä½ï¼èå
¥ãæçº¿åæ åç¹æ ç¾å
±ç¨åä¸èå
¥å¼ï¼çºµè½´æ ¼å¼æ²¿ç¨è¯¥å¾ç²¾åº¦ã |
| | | - **è¾¹ç**ï¼åªå¨æç»å¾è¡¨æ¾ç¤ºå±èå
¥ï¼ä¸æ¹æ°æ®åºã䏿¹ç´¯è®¡ãå¿«æ¥ã忝ãç¯æ¯æè°æ´å£å¾ï¼è¯¥è§åç¨äºéæåææ¥åå¾ï¼ä¸è½è®©æåå¼ç¨å¾ä¸æ æ³å辨çå¾®å°å·®å¼ã |
| | | - **éªè¯**ï¼`HolidayTrendChartServiceTest` èç¦æµè¯éè¿ï¼å端å
¨é `mvn -q test -f traffic-audit-server\pom.xml` éè¿ãæ°å¢æµè¯è¦çâ两个ä¸åå弿 2 使 ç¾èå
¥åçæç¸åå ä½âåç¾åæ¯ç²¾åº¦æ¢ç®ãæµè¯æ¥åï¼`docs/åè½æµè¯æ¥å/åè½æµè¯æ¥å_æçº¿å¾ææ ç¾ç²¾åº¦ç»å¶_2026-10-06.md`ï¼æ¬å°çåï¼ä¸å
¥åºï¼ã |
| | | |
| | | ### 10-06ï¼è§æµç«å¹³åæ¥æ¤åº¦èµ°å¿å¾æ¹ä¸º 3 ä½å°æ° |
| | | |
| | | - **é®é¢**ï¼ç¨æ·å¯¼å
¥ 10-05 è§æµç«æ°æ®ååé¦ï¼æåä¸å¼ æ¥æ¤åº¦å¾ä¸ 10-04ã10-05 æ ç¾é½æ¾ç¤º `0.49`ï¼ä½ç¹ä½é«åº¦ä¸åã |
| | | - **æ ¹å **ï¼å¹³åæ¥æ¤åº¦åå¼åå«ä¸º `0.490890014472` å `0.492414492754`ï¼æçº¿ææªåæ´åå¼ç»å¶ï¼æ ç¾å为æå¤ 2 ä½å°æ°ï¼å¯¼è´æ ç¾ç²¾åº¦ä¸å¾å½¢ç²¾åº¦ä¸ä¸è´ï¼ä¸æ¯æ¥æ±æ»æç¹ä½è®¡ç®é误ã |
| | | - **ä¿®æ£**ï¼`HolidayTrendChartService.lineChart` æ°å¢å¯é `valueDecimals`ï¼é»è®¤ä»ä¸º 2ï¼ä»
âæ®éå½çéè§æµç«å¹³åæ¥æ¤åº¦âå¾ä¼ 3 ä½ã10-04ã10-05 ç°å¨å嫿¾ç¤º `0.491`ã`0.492`ï¼å
¶ä½å¾å£å¾ä¸åã |
| | | - **éªè¯**ï¼å端å
¨é `mvn -q test -f traffic-audit-server\pom.xml` éè¿ï¼éå¯ååªè¯»çææ° DOCXï¼OCR æ ç¾ä¸º `0.504/0.476/0.467/0.491/0.492`ï¼ä¸ç¨æ·æä¾çææ¡£é徿¯å¯¹ï¼15 å¼ å¾ä¸ä»
æ¥æ¤åº¦å¾ `image15` ååï¼å
¶ä» 14 å¼ SHA-256 å®å
¨ä¸è´ãæµè¯æ¥åï¼`docs/åè½æµè¯æ¥å/åè½æµè¯æ¥å_æ¥æ¤åº¦èµ°å¿å¾ç²¾åº¦_2026-10-06.md`ï¼æ¬å°çåï¼ä¸å
¥åºï¼ã |
| | | - **æ°æ®è¾¹ç**ï¼æªå¯¼å
¥ãè¦çæé导任ä½ä¸å¡æ°æ®ã |
| | | |
| | | ### 10-06ï¼å¾å¹´å½çéè°æ´å表补å
è°æ´åå¼ |
| | | |
| | | - **æ¹å¨**ï¼è°æ´å表æ¥å£å¨åä¸å¹´ä»½ãå䏿¥æãæ®éå½çéå£å¾ä¸ï¼ä» `holiday_vehicle_flow` è¡¥è¿å `originalValue`ï¼æ²¡æåå§è®°å½æ¶ä¿æç©ºï¼é¡µé¢ä¸æ¾ç¤ºå ä½ç¬¦ãè¡¨æ ¼æ°å¢âè°æ´åå½çé车æµéï¼ä¸è¾æ¬¡ï¼âï¼åâè°æ´åå½çé车æµâæ¹ä¸ºâè°æ´åå½çé车æµéâã |
| | | - **å¼¹çª**ï¼æ°å¢/ç¼è¾å¼¹çªåæ¥å¢å åªè¯»âè°æ´åå½çé车æµéï¼ä¸è¾æ¬¡ï¼âï¼ç¼è¾å·²æè®°å½æ¶å±ç¤º `originalValue`ï¼æ å弿æ°éæ©æ¥ææ¶ä¸æ¾ç¤ºå
容ãåæ¢å¹´ä»½ææ¥æä¼æ¸
空æ§åå¼ï¼é¿å
鿥å±ç¤ºã |
| | | - **鿥æå·æ°**ï¼æ°å¢æ¥å£ `GET /api/holiday/adjustments/source-value`ï¼ææé年份忥æå®æ¶è¯»åæ®éå½çéåå¼ï¼æ°å¢/ç¼è¾å¼¹çªéæ¥æåèªå¨å·æ°ï¼æªæ¥å°æºæ°æ®æ¶æ¾ç¤ºä¸ºç©ºãå端ç¨è¯·æ±åºå·ä¸¢å¼è¿å°çæ§è¯·æ±ï¼é¿å
å¿«éæ¢æ¥ææ¶ä¸²å¼ã |
| | | - **è¾¹ç**ï¼`originalValue` åªæ¯å表å±ç¤ºåæ®µï¼æ 记为 `@TableField(exist = false)`ï¼ä¸ååºãä¸åä¸è°æ´ãç¯æ¯åæ¯ã累计æèµ°å¿å¾è®¡ç®ã |
| | | - **éªè¯**ï¼`mvn -o test` 11 个æµè¯å
¨é¨éè¿ï¼`npm run build` éè¿ï¼æ¬å°éå¯ååªè¯»æ ¸å¯¹ 2025-10-03/04/05 åå¼åå«ä¸º `362.0035/337.9501/334.2040`ï¼æ æºæ°æ®ç 2025-10-09 è¿åç©ºãæµè¯æ¥åï¼`docs/åè½æµè¯æ¥å/åè½æµè¯æ¥å_å¾å¹´å½çéè°æ´åå¼å±ç¤º_2026-10-06.md`ï¼æ¬å°çåï¼ä¸å
¥åºï¼ã |
| | | |
| | | ### 10-06ï¼æ¹æ¡ D å·²å®ç°ï¼å¾å¹´å½çéè°æ´è¡¨ + ç¯æ¯åºæåªä½ç¨äºå䏿¥åæ¯ï¼ |
| | | |
| | | - **æç»å£å¾**ï¼`HolidayStatService.loadVehicleByDate` å
ç¨ `holiday_vehicle_adjustment` è¦ç对åºåå²å¹´ä»½çâ彿¥å¼âï¼ä¾èµ°å¿å¾ã忝ã累计åå¿«æ¥ä½¿ç¨ï¼éåæé âå䏿¥ç¯æ¯åæ¯âï¼è¥å䏿¥æ®éå½çé C åæå¼ï¼åç¨ C æ¿æ¢è¯¥æ¥å䏿¥å½çéå¼ï¼å¹¶æ®æ¤éç®éè¥ä¸æ§äººåæµå¨éãå
¬è·¯äººåæµå¨éåæ»è®¡ãææç¯æ¯è¡åªç»§æ¿è¿ä»½åæ¯ã |
| | | - **å
³é®è¾¹ç**ï¼C åªå½±åâä¸ä¸æ¥çç¯æ¯åæ¯âï¼ä¸å½±å C æå¨æ¥ç彿¥å¼ã累计æèµ°å¿å¾ï¼è°æ´è¡¨åªç»´æ¤åå²å¹´ä»½ï¼`year < å½å年份`ï¼ï¼2026 å¹´è°æ´ä»ç±å¯¼å
¥ Excel ç D åè¿å
¥å½æ¥å¼ã |
| | | - **æ¬è½®å®ç°**ï¼æ°å¢ `holiday_vehicle_adjustment` 表å mapper/service/controllerï¼å端å¨ä¸é¡¹ç»è®¡-è忥页çâèåæ¥æ°æ®âåâè§æµç«åæ¥è°æ¥æ°æ®âä¹é´æ°å¢âå¾å¹´å½çéæ°æ®è°æ´âå¡çï¼åç»æç¨æ·æè®®ä¿æè¯¥ä½ç½®ï¼ï¼æ¯æå¹´ä»½çéãæ°å¢ãç¼è¾ãå é¤ååå ççï¼é¨ç½²èæ¬ä¸ migration æ¸
å已忥ã |
| | | - **10-04 导å
¥é¢æ£ï¼åªè¯»ï¼ä¸ååºï¼**ï¼ç¨ `C:\Users\jcxiong\Downloads\2026å¹´å䏿°æ® (2).xlsx` 以 `allowHistoricalOverwrite=false` è°å¯¼å
¥æ¥å£ï¼è¿åç¡®è®¤é¡¹ï¼ |
| | | - `第4æ¥ 2026-10-04 æ®éå½çéï¼åå¼ 323.6551ï¼å¯¼å
¥å¼ 322.7695` |
| | | - `第3æ¥ 2026-10-03 æ®éå½çéç¯æ¯åºæï¼åå¼ ç©ºï¼å¯¼å
¥å¼ 320.2418` |
| | | å æ¤ C5 ç±ç©ºåæå¼ä¸ä¼ç¡¬æç»å¯¼å
¥ï¼åªä¼è¦æ±åå²åæ´ç¡®è®¤ï¼ç¨æ·ç¡®è®¤åå³å¯éè°æ´åç Excel é导åå
¥ã |
| | | - **å½ååºç¶æ**ï¼æ¬å° `traffic_audit.holiday_vehicle_adjustment` å·²æ§è¡ DDLï¼0 è¡ï¼ï¼2026-10-04 ä¸å¡å¼ä»ä¸ºæ§å¼ `323.6551`ï¼çå¾
ç¨æ·èªè¡éå¯¼è°æ´åç Excelãå©ææªæ§è¡ä»»ä½ä¸å¡æ°æ®å¯¼å
¥ãè¦çæé导ã |
| | | - **éªè¯**ï¼å端 `mvn -o test` 9 个æµè¯å
¨ç»¿ï¼å« C5 åªæ¹ 10-04 ç¯æ¯åæ¯ãåå²è°æ´è¿å
¥ 2025 彿¥å¼ï¼ãå端 `npm run build` éè¿ï¼é¡µé¢å®æµâå¾å¹´å½çéæ°æ®è°æ´âå¡ç䏿°å¢å¼¹çªæ£å¸¸ã |
| | | - **åæ´å£å¾ï¼ç¨æ· 2026-10-06 确认ï¼**ï¼ä¸åâä»åå§å¼ä¸æ¬¡åæ´âçå¤éãå¯ä¸è¦æ±æ¯ä¸ Excel æ¾ç¤ºå¼ä¸è´ï¼å
ææº Excel åå
æ ¼æ ¼å¼çææ¾ç¤ºå¼ï¼åæå¿«æ¥ç»ä¸ 1 ä½å°æ°ã10-04 æ»è®¡ç¯æ¯åå§å¼çº¦ `4.5481%`ï¼æºæ ¼æ¾ç¤º `4.55%`ï¼å¿«æ¥å 1 ä½å¾å° `4.6%`ï¼è¿å±äºæ¢å®å£å¾ï¼ä¸æ¯éè¦ä¿®æ£çè¾¹çé®é¢ã |
| | | |
| | | > 说æï¼æ¬æä»¶æ©åâç¯æ¯åºæåªä½å¤ç¨ãä¸åä¸è®¡ç®âçæ®µè½å·²ä½åºãè¯¥çæ¬éå¯¹çæ¯æ§å®ç°è¯¯æ C æ¾å° C æå¨æ¥æ¬èº«è®¡ç®çé®é¢ï¼èªæ¹æ¡ D èµ·ï¼C çæ£ç¡®è¯ä¹æ¯**å䏿¥ç¯æ¯åæ¯çè°æ´å¼**ã |
| | | |
| | | ### 10-06ï¼å·²ä½åºï¼è§ä¸æ¹æ¹æ¡ Dï¼ï¼æ®éå½çéãç¯æ¯åºæãæ¹ä¸ºäººå·¥å¤ç¨å¼ï¼ä¸ååä¸è®¡ç®ï¼ |
| | | |
| | | - **èæ¯**ï¼10-04 éå·æ¨å®¶åºç«ä¸å°å®¢æµéæå´ä¸è·ï¼35419 -> 8856ï¼ï¼é¢å¯¼è¦æ±åé¤ãæå·¥ç¨¿å¿«æ¥çåæ³æ¯ï¼å
ç¨åç«åçæ°å¼ç®åº 10-04 ç¯æ¯ï¼æ 10-03 å½çé弿ªå°ãç¯æ¯åºæãC åå¤ç¨ï¼åæ 10-03 æ¢å¤åå¼ä»¥å
å½±å累计ãç¨æ·æç¡®å£å¾ï¼**ãç¯æ¯åºæãåªç¨äºæ°æ®å¤ç¨ï¼ä¸åä¸ä»»ä½è®¡ç®**ã |
| | | - **æ§è¡ä¸ºï¼éï¼**ï¼`HolidayStatService.nationalProvincialRingRatio` å¨ 2026 å¹´åªè¦ `comparison_base` æå¼å°±æã彿¥ / ç¯æ¯åºæ - 1ãç®å½çéç¯æ¯ï¼å¯¼åºæ¶ E åå `=(D{è¡}-C{è¡})/C{è¡}`ãææå·¥ç¨¿ç C åææ³ï¼åºææ¾å¨è¢«æ¯æ¥é£è¡ï¼ï¼è¿æ ·ç®è¿ä¼éä½ï¼10-03 ç¯æ¯è¢«ç®æ +1.11%ï¼ã |
| | | - **æ°è¡ä¸ºï¼æ¬æ¬¡å·²æ¹ï¼**ï¼å½çéç¯æ¯åºå®ã彿¥ / å䏿¥ - 1ãï¼2024/2025/2026 ä¸è´ï¼`comparison_base` åªå¨å¯¼å
¥/导åº/页é¢å±ç¤ºæ¶åæ ·ä¿åä¸ååï¼ä¸åä¸ç¯æ¯ãæç®ã累计ãå¯¼åº E åæ¹ä¸º `=(D{è¡}-D{ä¸ä¸æ¥è¡})/D{ä¸ä¸æ¥è¡}`ï¼C åç
§å¸¸ååå¤ç¨å¼ã |
| | | - **æ¹å¨**ï¼`HolidayStatService.java`ï¼`nationalProvincialRingRatio` 廿 year/comparisonBase å½¢åï¼ã`HolidayWorkbookService.java`ï¼æ°å¢ `ringRatioFormula`ï¼å¯¼åºæä¸ä¸è¡åå¹´ D åï¼ã`docs/database.md`ï¼`comparison_base` ä¸è®¡ç®å£å¾ä¸¤å¤ï¼ã |
| | | - **éªè¯**ï¼æ°å¢ `HolidayComparisonBaseTest`ï¼â åºå
å `comparison_base=320.2418` æ¶ï¼10-04 å½çéç¯æ¯ä»æå䏿¥ 323.7837 计ç®ï¼ä¸ä¸çäºæåºæç®åºçå¼ï¼â¡ 导åºå
¬å¼ = `=(D6-D5)/D5`ã`mvn -o test` å
¨ç»¿ï¼4 个æµè¯ç±»ï¼ã |
| | | |
| | | ### 10-06ï¼å¾
确认ââ10-04 å½çéåç«ï¼éå·æ¨å®¶åºï¼æä¹è¿ç³»ç» |
| | | |
| | | - **æå·¥å£å¾å·²éé¡¹æ ¸å¯¹ï¼ä¸åºå
`observation_station_flow` å®å
¨å¯¹å¾ä¸**ï¼ |
| | | - 10-04 å½çéå¼ï¼2026/2025/2024ï¼= åå¼ â æ¨å®¶åºå½æ¥å¼ -> 322.7695 / 337.5009 / 322.5277 ä¸è¾æ¬¡ï¼æ¨å®¶åº 8856 / 4492 / 2328ï¼ã |
| | | - 10-04 ç¯æ¯åæ¯ = 2026-10-03 åå¼ â æ¨å®¶åº 10-03 å¼ = 323.7837 â 3.5419 = **320.2418** ä¸è¾æ¬¡ -> ç¯æ¯ = 322.7695/320.2418 â 1 = **+0.79%**ã |
| | | - 10-03 æ¾ç¤ºå¼ä¸ç´¯è®¡ä»ç¨åå¼ 323.7837ï¼è¢«åç 320.2418 åªè½å° C åå¤ç¨ã |
| | | - **è¶å¿ç»è®ºï¼éå
è·é¢å¯¼å¯¹é½ï¼**ï¼åªåæ¨å®¶åºï¼10-04 å½çéç¯æ¯åªæ **+0.79%**ï¼åæ¥é«éå°å®¢è½¦ **+9.78%**ãéè·¯ +9.94%ãæ°èª +11.69%ãéè·¯ +4.99%ãæ°´è·¯ +4.33%ã**åä¸ç«å¡«ä¸å¹³çº¦ 9 个ç¾åç¹çè¶å¿å·®**ï¼ä¸ã让å½çéåé«éè¶å¿ä¸è´ãç颿并ä¸ç¸ç¬¦ï¼å»ºè®®åæ¶è¯·éå·æ ¸å®åå§è®°å½ã |
| | | - **å¾
å³ççä»£ç æ¹æ¡**ï¼å `comparison_base` å·²å®ä¸ºçº¯å¤ç¨ï¼ç³»ç»æ æ³ä»ã彿¥å¼ã忍åç«åºæï¼éè¦å¦è®¾æºå¶ãæ¨èæ°å¢ãå¼å¸¸ç«åé¤ãé
ç½®ï¼ç«ç¹ç¼å· + çææ¥æ + 年份ï¼ï¼ç®ç¯æ¯åæ¯æ¶ç¨ `observation_station_flow` çã该æ¥ç«ç¹å计 â åç«å¼ãä½åºæï¼æ¾ç¤ºå¼ä¸ç´¯è®¡ä»ç¨åå¼ã |
| | | - **å³çç¹**ï¼â åæå¯é
ç½®çéç¨ãå¼å¸¸ç«åé¤ãï¼æ¨èï¼å¯å¤ç¨äºå
¶ä»è忥/ç«ç¹ï¼ï¼è¿æ¯åªææ¬æ¬¡ 10-04 䏿¬¡æ§å¤çï¼â¡ åç«åªå½±åå½çéç¯æ¯åæ¯ï¼è¿æ¯ä¹è¦ä¸æ²å°éè¥ä¸æ§å°å®¢è½¦/å
¬è·¯/å
¨ç¤¾ä¼çç¯æ¯ï¼æå·¥ç¨¿å®é
æ 4.17% / 4.55% ä¹ä¸èµ·æ¹äºï¼ã |
| | | |
| | | ## ææ°ï¼2026-10-05ï¼ |
| | | |
| | | ### 10-05 ä¸åï¼å
³æºå¿«ç
§ï¼å½åç¶æï¼ |
| | | |
| | | - **已宿**ï¼v1.0.0 æ£å¼åå¸å
`deploy/traffic-audit-deploy-20261005-v1.0.0/`ï¼zip **77.78 MB**ï¼å·²çæï¼å»å¯èªæ£ãçæ¬èªæ£ãç»ææ ¸å¯¹ä¸çå®å¯å¨åçåéè¿ï¼å¾
ä¸ä¼ æå¡å¨ãçæ¬ç®¡ç代ç ä¸ææ¡£åå·²æäº¤æ¨éï¼`main` = `93b36c8`ï¼ã |
| | | - **è¿è¡ç¶æ**ï¼å
³æºåæ¬æº dev æå¡ä»å¨è¿è¡ï¼å端 8090 java PID 64112ãå端 8080 node PID 46472ï¼ï¼éå
³æºåæ¢ï¼æ éå¤çï¼æå
åçç¨ç 8095 å®ä¾å·²åæ¢ã端å£å·²éæ¾ã |
| | | - **å·¥ä½åº**ï¼é¤è¥å¹²æ¬å°ä¸´æ¶æä»¶ï¼`_tmp_*`ï¼ä¸ä¸ä»½ä¸é¡¹ç»è®¡ xls å¤ï¼git å·¥ä½åºä¸è¿ç«¯ `main` ä¸è´ï¼`docs/工使¥ç»/`ã`docs/åè½æµè¯æ¥å/` çæçº¦å®ä¸å
¥åºã |
| | | - **䏿¬¡æ¥æ**ï¼å
è¯»æ ¹ç®å½ `AGENTS.md` + æ¬æä»¶ï¼å¾
åè§ç¬¬ä¸èï¼æ°å¼ç²¾åº¦ç»ä¸æ¹é ãæå¿æç¥å¾ãAI 婿 RAGãç»æµææ è¯ç®çï¼ï¼é¦è¦äºé¡¹æ¯è·è¿æ£å¼å
卿å¡å¨çé¨ç½²ç»æã |
| | | |
| | | ### 10-05 ä¸åï¼æ£å¼åå¸å
ææ°çæ¬ç®¡çéæ°çæï¼v1.0.0ï¼å端å£ç¦å
ï¼ |
| | | |
| | | - **èæ¯**ï¼`8b71049` å·²ææå
èæ¬æ¹ä¸ºåºå® jar å + çæ¬èªæ£ï¼`traffic-audit-server.jar`ãå
å带 `v{version}`ï¼ï¼ä½æ£å¼åå¸å
è¿æ²¡ææ°èæ¬éæ°çæï¼çæ¬æ¹é å产åºç `deploy/traffic-audit-deploy-20261005-2/` 仿¯æ§ jar å `traffic-audit-server-1.0.0-SNAPSHOT.jar`ã |
| | | - **éæ°çæ**ï¼`powershell -NoProfile -ExecutionPolicy Bypass -File pack-deploy.ps1 -NoFrontendDist -SkipServiceWrapper`ï¼å端 clean package é建ãå端 `npm run build` é建ï¼ã |
| | | - **产ç©**ï¼`deploy/traffic-audit-deploy-20261005-v1.0.0/`ï¼351 æä»¶ / 94.58 MBï¼ï¼zip **77.78 MB**ã |
| | | - **æ ¡éª**ï¼å»å¯èªæ£ + çæ¬èªæ£éè¿ï¼pom / `app-version.properties` / `MANIFEST.MF` ä¸è
å `1.0.0`ï¼ï¼å
å
`backend\traffic-audit-server.jar` 为åºå®åï¼æ `frontend/`ãæ `trafficAudit.exe/.xml/install-service*`ï¼`backend\web` 44 æä»¶ä¸ `frontend\dist` éæä»¶ SHA-256 ä¸è´ï¼`README-é¨ç½²è¯´æ.txt` 头é¨ä¸ºã20261005ï¼çæ¬ v1.0.0ã并嫿§æå¡å¨ä¸æ¬¡æ§å级说æã |
| | | - **å¯å¨åçï¼çå®å
ãåªè¯»ï¼**ï¼ç¨æ¬å°å¼ååºé
ç½®å¨ 8095 端å£å¯å¨å
å
jarï¼`GET /api/version` è¿å `{"version":"1.0.0",...}`ï¼`GET /` è¿åç»å½é¡µ 200ï¼éªè¯åå·²æ PID 58652 ç²¾ç¡®åæ¢åçè¿ç¨ã |
| | | - **ç¶æ**ï¼å¾
ç¨æ·ä¸ä¼ æå¡å¨ã`deploy/traffic-audit-deploy-20261005-2/`ï¼åæ´æ©ç `20261005/`ï¼ä½åºå¿ç¨ãæ§æå¡å¨é¦æ¬¡å级æ READMEï¼å
æ¹ `trafficAudit.xml` ç `<arguments>` 为 `-Dfile.encoding=UTF-8 -jar "%BASE%\traffic-audit-server.jar"` 忢 jarï¼ä»¥ååªæ¿æ¢ jarã |
| | | |
| | | ### 10-05ï¼çæ¬ç®¡çéç¨æ¹æ¡ Aï¼åºå® jar åï¼ç¨æ·éªæ¶éè¿ï¼ |
| | | |
| | | - **ç¨æ·å³ç**ï¼éç¨æ¹æ¡ Aã`traffic-audit-server/pom.xml` 为å¯ä¸çæ¬æºï¼å½å对å¤çæ¬ `1.0.0`ã |
| | | - **æå»ºæ¹é **ï¼`<finalName>traffic-audit-server</finalName>`ï¼äº§ç©åºå®ä¸º `target/traffic-audit-server.jar`ï¼Maven èµæºè¿æ»¤çæ `app-version.properties`ï¼Spring Boot ç¶ POM ä½¿ç¨ `@project.version@` åé符ï¼ï¼`maven-jar-plugin` åå
¥ `Implementation-Version`ã |
| | | - **页颿¾ç¤º**ï¼æ°å¢å
ç»å½ `GET /api/version`ï¼ç»å½é¡µå页头æ¾ç¤º `V1.0.0`ãåæ¶ä¿®å¤å¯æ§è¡ jar ä¸ `buildTime` 为 `null` çåµå¥ URI è§£æé®é¢ã |
| | | - **åå¸é¾è·¯**ï¼æå¡ XMLãå¯å¨èæ¬ãæå
èæ¬å
¨é¨æ¹ç¨åºå® jar åï¼`pack-deploy.ps1` 伿 ¡éª pom / `app-version.properties` / `MANIFEST.MF` ä¸è
çæ¬ä¸è´ï¼åå¸å
å为 `traffic-audit-deploy-YYYYMMDD-v1.0.0(.zip)`ï¼å¹¶è¾åºå¸¦ `{version}` çé¨ç½² READMEã |
| | | - **æ§æå¡å¨é¦æ¬¡å级ï¼ä»
䏿¬¡ï¼**ï¼åæ¢æå¡åï¼æ `trafficAudit.xml` ç `<arguments>` æ¹ä¸º `-Dfile.encoding=UTF-8 -jar "%BASE%\traffic-audit-server.jar"`ï¼æ¿æ¢ jar åå¯å¨ã以ååçåªæ¿æ¢ jar å¹¶éå¯ï¼ä¸åæ¹ XMLã |
| | | - **éªè¯**ï¼`mvn -o test`ã`mvn -o clean package -DskipTests`ãåºå®å jar å®é
å¯å¨å¹¶è®¿é® `/api/version`ãå端 `npm run build`ãæ¬å°æµè§å¨ç»å½é¡µæ¾ç¤º `V1.0.0`ã`pack-deploy.ps1 -SkipFrontend -NoFrontendDist -SkipServiceWrapper` ç»è£
/å»å¯/çæ¬èªæ£å
¨é¨éè¿ï¼è´åéªè¯ç¡®è®¤çæ¬ä¸å¹é
å `SNAPSHOT` é½ä¼å¨æå
å䏿¢ãæµè¯æ¥åï¼æ¬å° `docs/åè½æµè¯æ¥å/åè½æµè¯æ¥å_çæ¬ç®¡çæ¹æ¡A_2026-10-05.md`ï¼ç®å½ä¸å
¥åºï¼ã |
| | | - **ç¶æ**ï¼çæ¬ç®¡çä»£ç æäº¤ä¸º `8b71049`ï¼`main` 已忥ï¼`v1.0.0` å·²æ¨éå°å
¬å¸ Gitblitå¹¶æå `21a5bf7`ã2026-10-05 ç¨æ·ç¡®è®¤æ¬å°ç»å½é¡µåææç»å½å页é¢åæ£ç¡®æ¾ç¤º `V1.0.0`ãæ£å¼åå¸å
å·²äº 10-05 ä¸åç¨æ°çæå
èæ¬éæ°çæï¼è§ä¸æ¹ 10-05 ä¸åä¸èï¼ã |
| | | |
| | | ### 10-04 æ·±å¤ï¼æçº¿æ°å¼æ ç¾åå°è´´è¿æ¬ç¹ï¼v35ï¼å¾
ç¨æ·éªæ¶ï¼ |
| | | |
| | | - **ç¨æ·åé¦**ï¼v31ãé¿å¼æçº¿ãçæ¬ä¸å¦ä¸ä¸ç v27 ââ è½ç¶æ ç¾ä¸å线äºï¼ä½å¾å¤æ ç¾ç¦»èªå·±çæ°æ®ç¹å¤ªè¿ï¼ä¸ç¼çä¸åºæ çæ¯åªä¸ªç¹ãè¦æ±åå°è´´è¿æ¬ç¹çææã |
| | | - **æ°é®é¢ä¸æ§éæ±çå²çª**ï¼æ´æ©çåå§è¯æ±æ¯ãæ°åå线ç䏿¸
ãï¼è¿æ¬¡è¯æ±æ¯ãæ°åå¿
é¡»è´´è¿æ¬ç¹ãã两è
å¹¶åï¼å¿
é¡»åæ¶æ»¡è¶³ã |
| | | - **å®ç°ï¼`HolidayTrendChartService`ï¼**ï¼ |
| | | - æ ç¾æç**宿´åéå° v27 ç®æ³**ï¼æåæ¥ç¹ä½è·ç¦»å代价ï¼`ownDistance Ã10`ï¼ï¼åéå«è¿/è¿çä¸ãä¸ãå·¦ãå³ååè§ï¼å¨æè§åä¿è¯åæ¥æ ç¾ä¸ç¹ä½ä¸ä¸é¡ºåºä¸è´ï¼`lineLabelCost`ã`lineLabelCandidates`ï¼ãv31 ççº¿æ¡æ©èãChamfer è·ç¦»åºãé¢è²äº²å项å
¨é¨ç§»é¤ã |
| | | - **æ°å¢ç²ç½æè¾¹ï¼haloï¼** è§£å³ãå线ç䏿¸
ãï¼`drawLabelWithHalo` ç¨ `TextLayout` åå形轮å»ï¼å
以ç½è² `HALO_OUTLINE_WIDTH=4.0f` åè§æè¾¹ï¼åç¨æ¬è²å¡«å
ãæ°åå³ä½¿è½å¨æçº¿ä¸ä¹æ¸
æ°å¯è¯»ï¼ä»èå
许æ ç¾è´´åæ¬ç¹ãæçº¿å¾ãå å å¾å¤ç½®æ ç¾ã饼徿 ç¾ç»ä¸èµ°è¯¥æ¹æ³ã |
| | | - 顺带æ¸
çè°è¯éçï¼å é¤ `LABEL_DEBUG`/`CHART_SEQ`/`TRENDDBG` æå°ï¼ä»¥åä¸å使ç¨ç `lineNormal(...)` ä¸ `DayLabel.normalX/normalY`ã |
| | | - **éåéªè¯ï¼RapidOCR æ ç¾å
å´æ¡ä¸å¿ â ç宿°æ®ç¹åæ ï¼**ï¼æ ç¾å°æ¬ç¹å¹³åè·ç¦» v27=`36.8px`ãv31=`53.2px`ãv35=`36.7px`ï¼è¶å°è¶è´´è¿æ¬ç¹ï¼ãv35 ä¸ç¨æ·è®¤å¯ç v27 åºæ¬ä¸è´ï¼ææ¾ä¼äºè¢«å¦ç v31ã |
| | | - **å
¶ä½éªè¯**ï¼`mvn compile`/`mvn test` éè¿ï¼æ ç¼è¯åè¦éçï¼ï¼Word COM 8 页 15 å¾ï¼1+2Ã7ï¼ï¼RapidOCR 夿£ 12 å¼ æçº¿å¾æ°å¼æ ç¾å
å´æ¡éå æ° = 0ï¼`label_affinity` 夿£å¾ 11 æ¯ä¸ªæ°æ®æ ç¾å°æ¬çº¿ 3~8px 䏿¬çº¿æ¯æè¿çº¿ï¼æ ãCLOSER TO å
¶ä»è²ãï¼ï¼æ¾å¤§ 6Ã å¤æ ¸å¾ 11 左䏿 ç¾ç¡®ä¸º `630.99`ï¼ä½å辨ç OCR æ¾è¯¯è¯»ä¸º `1630.99`ï¼ãv27âv35 åç´ å·®çº¦ 1%ï¼éä¸å¨æ ç¾å¤ï¼å³ç½æè¾¹çæä¸æªç ´åæçº¿ã |
| | | - **产ç©ä¸ç¶æ**ï¼`_tmp_holiday_verify/holiday_trend_2026-10-04_v35.docx`ï¼ä¸è½½å¯æ¬ `C:\Users\jcxiong\Downloads\2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾ï¼ä¿®æ¹ç-æ°åè´´è¿æçº¿-ç½è¾¹æ¸
æ°ï¼.docx`ãæªå¯¼å
¥ãè¦çæé坼任使°æ®ãå¾
ç¨æ·éªæ¶ï¼éªæ¶åä¸æäº¤ã䏿¨éï¼ç°æ `deploy/traffic-audit-deploy-20261004.zip` 仿¯ v24 è¡ä¸ºï¼éªæ¶åéé建ã |
| | | |
| | | ### 10-05ï¼èµ°å¿å¾ v35 éªæ¶éè¿ + å端å£åå¸å
ï¼-NoFrontendDist -SkipServiceWrapperï¼ |
| | | |
| | | - **éªæ¶**ï¼v35ï¼æçº¿æ°å¼æ ç¾åå°è´´è¿æ¬ç¹ + ç½æè¾¹ï¼2026-10-05 ç¨æ·éªæ¶éè¿ã |
| | | - **æå
éæ±ï¼ç¨æ·æ¾æ¸
ï¼**ï¼æå¡å¨å端å£ï¼åªéè¦ `backend\web`ï¼ä¸è¦ç¬ç«ç `frontend/dist`ï¼ä¸ºä¿è¯å
宿´ï¼`backend\web` + jar + docs é½è¦ãæå¡å¨é馿¬¡åå¸ãæå¡å·²è£
ï¼å¯å `-SkipServiceWrapper` 廿æå¡æ³¨åå¨ã |
| | | - **æå
å·¥å
·ä¿®æ£**ï¼å
åå ç `-NoFrontend` è¯ä¹éäºï¼æ `backend\web` ä¹å»æäºï¼ï¼ç°æ¹ä¸º `-NoFrontendDist`ãå®åªè·³è¿ç¬ç« `frontend\` ç®å½ï¼ä¸å¤å¶ dist / nginx 示ä¾ï¼ï¼**ä»æå»ºï¼æè¦æ±ï¼dist 并忥 `backend\web`**ï¼å端å£å¿
éï¼å¦åé¡µé¢ 404ï¼ã`-SkipFrontend` ä»ä¸ºâä¸é建ãå¤ç¨ç°æ distâã |
| | | - **åå¸å
**ï¼`deploy/traffic-audit-deploy-20261005-2/`ï¼351 æä»¶ / 94.55 MBï¼zip **77.78 MB**ï¼ãå·²æ ¸å¯¹ï¼æ `frontend/`ãæ `trafficAudit.exe/.xml/install-service*`ï¼æå¡å
è£
å¨å·²æé¤ï¼ï¼`backend\web` 44 æä»¶ä¸ `frontend\dist` éæä»¶åå¸ä¸è´ï¼jar å
ç½®é
ç½®å
¨å ä½ç¬¦ï¼å»å¯èªæ£éè¿ï¼ï¼å
å
`HolidayTrendChartService.class` ä¸ `target/classes` ä¸è´ï¼å« v35 代ç ï¼ãè¾ä¸ä¸å®æ´å
ï¼110.49 MB / zip 83.77 MBï¼ç约 16 MB è§£å / 6 MB å缩ã |
| | | - **ä½åº**ï¼`deploy/traffic-audit-deploy-20261005/`ï¼é误è¯ä¹ä¸ç产ç©ï¼ç¼º `backend\web`ï¼å·²è¢« `-2` å代ï¼å¿ç¨ã |
| | | - **é¨ç½²æ³¨æ**ï¼è¦ç jarï¼åæé `application.yml`ï¼å³å¯ï¼`backend\web` å·²éå
æä¾ãæå¡å·²è£
ï¼æ ééæ°æ³¨åã |
| | | - **ç¶æ**ï¼å·²æå
ï¼å¾
ç¨æ·ä¸ä¼ å°æå¡å¨ï¼è¯¥å
ä¸ºçæ¬æ¹é å产ç©ï¼å·²ç± 10-05 ä¸åç `traffic-audit-deploy-20261005-v1.0.0` å代ï¼å¿ç¨ï¼ã |
| | | |
| | | ### 10-04 æ·±å¤ï¼æçº¿æ°å¼æ ç¾é¿å¼æçº¿å¹¶è´´è¿æ¬çº¿ï¼v31ï¼å¾
ç¨æ·éªæ¶ï¼ |
| | | |
| | | - **ç¨æ·åé¦ï¼é¢å¯¼çåï¼ä¸¤è½®ï¼**ï¼â å年份æ°å离æ¬çº¿æè¿æè¿ï¼å¸æè´´è¿åèªè²çº¿ä¸ä¸éä¹±ï¼â¡ 仿æ°ååå¨æçº¿ä¸ç䏿¸
ï¼ç¬¬ä¸è½®åªæ¯âè´´è¿âï¼æ ç¾ä¼è½å¨çº¿ä¸ï¼ãç¨æ·å»ºè®®ï¼è°çºµè½´å»åº¦æå¼æçº¿ãæå å·¦å³é¿è®©è®©æ ç¾ä¸å线ã |
| | | - **å®ç°ï¼`HolidayTrendChartService`ï¼**ï¼ |
| | | - æ ç¾æçæ¹ä¸ºã卿è§å + 线æ¡åç´ é¿è®©ããå¾è¡¨å
ç»æçº¿åæ è®°ï¼åæææç³»åé¢è²åç´ ï¼å®¹å·® 45ï¼åææ©èï¼å¯¹æ¯ä¸ªåéæ ç¾ä½ç½®ç»è®¡å
¶è¦çç线æ¡åç´ æ°ï¼`Ã700` éç½ââæ°åä¸åå线ã |
| | | - ä¸ºæ¯æ¡ç³»åå»ºç« Chamfer è·ç¦»åºï¼å°è¯¥è²æè¿åç´ çè·ç¦»ï¼ã代价æ°å¢ä¸¤é¡¹ï¼`max(0, èªå·±é£æ¡çº¿çè·ç¦»-3)Ã40`ï¼ææ ç¾æåæ¬çº¿æï¼é²æ¢é£è¿ï¼ä¸é¢è²äº²å项 `max(0, èªå·±çº¿è· - å«ççº¿è· + 4)Ã200`ï¼è¦æ±æ¬çº¿æ¯æè¿ç线ï¼é¿å
è´´å°å«çç³»åï¼ã |
| | | - æ§çã离å«ç忥ç¹ä½æ´è¿ãæ©ç½ç± `Ã5000` éå° `Ã400`ï¼é¿å
å®åå亲å项ï¼`ITEM_LABEL_GAP` 12â10ï¼åéå«è¿/è¿çä¸ãä¸ãå·¦ãå³ååè§ã |
| | | - ä¿ç忥æ ç¾ä¸æ°æ®ç¹ä¸è´çä¸ä¸é¡ºåºï¼é¿å
æ ç¾ä¸æçº¿ä¸ä¸å转ã |
| | | - **è°åè¿ç¨æç¤º**ï¼v27ï¼åªæç¹ä½è·ç¦»æåºï¼â æ ç¾å线ï¼v28ï¼åªå è¦çåç´ æ©ç½ï¼â æ ç¾ç¦»æ¬çº¿ãè´´å°å«çè²çº¿æï¼v31ï¼è¦çæ©ç½ + æ¬çº¿è·ç¦» + é¢è²äº²åï¼â 两è
å
¼é¡¾ãéªè¯æ ç¾æ¯å¦âè´´é线âä¸è½åªç OCR å
å´æ¡å°å¼è²çè·ç¦»ï¼ç¸é»çå¼è²**æ ç¾**ä¼è¢«è¯¯å¤ä¸ºå¼è²**线**ï¼å¿
é¡»èç¼æ ¸å¯¹æ´å¾ã |
| | | - **éªè¯**ï¼`mvn test -f traffic-audit-server\pom.xml` éè¿ãçå®åºåªè¯»çæ `_tmp_holiday_verify/holiday_trend_2026-10-04_v31.docx`ï¼Word COM 8 页 15 å¾ï¼1+2Ã7ï¼ãRapidOCR 夿£ 15 å¼ å¾æ°å¼æ ç¾å
å´æ¡éå æ°ä¸º 0ãèç¼æ ¸å¯¹å¾ 1ï¼è·¨åºåäººåæµå¨éï¼ãå¾ 2ï¼ç´¯è®¡ï¼ãå¾ 4ï¼èªé©¾ï¼ãå¾ 6ï¼æ°´è·¯ï¼ãå¾ 9ï¼é«éï¼ãå¾ 11ï¼è½¦æµæ»éï¼ï¼æ°å¼æ ç¾åç¦»å¼æçº¿ãä½äºåèªè²çº¿æãé¢è²å¯¹åºæ£ç¡®ï¼å¾ 11 ç 630.99/601.04/571.31 åå«ç´§è´´è/红/绿æçº¿ã |
| | | - **产ç©ä¸ç¶æ**ï¼`C:\Users\jcxiong\Downloads\2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾ï¼ä¿®æ¹ç-æ°å¼æ ç¾é¿å¼æçº¿ï¼.docx`ãæªå¯¼å
¥ãè¦çæé坼任使°æ®ãå¾
ç¨æ·éªæ¶ï¼éªæ¶åä¸æäº¤ã䏿¨éï¼ç°æ `deploy/traffic-audit-deploy-20261004.zip` 仿¯ v24 è¡ä¸ºï¼ééªæ¶åé建ã |
| | | |
| | | ### 10-04 æï¼èµ°å¿å¾ 3 é¡¹åæ è½´è°æ´ï¼v24ï¼ç¨æ·å·²éªæ¶ï¼ |
| | | |
| | | - **ç¨æ·ä¿®æ¹é¡¹**ï¼â åæ è½´åä½ï¼ä¸äººæ¬¡ãä¸è¾æ¬¡ãå
¬é/å°æ¶ãæ¥æ¤åº¦ãæ¥æï¼æ¹æåå»åº¦ä¸æ ·çé¢è²ï¼â¡ 两个å å å¾çåæ è½´æ¹æä¸å
¶ä»å¾ä¸æ ·çé»è²ï¼â¢ âä¸äººæ¬¡/å æ¯âç»§ç»æ¾å¨å å å¾å³ä¸è§ã |
| | | - **å®ç°**ï¼`HolidayTrendChartService` æ `drawUnitLabel`ã`drawUnitLabelTopRight`ã`drawDateLabel` çæåè²ç±é»è²æ¹ä¸ºå»åº¦ç° `(102,102,102)`ï¼æ¨ªåå å å¾å¨æ°æ®åºåºè¾¹è¡¥ç» `2.2px` 纯é»åºçº¿ï¼è¦ç JFreeChart é»è®¤åç°ç 1px 轴线åæé¯é½¿è¾¹ç¼ï¼ä¸æ·»å ç®å¤´ãæçº¿å¾é»è²åæ è½´é»è¾æªæ¹ï¼å å å¾åä½ä»ç»å¶å¨å³ä¸è§ã |
| | | - **éªè¯**ï¼`mvn test -f traffic-audit-server\pom.xml` éè¿ãçå®åºåªè¯»çæ `_tmp_holiday_verify/holiday_trend_2026-10-04_v24.docx`ï¼Word COM 宿µ 8 页ã15 å¼ å¾ãåç´ æ£æ¥ï¼æçº¿å¾å·¦ä¸åä½ãæ¥æåå å å¾å³ä¸åä½å为 `(102,102,102)`ï¼å¾ 12/13 åºè½´å¨ y=529 为纯é»è¿ç»çº¿ï¼æ®éæçº¿å¾åºè½´ä»ä¸ºçº¯é»ã |
| | | - **产ç©ä¸ç¶æ**ï¼`C:\Users\jcxiong\Downloads\2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾ï¼ä¿®æ¹ç-ææ°ï¼.docx` 彿¶è¢« Word/WPS å ç¨ï¼å·²å¦å为 `C:\Users\jcxiong\Downloads\2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾ï¼ä¿®æ¹ç-ææ°-åæ è½´è°æ´ï¼.docx`ï¼åâææ°âæä»¶æªè¦çãæªå¯¼å
¥ãè¦çæé坼任使°æ®ã2026-10-04 ç¨æ·å·²éªæ¶éè¿ï¼éæ¬æ¬¡æäº¤æ¨éã |
| | | |
| | | ### 10-04 æï¼èµ°å¿å¾ 4 项修æ¹ï¼v23ï¼å¾
ç¨æ·éªæ¶ï¼ |
| | | |
| | | - **ç¨æ·ä¿®æ¹é¡¹**ï¼â æçº¿æ°å¼æ ç¾ä¸æçº¿ä¸ä¸é¡ºåºåäºï¼ç´¯è®¡æµå¨éå¾ï¼è线å¨ä¸ãæ ç¾å´å¨ä¸ï¼ç¬¬ä¸å¼ å¾é¦æ¥æé«æ¯èç¹ãæè¿çæ ç¾å´æ¯æ£è²ï¼ï¼â¡ å
¬è·¯åºè¡ç»æå¾ä¾æèªé©¾æ¾åé¢ï¼æ£è²ï¼ï¼ä¸æ¯æ¥å®¢æµææä¸è´ï¼â¢ æ¨ªåæ åºä¸ºä¸äººæ¬¡/å æ¯ï¼æ°å¼è½´æ¾å°åºé¨ï¼ãå»æçºµè½´çº¿ä¸ç®å¤´ï¼â£ æ¯æ¥å®¢æµæææ¢å¤ 5 æ¡ï¼èªé©¾/éè·¯/éè·¯/æ°èª/æ°´è·¯ï¼ï¼å æ¯å°çæ°åæ å¨è²åå³è¾¹ãç¨é¢è²åºåã |
| | | - **å®ç°**ï¼ |
| | | - `HolidayTrendChartService.drawXYItemLabels` éåï¼å
æå䏿¥æåç³»åæç¹ä½ y ä»ä¸å°ä¸æåºï¼æ ç¾åæè¯¥é¡ºåºä»ä¸å¾ä¸æï¼ä¸ç为åä¸ä¸ªæ ç¾åºé¨ +3ï¼ï¼**ä»èä¸ä¼åºç°æ ç¾ä¸æçº¿ä¸ä¸å转**ï¼é¿è®©æéæ¹ä¸ºãæ ç¾éå à 1000 + æ°æ®ç¹éå à 1ãï¼å¹¶å é¤åæ¥ä¼ææ ç¾ä¸è·¯æ¨å°å¾è¡¨ä¸æ¹çéç½åç´ æ©ç½é¡¹ï¼`nonWhiteRatio`ï¼ã |
| | | - 横åå å å¾ï¼`plot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_LEFT)` ææ°å¼è½´æ¾å°åºé¨ï¼ä¸äººæ¬¡ / å æ¯ å°±æ¯æ¨ªåæ ï¼ï¼`domainAxis.setAxisLineVisible(false)` + `setTickMarksVisible(false)` å»æçºµè½´çº¿åå»åº¦ï¼`render` 对横åå å å¾ä¸åç»åæ è½´ç®å¤´ï¼å使¹å°å³ä¸è§ï¼`drawUnitLabelTopRight`ï¼ã |
| | | - å å å¾åå
æ¾ä¸ä¸çå°æ°åï¼æ¹ä¸ºå¨è²åå³ä¾§ç¨åè²æåæ æ³¨ï¼åä¸è¡æè²åä½ç½®ææä¸åï¼å x åè¯¥è¡æå³è²å端 +8ï¼å¹¶éå¶å¨é¢ç 74px å³è¾¹è·å
ï¼ï¼æ¾å¾ä¸ä»ç¨ç½ååå¨åå
ã |
| | | - `HolidayTrendService.roadStructureSeries` é¡ºåºæ¹ä¸º èªé©¾åºè¡éï¼æ£ï¼ãé路客è¿éï¼èï¼ï¼`dailyShareSeries` æ¢å¤ èªé©¾/éè·¯/éè·¯/æ°èª/æ°´è·¯ 5 æ¡ï¼å é¤ä¸ä¸è½®ä¸´æ¶åå¹¶ç¨ç `sumNullableLists`ã |
| | | - **éªè¯**ï¼`mvn test -f traffic-audit-server\pom.xml` éè¿ï¼v23 Word COM 宿µ 8 页ï¼1+2Ã7ï¼15 å¼ å¾ï¼ãRapidOCR 夿£ 12 å¼ æçº¿å¾æ æ ç¾éå ï¼éæ¥æ¯å¯¹æ ç¾é¢è²é¡ºåºä¸æçº¿ç¹ä½é¡ºåºä¸è´ï¼ç´¯è®¡å¾ 10æ4æ¥ ç¹ä½ 2025 å¨ä¸/2024 å¨ä¸ï¼æ ç¾åæ · 2025 å¨ä¸ã2024 å¨ä¸ï¼ä¿®æ¹å v20 æ°å¥½ç¸åï¼ï¼é¦å¾ 10æ1æ¥æé«ç¹ï¼èè² 2025ï¼çæ°å¼æ ç¾å°±å¨è¯¥ç¹ä½æ£ä¸æ¹ãå¾ 12 å¾ä¾ä¸ºãèªé©¾åºè¡é·é路客è¿éãï¼æ°å¼è½´å¨åºé¨ 0â12,000ï¼åä½ä¸äººæ¬¡å¨å³ä¸è§ï¼æ ç®å¤´ãæ 纵轴线ï¼452/228 以èåæ å¨èåå³ä¾§ï¼648 ç½åå¨åå
ãå¾ 13 å¾ä¾æ¢å¤ 5 项ï¼87.8% çå¤§å æ¯ç½åå¨åå
ï¼éè·¯/æ°èª/æ°´è·¯çå°å æ¯ä»¥åè²æ°åæ å¨å³ä¾§ä¸åã |
| | | - **æ°æ®æä½ä¸ç¶æ**ï¼æªå¯¼å
¥ãè¦çæé坼任使°æ®ãæç»æä»¶å·²è¦çå° `C:\Users\jcxiong\Downloads\2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾ï¼ä¿®æ¹ç-ææ°ï¼.docx`ï¼å¾
ç¨æ·éªæ¶ï¼éªæ¶åä¸æäº¤ã䏿¨éã |
| | | |
| | | ### 10-04 åæï¼èµ°å¿å¾ 5 项微è°ï¼v20ï¼å¾
ç¨æ·éªæ¶ï¼ |
| | | |
| | | - **ç¨æ·ä¿®æ¹é¡¹**ï¼â 纵轴åä½ä»ä¸çºµè½´éå ï¼ç»§ç»ä¸ç§»ï¼â¡ 第 1 页餿 é¢å¤å
¨ç©ºç½ï¼éæ¾ 1 å¼ å¾å°ç¬¬ 1 页ï¼â¢ æ¨ªçºµåæ å»åº¦ä¸å¾ä¾åé»ï¼ç¨å¾®è°æ·¡ï¼â£ 饼徿 ç¾é¢è²æ¹ä¸ºä¸æåºä¸è´ï¼â¤ 两个å å 徿æä»ä¸å¥½ï¼ååå¤§ãæ°ååè¿åéã |
| | | - **å®ç°**ï¼`HolidayTrendChartService` ä¸ºåæ ç±»å¾è¡¨é¢ç `UNIT_LABEL_BAND=58px` çé¡¶é¨ç©ºç½ï¼`reserveUnitLabelBand` è° `plot.setInsets`ï¼ï¼å使 ç¾åºçº¿æ¹ä¸º `dataArea.getY()-22`ï¼å½»åºè±ç¦»è½´çº¿ä¸æé«å»åº¦ï¼å»åº¦/å¾ä¾æåè²ç± (70,70,70) è°æ·¡ä¸º (102,102,102)ï¼åæ 轴线ä¸ç®å¤´ä»ä¸ºé»è²ï¼é¥¼å¾æ ç¾ï¼å«å¼çº¿æ¡å
æåï¼éè¿ `TextFragment` åå°æ¹ä¸ºå¯¹åºæåºè²ï¼å å å¾ç»´ææ¨ªåï¼`categoryMargin 0.08->0.05`ã`itemMargin 0.05` 让è²åååï¼åå
æ¾å¾ä¸å°±ç¨ 17px ç½ååå¨åå
ï¼æ¾ä¸ä¸æç¨åè²å¼çº¿æå°åå¤ï¼æçº¿æ°æ®æ ç¾å·¦ä¾§æå°çç½ `dataArea.minX+16`ï¼é¿å
紧贴纵轴å»åº¦ã |
| | | - **éªè¯**ï¼`mvn test -f traffic-audit-server\pom.xml` éè¿ãçå®åºåªè¯»çæ `_tmp_holiday_verify/holiday_trend_2026-10-04_v20.docx`ï¼571 KBï¼15 å¼ å¾ï¼ï¼Word COM 宿µ 8 页ï¼ç¬¬ 1 页æ é¢+1 å¼ å¾ï¼ç¬¬ 2-8 页å 2 å¼ å¾ï¼æ 空ç½é¡µãRapidOCR 夿£ 12 å¼ æçº¿å¾æåå
å´æ¡éå æ°ä¸º 0ï¼åä½âä¸äººæ¬¡/å æ¯â䏿é«å»åº¦ã轴线已å离ï¼å¾ 1 åä½ y=17..44ãé¡¶é¨å»åº¦ y=53..77ï¼ï¼å å å¾ 12 ç 452/8,595ã648/10,410ã228/3,962 ä¸å å å¾ 13 ç 87.8%/7.4% åå¨åå
ï¼ä¸ªå«è¿çªç 4.8% ç¨åè²å¼çº¿æå°åå¤ï¼é¥¼å¾æ ç¾é¢è²ä¸æåºä¸è´ï¼éè·¯ç»¿ãæ°èªç´«ãéè·¯èãæ°´è·¯éï¼ã |
| | | - **å£å¾ååæé**ï¼å¾ 13ãæ¯æ¥å®¢æµææååãï¼éå½ä¸ï¼ç±åå
5 æ¡ï¼èªé©¾/éè·¯/éè·¯/æ°èª/æ°´è·¯ï¼å并为 3 æ¡ï¼èªé©¾/éè·¯/å
¶ä»æ¹å¼=éè·¯+æ°èª+æ°´è·¯ï¼ï¼ç®çæ¯è®©è²åè¶³å¤å®½ãæ°åè½åè¿åå
ï¼æ£æ 5 ç§è¿è¾æ¹å¼èµ°å¿å¾æªåå½±åã |
| | | - **æ°æ®æä½ä¸ç¶æ**ï¼æªå¯¼å
¥ãè¦çæé坼任使°æ®ãæç»æä»¶å·²è¦çå° `C:\Users\jcxiong\Downloads\2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾ï¼ä¿®æ¹ç-ææ°ï¼.docx`ï¼å¾
ç¨æ·éªæ¶ï¼éªæ¶åä¸æäº¤ã䏿¨éã |
| | | |
| | | ### 10-04 ä¸åï¼3ï¼ï¼èµ°å¿å¾ 8 项修æ¹ï¼å¾
ç¨æ·éªæ¶ï¼ |
| | | |
| | | - **ç¨æ·ä¿®æ¹é¡¹**ï¼çºµè½´åä½ä¸ç§»é¿å¼ç®å¤´ï¼å é¤éå½äºåæ¯èµ°å¿ï¼åæ¥æçä¸å¹´æçº¿ç¹æ¢å¤åç´å¯¹é½ï¼å¾è¡¨æ é¢å»æâ2026å¹´âï¼âè§æµç«âæ¹ä¸ºâæ®éå½çéè§æµç«âï¼å å å¾å°å æ¯æ¹ä¸ºæ±å¤å¼çº¿æ æ³¨ï¼ææ¡£æ¹ä¸ºæ¯é¡µ 2 å¾ï¼å¾ä¾âéè·¯è¥ä¸æ§å®¢è¿éâæ¹ä¸ºâé路客è¿éâãâéè¥ä¸æ§èªé©¾æç®å®¢æµâæ¹ä¸ºâèªé©¾åºè¡éâã |
| | | - **å®ç°**ï¼`HolidayTrendChartService` æ¢å¤ææå¹´ä»½åä¸ x åæ ï¼çºµè½´åä½ç»å¶å¨æ°æ®åºä¸æ¹ï¼å å å¾å°å æ¯ä½¿ç¨åè²å¼çº¿æå°æ±å¤ï¼æçº¿æ°å¼æ ç¾æ¹ä¸ºå¯¹å
¶ä»æ°å¼æ ç¾è®¾ç½®æé«é¿è®©ä¼å
级ï¼ç½æè¾¹ä¿çã`HolidayTrendService` å é¤éå½äºåæ¯ 2 å¾ï¼åéå½ä¸é¡ºå»¶ä¸ºéå½äºï¼å¹¶æ´æ°æ é¢åå¾ä¾æåï¼å½åå
± 15 å¼ å¾ã`HolidayTrendWordService` å°é¢å强å¶å页ï¼åæ¶ç« èæ é¢åçå¤ä½ç©ºæ®µï¼åææ¯ 2 å¾èªå¨å页ã |
| | | - **éªè¯**ï¼`mvn test -f traffic-audit-server\pom.xml` éè¿ãçå®åºåªè¯»çæ `_tmp_holiday_verify/holiday_trend_2026-10-04_v17.docx`ï¼Word COM 宿µ 9 页ï¼å°é¢åç¬ç¬¬ 1 页ï¼ç¬¬ 2â8 页å为 2 å¼ å¾ï¼æå 1 å¼ å¾åç¬ç¬¬ 9 é¡µï¼æ 空ç½é¡µãRapidOCR 夿£ 12 å¼ æçº¿å¾çæ°å¼æ ç¾éå æ°ä¸º 0ï¼å å å¾å¾ 12ã13 çå°å æ¯æ°ååå¯è§ï¼å¾ 3âæ°´è·¯ 0.23%âä»å¨å³ä¾§ï¼å¾ä¾å·²è¯å«ä¸ºâé路客è¿éââèªé©¾åºè¡éâï¼å¾è¡¨æ é¢ä¸ä¸ååºç°â2026å¹´âã |
| | | - **æ°æ®æä½ä¸ç¶æ**ï¼æªå¯¼å
¥ãè¦çæé坼任使°æ®ãæç»éªæ¶æä»¶å·²å¤å¶å° `C:\Users\jcxiong\Downloads\2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾ï¼ä¿®æ¹ç-ææ°ï¼.docx`ï¼åâä¿®æ¹çâæä»¶å½æ¶è¢« WPS å ç¨ï¼æªè¦çï¼ï¼å¾
ç¨æ·éªæ¶ï¼éªæ¶åä¸æäº¤ã䏿¨éã |
| | | |
| | | ### 10-04 ä¸åï¼2ï¼ï¼èµ°å¿å¾æ ç¾ãæçº¿é´è·ä¸æ¨ªåæ åç¡®æ§ä¿®æ£ï¼å¾
ç¨æ·éªæ¶ï¼ |
| | | |
| | | - **ç¨æ·åé¦**ï¼é¥¼å¾âæ°´è·¯âå¼çº¿é¾è¾¨è®¤éåºå®å°å³ä¾§ï¼å å å¾è¦æ¾ç¤ºæ°å¼ï¼å¾ä¾æ¹é»è²æ¾å¤§ï¼æ¨ªè½´âæ¥æâä¸ç§»ï¼çºµè½´å使¾å°ç®å¤´ä¸æ¹ï¼åæ è½´åå»åº¦æ¹é»è²å¹¶æ¾å¤§ï¼æçº¿å ç²ãå°½éæå¼ï¼ä¿®å¤æ¨ªè½´éä½ä¸å¤©ï¼æçº¿æ°å¼æ ç¾è¦ä¸æçº¿åè²ï¼å¹¶æ£æ¥æ ç¾éå ã |
| | | - **ä¿®å¤**ï¼`HolidayTrendChartService` 饼å¾åç±»æ¹ä¸ºå¨ JFreeChart å·²å好ç左峿 ç¾éåä¸æâæ°´è·¯âä»å·¦éåç§»å°å³éåï¼æçº¿å®½åº¦ç± 4px è°è³ 6pxï¼å¤åºåæ ±0.08 å轻微横åéå¼ï¼å
³é JFreeChart é»è®¤æçº¿æ ç¾ï¼æ¹ä¸ºéç¹èªå®ä¹æçï¼æä¸/ä¸/è¿ä¸/è¿ä¸/å·¦/å³åéä½ç½®é¿è®©å·²æ¾æ ç¾åæ°æ®ç¹ï¼å¹¶å¯¹åä½åºåçç©ºï¼æ ç¾ä½¿ç¨å¯¹åºæçº¿é¢è²å¹¶å ç½è²æè¾¹ã横轴é使 ¹å æ¯å `XYSeries.add(i + 1, ...)` ä¸ `SymbolAxis` å®é
æ´æ°ä½ç½® `0..7` ä¸ä¸è´ï¼å·²æ¹ä¸º `add(i, ...)`ã |
| | | - **éªè¯**ï¼`mvn test -f traffic-audit-server\pom.xml` éè¿ï¼çå®åºåªè¯»çæ `_tmp_holiday_verify/holiday_trend_2026-10-04_v14.docx`ãRapidOCR 夿£ 14 å¼ æçº¿å¾ï¼æ°å¼æ ç¾å
å´æ¡éå æ°ä¸º 0ï¼æªåè¯å«åºä¸¤ä¸ªæ°å¼ç²è¿ï¼ä¸å¹´æçº¿æ¯å¾åè¯å«åº 2026 å¹´ 3 个ã2025 å¹´ 8 个ã2024 å¹´ 7 个ï¼å
± 18 个æ°å¼æ ç¾ï¼é¢è²è®¡æ°ä¸ºçº¢ 3ãè 8ã绿 7ã饼å¾âæ°´è·¯ 0.23%âä½äºå³ä¾§ï¼OCR ä¸å¿ xâ801.5ï¼ï¼å
¶ä»å°å æ¯æ ç¾å¨å·¦ä¾§ï¼å å 徿°å¼å¯è§ã10 æ 1 æ¥è½´å»åº¦ xâ123.5ï¼é¦æ¥æ°æ®ç¹å·²è½å¨ç¬¬ä¸æ ¼ã |
| | | - **æ°æ®æä½ä¸ç¶æ**ï¼æªå¯¼å
¥ãè¦çæé坼任使°æ®ãæ°çå·²å¤å¶å° `C:\Users\jcxiong\Downloads\2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾ï¼ä¿®æ¹çï¼.docx`ï¼å¾
ç¨æ·éªæ¶ï¼éªæ¶åä¸æäº¤ã䏿¨éã |
| | | |
| | | ### 10-04 ä¸åï¼èµ°å¿å¾å¤è§ææ¨¡æ¿éå¶ï¼å¾
ç¨æ·éªæ¶ï¼ |
| | | |
| | | - **ç¨æ·åé¦ç 7 项é®é¢**ï¼ç« èæ é¢éå¤ã徿¯æ¨¡æ¿å¤§ãåå°çº¿ç»ãç½æ ¼åæµ
ç°èæ¯ãåæ è½´æ ç®å¤´ä¸åä½ä½ç½®ä¸ç¬¦ã纵轴èå´ä¸åéã饼å¾å°å æ¯æ ç¾åºæå°åå¤ã |
| | | - **ä¿®å¤**ï¼`HolidayTrendWordService` å
éç« èæ¶ç§»é¤æ¨¡æ¿èªå¨ç¼å·ï¼æ¯å¼ å¾ææ¨¡æ¿å¯¹åºå°ºå¯¸è®¾ç½® `wp:extent` åå
é¨ `a:ext`ï¼å¹¶ç§»é¤æ¨¡æ¿æ§ `srcRect`ã`HolidayTrendChartService` æ¹ä¸ºçº¯ç½èæ¯ãæ ç½æ ¼/æ 交æ¿ç°è²å¸¦ãé»è²åæ 轴带ç®å¤´ã左䏿¹æ°´å¹³åä½ãå³ä¸è§æ¥æï¼æçº¿åå·æ Word çº¸å¼ å®é
æ¾ç¤ºå°ºå¯¸æ¾å¤§ãå ç²çº¿å®½åç¹å¾ï¼çºµè½´ä¸å¼ºå¶ä» 0 å¼å§å¹¶ææ°æ®ä¸ä¸çç½ï¼ç¾åæ¯å 积å¾åºå® 0%-100%ï¼é¥¼å¾å
³é `simpleLabels`ï¼ä½¿ç¨å¤é¨æ ç¾åå¼çº¿ã |
| | | - **éªè¯**ï¼`mvn test` å
¨é¨éè¿ï¼æ°å¢å¾è¡¨æµ
ç°èæ¯ä¸ Word ç¼å·/尺寸å彿è¨ï¼å端 `npm run build` éè¿ï¼ä»
åæ bundle ä½ç§¯è¦åï¼ãçå®åºçææç»ç¨¿ `C:\Users\jcxiong\Downloads\2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾ï¼ä¿®æ¹çï¼.docx`ï¼Word COM 宿µ 9 页ã17 å¼ å¾ï¼ç¨åºæ£æ¥å
¨é¨ `gray243=0` ä¸ 6 ä¸ªç« èæ®µ `numPr=false`ï¼å¾çå°ºå¯¸ä¸æ¨¡æ¿ 7 ç§ç®æ æ¡ä¸è´ãæç»ç PNG ææ¨¡æ¿æ¾ç¤ºæ¡å®½é«æ¯è¾åºï¼ä¸å被 Word æä¼¸åå½¢ï¼ï¼å»åº¦/å¾ä¾ 18pxãæ°æ®æ ç¾ 20pxãåä½ 22pxï¼çº¿å®½ 4pxã |
| | | - **æ°æ®æä½**ï¼å
¨ç¨åªè¯»ï¼æªå¯¼å
¥ãè¦çæé坼任使°æ®ãç¶æï¼å¾
ç¨æ·éªæ¶ï¼éªæ¶ååæäº¤/æ¨éã |
| | | |
| | | ### 10-04 ä¸åï¼æ°å¢èåæ¥ç»¼åè¿è¾å®¢æµéèµ°å¿å¾åçï¼å¤è§å·²æåé¦éå¶ï¼ |
| | | |
| | | - **éæ±å£å¾ï¼ç¨æ· 2026-10-04 确认ï¼**ï¼èµ°å¿å¾å 12 å¼ æ£æ + éå½ï¼é¢å¯¼åè¯ä¸ºâåç§è¿è¾æ¹å¼å®¢æµéèµ°å¿å¾ï¼é«éåå½çé车æµéèµ°å¿å¾âãæç»æ£æå为ä¸é¨åï¼`ä¸ãæ»ä½æ
åµ`ï¼å½æ¥æ»å®¢æµã累计æ»å®¢æµãè¿è¾æ¹å¼å æ¯ï¼ï¼`äºãåç§è¿è¾æ¹å¼å®¢æµéèµ°å¿`ï¼èªé©¾ãéè·¯ãæ°´è·¯ãæ°èªãéè·¯ï¼å
± 5 ç§æ¹å¼ï¼ï¼`ä¸ã车æµéèµ°å¿`ï¼é«éãæ®éå½çéãè½¦æµæ»éãå
¬è·¯åºè¡ç»æï¼ãéå½åä¸ºæ¯æ¥å®¢æµææååãåæ¯èµ°å¿ãè§æµç«è·¯ç½è¿è¡ãèªé©¾ä½ä¸ºè¿è¾æ¹å¼ä¹ä¸ï¼ä½¿ç¨éè¥ä¸æ§èªé©¾æç®å®¢æµï¼åä½ä¸äººæ¬¡ï¼é«é/æ®éå½çé/è½¦æµæ»éåä½ä¸è¾æ¬¡ã |
| | | - **å®ç°**ï¼æ°å¢ `HolidayTrendService`ã`HolidayTrendChartService`ã`HolidayTrendWordService` å `ObservationDailyOperation`ï¼ä½¿ç¨ JFreeChart çæ 17 å¼ PNGï¼Word ä»ä»¥ `docs/ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾.docx` 为模æ¿ï¼éå»ºæ£æãä¿ç模æ¿é¡µç页èåå¾çå°ºå¯¸ï¼æ¿æ¢/追å åªä½å¹¶é建å¾çå
³ç³»ãæ°å¢å端æ¥å£ `GET /api/holiday/trend?year=2026&holidayType=NATIONAL_DAY`ï¼å端è忥ç»è®¡é¡µæ°å¢âä¸è½½èµ°å¿å¾âæé®ã |
| | | - **æ°æ®ä¸å£å¾**ï¼2024/2025/2026 ä¸å¹´ï¼æ¨ªè½´åºå® 8 天ï¼2024ã2026 æ 第 8 å¤©æ°æ®æ¶ç空ï¼2025 æ¾ç¤ºç¬¬ 8 天ã累计æ²çº¿åªè¿æ¥å®é
å·²ææ¥æï¼ç¼ºæ¥ä¸å»¶ç»æå累计å¼ãè§æµç«é彿 2026-10-01 è³ 10-08 æ¥è¯¢æ¯æ¥å¹³åæºå¨è½¦é度å平忥æ¤åº¦ï¼2026 å½åè¦ç 10-01/02/03ã |
| | | - **éªè¯**ï¼`mvn test` å
¨éè¿ï¼å端 `npm run build` æåï¼ä»
bundle ä½ç§¯åæè¦åï¼ãçå®åºæ°æ®çæ `_tmp_holiday_verify/holiday_trend_2026-10-04_v3.docx`ï¼Word ç»è®¡ 12 页ã17 å¼ å¾ï¼ä¸è½½æ¥å£è¿å约 399 KB DOCXãæªæ§è¡ä»»ä½æ°æ®å¯¼å
¥ãè¦çæé导ã |
| | | - **æªè·è¸ªé¡¹**ï¼èµ°å¿å¾æ¨¡æ¿å `_tmp_holiday_verify/` ä»ä¸ºæªè·è¸ªæä»¶ï¼æ¬å°å¯è¿è¡ï¼è¥åç»æäº¤/é¨ç½²éææ¨¡æ¿çº³å
¥é¨ç½²å
æä»åºãç¶æï¼å¤è§é®é¢å·²ç±æ¬æ¥ä¸å段修å¤ï¼æç»ç¨¿å¾
ç¨æ·éªæ¶ã |
| | | |
| | | ### 10-04 ä¸åï¼10 æ 3 æ¥æ°æ®å¯¼å
¥å¹¶çæå¿«æ¥ |
| | | |
| | | - **æºæä»¶**ï¼`D:\03_å
¬å¸\02_项ç®\27_æ¹åçéè·¯è¿è¾äºä¸åå±ä¸å¿\10_ä¸å¡\99_ä¸é¡¹ç»è®¡\01_éè¦è忥\2026\åä¸\10æ3æ¥\2026å¹´å䏿°æ®10æ3æ¥.xlsx`ï¼2026-10-04 10:11 æ´æ°ï¼ï¼è§æµç«åå§æ°æ®ä¸ºåç®å½ `è§æµç«åæ¥è°æ¥æ°æ®2026.10.3.xls`ã |
| | | - **导å
¥è¿ç¨ï¼ä¿®æ£ï¼**ï¼ç¨æ·å
äº 10:13:34 导å
¥ä¸»æ°æ®ï¼å©æéåå¨ 10:17:41 åç¨å䏿件éå¤å¯¼å
¥äºä¸æ¬¡ãæ¥å£è¿å `passengerRows=14, vehicleRows=6, skippedRows=24, successRows=20, failRows=0`ï¼æªæ£æµå° 10 æ 1ã2 æ¥å岿°æ®ååãå¯ä¸é®å½ä¸åä»
æ´æ° `updated_at`ï¼æ²¡ææ°å¢éå¤è¡ã |
| | | - **10 æ 3 æ¥å
¥åºå¼**ï¼éè·¯ 72.4ãæ°èª 5.7571ãéè·¯ 73.12ãæ°´è·¯ 5.5668 ä¸äººæ¬¡ï¼é«é 193.3634ãæ®éå½çé 323.7837 ä¸è¾æ¬¡ï¼åè®¡äººåæµå¨é 1397.99694 ä¸äººæ¬¡ã |
| | | - **è§æµç«æ°æ®ï¼ä¿®æ£ï¼**ï¼ç¨æ·å
导å
¥ï¼å©æéå¤å¯¼å
¥æ¶æåæ¥è¦çéæï¼å½å 2026-10-03 为 691 è¡ / 691 个å¯ä¸é®ï¼æ éå¤ï¼ä¸å°å®¢æµéå计 3,237,837 è¾æ¬¡ï¼323.7837 ä¸è¾æ¬¡ï¼ï¼ä¸æ®éå½çéå
¥åºå¼ä¸è´ã |
| | | - **å¿«æ¥**ï¼å·²çæ `C:\Users\jcxiong\Downloads\æ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥ï¼10æ3æ¥ï¼.docx`ãæ£æï¼å½æ¥ 1398.00 ä¸äººæ¬¡ï¼ç¯æ¯ä¸é 3.3%ï¼åæ¯ 2025 ä¸é 10.1%ï¼åæ¯ 2024 ä¸é 5.0%ï¼ç´¯è®¡ 4486.63 ä¸äººæ¬¡ï¼åæ¯ 2025 ä¸é 6.3%ï¼åæ¯ 2024 ä¸é 2.7%ã |
| | | - **人工稿æ¯å¯¹ï¼10-04 æ´æ°ï¼**ï¼æå·¥ç¨¿ä¸ç³»ç»ä¿®æ£çæåææ¬éè¡ä¸è´ï¼ç³»ç»åçä»
æä¸¤å¤ç´¯è®¡æ°å·®å¼ï¼å·²éæ¬æ¬¡ä»£ç ä¿®æ£æ¶é¤ã仿¥èµ°å¿å¾åè½æåã |
| | | |
| | | ### 10-04 ä¸åï¼2ï¼ï¼å¿«æ¥ç´¯è®¡æ°æ Excel 亿å使¾ç¤ºç²¾åº¦ååæ¢ç® |
| | | |
| | | - **é®é¢**ï¼ç³»ç»åçä¸æå·¥ç¨¿ä»
两å¤å·®å¼ï¼éè¥ä¸æ§å°å®¢è½¦ç´¯è®¡ `3961.76` åºä¸º `3961.77`ï¼å
¬è·¯å计累计 `4189.31` åºä¸º `4189.32`ã |
| | | - **æ ¹å **ï¼æºè¡¨ `äººåæµå¨éæ±æ»!X5/AA5`ï¼æ»è®¡å¯¹åº `U5`ï¼åçæ¯äº¿åä½ï¼åå
æ ¼æ ¼å¼ä¸º `0.0000000_`ã人å
çå° 7 ä½å°æ°çæ¾ç¤ºå¼ï¼åæ¢ç®æä¸äººæ¬¡å¹¶ä¿ç 2 ä½ï¼æ§ä»£ç ç´æ¥æ¿åå§ç´¯è®¡å¼æ¢ç®åå 2 ä½ï¼æ¼æäº Excel æ¾ç¤ºè¿ä¸æ¥ã |
| | | - **ä¿®å¤**ï¼`HolidayBulletinService` æ°å¢ `displayedCumulativeWan(...)`ï¼å
ææºåå
æ ¼ 7 ä½å°æ°åæ´ï¼åä¹ 10000ï¼æåæå¿«æ¥ 2 ä½è¾åºï¼åºç¨äºæ»è®¡ãéè¥ä¸æ§å°å®¢è½¦ãå
¬è·¯å计ä¸å¤ç´¯è®¡æ°ã |
| | | - **éªè¯**ï¼æ°å¢åæµè¦ç `3961.76472 -> 3961.77`ã`4189.31472 -> 4189.32`ã`4486.62652 -> 4486.63`ï¼`mvn test` å
¨éè¿ãéå¯ååºäºç°æåºéæ°çæ 10 æ 3 æ¥å¿«æ¥ï¼ä¸æå·¥ç¨¿æåææ¬éè¡æ¯å¯¹ç»æä¸º `NO DIFFERENCES`ãæ¬æ¬¡æªæ§è¡ä»»ä½æ°æ®å¯¼å
¥ã |
| | | - **产ç©**ï¼`_tmp_holiday_verify/bulletin_cumulative_display_2026-10-03.docx`ã |
| | | - **ç¶æ**ï¼å¾
ç¨æ·éªæ¶/æäº¤ï¼èµ°å¿å¾åè½ä»æåã |
| | | |
| | | ### 10-04 ä¸åï¼3ï¼ï¼å¿«æ¥å
¶ä½ Excel æ¾ç¤ºç²¾åº¦è·¯å¾å®¡è®¡ |
| | | |
| | | - **审计èå´**ï¼é项æ å°å¿«æ¥æ£æåè¡¨æ ¼çæææ°å¼æ¥æºï¼æ ¸å¯¹æºå·¥ä½ç°¿çå®é
åå
æ ¼æ ¼å¼ãExcel æ¾ç¤ºå¼å代ç ä¼ å
¥çæ¾ç¤ºç²¾åº¦ã |
| | | - **ç»è®º**ï¼é¤å·²ä¿®å¤çç´¯è®¡å¼ `U/X/AA` å¤ï¼æ²¡æåç°å
¶ä»æªå¤ççå¿«æ¥åå¼è·¯å¾ãæºè¡¨ç´æ¥åä¸å¿«æ¥ç `0.00%` åå
æ ¼å
± 11 å¤ï¼`äººåæµå¨éæ±æ»` D/P/JãV/W/Y/Z/AB/ACï¼éè·¯ Hï¼åæ¯2024å¹´ï¼ï¼æ°´è·¯ Dï¼å½æ¥ç¯æ¯ï¼ï¼ä»£ç åå
ææºæ ¼å¼å 2 ä½ï¼åæå¿«æ¥ 1 ä½å¤çã |
| | | - **补修边ç**ï¼éè·¯ H åæ 1 使ºæ ¼å¼å¤çï¼10 æ 3 æ¥ç¢°å·§ä»ä¸º `6.6%`ï¼æ¹ä¸ºå
æ `0.00%` å¾å° `6.62%` ååå 1 ä½ï¼è¾¹çå¼ä¸ä¼å·® 0.1 个ç¾åç¹ã |
| | | - **æªçº³å
¥ç´æ¥å¿«æ¥è·¯å¾çæ ¼å¼**ï¼`æ®éå½çé` E/N è½ä¸º `0.00%`ï¼å¿«æ¥ä½¿ç¨çæ¯ `äººåæµå¨éæ±æ»` æ±æ»åç P/R/Tï¼ä¸ä½¿ç¨è¯¥ä¸¤åï¼é«éå
¬è·¯/æ®éå½çéå
é¨ 4 ä½å°æ°äººåæµå¨éåçåªä½ä¸ºæºå
¬å¼ä¸é´åï¼ä¸ç´æ¥è¾åºã |
| | | - **éªè¯**ï¼`mvn test` å
¨éè¿ï¼éå¯å端åéæ°çæ 10 æ 3 æ¥å¿«æ¥ï¼ä¸æå·¥ç¨¿éåãéåå
æ ¼æ¯è¾æ ä»»ä½å·®å¼ã10 æ 2 æ¥ä¸æå·¥ç¨¿çå
容åè¡¨æ ¼ææ¬ä¸è´ï¼ä»
æ Word run åæ®µæ ¼å¼å·®å¼ã`_tmp_holiday_verify/bulletin_after_road_format_2026-10-01/02/03.docx` 为æ¬è½®æ ¸å¯¹äº§ç©ã |
| | | - **æ°æ®æä½**ï¼åªè¯»æ ¸å¯¹ï¼æªå¯¼å
¥ãè¦çæé坼任使°æ®ã |
| | | - **ç¶æ**ï¼å¾
ç¨æ·éªæ¶/æäº¤ã |
| | | |
| | | ### 10-04ï¼å¿«æ¥ç¾åæ¯æ Excel åå
æ ¼æ¾ç¤ºç²¾åº¦ååå 1 ä½ |
| | | |
| | | - **ç¨æ·ç¡®è®¤çä¸å¡å£å¾**ï¼äººå·¥æ ¸å¯¹ä»¥ `2026å¹´å䏿°æ®.xlsx` åå
æ ¼å®é
æ¾ç¤ºå¼ä¸ºåãæ¯å¦æ»è®¡ç´¯è®¡åæ¯ 2025 åºå±ç²¾ç¡®å¼ä¸º `-4.448602%`ï¼Excel åå
æ ¼æ ¼å¼ä¸º `0.00%`ï¼äººçå°å¹¶ç¨äºæ ¸å¯¹çæ¯ `-4.45%`ï¼ååå¿«æ¥ 1 ä½å°æ°å°±æ¯ `-4.5%`ãæ¤åâç´æ¥æåºå±å¼åä¸ä½å¾å° -4.4%âçç»è®ºå·²ä½åºã |
| | | - **å®ç°**ï¼`HolidayBulletinService` æ°å¢ `bulletinPercent(...)`ï¼ä¸¥æ ¼æç¾åæ¯æ¥æºåå
æ ¼æ ¼å¼å
åæ¾ç¤ºç²¾åº¦ï¼ååå¿«æ¥ 1 ä½å°æ°ï¼æ£ææªè¾ä¸è¡¨æ ¼ç¾åæ¯å
±ç¨åä¸é»è¾ãå½åæºè¡¨éåå£å¾ï¼å¤æ°æºåå
æ ¼ä¸º `0.0%`ï¼`äººåæµå¨éæ±æ»` ç彿¥ç¯æ¯ D/P/Jãç´¯è®¡åæ¯ V/W/Y/Z/AB/ACï¼ä»¥åæ°´è·¯å½æ¥ç¯æ¯ D 为 `0.00%`ã |
| | | - **é¢å¯¼åææ¥åæ ¸å¯¹è§å**ï¼é¢å¯¼åææ¥åä¸å¥ç¨å¿«æ¥ç 1 ä½å°æ°ï¼ä¸å¾æè¯¥æ¥åèªèº«æ¾ç¤ºçå°æ°ä½æ ¸å¯¹ï¼ç¡®ä¿ä¸äººå·¥å¨ææ¡£éçå°çå¼ä¸è´ã |
| | | - **éªè¯**ï¼æ°å¢/æ´æ°åå
æµè¯è¦ç `-4.448602%` ç `-4.4%`ï¼1 使ºæ ¼å¼ï¼ä¸ `-4.5%`ï¼2 使ºæ ¼å¼ï¼ï¼ä»¥å `-0.045% -> -0.05% -> -0.1%` è¾¹çï¼å端 `mvn test` å
¨é¨éè¿ãéå¯ 8090 åç¨çå®åºæ°æ®çæ 2026-10-02 å¿«æ¥ï¼æ£æåè¡¨æ ¼ä¸å·²äººå·¥æ ¸å¯¹ç模æ¿é项ä¸è´ï¼æ»è®¡ç´¯è®¡åæ¯ 2025/2024 为 `-4.5% / -1.6%`ã |
| | | - **æ¬å°éªè¯äº§ç©**ï¼`_tmp_holiday_verify/bulletin_excel_display_rounding_2026-10-02.docx`ï¼åè½æµè¯æ¥åä¸å·¥ä½æ¥ç»ææ¬å°çæ¡£è§ååå
¥ `docs/åè½æµè¯æ¥å/`ã`docs/工使¥ç»/`ï¼ä¸å
¥åºã |
| | | - **ç¶æ**ï¼ä»£ç åå£å¾è¯´æéæ¬æ¬¡æäº¤æ¨éï¼æå¡ä¿æè¿è¡ï¼è®¿é® `http://localhost:8080`ã |
| | | |
| | | ## ææ°ï¼2026-10-03ï¼ |
| | | |
| | | ### ãæ°å¯¹è¯å
读ãå½åç¶æä¸ä¸ä¸æ¥ï¼2026-10-03 æ·±å¤åå
¥ï¼ |
| | | |
| | | - **代ç ç¶æï¼å·²æ¨éï¼**ï¼`HEAD = origin/main = f329f50`ï¼å
容为 `feat(observation): æ°å¢è§æµç«åæ¥è°æ¥æ°æ®å¯¼å
¥ä¸æç»æ¥ç`ï¼ç¨æ·å·²ãéªæ¶éè¿ããå·¥ä½åºé¤æ¬ HANDOFF.md çäº¤æ¥æ´æ°ï¼å°æªæäº¤ï¼çç¨æ·ç¡®è®¤ï¼å¤ï¼æ å
¶ä»å·²è·è¸ªæä»¶æ¹å¨ï¼ä»
婿ªè·è¸ªç `_tmp_doc_compare/`ã`_tmp_holiday_verify/` å `docs/ä¸é¡¹ç»è®¡/éè¦è忥/è§æµç«åæ¥è°æ¥æ°æ® (2026å¹´10æ1æ¥).xls`ï¼è¿ç¨äº§ç©ä¸æºæ°æ®ï¼æçº¦å®ä¿çãä¸å
¥åºãä¸å é¤ï¼ã`docs/ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´å䏿°æ®.xlsx` å·²ç±ç¨æ·æ¹åååï¼æ¨¡æ¿ä¸è½½æ¥å£å®æµ HTTP 200 / 36335 åèã |
| | | - **æå¡ç¶æ**ï¼å端 8090ãå端 8080 åå¨è¿è¡ï¼è®¿é® `http://localhost:8080`ï¼ç»å½ admin/admin123ãæ°æ®åº `127.0.0.1:3305/traffic_audit`ï¼root/rootï¼ã |
| | | - **åºå
æ°æ®**ï¼è忥 2026-09-30ï¼éè·¯/æ°èªå䏿¥ï¼ã2026-10-01ã2026-10-02 已导å
¥ï¼**2026-10-03 å°æªå¯¼å
¥**ãè§æµç« `observation_station_flow` ç°æ 2026-10-01ï¼692 ç«ï¼ä¸ 2026-10-02ï¼689 ç«ï¼ã |
| | | - **ä¸ä¸æ¥ï¼2026-10-04 æ©ä¸ï¼**ï¼10 æ 3 æ¥æ°æ®åºæ¥åï¼ææ¢æå£å¾å¯¼å
¥ 2026-10-03 å¹¶çæå½å¤©ãæ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥ããé¢å¯¼è¦æ± 8:30 ååºç¬¬ä¸çï¼èæ°èªæ°æ®å¯è½æäº 8:30 ææï¼æ°èªæªå°å°±å
åºç¼ºæ°èªçæ¬ï¼ç´¯è®¡åæ¯åªä¸å¾å¹´å·²è¦çæ¥ææ¯è¾ï¼ï¼æ°èªå°é½å鿰坼å
¥å¹¶éæ°çæã宿´å£å¾ä¸å·²éªè¯ç»è®ºè§æ¬æä»¶ 10-03 åæ®µã |
| | | - **æ èªå¨å**ï¼æ¾å»ºçå¿è·³æéï¼id `10-03`ï¼å·²æç¨æ·è¦æ±å é¤ï¼ç¨æ· 2026-10-04 æç¡®ãå°æ¶æä¼æå·¥æä½ããæå¤©æ©ä¸ç±ç¨æ·æå·¥å¯¼å
¥ 10-03 æ°æ®å¹¶çæå¿«æ¥ï¼å©æåªå¨å
¶æåºæ¶é
åæ ¸å¯¹/ææ¥ãä¸è¦å¨æªè·ç¨æ·æç¡®è¦æ±æ¶èªè¡åå»ºå®æ¶ä»»å¡æèªå¨ååºã |
| | | - **æ°æ®åç°ï¼ä¾åèï¼æ 鿹代ç ï¼**ï¼2026-10-02 æ¯ 10-01 å° 3 ä¸ªè§æµç«ââ`G348L149420501` åé¨ï¼å®æå¸/å¸è¾åºï¼ã`S464L105422828` 大å
¸æ²³ï¼æ©æ½å·/鹤峰å¿ï¼ã`S483L105420801` å±å®¶å²ï¼èé¨å¸/å¸è¾åºï¼ãå·²æ ¸å¯¹ä¸¤ä»½æº Excelï¼ç¡®å±æºæ°æ®å½å¤©æªä¸æ¥ï¼ä¸æ¯å¯¼å
¥æ¼è¡ã |
| | | - **é¿æå¾
å**ï¼è§ä¸æ¹ç¬¬ä¸èãæ°å¼ç²¾åº¦ç»ä¸æ¹é ãï¼æ¢æ 16 å¼ è¡¨ã228 个 double åæ¹ `DECIMAL(30,15)` + Java `BigDecimal`ï¼ã |
| | | |
| | | ### 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 ä½å°æ°å£å¾ä¸åã |
| | | - **2026-10-04 ä¿®æ£**ï¼æ Excel åå
æ ¼æ¾ç¤ºç²¾åº¦å£å¾ï¼`-4.65%` ååä¸ä½åºä¸º `-4.7%`ï¼ä»¥æä»¶é¡¶é¨ææ°æ®µä¸ºåã |
| | | - **ãæç¼ºæ°èªãæç¤º**ï¼ç¨æ·ç¡®è®¤ç±äººå·¥å¨çæçå¿«æ¥ç¨¿ä¸ä¿®æ¹ï¼ç³»ç»ä¸çæè¯¥æç¤ºè¡ï¼**ä¸åä»£ç æ¹å¨**ã |
| | | - **éªè¯**ï¼`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 ä½å°æ°ï¼è¾åºç´¯è®¡å¼ï¼åæ¯ä»ä¸ºãå¢é¿/ä¸é + ä¸ä½å°æ°ãï¼**ä¿æä¸æ¬¡åæ´ä¸å**ï¼è¯¥å¤ 2026-10-04 已修æ£ï¼æºåå
æ ¼ä¸º `0.00%`ï¼Excel æ¾ç¤º -4.45%ï¼ååä¸ä½ä¸º -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 å¿«æ¥ä¸è½½ã |
| | | - **æ°æ®åå§å**ï¼ä» `docs/ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´å䏿°æ®.xlsx` åå§å 2024/2025 å¹´åä¸å®¢æµã车æµå 2024-2026 年客è¿ç³»æ°ï¼æºè¡¨ 90 ä¸ªæ°æ®åå
æ ¼å
¥åºé项æ¯å¯¹å·®å¼ 0ã |
| | | - **æ¥å¸¸å¯¼å
¥å£å¾**ï¼åªè¯»åæé年份çå½å¹´åï¼2026 年为å客æµå·¥ä½è¡¨ C åãæ®éå½çé D åï¼ï¼åå²å¹´ä»½ä¸è¦æ±æ¥å¸¸é导ã |
| | | - **å岿°æ®ä¿æ¤**ï¼å¯¼å
¥æ¶å
æ£æ¥æ´æ©å岿¥ææ¯å¦è¢«ä¿®æ¹ãæåååå
ä¸ååºï¼è¿å差弿¸
åï¼å端弹çªè®©ç¨æ·éæ©âå
许导å
¥/ä¸å
许âï¼ç¡®è®¤åæè¦çãæ¥å£åæ° `allowHistoricalOverwrite=true`ã |
| | | - **å¿«æ¥**ï¼é»è®¤ä¸è½½ç³»ç»æ¥æåä¸å¤©çãæ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥ï¼10æXæ¥ï¼.docxãï¼ç¨æ·å¯èªéæ¥æï¼ç¯æ¯/忝æå¢é¿ãä¸éãæå¹³è¾åºã |
| | | - **Excel æµ®ç¹ç²¾åº¦ï¼2026-10-02 追å ï¼**ï¼åè `docs/Excelæµ®ç¹æ°åå¨.txt`ï¼æ°å¢ `HolidayNumberUtils`ï¼`NumberToTextConverter.toText` + `BigDecimal`ï¼`MathContext(15, HALF_UP)`ï¼ï¼èåæ¥æ¨¡åç± `Double` å
¨é¢æ¹ `BigDecimal`ï¼ä¸å¼ 表æ°å¼åæ¹ `DECIMAL(30,15)`ï¼åºå
6 æ¡å®¢è¿ç³»æ°ç± 2.3999999999999995 æ ¡æ£ä¸º 2.4ãè¿ç§»èæ¬ `docs/sql_holiday_decimal_migration.sql`ã |
| | | - **èå顺åº**ï¼âä¸é¡¹ç»è®¡â已移å°âæ¥è¡¨çæâä¹åï¼å½å为 æ¥è¡¨çæ -> ä¸é¡¹ç»è®¡ -> è§å管ç -> ç¨æ·ç®¡ç -> ç³»ç»é
ç½®ã |
| | | - **è忥å é¤å¤æ´»ä¿®å¤ï¼2026-10-02 追å ï¼**ï¼ååå§åé»è¾æ¯æ¬¡å¯å¨è¡¥é½ç¼ºå¤±ç第 N æ¥ï¼å¯¼è´ admin å é¤ 2026-10-08 åè¢«éæ°æå
¥ï¼ç°æ¹ä¸ºä»
`holiday_calendar` æ´è¡¨ä¸ºç©ºæ¶é¦æ¬¡åå§åãéå¯å 2026 å¹´åä¸ä¿æ 10-01 è³ 10-07 å
± 7 天ã |
| | | - **æ®éå½çéç¯æ¯åºæï¼2026-10-02 追å ï¼2026-10-06 æ¹æ¡ D 已修æ£ï¼**ï¼C åè¯ä¹æç»ç¡®å®ä¸ºâä¸ä¸æ¥ç¯æ¯åæ¯çè°æ´å¼âï¼å¡«åååªå½±åä¸ä¸æ¥çç¯æ¯åæ¯ï¼ä¸å½±å C æå¨æ¥å累计ãèµ°å¿å¾ã`holiday_vehicle_flow.comparison_base` å·²å
¥åºï¼å¯¼å
¥å¯¼åºä¸åå²åæ´ç¡®è®¤åå·²æ¥å
¥ã |
| | | - **å¿«æ¥é¦æ¥å£å¾ï¼2026-10-02 追å ï¼**ï¼åæç¬¬1天没æå䏿¥ç¯æ¯åºæ°ï¼æ£æä¸è¾åºâç¯æ¯å¢é¿/ä¸é/æå¹³âçè¯ï¼è¡¨æ ¼å½æ¥ç¯æ¯å为âââï¼ç¬¬2å¤©èµ·ä»æå®é
å䏿¥è¾åºç¯æ¯ãå·²ç¨ 2025-10-01/02 å¿«æ¥åå½éªè¯ã |
| | | - **å¿«æ¥ç¾åæ¯ç²¾åº¦ï¼2026-10-02 追å ï¼**ï¼Word å¿«æ¥ä¸åæ¯/ç¯æ¯ç¾åæ¯ç»ä¸ä¿ç1ä½å°æ°ï¼å¦6.79%æ¾ç¤º6.8%ã11.46%æ¾ç¤º11.5%ï¼ï¼äººæ¬¡ã车æµçä¸å¡æ°å¼ä»ä¿ç2ä½å°æ°ã |
| | | - **éªè¯ç»æ**ï¼å端ç¼è¯ãå端æå»ºãæé/CRUD/è´å导å
¥ãåå²åæ´ä¸¤é¶æ®µå¯¼å
¥ãExcel 导åºãWord å¿«æ¥ãå端 admin/é admin çæµåéè¿ï¼èå顺åºã客è¿ç³»æ° 2.4 å±ç¤ºï¼ç³»ç»é
置页 + è忥ç»è®¡é¡µï¼ã`decimal(30,15)` ååãåå²åæ´ä¸¤é¶æ®µå¯¼å
¥åå½å已宿µï¼åè½æµè¯æ¥åä¸å·¥ä½æ¥ç»æ 2026-09-27 æäº¤è§èä»
æ¬å°çåï¼ä¸å
¥åºï¼ï¼`docs/åè½æµè¯æ¥å/åè½æµè¯æ¥å_éè¦è忥ä¸é¡¹ç»è®¡_2026-10-02.md`ã`docs/工使¥ç»/工使¥ç»_2026-10-02.md`ã |
| | | - **è¿è¡ç¶æ**ï¼å端 8090ãå端 8080 å·²å¯å¨ï¼æ°æ®åº `127.0.0.1:3305/traffic_audit`ãæµè¯äº§çç 2026 ä¸´æ¶æ°æ®å导å
¥æ¹æ¬¡å·²æ¸
çã |
| | | - **å
³æºæ¶å°¾ï¼2026-10-02ï¼**ï¼å·²åææ¬æ¬¡å¼åç¨å端 8090ãå端 8080 ååèªè¿ç¨æ ï¼ç«¯å£å已鿾ï¼MySQL æå¡ä¿çãå½åå¯å®å
¨å
³æºã |
| | | - **Git**ï¼ç¨æ·å·²äº 2026-10-02 æç¡®éªæ¶éè¿ï¼æ¬æ¬¡æè¦æ±åå¹¶è¿ç«¯ `origin/main` åæäº¤å¹¶æ¨éï¼ä¸è¦çè¿ç«¯åäºæäº¤ã |
| | | - **é¨ç½²æ¨¡æ¿åæ¥**ï¼è忥 4 å¼ è¡¨å·²åæ¥å° `deploy-templates/db/init.sql`ï¼`sql_holiday.sql` å两个æ§åºè¿ç§»èæ¬éé¨ç½²å
åå¸ï¼é¿å
æ°å
缺表ã |
| | | |
| | | ## ææ°ï¼2026-09-28ï¼ |
| | | |
| | | - **ç¨æ·ç¨ WPS ç¼è¾æ¯ç**ï¼æ¹ãç线å
车ãè¾¹æ¡ããéè·¯è¿è¾å¨è½¬éãåå·ï¼å¹¶å é¤ãä¸å£å¾æç»ã`AW` åï¼æå·¥å®ç¨¿åå ï¼ãå·²éªè¯çæç«¯æ£å¸¸ï¼ä¸å£å¾æç» 2024 åæ´æ©åæ´ä½å·¦ç§»ï¼è·¨é¡µ/å页å
¬å¼èªå¨æ¹å¼ç¨ï¼`#REF!` 2â0ï¼ãä¸å£å¾æåå J..R â A..I åæ£ç¡®å¸¦å
¥äº§ç©ã |
| | |
| | | - **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}ï¼ |
| | | TrafficAudit 交éè´§è¿ç»è®¡å®¡æ ¸ç³»ç» ââ é¨ç½²å
ï¼{stamp}ï¼çæ¬ v{version}ï¼ |
| | | ======================================================== |
| | | |
| | | ãæå¡å¨ä¸åªè¦åè¿ 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 å端ç¨åºï¼å«å
¨é¨ä¾èµï¼**å·²å
åµå端**ï¼ |
| | | traffic-audit-server.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_city_bus.sql åå¸å®¢è¿-å
¬äº¤æåº¦è¡¨ï¼å¿
é¡»æ§è¡ï¼ |
| | | sql_city_taxi.sql åå¸å®¢è¿-åºç§æåº¦è¡¨/åºç§ææè¡¨ï¼å¿
é¡»æ§è¡ï¼ |
| | | sql_wyc.sql ç½çº¦è½¦å
¨çæ»é表/订ååå¸å·è¡¨ï¼å¿
é¡»æ§è¡ï¼ |
| | | sql_holiday.sql ä¸é¡¹ç»è®¡-éè¦èåæ¥è¡¨ï¼æ§åºç¼ºè¡¨æ¶æ§è¡ï¼ |
| | | sql_holiday_decimal_migration.sql è忥æ°å¼å精度è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | sql_holiday_comparison_base_migration.sql èåæ¥ç¯æ¯åºæå段è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | sql_holiday_previous_day_migration.sql è忥å䏿¥é
ç½®è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | sql_holiday_vehicle_adjustment_migration.sql å¾å¹´å½çéæ°æ®è°æ´è¡¨ï¼æ§åºåçº§ï¼ |
| | | sql_observation_station.sql è§æµç«åæ¥è°æ¥æ°æ®è¡¨ï¼æ°åè½ï¼æ§åºç¼ºè¡¨æ¶æ§è¡ï¼ |
| | | README-é¨ç½²è¯´æ.txt |
| | | |
| | | ãé¨ç½²æºç¯å¢è¦æ±ã |
| | |
| | | **ä¸éè¦ 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 å以ä¸ã |
| | | |
| | | ã仿¬çèµ·ï¼å端 jar 使ç¨åºå®æä»¶åã |
| | | backend\traffic-audit-server.jar |
| | | - æ°æå¡å¨ï¼æä¸é¢çæ®éé¨ç½²æ¥éª¤æ§è¡å³å¯ã |
| | | - å·²å®è£
æ§çæå¡çæå¡å¨ï¼é¦æ¬¡åçº§å°æ¬çï¼ï¼ |
| | | 1) 忢æå¡ï¼Stop-Service TrafficAuditBackend |
| | | 2) å¤ä»½å¹¶ç¼è¾ backend\trafficAudit.xmlï¼æ <arguments> éçæ§ jar åæ¹æ traffic-audit-server.jar |
| | | 3) æ¿æ¢ backend\traffic-audit-server.jarï¼åæ§è¡ Start-Service TrafficAuditBackend |
| | | - ä»¥åæ¯æ¬¡å级ï¼åªæ¿æ¢ backend\traffic-audit-server.jarï¼åé坿å¡ï¼ä¸åéè¦ä¿®æ¹ trafficAudit.xmlã |
| | | - 页é¢ç»å½é¡µåå³ä¸è§æ¾ç¤ºççæ¬å·ä¸æ¬åå¸å
åç§°ä¸ççæ¬å·ä¸è´ã |
| | | |
| | | ãé¨ç½²æ¥éª¤ã |
| | | 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 å·²å«è忥åç¸å
³è¡¨ï¼è¥æ¯å¨æ©æçæ¬åºä¸è¡¥èåæ¥æ¨¡åï¼åæ§è¡ 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.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.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.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ï¼ |
| | | echo æ¥å¿æ¾ç¤ºå¨æ¬çªå£ï¼å
³éçªå£å³åæ¢æå¡ |
| | | echo ============================================ |
| | | java -jar traffic-audit-server-1.0.0-SNAPSHOT.jar |
| | | java -jar traffic-audit-server.jar |
| | | pause |
| | |
| | | #!/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 |
| | | java -jar traffic-audit-server.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.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> |
| | |
| | | region_code VARCHAR(20) COMMENT 'æå±å°åº', |
| | | enterprise_code VARCHAR(50) COMMENT 'ä¼ä¸ä»£ç ', |
| | | enterprise_name VARCHAR(200) COMMENT 'ä¼ä¸åç§°', |
| | | unified_credit_code VARCHAR(50) COMMENT 'ç»ä¸ç¤¾ä¼ä¿¡ç¨ä»£ç ', |
| | | report_unit VARCHAR(200) COMMENT 'å¡«æ¥åä½', |
| | | vehicle_total INT COMMENT '客è¿è½¦è¾_æ»è®¡', |
| | | seat_total INT COMMENT 'æ ¸å®è½½å®¢ä½_æ»è®¡', |
| | | vehicle_schedule INT COMMENT 'è½¦è¾æ°_客è¿ç车ï¼å«å®çº¿æ
游客è¿å®¢è½¦ï¼', |
| | |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | KEY idx_type_period (report_type, report_period) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='æ¥è¡¨æå®¡æ ¸éè¿æ è®°'; |
| | | |
| | | |
| | | -- ============================================ |
| | | -- åä¸ãä¸é¡¹ç»è®¡-éè¦è忥 |
| | | -- ============================================ |
| | | |
| | | -- èåæ¥æ¥æé
ç½® |
| | | DROP TABLE IF EXISTS holiday_calendar; |
| | | CREATE TABLE holiday_calendar ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | holiday_type VARCHAR(32) NOT NULL COMMENT 'è忥åç±»ï¼SPRING_FESTIVAL/LABOR_DAY/NATIONAL_DAY', |
| | | holiday_date DATE NOT NULL COMMENT 'è忥å
·ä½æ¥æ', |
| | | day_index INT NOT NULL COMMENT '0=å䏿¥ï¼1..N=åæç¬¬Næ¥', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_calendar_seq (year, holiday_type, day_index), |
| | | UNIQUE KEY uk_holiday_calendar_date (year, holiday_type, holiday_date), |
| | | KEY idx_holiday_calendar_date (holiday_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='éè¦èåæ¥æ¥æé
ç½®'; |
| | | |
| | | -- èåæ¥ç»¼åè¿è¾è¥ä¸æ§å®¢æµ |
| | | DROP TABLE IF EXISTS holiday_passenger_flow; |
| | | CREATE TABLE holiday_passenger_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | flow_date DATE NOT NULL COMMENT 'æ¥æ', |
| | | transport_mode VARCHAR(32) NOT NULL COMMENT 'è¿è¾æ¹å¼ï¼RAIL/CIVIL_AVIATION/ROAD/WATER', |
| | | passenger_count DECIMAL(30,15) NOT NULL COMMENT 'åéæ
客æ°ï¼ä¸äººæ¬¡ï¼', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_passenger (year, flow_date, transport_mode), |
| | | KEY idx_holiday_passenger_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='èåæ¥ç»¼åè¿è¾è¥ä¸æ§å®¢æµæ
åµ'; |
| | | |
| | | -- èåæ¥ç»¼åè¿è¾éè¥ä¸æ§è½¦æµ |
| | | DROP TABLE IF EXISTS holiday_vehicle_flow; |
| | | CREATE TABLE holiday_vehicle_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | flow_date DATE NOT NULL COMMENT 'æ¥æ', |
| | | road_category VARCHAR(32) NOT NULL COMMENT 'éè·¯åç±»ï¼EXPRESSWAY/NATIONAL_PROVINCIAL', |
| | | vehicle_type VARCHAR(32) NOT NULL COMMENT '车ååç±»ï¼SMALL_PASSENGER/MEDIUM_SMALL_PASSENGER', |
| | | vehicle_count DECIMAL(30,15) NOT NULL COMMENT '车æµéï¼ä¸è¾æ¬¡ï¼', |
| | | comparison_base DECIMAL(30,15) NULL COMMENT 'æ®éå½çéç¯æ¯åºæï¼ä¸è¾æ¬¡ï¼ä»
å½å¹´ä½¿ç¨ï¼', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_vehicle (year, flow_date, road_category, vehicle_type), |
| | | KEY idx_holiday_vehicle_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='èåæ¥ç»¼åè¿è¾éè¥ä¸æ§è½¦æµæ
åµ'; |
| | | |
| | | -- å¾å¹´å½çé车æµéè°æ´ |
| | | DROP TABLE IF EXISTS holiday_vehicle_adjustment; |
| | | CREATE TABLE holiday_vehicle_adjustment ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | flow_date DATE NOT NULL COMMENT 'æ¥æ', |
| | | adjusted_value DECIMAL(30,15) NOT NULL COMMENT 'è°æ´åæ®éå½çé车æµéï¼ä¸è¾æ¬¡ï¼', |
| | | reason VARCHAR(500) NOT NULL COMMENT 'è°æ´åå ', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_vehicle_adjustment (year, flow_date), |
| | | KEY idx_holiday_vehicle_adjustment_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='å¾å¹´å½çé车æµéè°æ´'; |
| | | |
| | | -- 客è¿ç³»æ°é
ç½® |
| | | DROP TABLE IF EXISTS holiday_passenger_coefficient; |
| | | CREATE TABLE holiday_passenger_coefficient ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | date_category VARCHAR(32) NOT NULL COMMENT 'æ¥æåç±»ï¼HOLIDAY/NON_HOLIDAY', |
| | | coefficient DECIMAL(30,15) NOT NULL COMMENT '客è¿ç³»æ°', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_coefficient (year, date_category) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客è¿ç³»æ°é
ç½®'; |
| New file |
| | |
| | | -- Holiday statistics module. Run against the traffic_audit database. |
| | | CREATE TABLE IF NOT EXISTS holiday_calendar ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | holiday_type VARCHAR(32) NOT NULL, |
| | | holiday_date DATE NOT NULL, |
| | | -- 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, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_calendar_seq (year, holiday_type, day_index), |
| | | UNIQUE KEY uk_holiday_calendar_date (year, holiday_type, holiday_date), |
| | | KEY idx_holiday_calendar_date (holiday_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| | | |
| | | CREATE TABLE IF NOT EXISTS holiday_passenger_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | flow_date DATE NOT NULL, |
| | | transport_mode VARCHAR(32) NOT NULL, |
| | | passenger_count DECIMAL(30,15) NOT NULL, |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_passenger (year, flow_date, transport_mode), |
| | | KEY idx_holiday_passenger_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| | | |
| | | CREATE TABLE IF NOT EXISTS holiday_vehicle_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | flow_date DATE NOT NULL, |
| | | road_category VARCHAR(32) NOT NULL, |
| | | vehicle_type VARCHAR(32) NOT NULL, |
| | | vehicle_count DECIMAL(30,15) NOT NULL, |
| | | comparison_base DECIMAL(30,15) NULL COMMENT 'æ®éå½çéç¯æ¯åºæï¼ä¸è¾æ¬¡ï¼ä»
å½å¹´ä½¿ç¨ï¼', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_vehicle (year, flow_date, road_category, vehicle_type), |
| | | KEY idx_holiday_vehicle_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| | | |
| | | CREATE TABLE IF NOT EXISTS holiday_vehicle_adjustment ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | flow_date DATE NOT NULL, |
| | | adjusted_value DECIMAL(30,15) NOT NULL, |
| | | reason VARCHAR(500) NOT NULL, |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_vehicle_adjustment (year, flow_date), |
| | | KEY idx_holiday_vehicle_adjustment_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| | | |
| | | CREATE TABLE IF NOT EXISTS holiday_passenger_coefficient ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | date_category VARCHAR(32) NOT NULL, |
| | | coefficient DECIMAL(30,15) NOT NULL, |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_coefficient (year, date_category) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| New file |
| | |
| | | -- Add the optional ordinary national/provincial road comparison base. |
| | | -- The field is used only for the 2026 holiday data. |
| | | ALTER TABLE holiday_vehicle_flow |
| | | ADD COLUMN comparison_base DECIMAL(30,15) NULL |
| | | COMMENT 'æ®éå½çéç¯æ¯åºæï¼ä¸è¾æ¬¡ï¼ä»
å½å¹´ä½¿ç¨ï¼' |
| | | AFTER vehicle_count; |
| New file |
| | |
| | | -- Convert holiday statistics numeric columns from binary floating point to exact decimal storage. |
| | | -- Run once against the traffic_audit database. |
| | | ALTER TABLE holiday_passenger_flow |
| | | MODIFY COLUMN passenger_count DECIMAL(30,15) NOT NULL COMMENT 'åéæ
客æ°ï¼ä¸äººæ¬¡ï¼'; |
| | | |
| | | ALTER TABLE holiday_vehicle_flow |
| | | MODIFY COLUMN vehicle_count DECIMAL(30,15) NOT NULL COMMENT '车æµéï¼ä¸è¾æ¬¡ï¼'; |
| | | |
| | | ALTER TABLE holiday_passenger_coefficient |
| | | MODIFY COLUMN coefficient DECIMAL(30,15) NOT NULL COMMENT '客è¿ç³»æ°'; |
| | | |
| | | -- The seeded coefficient was averaged from repeated 2.4 cells through a Java double. |
| | | -- Normalize the migrated value to the exact Excel decimal. |
| | | UPDATE holiday_passenger_coefficient |
| | | SET coefficient = 2.4 |
| | | WHERE year IN (2024, 2025, 2026); |
| 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 |
| | |
| | | -- Add the historical national/provincial road adjustment table. |
| | | -- Run against the traffic_audit database on upgrade. |
| | | CREATE TABLE IF NOT EXISTS holiday_vehicle_adjustment ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT 'year', |
| | | flow_date DATE NOT NULL COMMENT 'holiday date', |
| | | adjusted_value DECIMAL(30,15) NOT NULL COMMENT 'adjusted national/provincial vehicle flow (10k vehicle trips)', |
| | | reason VARCHAR(500) NOT NULL COMMENT 'adjustment reason', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_vehicle_adjustment (year, flow_date), |
| | | KEY idx_holiday_vehicle_adjustment_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='historical national/provincial vehicle flow adjustments'; |
| 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 |
| New file |
| | |
| | | excelçæ°å¼è®¡ç®é常精确ï¼å
¶å
鍿¯æä¹å卿°å¼çå¢ï¼æ¯ç¨æµ®ç¹æ°å |
| | | ç¨æ·æ³ç¥éExcelå
é¨å¦ä½å卿°å¼ï¼æ¯å¦ä½¿ç¨æµ®ç¹æ°ãæéè¦æä¾åç¡®çè§£éã为äºå
¨é¢è·åä¿¡æ¯ï¼æå°åæ¶è¿è¡å¤é¡¹æç´¢ï¼è¦çExcelæ°å¼å卿ºå¶ãæµ®ç¹æ°ç²¾åº¦é®é¢ãä¸IEEE 754æ åçå
³ç³»ä»¥åå¯è½çä¼åææ¯ã |
| | | |
| | | æç´¢å° 16 个ç½é¡µ |
| | | æç´¢ç»ææ¾ç¤ºäºä¸äºç¸å
³é¡µé¢ã为äºå
¨é¢è·åä¿¡æ¯ï¼æä»¬éè¦åæ¶æå¼è¿äºé¡µé¢ã |
| | | |
| | | æµè§ 1 ä¸ªé¡µé¢ |
| | | Floating Point Errors in Excel |
| | | è¿äºèµææä¾äºå
³äºExcelæ°å¼åå¨ç详ç»ä¿¡æ¯ãå¾®è½¯çææ¡£æç¡®æåºExceléµå¾ªIEEE 754æ åï¼ä½¿ç¨åç²¾åº¦æµ®ç¹æ°ï¼æææ°å为15ä½ãStack Overflowç讨论ä¹ç¡®è®¤äºè¿ä¸ç¹ãExcelJetçæç« è§£éäºæµ®ç¹æ°è¯¯å·®çåå ãæçåçå°æ¶µç以ä¸å 个æ¹é¢ï¼Excel使ç¨IEEE 754åç²¾åº¦æµ®ç¹æ°ï¼64ä½ï¼ï¼ææç²¾åº¦ä¸º15ä½ï¼åå¨èå´ï¼ä»¥åæµ®ç¹æ°è¯¯å·®åå
¶åå ãæè¿ä¼æåä¸äºç¹æ®æ
åµï¼æ¯å¦æ´æ°åå¨åæ¥ææ¶é´åå¨ãä¸ºäºæ´å
¨é¢å°è¦çè¿äºæ¹é¢ï¼æéè¦åæ¶è¿è¡å¤é¡¹æç´¢ã |
| | | |
| | | æç´¢å° 12 个ç½é¡µ |
| | | ç°å¨å¯ä»¥ç»ç»çæ¡äºãçæ¡å°æ¶µçï¼Excel使ç¨IEEE 754åç²¾åº¦æµ®ç¹æ°ï¼64ä½ï¼ï¼ææç²¾åº¦ä¸º15ä½ï¼åå¨èå´ï¼ä»¥åæµ®ç¹æ°è¯¯å·®åå
¶åå ãè¿ä¼æåæ´æ°åæ¥ææ¶é´çå卿¹å¼ãåçæ¶ä¼å¼ç¨å¾®è½¯ææ¡£ãStack Overflowçæ¥æºã |
| | | |
| | | æ¯çï¼Excel çæ°å¼è®¡ç®ç¡®å®åºäºæµ®ç¹æ°ï¼å
·ä½æ¥è¯´ï¼å®éµå¾ª IEEE 754 æ åç 64 ä½åç²¾åº¦æµ®ç¹æ°æ ¼å¼æ¥åå¨åè®¡ç®æ°å¼ã |
| | | |
| | | æ ¸å¿æºå¶ï¼64ä½åç²¾åº¦æµ®ç¹æ° |
| | | Excel å
é¨å°ææå·¥ä½è¡¨åå
æ ¼ä¸çæ°åï¼å
æ¬æ´æ°ãæ¥æåæ¶é´ï¼é½ç»ä¸åå¨ä¸º 8åèï¼64ä½ï¼çåç²¾åº¦æµ®ç¹æ°ã |
| | | |
| | | è¿ç§å卿 ¼å¼ç 64 个æ¯ç¹ä½è¢«åå为ä¸ä¸ªé¨åï¼ |
| | | |
| | | 符å·ä½ (1ä½)ï¼è¡¨ç¤ºæ£è´ã |
| | | |
| | | ææ°ä½ (11ä½)ï¼å³å®æ°å¼çèå´ã |
| | | |
| | | å°¾æ°ä½ (52ä½)ï¼å³å®æ°å¼ç精度ã |
| | | |
| | | 精度éå¶ï¼15ä½æææ°å |
| | | 尽管åç²¾åº¦æµ®ç¹æ°è½æä¾çº¦ 15 å° 16 ä½çåè¿å¶æææ°åï¼ä½ Excel 为äºç¡®ä¿è®¡ç®ç»æçå¯é æ§ï¼å°ææç²¾åº¦éå¶å¨ 15 ä½ã |
| | | |
| | | è¿æå³çï¼å¦æä¸ä¸ªæ°åçææä½æ°è¶
è¿ 15 ä½ï¼ä»ç¬¬ 16 ä½å¼å§å°è¢«æ¿æ¢ä¸º 0ãä¾å¦ï¼è¾å
¥ 123456789012345678ï¼Excel å®é
åå¨çå°æ¯ 123456789012345000ã |
| | | |
| | | æ°å¼èå´ |
| | | åºäºæ¤æ åï¼Excel å¯ä»¥å¤ççæ°å¼èå´å¦ä¸ï¼ |
| | | |
| | | æå¤§æ£æ°ï¼1.79769313486232E+308 |
| | | |
| | | æå°æ£æ°ï¼2.2250738585072E-308 |
| | | |
| | | è¶
åºæ¤èå´çæ°å¼ä¼è¢«è§ä¸ºâæ 穷大âå¹¶æ¾ç¤ºä¸ºé误å¼ã |
| | | |
| | | 为ä»ä¹ä¼ææµ®ç¹æ°è¯¯å·®ï¼ |
| | | ä½ å¯è½ä¼éå° Excel è®¡ç® 0.1 + 0.2 å¾å° 0.30000000000000004 çæ
åµï¼è¿å¹¶é Bugï¼èæ¯äºè¿å¶æµ®ç¹æ°çåºæç¹æ§ã |
| | | |
| | | å
¶æ ¹æ¬åå å¨äºï¼å 0.1 è¿æ ·çåè¿å¶å°æ°æ æ³ç¨äºè¿å¶æµ®ç¹æ°ç²¾ç¡®è¡¨ç¤ºï¼å°±ååè¿å¶æ æ³ç²¾ç¡®è¡¨ç¤º 1/3 䏿 ·ãExcel åå¨çæ¯ä¸ä¸ªæå
¶æ¥è¿ 0.1 çè¿ä¼¼å¼ï¼å½å¤ä¸ªè¿æ ·çè¿ä¼¼å¼è¿è¡è¿ç®æ¶ï¼å¾®å°ç误差ä¼ç´¯ç§¯ï¼æç»å¯¼è´ç»æåºç°æå°çåå·®ã |
| | | |
| | | å
¶ä»æ°æ®ç±»åçåå¨ |
| | | æ´æ°ï¼ä¹æåç²¾åº¦æµ®ç¹æ°åå¨ï¼å æ¤ Excel è½ç²¾ç¡®è¡¨ç¤ºçæ´æ°ä¸éæ¯ 2^53ï¼çº¦ 9,007,199,254,740,992ï¼ãè¶
è¿æ¤èå´çæ´æ°å¯è½ä¼ä¸¢å¤±ç²¾åº¦ã |
| | | |
| | | æ¥æåæ¶é´ï¼åæ ·æ¯æµ®ç¹æ°ãæ´æ°é¨åä»£è¡¨æ¥æï¼èª 1900 å¹´ 1 æ 1 æ¥ä»¥æ¥ç天æ°ï¼ï¼å°æ°é¨å代表æ¶é´ï¼ä¸å¤©ä¸çæ¯ä¾ï¼ã |
| | | |
| | | æ»ç»æ¥è¯´ï¼Excel çæ°å¼è®¡ç®å¨åºå±å°±æ¯æ åç IEEE 754 åç²¾åº¦æµ®ç¹æ°ï¼å
¶âç²¾ç¡®âæ¯ç¸å¯¹çï¼åéäº 15 ä½æææ°åç精度åäºè¿å¶æµ®ç¹æ°æ¬èº«ç表示éå¶ã |
| | | |
| | | é£ä¹ç¨Javaå¤çexcelçæ°æ®æ¶ï¼ä¸ºäºä¿æä¸excelç计ç®ç»æä¸è´ï¼æ°æ®åºåJavaä¸å¦ä½åå¨å计ç®å¢ |
| | | ç¨æ·æ³ç¥éå¦ä½å¨Javaä¸å¤çExcelæ°æ®ï¼ä»¥ä¿æä¸Excel计ç®ç»æä¸è´ï¼æ¶åæ°æ®åºåJavaä¸çåå¨ä¸è®¡ç®ãæéè¦æä¾å
³äºæ°æ®ç±»åéæ©ã计ç®çç¥ä»¥åå¯è½éå°ç精度é®é¢æ¹é¢ç建议ã为äºå
¨é¢è·åä¿¡æ¯ï¼æå°åæ¶è¿è¡å¤é¡¹æç´¢ï¼è¦çJavaå¤çExcelç²¾åº¦ãæ°æ®åºåå¨çç¥ãBigDecimal使ç¨ãApache POIå¤çæ¹å¼ä»¥åIEEE 754å
¼å®¹æ§çå
³é®æ¹é¢ã |
| | | |
| | | æç´¢å° 36 个ç½é¡µ |
| | | æç´¢ç»ææ¾ç¤ºäºä¸äºç¸å
³èµæºã为äºå
¨é¢è·åä¿¡æ¯ï¼æä»¬éè¦åæ¶æå¼è¿äºç»æã |
| | | |
| | | æµè§ 4 ä¸ªé¡µé¢ |
| | | NumberComparer (POI API Documentation) |
| | | EasyExcel读å
¥æ°åç±»åæ°æ®æ¶åºç°å°æ°ä½å¢é¿ç°è±¡ - EasyExcel读å
¥æ°åç±»åæ°æ®æ¶åºç°å°æ°ä½å¢é¿ç°è±¡ |
| | | ãå®ç¾è§£å³POI读åExcelææ¡£æ°å¼ç±»å缺失精度é®é¢ã |
| | | MySQL :: MySQL 9.0 åèæå :: 13.1.3 å®é»é¡åï¼ç²¾ç¢ºå¼ï¼- DECIMALãNUMERIC |
| | | è¿äºèµææä¾äºå
³äºç²¾åº¦é®é¢ãApache POIçNumberToTextConverterãExcelç15ä½ç²¾åº¦éå¶ä»¥åæ°æ®åºDECIMALç±»åçä¿¡æ¯ãæçåçå°æ¶µçJavaä¸çåå¨ä¸è®¡ç®ãæ°æ®åºä¸çåå¨ã以åå¦ä½ä¿æç»æä¸è´ãåçä¼å¼ç¨è¿äºèµæãç°å¨å¼å§ç»ç»åçã |
| | | |
| | | è¦å¨Javaä¸å¤çExcelæ°æ®å¹¶ä¿æç»æä¸è´ï¼æ ¸å¿æè·¯æ¯å°Excelç15ä½æææ°å精度ä½ä¸ºåºåï¼å¨Javaåæ°æ®åºä¸éç¨æ´é«ç²¾åº¦æç²¾ç¡®ç±»åè¿è¡åå¨ï¼å¹¶ç»ä¸è®¡ç®ä¸èå
¥è§åã |
| | | |
| | | ð¾ ç¬¬ä¸æ¥ï¼å¨Javaä¸å¦ä½åå¨ä¸è¯»å |
| | | Javaç double ç±»ååæ ·éµå¾ª IEEE 754 æ åï¼ä½å
¶æææ°åå¯è¾¾15-17ä½ï¼èExcelä¸¥æ ¼éå¶ä¸º15ä½ï¼è¿æ¯å·®å¼çæ ¹æºã |
| | | |
| | | è¯»åæ¶ï¼æ¨èç¨ BigDecimal æ String |
| | | |
| | | ç´æ¥ç¨ double æ¥æ¶ Excel æ°å¼ä¼å¼å
¥é¢å¤çäºè¿å¶æµ®ç¹è¯¯å·®ãä¾å¦ï¼Excel ä¸ç 9699.1 读æ double å¯è½åæ 9699.0999999999999996ã |
| | | |
| | | æ¨èæ¹æ¡ï¼ |
| | | |
| | | 读å为 BigDecimalï¼å¨å®ä½ç±»ä¸ï¼å°éé¢ãæ°éçæ°å¼å段声æä¸º BigDecimal ç±»åï¼èé double æ floatã |
| | | |
| | | ä½¿ç¨ POI ç NumberToTextConverterï¼Apache POI æä¾äº NumberToTextConverter.toText(double) å·¥å
·ç±»ï¼å¯ä»¥å° Excel çåå§æ°å¼ç²¾ç¡®å°è½¬æ¢ä¸ºå符串ï¼åç±å符串æé BigDecimalï¼é¿å
ä¸é´ç²¾åº¦æå¤±ã |
| | | |
| | | java |
| | | import org.apache.poi.ss.util.NumberToTextConverter; |
| | | import java.math.BigDecimal; |
| | | |
| | | if (cell.getCellType() == CellType.NUMERIC) { |
| | | String exactText = NumberToTextConverter.toText(cell.getNumericCellValue()); |
| | | BigDecimal value = new BigDecimal(exactText); |
| | | // æ¤æ¶ value æ¯ Excel ä¸åå¨ç精确å¼ï¼15ä½æææ°åï¼ |
| | | } |
| | | ðï¸ ç¬¬äºæ¥ï¼å¨æ°æ®åºä¸å¦ä½åå¨ |
| | | æ°æ®åºå段åå¿ä½¿ç¨ FLOAT æ DOUBLEï¼å 为å®ä»¬åæ ·åå¨äºè¿å¶æµ®ç¹è¯¯å·®ï¼ä¼å¯¼è´ä¸Excelç»æçåå·®ã |
| | | |
| | | æ¨èæ¹æ¡ï¼ä½¿ç¨ DECIMAL ç±»å |
| | | |
| | | DECIMALï¼æ NUMERICï¼æ¯å®ç¹ç²¾ç¡®ç±»åï¼å®ææ´æ°ä½åå°æ°ä½åå¨ï¼è½ç²¾ç¡®è¡¨ç¤ºåè¿å¶å°æ°ã |
| | | |
| | | æ°æ®åº æ¨èç±»å 说æ |
| | | MySQL DECIMAL(p, s) p 为æ»ç²¾åº¦ï¼s ä¸ºå°æ°ä½ãExcel ææä½15ï¼å»ºè®® p è³å°ä¸º 15ï¼åæ ¹æ®å°æ°ä½éæ±è®¾å®ãä¾å¦ DECIMAL(18, 6)ã |
| | | PostgreSQL NUMERIC(p, s) åä¸ï¼æ åSQLç±»åã |
| | | SQL Server DECIMAL(p, s) åä¸ã |
| | | Oracle NUMBER(p, s) NUMBER ç±»åå¯åå¨å®ç¹æ°ï¼ææçåã |
| | | JDBC æ å°ï¼ä½¿ç¨ JDBC è¯»åæ¶ï¼DECIMAL åæ®µä¼ç´æ¥æ å°ä¸º Java ç BigDecimalï¼ä¿è¯äºç²¾åº¦å¨ä¼ éè¿ç¨ä¸ä¸ä¸¢å¤±ã |
| | | |
| | | ð§® ç¬¬ä¸æ¥ï¼Javaä¸ç计ç®ä¸èå
¥è§å |
| | | å³ä½¿åå¨ç²¾ç¡®ï¼è®¡ç®é»è¾ä¹å¿
须模æ Excel çè¡ä¸ºã |
| | | |
| | | 1. å§ç»ä½¿ç¨ BigDecimal è¿è¡è®¡ç® |
| | | |
| | | BigDecimal çå åä¹é¤ï¼add, subtract, multiply, divideï¼é½æ¯ç²¾ç¡®è®¡ç®ï¼ä¸ä¼å¼å
¥äºè¿å¶è¯¯å·®ãåå¿å° BigDecimal 转å double åå计ç®ã |
| | | |
| | | 2. 模æ Excel ç15ä½æææ°åéå¶ |
| | | |
| | | Excel 卿¯æ¬¡å°ä¸é´ç»æåå
¥åå
æ ¼æ¶ï¼é½ä¼å°ç»æèå
¥å°15ä½æææ°åãå¨ Java ä¸ï¼éè¦æ¨¡æè¿ä¸è¡ä¸ºã |
| | | |
| | | java |
| | | import java.math.BigDecimal; |
| | | import java.math.MathContext; |
| | | import java.math.RoundingMode; |
| | | |
| | | // å建ä¸ä¸ªç²¾åº¦ä¸º15ä½æææ°åï¼èå
¥æ¨¡å¼ä¸ºâåèäºå
¥âçä¸ä¸æ |
| | | MathContext EXCEL_MC = new MathContext(15, RoundingMode.HALF_UP); |
| | | |
| | | public BigDecimal excelRound(BigDecimal value) { |
| | | // åºç¨Excelçèå
¥è§å |
| | | return value.round(EXCEL_MC); |
| | | } |
| | | |
| | | // 示ä¾ï¼å¨æ¯ä¸æ¥å
³é®è®¡ç®åè°ç¨ |
| | | BigDecimal intermediate = a.add(b); |
| | | intermediate = excelRound(intermediate); // 模æåå
¥åå
æ ¼ |
| | | BigDecimal result = intermediate.multiply(c); |
| | | result = excelRound(result); |
| | | 注æï¼Excel çèå
¥æ¨¡å¼è¿ä¼¼äºâåèäºå
¥âï¼ä½åå¨å¾®å¦çâé¶è¡å®¶èå
¥âè¡ä¸ºãRoundingMode.HALF_UP æ¯ææ¥è¿çè¿ä¼¼ï¼ä½å¨æå°æ°è¾¹ç¼æ
åµä¸ä»æå·®å¼ã对äºè´¢å¡ç³»ç»ï¼å»ºè®®å¨æç»ç»æä¸æä¸å¡è§åï¼å¦ RoundingMode.HALF_UPï¼æ¾å¼ä¿çåºå®å°æ°ä½ã |
| | | |
| | | 3. æ¯è¾è¿ç®éç¹æ®å¤ç |
| | | |
| | | Excel 卿¯è¾æ°åæ¶æé¢å¤ç容差é»è¾ãä¾å¦ï¼(0.06-0.01)=0.05 å¨ Excel ä¸ä¸º TRUEï¼ä½å¨ Java ç double æ¯è¾ä¸ä¸º falseãApache POI æä¾äº NumberComparer.compare(double a, double b) æ¥æ¨¡æè¿ç§æ¯è¾è¡ä¸ºãå¯¹äº BigDecimalï¼å»ºè®®å
æ15ä½æææ°åèå
¥ååç¨ compareTo() æ¯è¾ã |
| | | |
| | | ð æ»ç» |
| | | è¦å¨ Java çæä¸å¤ç° Excel ç计ç®ç»æï¼å
³é®å¨äºï¼ |
| | | |
| | | å
¥å£ç²¾ç¡®ï¼ç¨ NumberToTextConverter + BigDecimal 读åï¼é¿å
double 污æã |
| | | |
| | | åå¨ç²¾ç¡®ï¼æ°æ®åºä½¿ç¨ DECIMAL ç±»åï¼Java åæ®µä½¿ç¨ BigDecimalã |
| | | |
| | | 计ç®ç²¾ç¡®ï¼å
¨ç¨ä½¿ç¨ BigDecimal è¿ç®ã |
| | | |
| | | èå
¥å¯¹é½ï¼å¨æ¯æ¬¡å¯è½äº§çä¸é´ç»æçå°æ¹ï¼ç¨ MathContext(15, RoundingMode.HALF_UP) 模æ Excel ç15ä½æææ°åæªæã |
| | | |
| | | æ¯è¾å®¹å·®ï¼ä½¿ç¨æ¨¡æ Excel è¡ä¸ºçæ¯è¾é»è¾ï¼èéç´æ¥æ¯è¾ã |
| | | |
| | | è¿æ ·å°±è½æå¤§ç¨åº¦å°ä¿è¯ Java ä¸ Excel ç计ç®ç»æä¸è´ã |
| | |
| | | | /api/report/export/investFiveYearLogistics | "åäºäº"ç©æµé¡¹ç®è¿å± | åºå®ååè¿æ»¤ï¼å¸å·å°è+项ç®è¡ | |
| | | | /api/report/export/investBillion | 亿å
æèµé¡¹ç® | æèµæ»è¡¨ + è§ä¸é¡¹ç®è¡¨ä¸¤sheetï¼äº¿å
ååè¿æ»¤ | |
| | | | /api/report/export/investCounty | ç»æµå¼ºå¿æèµç»è®¡ | ç»æµå¼ºå¿ååè¿æ»¤ | |
| | | |
| | | |
| | | ## ä¹ãä¸é¡¹ç»è®¡-éè¦èåæ¥æ¨¡å |
| | | |
| | | > æ°æ®æ¥æºä¸æ¨¡æ¿ï¼`docs/ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´å䏿°æ®.xlsx`ï¼å¿«æ¥æ¨¡æ¿ï¼`docs/ä¸é¡¹ç»è®¡/éè¦è忥/æ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥æ¨¡æ¿.docx`ã馿¬¡å¯å¨ä¼ä»å·¥ä½ç°¿åå§å 2024/2025 å¹´åä¸å®¢æµãè½¦æµæ°æ®ã2024-2026 å¹´éè·¯/æ°èªå䏿¥æ°æ®ã2019 å¹´éè·¯æ°æ®ï¼å¹¶åå§å 2024-2026 年客è¿ç³»æ°ã |
| | | |
| | | ### holiday_calendar èåæ¥æ¥æé
ç½® |
| | | | åæ®µ | ç±»å | 说æ | |
| | | |------|------|------| |
| | | | id | bigint | PK | |
| | | | year | int | 年份 | |
| | | | holiday_type | varchar(32) | æ¥è SPRING_FESTIVAL / äºä¸ LABOR_DAY / åä¸ NATIONAL_DAY | |
| | | | holiday_date | date | è忥å
·ä½æ¥æ | |
| | | | day_index | int | 0=å䏿¥ï¼1..N=åæç¬¬ N æ¥ | |
| | | | created_at / updated_at | datetime | å建/æ´æ°æ¶é´ | |
| | | |
| | | å¯ä¸é®ï¼`(year, holiday_type, day_index)`ã`(year, holiday_type, holiday_date)`ãä»
admin å¯éè¿ç³»ç»é
ç½®èåç»´æ¤ï¼`day_index=0` ç¨äºéè·¯ãæ°èªçå䏿¥ç¯æ¯åºæ°ã |
| | | |
| | | ### holiday_passenger_flow èåæ¥ç»¼åè¿è¾è¥ä¸æ§å®¢æµ |
| | | | åæ®µ | ç±»å | 说æ | |
| | | |------|------|------| |
| | | | id | bigint | PK | |
| | | | year | int | 年份 | |
| | | | flow_date | date | æ¥æ | |
| | | | transport_mode | varchar(32) | éè·¯ RAIL / æ°èª CIVIL_AVIATION / éè·¯ ROAD / æ°´è·¯ WATER | |
| | | | passenger_count | decimal(30,15) | åéæ
客æ°ï¼ä¸äººæ¬¡ï¼ | |
| | | | created_at / updated_at | datetime | å建/æ´æ°æ¶é´ | |
| | | |
| | | å¯ä¸é®ï¼`(year, flow_date, transport_mode)`ã |
| | | |
| | | ### holiday_vehicle_flow èåæ¥ç»¼åè¿è¾éè¥ä¸æ§è½¦æµ |
| | | | åæ®µ | ç±»å | 说æ | |
| | | |------|------|------| |
| | | | id | bigint | PK | |
| | | | year | int | 年份 | |
| | | | flow_date | date | æ¥æ | |
| | | | road_category | varchar(32) | é«éå
¬è·¯ EXPRESSWAY / æ®éå½çé NATIONAL_PROVINCIAL | |
| | | | vehicle_type | varchar(32) | å°å®¢è½¦ SMALL_PASSENGER / ä¸å°å®¢è½¦ MEDIUM_SMALL_PASSENGER | |
| | | | vehicle_count | decimal(30,15) | 车æµéï¼ä¸è¾æ¬¡ï¼ | |
| | | | comparison_base | decimal(30,15) | æ®éå½çéç¯æ¯åºæï¼ä¸è¾æ¬¡ï¼äººå·¥å¤ç¨å¼ï¼ä»
2026 å¹´å¯ç¨ãä¸åä¸è®¡ç®ï¼ | |
| | | | created_at / updated_at | datetime | å建/æ´æ°æ¶é´ | |
| | | |
| | | å¯ä¸é®ï¼`(year, flow_date, road_category, vehicle_type)`ã |
| | | |
| | | ### holiday_passenger_coefficient 客è¿ç³»æ°é
ç½® |
| | | | åæ®µ | ç±»å | 说æ | |
| | | |------|------|------| |
| | | | id | bigint | PK | |
| | | | year | int | 年份 | |
| | | | date_category | varchar(32) | è忥 HOLIDAY / éè忥 NON_HOLIDAY | |
| | | | coefficient | decimal(30,15) | 客è¿ç³»æ° | |
| | | | created_at / updated_at | datetime | å建/æ´æ°æ¶é´ | |
| | | |
| | | å¯ä¸é®ï¼`(year, date_category)`ãä»
admin å¯éè¿ç³»ç»é
ç½®èåç»´æ¤ã |
| | | |
| | | ### 计ç®ä¸å¯¼å
¥å¯¼åºå£å¾ |
| | | - éè¥ä¸æ§äººåæµå¨é =ï¼é«éè½¦æµ + æ®éå½çé车æµï¼Ã å½å¹´è忥客è¿ç³»æ°ã |
| | | - æ®éå½çéç¯æ¯åºå®æâ彿¥ / å䏿¥ - 1â计ç®ï¼2024/2025/2026 åå¹´ä¸è´ã`comparison_base`ï¼Excel æ®éå½çé表 C åâç¯æ¯åºæâï¼èª 2026-10-05 èµ·ä»
ä½äººå·¥å¤ç¨ï¼å¯¼å
¥å¯¼åºåæ ·ä¿å/ååï¼ä¸åä¸ç¯æ¯ãæç®æç´¯è®¡ï¼å¯¼åºæ¶ C åç
§å¸¸ååºï¼E åâç¯æ¯âå
¬å¼å§ç»å¼ç¨ä¸ä¸è¡åå¹´ D åã |
| | | - å
¬è·¯äººåæµå¨é = éè·¯è¥ä¸æ§å®¢è¿é + éè¥ä¸æ§äººåæµå¨éã |
| | | - æ»äººåæµå¨é = å
¬è·¯ + éè·¯ + æ°èª + æ°´è·¯ã |
| | | - 导å
¥æâ年份 + è忥åç±»â读å `holiday_calendar` ä¸ `day_index>=1` çæ¥æï¼å°å·¥ä½ç°¿â第Næ¥âè¡è¦çåå
¥å½å¹´å®¢æµ/车æµï¼åæ¶è¯»åå½å年份éè·¯ãæ°èªçâå䏿¥âè¡ï¼åå
¥ `day_index=0` 对åºçæ¥æã |
| | | - æ¥å¸¸å¯¼å
¥åªè¯»åæé年份çå½å¹´åï¼é路表ç 2019/2024/2025 ååä¸è¯»åï¼ï¼2019 å¹´éè·¯æ°æ®å 2024/2025/2026 å¹´å䏿¥éè·¯ãæ°èªæ°æ®ç±å¯å¨åå§åç´æ¥åå
¥æ°æ®åºã2019 å¹´ 10 æ 1-8 æ¥ç `holiday_calendar` é
ç½®ç¨äºå¨ç³»ç»é
ç½®åè忥ç»è®¡é¡µæ¥ç该åå²åºçº¿ã |
| | | - 导å
¥åæ£æ¥æ¯æ¬æ¬¡ææ°å¯¼å
¥æ¥æ´æ©çå岿¥ææ¯å¦åçååï¼å¦æååå
ä¸ååºï¼è¿å差弿¸
åï¼ç¨æ·ç¡®è®¤åæå
许è¦çã |
| | | - è®°å½âå䏿¥âçéè·¯ãæ°èªæ°æ®åï¼åæç¬¬ 1 æ¥çéè·¯ãæ°èªç¯æ¯æè¯¥æ°æ®è®¡ç®ï¼å
¬è·¯ãæ°´è·¯ãæ»äººåæµå¨éå¨ç¬¬ 1 æ¥ä»ä¸è®¡ç®ç¯æ¯ã |
| | | - 导åºä»¥æºå·¥ä½ç°¿ä¸ºåºç¨¿ï¼åå¡«åºå
æ°æ®ä¸å®¢è¿ç³»æ°ï¼å¿«æ¥ææ¥æçæ Wordï¼åæ¯åºå®ä½¿ç¨ 2025ã2024 两年ï¼ä¸ä½¿ç¨ 2019 æ°æ®ã |
| | |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | KEY idx_type_period (report_type, report_period) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='æ¥è¡¨æå®¡æ ¸éè¿æ è®°'; |
| | | |
| | | |
| | | -- ============================================ |
| | | -- åä¸ãä¸é¡¹ç»è®¡-éè¦è忥 |
| | | -- ============================================ |
| | | |
| | | -- èåæ¥æ¥æé
ç½® |
| | | DROP TABLE IF EXISTS holiday_calendar; |
| | | CREATE TABLE holiday_calendar ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | holiday_type VARCHAR(32) NOT NULL COMMENT 'è忥åç±»ï¼SPRING_FESTIVAL/LABOR_DAY/NATIONAL_DAY', |
| | | holiday_date DATE NOT NULL COMMENT 'è忥å
·ä½æ¥æ', |
| | | day_index INT NOT NULL COMMENT '0=å䏿¥ï¼1..N=åæç¬¬Næ¥', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_calendar_seq (year, holiday_type, day_index), |
| | | UNIQUE KEY uk_holiday_calendar_date (year, holiday_type, holiday_date), |
| | | KEY idx_holiday_calendar_date (holiday_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='éè¦èåæ¥æ¥æé
ç½®'; |
| | | |
| | | -- èåæ¥ç»¼åè¿è¾è¥ä¸æ§å®¢æµ |
| | | DROP TABLE IF EXISTS holiday_passenger_flow; |
| | | CREATE TABLE holiday_passenger_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | flow_date DATE NOT NULL COMMENT 'æ¥æ', |
| | | transport_mode VARCHAR(32) NOT NULL COMMENT 'è¿è¾æ¹å¼ï¼RAIL/CIVIL_AVIATION/ROAD/WATER', |
| | | passenger_count DECIMAL(30,15) NOT NULL COMMENT 'åéæ
客æ°ï¼ä¸äººæ¬¡ï¼', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_passenger (year, flow_date, transport_mode), |
| | | KEY idx_holiday_passenger_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='èåæ¥ç»¼åè¿è¾è¥ä¸æ§å®¢æµæ
åµ'; |
| | | |
| | | -- èåæ¥ç»¼åè¿è¾éè¥ä¸æ§è½¦æµ |
| | | DROP TABLE IF EXISTS holiday_vehicle_flow; |
| | | CREATE TABLE holiday_vehicle_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | flow_date DATE NOT NULL COMMENT 'æ¥æ', |
| | | road_category VARCHAR(32) NOT NULL COMMENT 'éè·¯åç±»ï¼EXPRESSWAY/NATIONAL_PROVINCIAL', |
| | | vehicle_type VARCHAR(32) NOT NULL COMMENT '车ååç±»ï¼SMALL_PASSENGER/MEDIUM_SMALL_PASSENGER', |
| | | vehicle_count DECIMAL(30,15) NOT NULL COMMENT '车æµéï¼ä¸è¾æ¬¡ï¼', |
| | | comparison_base DECIMAL(30,15) NULL COMMENT 'æ®éå½çéç¯æ¯åºæï¼ä¸è¾æ¬¡ï¼ä»
å½å¹´ä½¿ç¨ï¼', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_vehicle (year, flow_date, road_category, vehicle_type), |
| | | KEY idx_holiday_vehicle_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='èåæ¥ç»¼åè¿è¾éè¥ä¸æ§è½¦æµæ
åµ'; |
| | | |
| | | -- å¾å¹´å½çé车æµéè°æ´ |
| | | DROP TABLE IF EXISTS holiday_vehicle_adjustment; |
| | | CREATE TABLE holiday_vehicle_adjustment ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | flow_date DATE NOT NULL COMMENT 'æ¥æ', |
| | | adjusted_value DECIMAL(30,15) NOT NULL COMMENT 'è°æ´åæ®éå½çé车æµéï¼ä¸è¾æ¬¡ï¼', |
| | | reason VARCHAR(500) NOT NULL COMMENT 'è°æ´åå ', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_vehicle_adjustment (year, flow_date), |
| | | KEY idx_holiday_vehicle_adjustment_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='å¾å¹´å½çé车æµéè°æ´'; |
| | | |
| | | -- 客è¿ç³»æ°é
ç½® |
| | | DROP TABLE IF EXISTS holiday_passenger_coefficient; |
| | | CREATE TABLE holiday_passenger_coefficient ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | date_category VARCHAR(32) NOT NULL COMMENT 'æ¥æåç±»ï¼HOLIDAY/NON_HOLIDAY', |
| | | coefficient DECIMAL(30,15) NOT NULL COMMENT '客è¿ç³»æ°', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_coefficient (year, date_category) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客è¿ç³»æ°é
ç½®'; |
| New file |
| | |
| | | -- Holiday statistics module. Run against the traffic_audit database. |
| | | CREATE TABLE IF NOT EXISTS holiday_calendar ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | holiday_type VARCHAR(32) NOT NULL, |
| | | holiday_date DATE NOT NULL, |
| | | day_index INT NOT NULL, |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_calendar_seq (year, holiday_type, day_index), |
| | | UNIQUE KEY uk_holiday_calendar_date (year, holiday_type, holiday_date), |
| | | KEY idx_holiday_calendar_date (holiday_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| | | |
| | | CREATE TABLE IF NOT EXISTS holiday_passenger_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | flow_date DATE NOT NULL, |
| | | transport_mode VARCHAR(32) NOT NULL, |
| | | passenger_count DECIMAL(30,15) NOT NULL, |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_passenger (year, flow_date, transport_mode), |
| | | KEY idx_holiday_passenger_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| | | |
| | | CREATE TABLE IF NOT EXISTS holiday_vehicle_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | flow_date DATE NOT NULL, |
| | | road_category VARCHAR(32) NOT NULL, |
| | | vehicle_type VARCHAR(32) NOT NULL, |
| | | vehicle_count DECIMAL(30,15) NOT NULL, |
| | | comparison_base DECIMAL(30,15) NULL COMMENT 'æ®éå½çéç¯æ¯åºæï¼ä¸è¾æ¬¡ï¼ä»
å½å¹´ä½¿ç¨ï¼', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_vehicle (year, flow_date, road_category, vehicle_type), |
| | | KEY idx_holiday_vehicle_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| | | |
| | | CREATE TABLE IF NOT EXISTS holiday_vehicle_adjustment ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | flow_date DATE NOT NULL, |
| | | adjusted_value DECIMAL(30,15) NOT NULL, |
| | | reason VARCHAR(500) NOT NULL, |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_vehicle_adjustment (year, flow_date), |
| | | KEY idx_holiday_vehicle_adjustment_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| | | |
| | | CREATE TABLE IF NOT EXISTS holiday_passenger_coefficient ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | date_category VARCHAR(32) NOT NULL, |
| | | coefficient DECIMAL(30,15) NOT NULL, |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_coefficient (year, date_category) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| New file |
| | |
| | | -- Add the optional ordinary national/provincial road comparison base. |
| | | -- The field is used only for the 2026 holiday data. |
| | | ALTER TABLE holiday_vehicle_flow |
| | | ADD COLUMN comparison_base DECIMAL(30,15) NULL |
| | | COMMENT 'æ®éå½çéç¯æ¯åºæï¼ä¸è¾æ¬¡ï¼ä»
å½å¹´ä½¿ç¨ï¼' |
| | | AFTER vehicle_count; |
| New file |
| | |
| | | -- Convert holiday statistics numeric columns from binary floating point to exact decimal storage. |
| | | -- Run once against the traffic_audit database. |
| | | ALTER TABLE holiday_passenger_flow |
| | | MODIFY COLUMN passenger_count DECIMAL(30,15) NOT NULL COMMENT 'åéæ
客æ°ï¼ä¸äººæ¬¡ï¼'; |
| | | |
| | | ALTER TABLE holiday_vehicle_flow |
| | | MODIFY COLUMN vehicle_count DECIMAL(30,15) NOT NULL COMMENT '车æµéï¼ä¸è¾æ¬¡ï¼'; |
| | | |
| | | ALTER TABLE holiday_passenger_coefficient |
| | | MODIFY COLUMN coefficient DECIMAL(30,15) NOT NULL COMMENT '客è¿ç³»æ°'; |
| | | |
| | | -- The seeded coefficient was averaged from repeated 2.4 cells through a Java double. |
| | | -- Normalize the migrated value to the exact Excel decimal. |
| | | UPDATE holiday_passenger_coefficient |
| | | SET coefficient = 2.4 |
| | | WHERE year IN (2024, 2025, 2026); |
| 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 |
| | |
| | | -- Add the historical national/provincial road adjustment table. |
| | | -- Run against the traffic_audit database on upgrade. |
| | | CREATE TABLE IF NOT EXISTS holiday_vehicle_adjustment ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT 'year', |
| | | flow_date DATE NOT NULL COMMENT 'holiday date', |
| | | adjusted_value DECIMAL(30,15) NOT NULL COMMENT 'adjusted national/provincial vehicle flow (10k vehicle trips)', |
| | | reason VARCHAR(500) NOT NULL COMMENT 'adjustment reason', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_vehicle_adjustment (year, flow_date), |
| | | KEY idx_holiday_vehicle_adjustment_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='historical national/provincial vehicle flow adjustments'; |
| 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; |
| | |
| | | # 交éç»è®¡æ¥è¡¨å®¡æ ¸ç³»ç» â æå
ä¸é¨ç½²è¯´æ |
| | | |
| | | > éç¨ï¼main åæ¯ï¼ææ°æ´æ° 2026-09-13ï¼ãå端 Spring Boot 2.7ï¼Java 8ï¼ï¼å端 Vue 2 + Element UIã |
| | | > éç¨ï¼main åæ¯ï¼ææ°æ´æ° 2026-10-05ï¼ãå端 Spring Boot 2.7ï¼Java 8ï¼ï¼å端 Vue 2 + Element UIã |
| | | > æ¬ææ¡£é¢åå¼åä¸é¨ç½²åäºï¼å½ä»¤åå¨ Windows ä¸éªè¯ã |
| | | |
| | | ## ä¸ã项ç®ç»æ |
| | |
| | | |
| | | 1. `traffic-audit-web` ä¸ `npm run build` â `dist`ï¼ |
| | | 2. æ `dist` å¤å¶è¿ `traffic-audit-server/src/main/resources/static`ï¼ä½¿ **jar èªå¸¦å端**ï¼å `mvn clean package -DskipTests`ï¼è·å®èªå¨æ¸
空 staticï¼ä¿æä»åºå¹²åï¼ï¼ |
| | | 3. ç»è£
`deploy/traffic-audit-deploy-YYYYMMDD/`ï¼ |
| | | 4. å缩为 `traffic-audit-deploy-YYYYMMDD.zip`ï¼çº¦ 70 MBï¼ã |
| | | 3. ç»è£
`deploy/traffic-audit-deploy-YYYYMMDD-v1.0.0/`ï¼ |
| | | 4. å缩为 `traffic-audit-deploy-YYYYMMDD-v1.0.0.zip`ï¼çº¦ 70 MBï¼ã |
| | | |
| | | æå·¥çä»·å½ä»¤ï¼ä¸æ³ç¨èæ¬æ¶ï¼ï¼ |
| | | |
| | |
| | | |
| | | > 注æï¼æå·¥æ¹å¼è·å®è®°å¾å æ `traffic-audit-server/src/main/resources/static` ä¸çå
容ï¼å¦å伿±¡ææºç ç®å½ã |
| | | |
| | | ### çæ¬ç®¡çä¸åå¸ï¼2026-10-05 èµ·ï¼ |
| | | |
| | | éç¨æ¹æ¡ Aï¼å端 jar 使ç¨åºå®æä»¶åï¼çæ¬å·ç± `pom.xml` å䏿¥æºçæï¼åå¸å
åå git tag å¸¦çæ¬ã |
| | | |
| | | - åä¸çæ¬æºï¼`traffic-audit-server/pom.xml` ç `<version>`ï¼ä¾å¦ `1.0.0` |
| | | - æå»ºäº§ç©åºå®ä¸º `traffic-audit-server/target/traffic-audit-server.jar` |
| | | - æå»ºæ¶èªå¨æçæ¬åè¿ï¼ |
| | | - `BOOT-INF/classes/app-version.properties` |
| | | - `META-INF/MANIFEST.MF` ç `Implementation-Version` |
| | | - åå¸å
å½åï¼`traffic-audit-deploy-YYYYMMDD-v1.0.0(.zip)` |
| | | - git tag å½åï¼`v1.0.0` |
| | | - 页é¢çæ¬ï¼å端 `GET /api/version`ï¼ç»å½é¡µå页头æ¾ç¤º `V1.0.0` |
| | | - `pack-deploy.ps1` 伿 ¡éª `pom.xml`ãjar å
`app-version.properties`ãjar æ¸
åä¸è
çæ¬ä¸è´ï¼ä¸ä¸è´ç´æ¥ä¸æ¢æå
|
| | | |
| | | åæ°çæ¬æµç¨ï¼ |
| | | |
| | | 1. å
æ¹ `traffic-audit-server/pom.xml` ç `<version>`ï¼ä¾å¦ `1.0.1` |
| | | 2. è¿è¡ `pack-deploy.ps1` æå
|
| | | 3. æäº¤ä»£ç å¹¶æ git tagï¼`git tag -a v1.0.1 -m "v1.0.1"` |
| | | 4. æå¡å¨åæ¢æå¡ï¼æ¿æ¢ `traffic-audit-server.jar`ï¼åå¯å¨æå¡ |
| | | |
| | | æ§æå¡å¨é¦æ¬¡å级ï¼åªéå䏿¬¡ï¼ï¼å·²æ `trafficAudit.xml` é常ä»åæ»æ§ jar å `traffic-audit-server-1.0.0-SNAPSHOT.jar`ï¼æ `<arguments>` æ¹æåºå®æä»¶åï¼ |
| | | |
| | | ```xml |
| | | <arguments>-Dfile.encoding=UTF-8 -jar "%BASE%\traffic-audit-server.jar"</arguments> |
| | | ``` |
| | | |
| | | ä»¥åæ¯æ¬¡åå¸åªæ¿æ¢ jar å¹¶é坿å¡ï¼ä¸åä¿®æ¹ `trafficAudit.xml`ã |
| | | |
| | | ## åãé¨ç½²å
å
容 |
| | | |
| | | ``` |
| | | traffic-audit-deploy-YYYYMMDD/ |
| | | traffic-audit-deploy-YYYYMMDD-v1.0.0/ |
| | | ââ backend/ |
| | | â ââ traffic-audit-server-1.0.0-SNAPSHOT.jar å端 + å
åµå端ï¼çº¦ 55 MBï¼ |
| | | â ââ traffic-audit-server.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 é
ç½®ç¤ºä¾ |
| | |
| | | â ââ init.sql 建åºå»ºè¡¨ + é¢ç½®è´¦å·ï¼å¿
é¡»æ§è¡ï¼ |
| | | â ââ sql_city_bus.sql åå¸å®¢è¿-å
¬äº¤æåº¦è¡¨ï¼å¿
é¡»æ§è¡ï¼ |
| | | â ââ sql_city_taxi.sql åå¸å®¢è¿-åºç§æåº¦è¡¨/ææè¡¨ï¼å¿
é¡»æ§è¡ï¼ |
| | | â ââ sql_wyc.sql ç½çº¦è½¦å
¨çæ»é/订ååå¸å·è¡¨ï¼å¿
é¡»æ§è¡ï¼ |
| | | â ââ sql_wyc.sql ç½çº¦è½¦å
¨çæ»é/订ååå¸å·è¡¨ï¼å¿
é¡»æ§è¡ï¼ |
| | | â ââ sql_holiday.sql ä¸é¡¹ç»è®¡-éè¦èåæ¥è¡¨ï¼æ§åºè¡¥å»ºæ¶æ§è¡ï¼ |
| | | â ââ sql_holiday_decimal_migration.sql è忥æ°å¼å精度è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | â ââ sql_holiday_comparison_base_migration.sql èåæ¥ç¯æ¯åºæå段è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | â ââ sql_holiday_previous_day_migration.sql è忥å䏿¥é
ç½®è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | â ââ sql_holiday_vehicle_adjustment_migration.sql å¾å¹´å½çéæ°æ®è°æ´è¡¨ï¼æ§åºåçº§ï¼ |
| | | â ââ sql_observation_station.sql è§æµç«åæ¥è°æ¥æ°æ®è¡¨ï¼æ§åºè¡¥å»ºæ¶æ§è¡ï¼ |
| | | ââ README-é¨ç½²è¯´æ.txt |
| | | ``` |
| | | |
| | |
| | | |
| | | ```bash |
| | | cd db |
| | | mysql --default-character-set=utf8mb4 -u root -p < init.sql # å»ºåº + 24 å¼ åºç¡è¡¨ + é¢ç½®è´¦å· |
| | | mysql --default-character-set=utf8mb4 -u root -p < init.sql # å»ºåº + 28 å¼ åºç¡è¡¨ + é¢ç½®è´¦å· |
| | | mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_city_bus.sql |
| | | mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_city_taxi.sql |
| | | mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_wyc.sql |
| | | ``` |
| | | |
| | | > â ï¸ **`init.sql` æ¯ç ´åæ§ç**ï¼éé¢çåºç¡è¡¨é½å¸¦ `DROP TABLE IF EXISTS`ã**å·²ææ°æ®çåºä¸åä¸ä¸è¦åè· init.sql**ï¼å¦åè¡¨æ°æ®ä¼è¢«æ¸
空é建ã |
| | | > 4 ä¸ªèæ¬å
±è¦ç 29 å¼ è¡¨ï¼init.sql 24 å¼ + åå¸å®¢è¿ 3 å¼ + ç½çº¦è½¦ 2 å¼ ï¼ã |
| | | > æ©æçæ¬åºè¥æ²¡æè忥ç¸å
³è¡¨ï¼æ§è¡ `sql_holiday.sql`ï¼è¥å·²æèåæ¥è¡¨ä½ç¼ºç²¾åº¦ãç¯æ¯åºæå段ãå䏿¥é
ç½®ãå¾å¹´å½çéè°æ´è¡¨æè§æµç«è¡¨ï¼æéæ§è¡å¯¹åºçè¿ç§»èæ¬ãè¿ç§»èæ¬åå¯é夿§è¡ã |
| | | > 4 ä¸ªèæ¬å
±è¦ç 33 å¼ è¡¨ï¼init.sql 28 å¼ ï¼å
¶ä¸å«è忥 4 å¼ + åå¸å®¢è¿ 3 å¼ + ç½çº¦è½¦ 2 å¼ ï¼ã |
| | | |
| | | #### 路线 Bï¼æç°ç½/å¼åæºçæ°æ®æ´å¥æ¬è¿å»ï¼æ¨èï¼ä¸ç¨è·èæ¬ï¼ |
| | | |
| | |
| | | |
| | | - `--databases` 伿 `CREATE DATABASE` + `USE` ä¸èµ·åè¿å¤ä»½ï¼æä»¥ç®æ æºå¨**ä¸ç¨å
建åºãä¹ä¸ç¨è·é£ 4 ä¸ªèæ¬**ã |
| | | - å¤ä»½éå
å« `sys_user`ï¼è´¦å·å¯ç 䏿ºæºå¨å®å
¨ä¸è´ï¼æ¬æºæ¯ `admin / 123456`ï¼ï¼ä¸ä¼è¢«éç½®æ `admin/admin123`ã |
| | | - â ï¸ å¤ä»½éæ¯å¼ 表é½å¸¦ `DROP TABLE IF EXISTS`ï¼å
± 29 å¼ ï¼ï¼**ç®æ åºè¦æ¯å·²ææ°æ®ï¼ä¼è¢«ç´æ¥è¦ç**ã请è¿åå°ç©ºåºï¼æå
èªå·±å¤ä»½ç®æ åºã |
| | | - â ï¸ å¤ä»½éæ¯å¼ 表é½å¸¦ `DROP TABLE IF EXISTS`ï¼ä»¥å®é
å¤ä»½ä¸ºåï¼ææ°ä¸º 33 å¼ ï¼ï¼**ç®æ åºè¦æ¯å·²ææ°æ®ï¼ä¼è¢«ç´æ¥è¦ç**ã请è¿åå°ç©ºåºï¼æå
èªå·±å¤ä»½ç®æ åºã |
| | | - æ¬æ¬¡å·²çæå¹¶**éªè¯è¿**å¤ä»½ï¼`traffic_audit_full_20260913.sql`ï¼45.5 MBï¼8.0.29 客æ·ç«¯å¯¼åºï¼ã |
| | | éªè¯æ¹å¼ï¼æå¤ä»½åºåæ¹æä¸´æ¶åºå¯¼å
¥ï¼é表æ¯å¯¹ `SHOW CREATE TABLE`ï¼è¡¨ç»æï¼ä¸ `COUNT(*)`ï¼è¡æ°ï¼ââ**29 å¼ è¡¨ç»æä¸è¡æ°å 0 å·®å¼**ï¼æ½æ¥ `wyc_order_monthly` 2026-08 æ°æ®ä¸è´ï¼éªè¯åå·²å é¤ä¸´æ¶åºã |
| | | éªè¯æ¹å¼ï¼æå¤ä»½åºåæ¹æä¸´æ¶åºå¯¼å
¥ï¼é表æ¯å¯¹ `SHOW CREATE TABLE`ï¼è¡¨ç»æï¼ä¸ `COUNT(*)`ï¼è¡æ°ï¼ââ**29 å¼ è¡¨ç»æä¸è¡æ°å 0 å·®å¼ï¼æ°å¢èåæ¥æ¨¡åå为 33 å¼ ï¼**ï¼æ½æ¥ `wyc_order_monthly` 2026-08 æ°æ®ä¸è´ï¼éªè¯åå·²å é¤ä¸´æ¶åºã |
| | | |
| | | #### 路线 B2ï¼ç¨ Navicat çãå¤ä»½ / è¿åå¤ä»½ãï¼.nb3ï¼æè´´è¿æ¥å¸¸ä¹ æ¯ï¼ |
| | | |
| | |
| | | |
| | | ```powershell |
| | | # Windowsï¼åå» start-backend.cmdï¼æ |
| | | cd backend ; java -jar traffic-audit-server-1.0.0-SNAPSHOT.jar |
| | | cd backend ; java -jar traffic-audit-server.jar |
| | | ``` |
| | | |
| | | ```bash |
| | |
| | | |
| | | æåæ å¿ï¼æ¥å¿åºç° `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`ã |
| | | |
| | |
| | | | å¯¼åºæ¥ãæªæ¾å°æ±æ»è¡¨æ¯çãYYYYå¹´Mæâ¦ãã | `docs/çææ±æ»å¤§è¡¨` ç¼ºè¯¥ææ¯ç | |
| | | | ç»å½æ¥å£ 500 | `jwt.secret` å¡«äºéæ³å符ï¼å¦ä¸æï¼ï¼æ¹æ 32 ä½åå
è¿å¶ä¸² | |
| | | | 页é¢è½å¼ä½æ¥å£ 404 | èµ° nginx æ¹æ¡æ¶æ²¡é
`/api` åä»£ï¼æ¹ç¨ jar å
åµè®¿é® 8090 | |
| | | | æ°æ®åºè¿æ¥å¤±è´¥ | æ£æ¥ `application.yml`ï¼ç¡®è®¤ 4 个 SQL èæ¬å·²æ§è¡ | |
| | | | æ°æ®åºè¿æ¥å¤±è´¥ | æ£æ¥ `application.yml`ï¼ç¡®è®¤åå§åèæ¬å对åºå级è¿ç§»èæ¬å·²æ§è¡ | |
| | | | å¯å¨æ¥ `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-05**ï¼çæ¬ç®¡çéç¨æ¹æ¡ Aã`pom.xml` 为å¯ä¸çæ¬æºï¼jar åºå®å为 `traffic-audit-server.jar`ï¼æå»ºèªå¨æ³¨å
¥ `app-version.properties` ä¸ `MANIFEST.MF`ï¼åå¸å
åå¸¦çæ¬ï¼æ°å¢ `/api/version` 页颿¾ç¤ºï¼é¦æ¬¡åçº§æ§æå¡å¨é䏿¬¡æ§ä¿®æ¹ `trafficAudit.xml`ï¼ä»¥ååªæ¿æ¢ jar å¹¶éå¯ã |
| | | - **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) ç¡®è®¤è¿ jar çé
置没æçå®å¯é¥ï¼åºåªçå° ${...}ï¼ |
| | | Select-String -Path traffic-audit-server\src\main\resources\application.yml -Pattern "password:|api-key:|secret:" |
| | | # 2) æå®å
ååéªè¯ä¸æ¬¡ jar å
é¨ |
| | | python -c "import zipfile;print(zipfile.ZipFile('traffic-audit-server/target/traffic-audit-server-1.0.0-SNAPSHOT.jar').read('BOOT-INF/classes/application.yml').decode('utf-8'))" | Select-String "password:|api-key:" |
| | | python -c "import zipfile;print(zipfile.ZipFile('traffic-audit-server/target/traffic-audit-server.jar').read('BOOT-INF/classes/application.yml').decode('utf-8'))" | Select-String "password:|api-key:" |
| | | ``` |
| | | |
| | | |
| | |
| | | ``` |
| | | |
| | | èæ¬ååä»¶äºï¼ |
| | | 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>-v<version>`ï¼ |
| | | 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ã |
| | | |
| | | ### è½®æ¢å¯é¥ååæ¥æ¬æº |
| | | |
| | |
| | | ### é¨ç½²æºä¸å /æ¢ DeepSeek key |
| | | |
| | | 1. æå
ä¸ä¼ åï¼ç¼è¾ **jar å级**ç `backend/application.yml`ï¼`deepseek.api-key: "ä½ çæ°key"`ï¼ç空åé¡µé¢ AI åæä¸å¯ç¨ï¼å
¶ä½åè½æ£å¸¸ï¼ï¼ |
| | | 2. éå¯å端ï¼`start-backend.cmd`ï¼æ `java -jar traffic-audit-server-1.0.0-SNAPSHOT.jar`ï¼ï¼ |
| | | 2. éå¯å端ï¼`start-backend.cmd`ï¼æ `java -jar traffic-audit-server.jar`ï¼ï¼ |
| | | 3. 页é¢éç¹ä¸æ¬¡ AI åæç¡®è®¤å¯ç¨ï¼ |
| | | 4. **ä¸è¦**廿¹ jar å
é¨çé
ç½®ï¼æ¹äºä¸æ¬¡åå
就丢ï¼ï¼ä¹ä¸è¦æ key æäº¤è¿ä»»ä½ä»åºæä»¶ã |
| | | |
| | |
| | | <# |
| | | ä¸é®æå
é¨ç½²å
ï¼å«"å»å¯èªæ£"ï¼ââ 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 |
| | | powershell -NoProfile -ExecutionPolicy Bypass -File pack-deploy.ps1 -NoFrontendDist -SkipServiceWrapper |
| | | ââ å端å£åå¸ï¼ä¸çæç¬ç«ç frontend\dist ç®å½ï¼ä»æå»ºå¹¶åæ¥ backend\webï¼å端å£å¿
éï¼ï¼ |
| | | æå¡å¨å·²è£
æå¡æ¶é
å -SkipServiceWrapper å»æçº¦ 17 MB ç WinSWï¼å¯æ¾èåå°å
ä½ç§¯ã |
| | | |
| | | åäºä»¶äºï¼ |
| | | 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(å¯éï¼-NoFrontendDist 䏿) / 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]$NoFrontendDist, # å端å£åå¸ï¼ä¸æç¬ç«ç frontend\dist ç®å½ï¼ä½ä»æå»º/忥 backend\webï¼å端å£å¿
éï¼ |
| | | [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 |
| | | $pomPath = Join-Path $root 'traffic-audit-server\pom.xml' |
| | | if (-not (Test-Path $pomPath)) { throw "Missing Maven project: $pomPath" } |
| | | [xml]$pom = Get-Content -LiteralPath $pomPath -Raw -Encoding UTF8 |
| | | $productVersion = [string]$pom.project.version |
| | | if (-not $productVersion) { throw 'Missing <version> in traffic-audit-server\pom.xml' } |
| | | if ($productVersion -match '(?i)-SNAPSHOT$') { throw "Release builds must not use a SNAPSHOT version: $productVersion" } |
| | | 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.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 = New-Object System.IO.StreamReader($entry.Open()) |
| | | $text = $sr.ReadToEnd() |
| | | $sr.Close() |
| | | |
| | | $versionEntry = $zip.Entries | Where-Object { $_.FullName -eq 'BOOT-INF/classes/app-version.properties' } |
| | | if (-not $versionEntry) { throw 'jar å
ç¼ºå° BOOT-INF/classes/app-version.properties' } |
| | | $vr = New-Object System.IO.StreamReader($versionEntry.Open()) |
| | | $versionText = $vr.ReadToEnd() |
| | | $vr.Close() |
| | | |
| | | $manifestEntry = $zip.Entries | Where-Object { $_.FullName -eq 'META-INF/MANIFEST.MF' } |
| | | if (-not $manifestEntry) { throw 'jar å
ç¼ºå° META-INF/MANIFEST.MF' } |
| | | $mr = New-Object System.IO.StreamReader($manifestEntry.Open()) |
| | | $manifestText = $mr.ReadToEnd() |
| | | $mr.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 å
ç½®é
ç½®å
¨é¨ä¸ºå ä½ç¬¦ï¼æ ææå¯é¥/å£ä»¤ã' |
| | | |
| | | $builtVersion = '' |
| | | if ($versionText -match '(?m)^\s*version\s*=\s*(.+?)\s*$') { $builtVersion = $matches[1].Trim() } |
| | | if ($builtVersion -ne $productVersion) { |
| | | throw "çæ¬ä¸ä¸è´ï¼pom.xml=$productVersion, app-version.properties=$builtVersion" |
| | | } |
| | | |
| | | $manifestVersion = '' |
| | | if ($manifestText -match '(?m)^Implementation-Version:\s*(.+?)\s*$') { $manifestVersion = $matches[1].Trim() } |
| | | if ($manifestVersion -ne $productVersion) { |
| | | throw "çæ¬ä¸ä¸è´ï¼pom.xml=$productVersion, MANIFEST.MF=$manifestVersion" |
| | | } |
| | | Write-Host " çæ¬èªæ£éè¿ï¼$productVersion (pom / app-version.properties / MANIFEST.MF)" |
| | | |
| | | # ---------- [3/5] å端æå»º ---------- |
| | | # å端å£é¨ç½²å¿
é¡»æ backend\webï¼æä»¥å³ä½¿ -NoFrontendDist ä¹éè¦ distï¼åªæ -SkipFrontend æå¤ç¨ç°æ distã |
| | | 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) |
| | | $dest = Join-Path $OutRoot ("traffic-audit-deploy-" + $stamp + "-v" + $productVersion) |
| | | $n = 2 |
| | | while (Test-Path $dest) { $dest = Join-Path $OutRoot ("traffic-audit-deploy-" + $stamp + "-" + $n); $n++ } |
| | | New-Item -ItemType Directory -Force -Path (Join-Path $dest 'backend'), (Join-Path $dest 'frontend'), (Join-Path $dest 'db') | Out-Null |
| | | while (Test-Path $dest) { $dest = Join-Path $OutRoot ("traffic-audit-deploy-" + $stamp + "-v" + $productVersion + "-" + $n); $n++ } |
| | | if ($NoFrontendDist) { |
| | | New-Item -ItemType Directory -Force -Path (Join-Path $dest 'backend'), (Join-Path $dest 'db') | Out-Null |
| | | } else { |
| | | New-Item -ItemType Directory -Force -Path (Join-Path $dest 'backend'), (Join-Path $dest 'frontend'), (Join-Path $dest 'db') | Out-Null |
| | | } |
| | | |
| | | Copy-Item $jar (Join-Path $dest 'backend') -Force |
| | | 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 'frontend\nginx.conf.example') (Join-Path $dest 'frontend') -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 |
| | | } |
| | | if (-not $NoFrontendDist) { |
| | | 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 å
容ä¸è´ã |
| | | # å端å£åå¸ï¼-NoFrontendDistï¼åæ ·å¿
须忥ï¼å¦åé¡µé¢ 404ã |
| | | 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') $docsDest -Force -ErrorAction SilentlyContinue |
| | | Copy-Item (Join-Path $tplDir 'db\init.sql'), (Join-Path $tplDir 'db\sql_city_bus.sql'), (Join-Path $tplDir 'db\sql_city_taxi.sql'), (Join-Path $tplDir 'db\sql_wyc.sql'), (Join-Path $tplDir 'db\sql_holiday.sql'), (Join-Path $tplDir 'db\sql_holiday_decimal_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_comparison_base_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_previous_day_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_vehicle_adjustment_migration.sql'), (Join-Path $tplDir 'db\sql_observation_station.sql') $docsDest -Force -ErrorAction SilentlyContinue |
| | | |
| | | # READMEï¼ç¨æ§å
模æ¿å¹¶æ¿æ¢æ¥æå ä½ |
| | | # READMEï¼ç¨æ¨¡æ¿å¹¶æ¿æ¢æ¥æå ä½ |
| | | $readme = Get-Content (Join-Path $tplDir 'README-é¨ç½²è¯´æ.txt') -Raw -Encoding UTF8 |
| | | $readme = $readme.Replace('{stamp}', $stamp) |
| | | $readme = $readme.Replace('{stamp}', $stamp).Replace('{version}', $productVersion) |
| | | 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ï¼åå¯å¨ã' |
| | |
| | | """TrafficAudit ä¸é®æå
èæ¬ï¼å端æå»º -> å
åµè¿å端 jar -> ç»è£
é¨ç½²å
-> æ zip |
| | | ç¨æ³ï¼python package-deploy.py [--no-build] |
| | | """ |
| | | import io, os, sys, shutil, zipfile, subprocess, datetime |
| | | import io, os, sys, shutil, zipfile, subprocess, datetime, xml.etree.ElementTree as ET |
| | | |
| | | ROOT = os.path.dirname(os.path.abspath(__file__)) |
| | | WEB = os.path.join(ROOT, "traffic-audit-web") |
| | | SERVER = os.path.join(ROOT, "traffic-audit-server") |
| | | STATIC = os.path.join(SERVER, "src", "main", "resources", "static") |
| | | _pom_tree = ET.parse(os.path.join(SERVER, "pom.xml")) |
| | | _pom_ns = {"m": "http://maven.apache.org/POM/4.0.0"} |
| | | PRODUCT_VERSION = (_pom_tree.getroot().findtext("m:version", namespaces=_pom_ns) or "").strip() |
| | | if not PRODUCT_VERSION: |
| | | raise SystemExit("Missing <version> in traffic-audit-server/pom.xml") |
| | | STAMP = datetime.datetime.now().strftime("%Y%m%d") |
| | | STAGE = os.path.join(ROOT, "deploy", "traffic-audit-deploy-" + STAMP) |
| | | ZIP = os.path.join(ROOT, "traffic-audit-deploy-" + STAMP + ".zip") |
| | | JAR_NAME = "traffic-audit-server-1.0.0-SNAPSHOT.jar" |
| | | STAGE = os.path.join(ROOT, "deploy", "traffic-audit-deploy-" + STAMP + "-v" + PRODUCT_VERSION) |
| | | ZIP = os.path.join(ROOT, "traffic-audit-deploy-" + STAMP + "-v" + PRODUCT_VERSION + ".zip") |
| | | JAR_NAME = "traffic-audit-server.jar" |
| | | SKIP_DOC_DIRS = {"ai_demo", "usage_screenshots", "åè½æµè¯æ¥å", "工使¥ç»"} |
| | | |
| | | def rmtree_force(path): |
| | |
| | | io.open(os.path.join(be, "start-backend.cmd"), "w", encoding="utf-8", newline="\r\n").write(START_CMD) |
| | | io.open(os.path.join(be, "start-backend.sh"), "w", encoding="utf-8", newline="\n").write(START_SH) |
| | | io.open(os.path.join(STAGE, "frontend", "nginx.conf.example"), "w", encoding="utf-8", newline="\n").write(NGINX) |
| | | io.open(os.path.join(STAGE, "README-é¨ç½²è¯´æ.txt"), "w", encoding="utf-8", newline="\r\n").write(README) |
| | | io.open(os.path.join(STAGE, "README-é¨ç½²è¯´æ.txt"), "w", encoding="utf-8", newline="\r\n").write(README.replace("{version}", PRODUCT_VERSION)) |
| | | |
| | | print("[5/6] æ zip ...") |
| | | if os.path.isfile(ZIP): |
| | |
| | | echo å端已å
åµï¼ç´æ¥è®¿é® 8090 å³å¯ï¼ä¸éè¦ nginxï¼ |
| | | echo æ¥å¿æ¾ç¤ºå¨æ¬çªå£ï¼å
³éçªå£å³åæ¢æå¡ |
| | | echo ============================================ |
| | | java -jar traffic-audit-server-1.0.0-SNAPSHOT.jar |
| | | java -jar traffic-audit-server.jar |
| | | pause |
| | | """ |
| | | |
| | | START_SH = u"""#!/bin/sh |
| | | cd "$(dirname "$0")" || exit 1 |
| | | echo "TrafficAudit å¯å¨ä¸ï¼http://localhost:8090 ï¼Ctrl+C 忢ï¼" |
| | | java -jar traffic-audit-server-1.0.0-SNAPSHOT.jar 2>&1 | tee app.log |
| | | java -jar traffic-audit-server.jar 2>&1 | tee app.log |
| | | """ |
| | | |
| | | NGINX = u"""# å端 nginx é
置示ä¾ï¼åªæå¨ä¸æ³ç¨ãjar å
åµåç«¯ãæ¶æéè¦ï¼ |
| | |
| | | } |
| | | """ |
| | | |
| | | README = u"""TrafficAudit 交éè´§è¿ç»è®¡å®¡æ ¸ç³»ç» ââ é¨ç½²å
ï¼{stamp}ï¼ |
| | | README = u"""TrafficAudit 交éè´§è¿ç»è®¡å®¡æ ¸ç³»ç» ââ é¨ç½²å
ï¼{stamp}ï¼çæ¬ v{version}ï¼ |
| | | ======================================================== |
| | | |
| | | ãç®å½ç»æã |
| | | backend/ |
| | | traffic-audit-server-1.0.0-SNAPSHOT.jar å端ç¨åºï¼å«å
¨é¨ä¾èµï¼**å·²å
åµå端**ï¼ |
| | | traffic-audit-server.jar å端ç¨åºï¼å«å
¨é¨ä¾èµï¼**å·²å
åµå端**ï¼åºå®æä»¶åï¼ |
| | | docs/ æ¥è¡¨æ¨¡æ¿ä¸è¿è¡æ°æ®ç®å½ï¼å¿
éï¼å¿å ï¼ |
| | | application.yml å¤é¨é
ç½®ï¼æ¹è¿éå³å¯ï¼æ ééæ°æå
ï¼ |
| | | start-backend.cmd / start-backend.sh å¯å¨èæ¬ |
| | |
| | | </parent> |
| | | <groupId>com.trafficaudit</groupId> |
| | | <artifactId>traffic-audit-server</artifactId> |
| | | <version>1.0.0-SNAPSHOT</version> |
| | | <version>1.0.0</version> |
| | | <packaging>jar</packaging> |
| | | <name>traffic-audit-server</name> |
| | | <properties> |
| | |
| | | <hutool.version>5.4.0</hutool.version> |
| | | <easyexcel.version>4.0.3</easyexcel.version> |
| | | <jjwt.version>0.9.1</jjwt.version> |
| | | <jfreechart.version>1.5.4</jfreechart.version> |
| | | </properties> |
| | | <dependencies> |
| | | <dependency> |
| | |
| | | <version>${hutool.version}</version> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.jfree</groupId> |
| | | <artifactId>jfreechart</artifactId> |
| | | <version>${jfreechart.version}</version> |
| | | </dependency> |
| | | <dependency> |
| | | <groupId>org.projectlombok</groupId> |
| | | <artifactId>lombok</artifactId> |
| | | <optional>true</optional> |
| | |
| | | </dependency> |
| | | </dependencies> |
| | | <build> |
| | | <finalName>traffic-audit-server</finalName> |
| | | <resources> |
| | | <resource> |
| | | <directory>src/main/resources</directory> |
| | | <filtering>false</filtering> |
| | | <excludes> |
| | | <exclude>app-version.properties</exclude> |
| | | </excludes> |
| | | </resource> |
| | | <resource> |
| | | <directory>src/main/resources</directory> |
| | | <filtering>true</filtering> |
| | | <includes> |
| | | <include>app-version.properties</include> |
| | | </includes> |
| | | </resource> |
| | | </resources> |
| | | <plugins> |
| | | <plugin> |
| | | <groupId>org.apache.maven.plugins</groupId> |
| | | <artifactId>maven-jar-plugin</artifactId> |
| | | <configuration> |
| | | <archive> |
| | | <manifest> |
| | | <addDefaultImplementationEntries>true</addDefaultImplementationEntries> |
| | | </manifest> |
| | | </archive> |
| | | </configuration> |
| | | </plugin> |
| | | <plugin> |
| | | <groupId>org.springframework.boot</groupId> |
| | | <artifactId>spring-boot-maven-plugin</artifactId> |
| | | </plugin> |
| New file |
| | |
| | | package com.trafficaudit.holiday; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | public final class HolidayConstants { |
| | | public static final int COMPARISON_BASE_YEAR = 2026; |
| | | |
| | | public static final String TYPE_SPRING_FESTIVAL = "SPRING_FESTIVAL"; |
| | | |
| | | public static final String TYPE_LABOR_DAY = "LABOR_DAY"; |
| | | public static final String TYPE_NATIONAL_DAY = "NATIONAL_DAY"; |
| | | |
| | | public static final String CATEGORY_HOLIDAY = "HOLIDAY"; |
| | | public static final String CATEGORY_NON_HOLIDAY = "NON_HOLIDAY"; |
| | | |
| | | public static final String MODE_RAIL = "RAIL"; |
| | | public static final String MODE_CIVIL_AVIATION = "CIVIL_AVIATION"; |
| | | public static final String MODE_ROAD = "ROAD"; |
| | | public static final String MODE_WATER = "WATER"; |
| | | |
| | | public static final String ROAD_EXPRESSWAY = "EXPRESSWAY"; |
| | | public static final String ROAD_NATIONAL_PROVINCIAL = "NATIONAL_PROVINCIAL"; |
| | | |
| | | public static final String VEHICLE_SMALL_PASSENGER = "SMALL_PASSENGER"; |
| | | public static final String VEHICLE_MEDIUM_SMALL_PASSENGER = "MEDIUM_SMALL_PASSENGER"; |
| | | |
| | | public static final Map<String, String> HOLIDAY_TYPE_LABELS; |
| | | public static final Map<String, String> DATE_CATEGORY_LABELS; |
| | | public static final Map<String, String> TRANSPORT_MODE_LABELS; |
| | | public static final Map<String, String> ROAD_CATEGORY_LABELS; |
| | | public static final Map<String, String> VEHICLE_TYPE_LABELS; |
| | | |
| | | private static final Set<String> HOLIDAY_TYPES; |
| | | private static final Set<String> DATE_CATEGORIES; |
| | | private static final Set<String> TRANSPORT_MODES; |
| | | private static final Set<String> ROAD_CATEGORIES; |
| | | private static final Set<String> VEHICLE_TYPES; |
| | | |
| | | static { |
| | | Map<String, String> holidayTypes = new LinkedHashMap<>(); |
| | | holidayTypes.put(TYPE_SPRING_FESTIVAL, "æ¥è"); |
| | | holidayTypes.put(TYPE_LABOR_DAY, "äºä¸"); |
| | | holidayTypes.put(TYPE_NATIONAL_DAY, "åä¸"); |
| | | HOLIDAY_TYPE_LABELS = Collections.unmodifiableMap(holidayTypes); |
| | | HOLIDAY_TYPES = new LinkedHashSet<>(holidayTypes.keySet()); |
| | | |
| | | Map<String, String> dateCategories = new LinkedHashMap<>(); |
| | | dateCategories.put(CATEGORY_HOLIDAY, "è忥"); |
| | | dateCategories.put(CATEGORY_NON_HOLIDAY, "éè忥"); |
| | | DATE_CATEGORY_LABELS = Collections.unmodifiableMap(dateCategories); |
| | | DATE_CATEGORIES = new LinkedHashSet<>(dateCategories.keySet()); |
| | | |
| | | Map<String, String> transportModes = new LinkedHashMap<>(); |
| | | transportModes.put(MODE_RAIL, "éè·¯"); |
| | | transportModes.put(MODE_CIVIL_AVIATION, "æ°èª"); |
| | | transportModes.put(MODE_ROAD, "éè·¯"); |
| | | transportModes.put(MODE_WATER, "æ°´è·¯"); |
| | | TRANSPORT_MODE_LABELS = Collections.unmodifiableMap(transportModes); |
| | | TRANSPORT_MODES = new LinkedHashSet<>(transportModes.keySet()); |
| | | |
| | | Map<String, String> roadCategories = new LinkedHashMap<>(); |
| | | roadCategories.put(ROAD_EXPRESSWAY, "é«éå
¬è·¯"); |
| | | roadCategories.put(ROAD_NATIONAL_PROVINCIAL, "æ®éå½çé"); |
| | | ROAD_CATEGORY_LABELS = Collections.unmodifiableMap(roadCategories); |
| | | ROAD_CATEGORIES = new LinkedHashSet<>(roadCategories.keySet()); |
| | | |
| | | Map<String, String> vehicleTypes = new LinkedHashMap<>(); |
| | | vehicleTypes.put(VEHICLE_SMALL_PASSENGER, "å°å®¢è½¦"); |
| | | vehicleTypes.put(VEHICLE_MEDIUM_SMALL_PASSENGER, "ä¸å°å®¢è½¦"); |
| | | VEHICLE_TYPE_LABELS = Collections.unmodifiableMap(vehicleTypes); |
| | | VEHICLE_TYPES = new LinkedHashSet<>(vehicleTypes.keySet()); |
| | | } |
| | | |
| | | private HolidayConstants() { |
| | | } |
| | | |
| | | public static boolean supportsComparisonBase(int year) { |
| | | return year == COMPARISON_BASE_YEAR; |
| | | } |
| | | |
| | | public static boolean isValidHolidayType(String value) { |
| | | return HOLIDAY_TYPES.contains(value); |
| | | } |
| | | |
| | | public static boolean isValidDateCategory(String value) { |
| | | return DATE_CATEGORIES.contains(value); |
| | | } |
| | | |
| | | public static boolean isValidTransportMode(String value) { |
| | | return TRANSPORT_MODES.contains(value); |
| | | } |
| | | |
| | | public static boolean isValidRoadCategory(String value) { |
| | | return ROAD_CATEGORIES.contains(value); |
| | | } |
| | | |
| | | public static boolean isValidVehicleType(String value) { |
| | | return VEHICLE_TYPES.contains(value); |
| | | } |
| | | |
| | | public static Map<String, Object> optionMap() { |
| | | Map<String, Object> options = new LinkedHashMap<>(); |
| | | options.put("holidayTypes", HOLIDAY_TYPE_LABELS); |
| | | options.put("dateCategories", DATE_CATEGORY_LABELS); |
| | | options.put("transportModes", TRANSPORT_MODE_LABELS); |
| | | options.put("roadCategories", ROAD_CATEGORY_LABELS); |
| | | options.put("vehicleTypes", VEHICLE_TYPE_LABELS); |
| | | options.put("years", Arrays.asList(2019, 2024, 2025, 2026, 2027, 2028, 2029, 2030)); |
| | | return options; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday; |
| | | |
| | | import org.apache.poi.ss.util.NumberToTextConverter; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.MathContext; |
| | | import java.math.RoundingMode; |
| | | |
| | | public final class HolidayNumberUtils { |
| | | |
| | | public static final MathContext EXCEL_MC = new MathContext(15, RoundingMode.HALF_UP); |
| | | public static final BigDecimal ZERO = BigDecimal.ZERO; |
| | | |
| | | private HolidayNumberUtils() { |
| | | } |
| | | |
| | | public static BigDecimal excelValue(double value) { |
| | | return normalize(new BigDecimal(NumberToTextConverter.toText(value))); |
| | | } |
| | | |
| | | public static BigDecimal decimalValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return normalize(new BigDecimal(value.toString().trim())); |
| | | } |
| | | |
| | | public static BigDecimal normalize(BigDecimal value) { |
| | | if (value == null) return null; |
| | | BigDecimal rounded = value.round(EXCEL_MC); |
| | | return rounded.signum() == 0 ? ZERO : rounded.stripTrailingZeros(); |
| | | } |
| | | |
| | | public static BigDecimal valueOrZero(BigDecimal value) { |
| | | return value == null ? ZERO : value; |
| | | } |
| | | |
| | | public static BigDecimal sumNullable(BigDecimal left, BigDecimal right) { |
| | | if (left == null && right == null) return null; |
| | | return normalize(valueOrZero(left).add(valueOrZero(right))); |
| | | } |
| | | |
| | | public static BigDecimal multiply(BigDecimal left, BigDecimal right) { |
| | | if (left == null || right == null) return null; |
| | | return normalize(left.multiply(right)); |
| | | } |
| | | |
| | | public static BigDecimal divide(BigDecimal dividend, BigDecimal divisor) { |
| | | if (dividend == null || divisor == null || divisor.signum() == 0) return null; |
| | | return normalize(dividend.divide(divisor, EXCEL_MC)); |
| | | } |
| | | |
| | | public static boolean sameValue(BigDecimal left, BigDecimal right) { |
| | | if (left == null || right == null) return false; |
| | | return normalize(left).compareTo(normalize(right)) == 0; |
| | | } |
| | | |
| | | public static String changeText(BigDecimal value) { |
| | | if (value == null) return ""; |
| | | return normalize(value).toPlainString(); |
| | | } |
| | | |
| | | public static String format1(BigDecimal value) { |
| | | if (value == null) return ""; |
| | | return value.setScale(1, RoundingMode.HALF_UP).toPlainString(); |
| | | } |
| | | |
| | | public static String format2(BigDecimal value) { |
| | | if (value == null) return ""; |
| | | return value.setScale(2, RoundingMode.HALF_UP).toPlainString(); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.controller; |
| | | |
| | | import com.trafficaudit.common.Result; |
| | | import com.trafficaudit.holiday.HolidayConstants; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerCoefficient; |
| | | import com.trafficaudit.holiday.service.HolidayConfigService; |
| | | import com.trafficaudit.security.utils.JwtUtils; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestHeader; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @RestController |
| | | @RequestMapping("/api/holiday/config") |
| | | public class HolidayConfigController { |
| | | |
| | | @Resource |
| | | private HolidayConfigService configService; |
| | | @Resource |
| | | private JwtUtils jwtUtils; |
| | | |
| | | @GetMapping("/options") |
| | | public Result<Map<String, Object>> options() { |
| | | return Result.ok(HolidayConstants.optionMap()); |
| | | } |
| | | |
| | | @GetMapping("/holidays") |
| | | public Result<List<HolidayCalendar>> holidays(@RequestParam(value = "year", required = false) Integer year, |
| | | @RequestParam(value = "holidayType", required = false) String holidayType) { |
| | | return Result.ok(configService.listHolidays(year, holidayType)); |
| | | } |
| | | |
| | | @PostMapping("/holidays/save") |
| | | public Result<HolidayCalendar> saveHoliday(@RequestHeader(value = "Authorization", required = false) String auth, |
| | | @RequestBody Map<String, Object> params) { |
| | | if (!isAdmin(auth)) return Result.error(403, "ä»
admin å¯ä»¥ç®¡çè忥é
ç½®"); |
| | | try { |
| | | return Result.ok(configService.saveHoliday(params)); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @PostMapping("/holidays/delete") |
| | | public Result<Void> deleteHoliday(@RequestHeader(value = "Authorization", required = false) String auth, |
| | | @RequestParam("id") Long id) { |
| | | if (!isAdmin(auth)) return Result.error(403, "ä»
admin å¯ä»¥ç®¡çè忥é
ç½®"); |
| | | try { |
| | | configService.deleteHoliday(id); |
| | | return Result.ok(); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @GetMapping("/coefficients") |
| | | public Result<List<HolidayPassengerCoefficient>> coefficients(@RequestParam(value = "year", required = false) Integer year) { |
| | | return Result.ok(configService.listCoefficients(year)); |
| | | } |
| | | |
| | | @PostMapping("/coefficients/save") |
| | | public Result<HolidayPassengerCoefficient> saveCoefficient(@RequestHeader(value = "Authorization", required = false) String auth, |
| | | @RequestBody Map<String, Object> params) { |
| | | if (!isAdmin(auth)) return Result.error(403, "ä»
admin å¯ä»¥ç®¡ç客è¿ç³»æ°"); |
| | | try { |
| | | return Result.ok(configService.saveCoefficient(params)); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @PostMapping("/coefficients/delete") |
| | | public Result<Void> deleteCoefficient(@RequestHeader(value = "Authorization", required = false) String auth, |
| | | @RequestParam("id") Long id) { |
| | | if (!isAdmin(auth)) return Result.error(403, "ä»
admin å¯ä»¥ç®¡ç客è¿ç³»æ°"); |
| | | try { |
| | | configService.deleteCoefficient(id); |
| | | return Result.ok(); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | private boolean isAdmin(String auth) { |
| | | return "admin".equals(jwtUtils.resolveUsername(auth)); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.controller; |
| | | |
| | | import com.trafficaudit.common.Result; |
| | | import com.trafficaudit.holiday.service.HolidayBulletinService; |
| | | import com.trafficaudit.holiday.service.HolidayTrendService; |
| | | import com.trafficaudit.holiday.service.HolidayStatService; |
| | | import org.springframework.http.HttpHeaders; |
| | | import org.springframework.http.MediaType; |
| | | import org.springframework.http.ResponseEntity; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.net.URLEncoder; |
| | | import java.time.LocalDate; |
| | | import java.util.Map; |
| | | |
| | | @RestController |
| | | @RequestMapping("/api/holiday") |
| | | public class HolidayStatController { |
| | | |
| | | @Resource |
| | | private HolidayStatService statService; |
| | | @Resource |
| | | private HolidayBulletinService bulletinService; |
| | | |
| | | @Resource |
| | | private HolidayTrendService trendService; |
| | | |
| | | @GetMapping("/overview") |
| | | public Result<Map<String, Object>> overview(@RequestParam("year") int year, |
| | | @RequestParam("holidayType") String holidayType) { |
| | | try { |
| | | return Result.ok(statService.overview(year, holidayType)); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @PostMapping("/import") |
| | | public Result<Map<String, Object>> importWorkbook(@RequestParam(value = "file", required = false) MultipartFile file, |
| | | @RequestParam("year") int year, |
| | | @RequestParam("holidayType") String holidayType, |
| | | @RequestParam(value = "allowHistoricalOverwrite", defaultValue = "false") boolean allowHistoricalOverwrite) { |
| | | try { |
| | | return Result.ok(statService.importWorkbook(file, year, holidayType, allowHistoricalOverwrite)); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @GetMapping("/export") |
| | | public ResponseEntity<byte[]> exportWorkbook(@RequestParam("year") int year, |
| | | @RequestParam("holidayType") String holidayType) throws Exception { |
| | | byte[] data = statService.exportWorkbook(year, holidayType); |
| | | String fileName = year + "å¹´" + holidayTypeFileLabel(holidayType) + "æ°æ®.xlsx"; |
| | | String encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20"); |
| | | return ResponseEntity.ok() |
| | | .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded) |
| | | .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) |
| | | .body(data); |
| | | } |
| | | |
| | | @GetMapping("/bulletin") |
| | | public ResponseEntity<byte[]> bulletin(@RequestParam(value = "date", required = false) String date) throws Exception { |
| | | LocalDate targetDate = (date == null || date.trim().isEmpty()) |
| | | ? LocalDate.now().minusDays(1) : LocalDate.parse(date); |
| | | byte[] data = bulletinService.generate(targetDate); |
| | | String fileName = String.format("æ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥ï¼10æ%dæ¥ï¼.docx", targetDate.getDayOfMonth()); |
| | | String encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20"); |
| | | return ResponseEntity.ok() |
| | | .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded) |
| | | .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document")) |
| | | .body(data); |
| | | } |
| | | |
| | | @GetMapping("/trend") |
| | | public ResponseEntity<byte[]> trend(@RequestParam("year") int year, |
| | | @RequestParam("holidayType") String holidayType) throws Exception { |
| | | byte[] data = trendService.generate(year, holidayType); |
| | | String fileName = year + "å¹´" + holidayTypeFileLabel(holidayType) + "åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾.docx"; |
| | | String encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20"); |
| | | return ResponseEntity.ok() |
| | | .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded) |
| | | .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document")) |
| | | .body(data); |
| | | } |
| | | |
| | | private String holidayTypeFileLabel(String holidayType) { |
| | | if ("SPRING_FESTIVAL".equals(holidayType)) return "æ¥è"; |
| | | if ("LABOR_DAY".equals(holidayType)) return "äºä¸"; |
| | | return "åä¸"; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.controller; |
| | | |
| | | import com.trafficaudit.common.Result; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment; |
| | | import com.trafficaudit.holiday.service.HolidayVehicleAdjustmentService; |
| | | import com.trafficaudit.security.utils.JwtUtils; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestHeader; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @RestController |
| | | @RequestMapping("/api/holiday/adjustments") |
| | | public class HolidayVehicleAdjustmentController { |
| | | |
| | | @Resource |
| | | private HolidayVehicleAdjustmentService adjustmentService; |
| | | @Resource |
| | | private JwtUtils jwtUtils; |
| | | |
| | | @GetMapping |
| | | public Result<List<HolidayVehicleAdjustment>> list(@RequestParam(value = "year", required = false) Integer year) { |
| | | return Result.ok(adjustmentService.list(year)); |
| | | } |
| | | |
| | | @GetMapping("/source-value") |
| | | public Result<BigDecimal> sourceValue(@RequestParam("year") Integer year, |
| | | @RequestParam("flowDate") String flowDate) { |
| | | try { |
| | | return Result.ok(adjustmentService.findOriginalValue(year, LocalDate.parse(flowDate))); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @PostMapping("/save") |
| | | public Result<HolidayVehicleAdjustment> save(@RequestHeader(value = "Authorization", required = false) String auth, |
| | | @RequestBody Map<String, Object> params) { |
| | | if (!isAdmin(auth)) return Result.error(403, "ä»
admin å¯ä»¥ç»´æ¤å¾å¹´å½çéæ°æ®è°æ´"); |
| | | try { |
| | | return Result.ok(adjustmentService.save(params)); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @PostMapping("/delete") |
| | | public Result<Void> delete(@RequestHeader(value = "Authorization", required = false) String auth, |
| | | @RequestParam("id") Long id) { |
| | | if (!isAdmin(auth)) return Result.error(403, "ä»
admin å¯ä»¥ç»´æ¤å¾å¹´å½çéæ°æ®è°æ´"); |
| | | try { |
| | | adjustmentService.delete(id); |
| | | return Result.ok(); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | private boolean isAdmin(String auth) { |
| | | return "admin".equals(jwtUtils.resolveUsername(auth)); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Data; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | |
| | | @Data |
| | | @TableName("holiday_calendar") |
| | | public class HolidayCalendar { |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | private Integer year; |
| | | private String holidayType; |
| | | private LocalDate holidayDate; |
| | | private Integer dayIndex; |
| | | private LocalDateTime createdAt; |
| | | private LocalDateTime updatedAt; |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | |
| | | @Data |
| | | @TableName("holiday_passenger_coefficient") |
| | | public class HolidayPassengerCoefficient { |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | private Integer year; |
| | | private String dateCategory; |
| | | private BigDecimal coefficient; |
| | | private LocalDateTime createdAt; |
| | | private LocalDateTime updatedAt; |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | |
| | | @Data |
| | | @TableName("holiday_passenger_flow") |
| | | public class HolidayPassengerFlow { |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | private Integer year; |
| | | private LocalDate flowDate; |
| | | private String transportMode; |
| | | private BigDecimal passengerCount; |
| | | private LocalDateTime createdAt; |
| | | private LocalDateTime updatedAt; |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | |
| | | @Data |
| | | @TableName("holiday_vehicle_adjustment") |
| | | public class HolidayVehicleAdjustment { |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | private Integer year; |
| | | private LocalDate flowDate; |
| | | private BigDecimal adjustedValue; |
| | | @TableField(exist = false) |
| | | private BigDecimal originalValue; |
| | | private String reason; |
| | | private LocalDateTime createdAt; |
| | | private LocalDateTime updatedAt; |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.FieldStrategy; |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | |
| | | @Data |
| | | @TableName("holiday_vehicle_flow") |
| | | public class HolidayVehicleFlow { |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | private Integer year; |
| | | private LocalDate flowDate; |
| | | private String roadCategory; |
| | | private String vehicleType; |
| | | private BigDecimal vehicleCount; |
| | | @TableField(updateStrategy = FieldStrategy.IGNORED) |
| | | private BigDecimal comparisonBase; |
| | | private LocalDateTime createdAt; |
| | | private LocalDateTime updatedAt; |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface HolidayCalendarMapper extends BaseMapper<HolidayCalendar> { |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerCoefficient; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface HolidayPassengerCoefficientMapper extends BaseMapper<HolidayPassengerCoefficient> { |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerFlow; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface HolidayPassengerFlowMapper extends BaseMapper<HolidayPassengerFlow> { |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface HolidayVehicleAdjustmentMapper extends BaseMapper<HolidayVehicleAdjustment> { |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleFlow; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface HolidayVehicleFlowMapper extends BaseMapper<HolidayVehicleFlow> { |
| | | } |
| New file |
| | |
| | | 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.springframework.stereotype.Service; |
| | | import org.w3c.dom.Document; |
| | | import org.w3c.dom.Element; |
| | | import org.w3c.dom.Node; |
| | | import org.w3c.dom.NodeList; |
| | | |
| | | import javax.annotation.Resource; |
| | | import javax.xml.parsers.DocumentBuilderFactory; |
| | | import javax.xml.transform.OutputKeys; |
| | | import javax.xml.transform.Transformer; |
| | | import javax.xml.transform.TransformerFactory; |
| | | import javax.xml.transform.dom.DOMSource; |
| | | import javax.xml.transform.stream.StreamResult; |
| | | import java.io.ByteArrayInputStream; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.InputStream; |
| | | import java.math.BigDecimal; |
| | | import java.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; |
| | | |
| | | @Slf4j |
| | | @Service |
| | | public class HolidayBulletinService { |
| | | |
| | | private static final String WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; |
| | | private static final BigDecimal EPSILON = new BigDecimal("0.0000001"); |
| | | private static final int EXCEL_ONE_DECIMAL_PERCENT = 1; |
| | | private static final int EXCEL_TWO_DECIMAL_PERCENT = 2; |
| | | private static final int EXCEL_SEVEN_DECIMAL_YI = 7; |
| | | private static final BigDecimal TEN_THOUSAND = new BigDecimal("10000"); |
| | | |
| | | @Value("${holiday.bulletin-template:docs/ä¸é¡¹ç»è®¡/éè¦è忥/æ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥æ¨¡æ¿.docx}") |
| | | private String bulletinTemplatePath; |
| | | |
| | | private final HolidayStatService statService; |
| | | @Resource |
| | | private HolidayConfigService configService; |
| | | private static final ThreadLocal<Document> CURRENT_DOCUMENT = new ThreadLocal<>(); |
| | | |
| | | public HolidayBulletinService(HolidayStatService statService) { |
| | | this.statService = statService; |
| | | } |
| | | |
| | | public byte[] generate(LocalDate date) { |
| | | DailyMetrics current = statService.findMetricsByDate(date); |
| | | if (current == null) { |
| | | throw new IllegalArgumentException("æéæ¥æä¸å¨å·²é
ç½®çè忥èå´å
"); |
| | | } |
| | | DailyMetrics previousDay = current.getDayIndex() > 1 |
| | | ? statService.ringRatioBaseMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex()) : null; |
| | | HolidayCalendar previousDayConfig = configService.findPreviousDay(current.getYear(), current.getHolidayType()); |
| | | LocalDate previousDayDate = previousDayConfig == null || previousDayConfig.getHolidayDate() == null |
| | | ? current.getDate().minusDays(1) : previousDayConfig.getHolidayDate(); |
| | | 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 ringRatioClause = previousDay == null ? "" : "ï¼ç¯æ¯" + ratioPhrase( |
| | | current.getTotalPersonFlow(), previousDay.getTotalPersonFlow(), EXCEL_TWO_DECIMAL_PERCENT); |
| | | 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(), EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratioPhrase(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(), EXCEL_ONE_DECIMAL_PERCENT)); |
| | | String paragraph2 = cumulativeParagraph(currentCumulative, previousYearCumulative, twoYearsAgoCumulative); |
| | | |
| | | File template = resolveTemplateFile(); |
| | | if (!template.exists() || !template.isFile()) { |
| | | throw new IllegalArgumentException("å¿«æ¥æ¨¡æ¿æä»¶ä¸åå¨: " + template.getAbsolutePath()); |
| | | } |
| | | |
| | | try { |
| | | byte[] templateBytes; |
| | | try (InputStream in = new FileInputStream(template)) { |
| | | templateBytes = readAll(in); |
| | | } |
| | | byte[] documentXml = readZipEntry(templateBytes, "word/document.xml"); |
| | | Document document = parseDocument(documentXml); |
| | | CURRENT_DOCUMENT.set(document); |
| | | try { |
| | | replaceParagraph(document, text -> text.startsWith("10æ") && text.contains("åæç¬¬"), paragraph1); |
| | | replaceParagraph(document, text -> text.startsWith("åæä»¥æ¥"), paragraph2); |
| | | fillTable(document, current, previousDay, previousCalendarDay, previousYear, twoYearsAgo, |
| | | currentCumulative, previousYearCumulative, twoYearsAgoCumulative); |
| | | byte[] replacedXml = serializeDocument(document); |
| | | return replaceZipEntry(templateBytes, "word/document.xml", replacedXml); |
| | | } finally { |
| | | CURRENT_DOCUMENT.remove(); |
| | | } |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("å¿«æ¥çæå¤±è´¥: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | private void fillTable(Document document, DailyMetrics current, DailyMetrics previousDay, |
| | | DailyMetrics 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(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(current.getNonBusinessPersonFlow(), previousYear == null ? null : previousYear.getNonBusinessPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getNonBusinessPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getNonBusinessPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | numberCumulative(currentCumulative.getNonBusinessPersonFlow()), |
| | | ratio(currentCumulative.getNonBusinessPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getNonBusinessPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(currentCumulative.getNonBusinessPersonFlow(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getNonBusinessPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT) |
| | | }); |
| | | writeTableRow(table, 3, new String[]{ |
| | | number(current.getRoad()), |
| | | ratio(current.getRoad(), previousDay == null ? null : previousDay.getRoad(), EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getRoad(), previousYear == null ? null : previousYear.getRoad(), EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getRoad(), twoYearsAgo == null ? null : twoYearsAgo.getRoad(), EXCEL_TWO_DECIMAL_PERCENT), |
| | | number(currentCumulative.getRoad()), |
| | | ratio(currentCumulative.getRoad(), previousYearCumulative == null ? null : previousYearCumulative.getRoad(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(currentCumulative.getRoad(), twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoad(), |
| | | EXCEL_ONE_DECIMAL_PERCENT) |
| | | }); |
| | | writeTableRow(table, 4, new String[]{ |
| | | number(current.getRoadPersonFlow()), |
| | | ratio(current.getRoadPersonFlow(), previousDay == null ? null : previousDay.getRoadPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(current.getRoadPersonFlow(), previousYear == null ? null : previousYear.getRoadPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getRoadPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getRoadPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | numberCumulative(currentCumulative.getRoadPersonFlow()), |
| | | ratio(currentCumulative.getRoadPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(currentCumulative.getRoadPersonFlow(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoadPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT) |
| | | }); |
| | | 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(), |
| | | EXCEL_ONE_DECIMAL_PERCENT)); |
| | | 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(), |
| | | EXCEL_ONE_DECIMAL_PERCENT)); |
| | | 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(), |
| | | EXCEL_TWO_DECIMAL_PERCENT)); |
| | | writeTableRow(table, 8, new String[]{ |
| | | number(current.getTotalPersonFlow()), |
| | | ratio(current.getTotalPersonFlow(), previousDay == null ? null : previousDay.getTotalPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | numberCumulative(currentCumulative.getTotalPersonFlow()), |
| | | ratio(currentCumulative.getTotalPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(currentCumulative.getTotalPersonFlow(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT) |
| | | }); |
| | | } |
| | | |
| | | private String[] transportRow(BigDecimal current, BigDecimal previousDay, BigDecimal previousYear, |
| | | BigDecimal twoYearsAgo, BigDecimal currentCumulative, |
| | | BigDecimal previousYearCumulative, BigDecimal twoYearsAgoCumulative, |
| | | int ringRatioExcelScale) { |
| | | return new String[]{ |
| | | number(current), |
| | | ratio(current, previousDay, ringRatioExcelScale), |
| | | ratio(current, previousYear, EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current, twoYearsAgo, EXCEL_ONE_DECIMAL_PERCENT), |
| | | number(currentCumulative), |
| | | ratio(currentCumulative, previousYearCumulative, EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(currentCumulative, twoYearsAgoCumulative, EXCEL_ONE_DECIMAL_PERCENT) |
| | | }; |
| | | } |
| | | |
| | | private void writeTableRow(Element table, int rowIndex, String[] values) { |
| | | List<Element> rows = childElements(table, "tr"); |
| | | if (rowIndex >= rows.size()) return; |
| | | Element row = rows.get(rowIndex); |
| | | List<Element> cells = cellRefs(row); |
| | | int logicalColumn = 0; |
| | | for (Element cell : cells) { |
| | | int span = gridSpan(cell); |
| | | if (logicalColumn >= 2) { |
| | | int valueIndex = logicalColumn - 2; |
| | | if (valueIndex < values.length) { |
| | | setCellText(cell, values[valueIndex]); |
| | | } |
| | | } |
| | | logicalColumn += span; |
| | | } |
| | | } |
| | | |
| | | private List<Element> cellRefs(Element row) { |
| | | List<Element> cells = new ArrayList<>(); |
| | | for (Element child : childElements(row, "tc")) cells.add(child); |
| | | return cells; |
| | | } |
| | | |
| | | private int gridSpan(Element cell) { |
| | | Element tcPr = firstChildElement(cell, "tcPr"); |
| | | if (tcPr == null) return 1; |
| | | Element span = firstChildElement(tcPr, "gridSpan"); |
| | | if (span == null) return 1; |
| | | String value = span.getAttributeNS(WORD_NS, "val"); |
| | | try { |
| | | return Integer.parseInt(value); |
| | | } catch (Exception e) { |
| | | return 1; |
| | | } |
| | | } |
| | | |
| | | private void setCellText(Element cell, String text) { |
| | | Element tcPr = firstChildElement(cell, "tcPr"); |
| | | Element templateParagraph = firstChildElement(cell, "p"); |
| | | Element paragraph = createElement("p"); |
| | | if (templateParagraph != null) { |
| | | Element pPr = firstChildElement(templateParagraph, "pPr"); |
| | | if (pPr != null) paragraph.appendChild(pPr.cloneNode(true)); |
| | | } |
| | | Element 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); |
| | | run.appendChild(t); |
| | | paragraph.appendChild(run); |
| | | |
| | | List<Node> removals = new ArrayList<>(); |
| | | Node child = cell.getFirstChild(); |
| | | while (child != null) { |
| | | Node next = child.getNextSibling(); |
| | | if (child instanceof Element && "p".equals(localName(child))) { |
| | | removals.add(child); |
| | | } |
| | | child = next; |
| | | } |
| | | for (Node node : removals) cell.removeChild(node); |
| | | if (tcPr != null && tcPr.getNextSibling() != null) { |
| | | cell.insertBefore(paragraph, tcPr.getNextSibling()); |
| | | } else if (tcPr != null) { |
| | | cell.appendChild(paragraph); |
| | | } else { |
| | | cell.insertBefore(paragraph, cell.getFirstChild()); |
| | | } |
| | | } |
| | | |
| | | private void replaceParagraph(Document document, ParagraphMatcher matcher, String replacement) { |
| | | NodeList paragraphs = document.getElementsByTagNameNS(WORD_NS, "p"); |
| | | for (int i = 0; i < paragraphs.getLength(); i++) { |
| | | Element paragraph = (Element) paragraphs.item(i); |
| | | String text = paragraphText(paragraph); |
| | | if (matcher.matches(text)) { |
| | | setParagraphText(paragraph, replacement); |
| | | return; |
| | | } |
| | | } |
| | | throw new IllegalArgumentException("å¿«æ¥æ¨¡æ¿ç¼ºå°å¯æ¿æ¢æ®µè½: " + replacement); |
| | | } |
| | | |
| | | private void setParagraphText(Element paragraph, String text) { |
| | | Element pPr = firstChildElement(paragraph, "pPr"); |
| | | Element firstRun = firstChildElement(paragraph, "r"); |
| | | Element rPr = firstRun == null ? null : firstChildElement(firstRun, "rPr"); |
| | | List<Node> removals = new ArrayList<>(); |
| | | Node child = paragraph.getFirstChild(); |
| | | while (child != null) { |
| | | Node next = child.getNextSibling(); |
| | | if (child != pPr) removals.add(child); |
| | | child = next; |
| | | } |
| | | for (Node node : removals) paragraph.removeChild(node); |
| | | |
| | | Element run = createElement("r"); |
| | | if (rPr != null) run.appendChild(rPr.cloneNode(true)); |
| | | Element t = createElement("t"); |
| | | t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve"); |
| | | t.setTextContent(text); |
| | | run.appendChild(t); |
| | | paragraph.appendChild(run); |
| | | } |
| | | |
| | | private String paragraphText(Element paragraph) { |
| | | StringBuilder builder = new StringBuilder(); |
| | | NodeList texts = paragraph.getElementsByTagNameNS(WORD_NS, "t"); |
| | | for (int i = 0; i < texts.getLength(); i++) { |
| | | builder.append(texts.item(i).getTextContent()); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private String ratioPhrase(BigDecimal current, BigDecimal base, int excelPercentScale) { |
| | | BigDecimal percent = bulletinPercent(current, base, excelPercentScale); |
| | | if (percent == null) return "æ 坿¯æ°æ®"; |
| | | if (percent.signum() == 0) return "æå¹³"; |
| | | 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(displayedCumulativeWan(total)), |
| | | ratioPhrase(total, previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratioPhrase(total, twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT)); |
| | | } |
| | | |
| | | private String ratio(BigDecimal current, BigDecimal base, int excelPercentScale) { |
| | | BigDecimal percent = bulletinPercent(current, base, excelPercentScale); |
| | | if (percent == null) return "â"; |
| | | return percent.signum() == 0 ? "0%" : percent.toPlainString() + "%"; |
| | | } |
| | | |
| | | /** |
| | | * Mirrors the source Excel percentage cell format, then applies the bulletin's one-decimal rule. |
| | | * Excel stores full precision but displays 0.0% or 0.00%; reviewers compare against that display. |
| | | */ |
| | | private BigDecimal bulletinPercent(BigDecimal current, BigDecimal base, int excelPercentScale) { |
| | | if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return null; |
| | | BigDecimal ratio = HolidayNumberUtils.divide(current, base); |
| | | if (ratio == null) return null; |
| | | BigDecimal percent = HolidayNumberUtils.multiply(ratio.subtract(BigDecimal.ONE), BigDecimal.valueOf(100)); |
| | | if (percent == null) return null; |
| | | BigDecimal displayed = percent.setScale(excelPercentScale, RoundingMode.HALF_UP); |
| | | return displayed.setScale(1, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | private String number(BigDecimal value) { |
| | | return value == null ? "â" : formatNumber(value); |
| | | } |
| | | |
| | | private String numberCumulative(BigDecimal value) { |
| | | return value == null ? "â" : formatNumber(displayedCumulativeWan(value)); |
| | | } |
| | | |
| | | /** |
| | | * Source cumulative cells U5/X5/AA5 are stored in 100-million units and displayed with seven decimals. |
| | | * Mirror that displayed precision before the bulletin converts the value to 10-thousand units. |
| | | */ |
| | | private BigDecimal displayedCumulativeWan(BigDecimal cumulativeWan) { |
| | | if (cumulativeWan == null) return null; |
| | | BigDecimal yi = cumulativeWan.divide(TEN_THOUSAND, EXCEL_SEVEN_DECIMAL_YI, RoundingMode.HALF_UP); |
| | | return HolidayNumberUtils.normalize(yi.multiply(TEN_THOUSAND)); |
| | | } |
| | | |
| | | private String formatNumber(BigDecimal value) { |
| | | if (value == null) return ""; |
| | | return HolidayNumberUtils.format2(value); |
| | | } |
| | | |
| | | private File resolveTemplateFile() { |
| | | List<File> candidates = new ArrayList<>(); |
| | | File configured = new File(bulletinTemplatePath); |
| | | candidates.add(configured); |
| | | if (!configured.isAbsolute()) { |
| | | candidates.add(new File(System.getProperty("user.dir"), bulletinTemplatePath)); |
| | | candidates.add(new File(System.getProperty("user.dir"), "../" + bulletinTemplatePath)); |
| | | } |
| | | for (File candidate : candidates) { |
| | | try { |
| | | if (candidate.exists() && candidate.isFile()) { |
| | | return candidate.getCanonicalFile(); |
| | | } |
| | | } catch (Exception ignored) { |
| | | // Keep checking. |
| | | } |
| | | } |
| | | return configured; |
| | | } |
| | | |
| | | private byte[] readZipEntry(byte[] zipBytes, String entryName) throws Exception { |
| | | try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { |
| | | ZipEntry entry; |
| | | while ((entry = zin.getNextEntry()) != null) { |
| | | if (entryName.equals(entry.getName())) { |
| | | return readAll(zin); |
| | | } |
| | | } |
| | | } |
| | | throw new IllegalArgumentException("模æ¿ä¸ç¼ºå° " + entryName); |
| | | } |
| | | |
| | | private byte[] replaceZipEntry(byte[] zipBytes, String entryName, byte[] replacement) throws Exception { |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zipBytes)); |
| | | ZipOutputStream zout = new ZipOutputStream(out)) { |
| | | ZipEntry entry; |
| | | byte[] buffer = new byte[8192]; |
| | | while ((entry = zin.getNextEntry()) != null) { |
| | | ZipEntry target = new ZipEntry(entry.getName()); |
| | | zout.putNextEntry(target); |
| | | if (entryName.equals(entry.getName())) { |
| | | zout.write(replacement); |
| | | } else { |
| | | int length; |
| | | while ((length = zin.read(buffer)) > 0) { |
| | | zout.write(buffer, 0, length); |
| | | } |
| | | } |
| | | zout.closeEntry(); |
| | | } |
| | | } |
| | | return out.toByteArray(); |
| | | } |
| | | |
| | | private Document parseDocument(byte[] xml) throws Exception { |
| | | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| | | factory.setNamespaceAware(true); |
| | | return factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml)); |
| | | } |
| | | |
| | | private byte[] serializeDocument(Document document) throws Exception { |
| | | Transformer transformer = TransformerFactory.newInstance().newTransformer(); |
| | | transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); |
| | | transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | transformer.transform(new DOMSource(document), new StreamResult(out)); |
| | | return out.toByteArray(); |
| | | } |
| | | |
| | | private Element firstElement(Document document, String localName) { |
| | | NodeList list = document.getElementsByTagNameNS(WORD_NS, localName); |
| | | return list.getLength() == 0 ? null : (Element) list.item(0); |
| | | } |
| | | |
| | | private Element createElement(String localName) { |
| | | Document document = CURRENT_DOCUMENT.get(); |
| | | if (document == null) { |
| | | throw new IllegalStateException("Word document is not initialized"); |
| | | } |
| | | return document.createElementNS(WORD_NS, "w:" + localName); |
| | | } |
| | | |
| | | private List<Element> childElements(Element parent, String localName) { |
| | | List<Element> result = new ArrayList<>(); |
| | | Node child = parent.getFirstChild(); |
| | | while (child != null) { |
| | | if (child instanceof Element && localName.equals(localName(child))) { |
| | | result.add((Element) child); |
| | | } |
| | | child = child.getNextSibling(); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Element firstChildElement(Element parent, String localName) { |
| | | Node child = parent.getFirstChild(); |
| | | while (child != null) { |
| | | if (child instanceof Element && localName.equals(localName(child))) { |
| | | return (Element) child; |
| | | } |
| | | child = child.getNextSibling(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private String localName(Node node) { |
| | | return node.getLocalName() == null ? node.getNodeName().replaceFirst("^.*:", "") : node.getLocalName(); |
| | | } |
| | | |
| | | private byte[] readAll(InputStream in) throws Exception { |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | byte[] buffer = new byte[8192]; |
| | | int length; |
| | | while ((length = in.read(buffer)) > 0) { |
| | | out.write(buffer, 0, length); |
| | | } |
| | | return out.toByteArray(); |
| | | } |
| | | |
| | | private interface ParagraphMatcher { |
| | | boolean matches(String text); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.trafficaudit.holiday.HolidayConstants; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerCoefficient; |
| | | import com.trafficaudit.holiday.mapper.HolidayCalendarMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayPassengerCoefficientMapper; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Service |
| | | public class HolidayConfigService { |
| | | |
| | | @Resource |
| | | private HolidayCalendarMapper holidayCalendarMapper; |
| | | |
| | | @Resource |
| | | private HolidayPassengerCoefficientMapper coefficientMapper; |
| | | |
| | | public List<HolidayCalendar> listHolidays(Integer year, String holidayType) { |
| | | LambdaQueryWrapper<HolidayCalendar> wrapper = new LambdaQueryWrapper<HolidayCalendar>() |
| | | .eq(year != null, HolidayCalendar::getYear, year) |
| | | .eq(holidayType != null && !holidayType.trim().isEmpty(), HolidayCalendar::getHolidayType, holidayType) |
| | | .orderByAsc(HolidayCalendar::getYear) |
| | | .orderByAsc(HolidayCalendar::getHolidayType) |
| | | .orderByAsc(HolidayCalendar::getDayIndex); |
| | | return holidayCalendarMapper.selectList(wrapper); |
| | | } |
| | | |
| | | public List<HolidayCalendar> findHolidayDays(int year, String holidayType) { |
| | | return holidayCalendarMapper.selectList(new LambdaQueryWrapper<HolidayCalendar>() |
| | | .eq(HolidayCalendar::getYear, year) |
| | | .eq(HolidayCalendar::getHolidayType, holidayType) |
| | | .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) { |
| | | return holidayCalendarMapper.selectOne(new LambdaQueryWrapper<HolidayCalendar>() |
| | | .eq(HolidayCalendar::getHolidayDate, date) |
| | | .orderByAsc(HolidayCalendar::getDayIndex) |
| | | .last("limit 1")); |
| | | } |
| | | |
| | | @Transactional |
| | | public HolidayCalendar saveHoliday(Map<String, Object> params) { |
| | | Long id = longValue(params.get("id")); |
| | | Integer year = intValue(params.get("year")); |
| | | String holidayType = stringValue(params.get("holidayType")); |
| | | LocalDate holidayDate = dateValue(params.get("holidayDate")); |
| | | Integer dayIndex = intValue(params.get("dayIndex")); |
| | | if (year == null || year < 2000 || year > 2100) { |
| | | throw new IllegalArgumentException("年份䏿£ç¡®"); |
| | | } |
| | | if (!HolidayConstants.isValidHolidayType(holidayType)) { |
| | | throw new IllegalArgumentException("è忥åç±»ä¸æ£ç¡®"); |
| | | } |
| | | if (holidayDate == null || holidayDate.getYear() != year) { |
| | | throw new IllegalArgumentException("æ¥æå¿
é¡»å±äºæé年份"); |
| | | } |
| | | if (dayIndex == null || dayIndex < 0) { |
| | | throw new IllegalArgumentException("æ¥æåºå·ä¸è½å°äº 0"); |
| | | } |
| | | LambdaQueryWrapper<HolidayCalendar> duplicate = new LambdaQueryWrapper<HolidayCalendar>() |
| | | .eq(HolidayCalendar::getYear, year) |
| | | .eq(HolidayCalendar::getHolidayType, holidayType) |
| | | .and(w -> w.eq(HolidayCalendar::getHolidayDate, holidayDate) |
| | | .or().eq(HolidayCalendar::getDayIndex, dayIndex)); |
| | | if (id != null) { |
| | | duplicate.ne(HolidayCalendar::getId, id); |
| | | } |
| | | if (holidayCalendarMapper.selectCount(duplicate) > 0) { |
| | | throw new IllegalArgumentException("åä¸è忥ä¸çæ¥ææç¬¬å æ¥å·²åå¨"); |
| | | } |
| | | |
| | | HolidayCalendar entity = id == null ? new HolidayCalendar() : holidayCalendarMapper.selectById(id); |
| | | if (entity == null) { |
| | | throw new IllegalArgumentException("èåæ¥è®°å½ä¸åå¨"); |
| | | } |
| | | if (id == null) { |
| | | entity.setCreatedAt(LocalDateTime.now()); |
| | | } |
| | | entity.setYear(year); |
| | | entity.setHolidayType(holidayType); |
| | | entity.setHolidayDate(holidayDate); |
| | | entity.setDayIndex(dayIndex); |
| | | entity.setUpdatedAt(LocalDateTime.now()); |
| | | if (id == null) { |
| | | holidayCalendarMapper.insert(entity); |
| | | } else { |
| | | holidayCalendarMapper.updateById(entity); |
| | | } |
| | | return entity; |
| | | } |
| | | |
| | | @Transactional |
| | | public void deleteHoliday(Long id) { |
| | | if (id == null) { |
| | | throw new IllegalArgumentException("缺å°è®°å½ id"); |
| | | } |
| | | holidayCalendarMapper.deleteById(id); |
| | | } |
| | | |
| | | public List<HolidayPassengerCoefficient> listCoefficients(Integer year) { |
| | | return coefficientMapper.selectList(new LambdaQueryWrapper<HolidayPassengerCoefficient>() |
| | | .eq(year != null, HolidayPassengerCoefficient::getYear, year) |
| | | .orderByAsc(HolidayPassengerCoefficient::getYear) |
| | | .orderByAsc(HolidayPassengerCoefficient::getDateCategory)); |
| | | } |
| | | |
| | | public BigDecimal coefficientOf(int year, String dateCategory) { |
| | | HolidayPassengerCoefficient row = coefficientMapper.selectOne( |
| | | new LambdaQueryWrapper<HolidayPassengerCoefficient>() |
| | | .eq(HolidayPassengerCoefficient::getYear, year) |
| | | .eq(HolidayPassengerCoefficient::getDateCategory, dateCategory) |
| | | .last("limit 1")); |
| | | return row == null ? null : row.getCoefficient(); |
| | | } |
| | | |
| | | @Transactional |
| | | public HolidayPassengerCoefficient saveCoefficient(Map<String, Object> params) { |
| | | Long id = longValue(params.get("id")); |
| | | Integer year = intValue(params.get("year")); |
| | | String dateCategory = stringValue(params.get("dateCategory")); |
| | | BigDecimal coefficient = decimalValue(params.get("coefficient")); |
| | | if (year == null || year < 2000 || year > 2100) { |
| | | throw new IllegalArgumentException("年份䏿£ç¡®"); |
| | | } |
| | | if (!HolidayConstants.isValidDateCategory(dateCategory)) { |
| | | throw new IllegalArgumentException("æ¥æåç±»ä¸æ£ç¡®"); |
| | | } |
| | | if (coefficient == null || coefficient.compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new IllegalArgumentException("客è¿ç³»æ°å¿
é¡»å¤§äº 0"); |
| | | } |
| | | LambdaQueryWrapper<HolidayPassengerCoefficient> duplicate = new LambdaQueryWrapper<HolidayPassengerCoefficient>() |
| | | .eq(HolidayPassengerCoefficient::getYear, year) |
| | | .eq(HolidayPassengerCoefficient::getDateCategory, dateCategory); |
| | | if (id != null) { |
| | | duplicate.ne(HolidayPassengerCoefficient::getId, id); |
| | | } |
| | | if (coefficientMapper.selectCount(duplicate) > 0) { |
| | | throw new IllegalArgumentException("è¯¥å¹´ä»½åæ¥æåç±»ç客è¿ç³»æ°å·²åå¨"); |
| | | } |
| | | |
| | | HolidayPassengerCoefficient entity = id == null ? new HolidayPassengerCoefficient() : coefficientMapper.selectById(id); |
| | | if (entity == null) { |
| | | throw new IllegalArgumentException("客è¿ç³»æ°è®°å½ä¸åå¨"); |
| | | } |
| | | if (id == null) { |
| | | entity.setCreatedAt(LocalDateTime.now()); |
| | | } |
| | | entity.setYear(year); |
| | | entity.setDateCategory(dateCategory); |
| | | entity.setCoefficient(coefficient); |
| | | entity.setUpdatedAt(LocalDateTime.now()); |
| | | if (id == null) { |
| | | coefficientMapper.insert(entity); |
| | | } else { |
| | | coefficientMapper.updateById(entity); |
| | | } |
| | | return entity; |
| | | } |
| | | |
| | | @Transactional |
| | | public void deleteCoefficient(Long id) { |
| | | if (id == null) { |
| | | throw new IllegalArgumentException("缺å°è®°å½ id"); |
| | | } |
| | | coefficientMapper.deleteById(id); |
| | | } |
| | | |
| | | private Long longValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return Long.valueOf(value.toString()); |
| | | } |
| | | |
| | | private Integer intValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return Integer.valueOf(value.toString()); |
| | | } |
| | | |
| | | private BigDecimal decimalValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return com.trafficaudit.holiday.HolidayNumberUtils.decimalValue(value); |
| | | } |
| | | |
| | | private String stringValue(Object value) { |
| | | return value == null ? null : value.toString().trim(); |
| | | } |
| | | |
| | | private LocalDate dateValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return LocalDate.parse(value.toString()); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import org.springframework.boot.ApplicationArguments; |
| | | import org.springframework.boot.ApplicationRunner; |
| | | import org.springframework.core.annotation.Order; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import javax.annotation.Resource; |
| | | |
| | | @Component |
| | | @Order(100) |
| | | public class HolidayReferenceDataInitializer implements ApplicationRunner { |
| | | |
| | | @Resource |
| | | private HolidayWorkbookService workbookService; |
| | | |
| | | @Override |
| | | public void run(ApplicationArguments args) { |
| | | workbookService.seedReferenceDataIfNeeded(); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.trafficaudit.dataimport.entity.ImportBatch; |
| | | import com.trafficaudit.dataimport.mapper.ImportBatchMapper; |
| | | import com.trafficaudit.holiday.HolidayConstants; |
| | | import com.trafficaudit.holiday.HolidayNumberUtils; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerFlow; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleFlow; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment; |
| | | import com.trafficaudit.holiday.mapper.HolidayPassengerFlowMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayVehicleAdjustmentMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper; |
| | | import lombok.Data; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Service |
| | | public class HolidayStatService { |
| | | |
| | | @Resource |
| | | private HolidayConfigService configService; |
| | | @Resource |
| | | private HolidayWorkbookService workbookService; |
| | | @Resource |
| | | private HolidayPassengerFlowMapper passengerFlowMapper; |
| | | @Resource |
| | | private HolidayVehicleFlowMapper vehicleFlowMapper; |
| | | @Resource |
| | | private HolidayVehicleAdjustmentMapper vehicleAdjustmentMapper; |
| | | @Resource |
| | | private ImportBatchMapper importBatchMapper; |
| | | |
| | | public Map<String, Object> overview(int year, String holidayType) { |
| | | List<HolidayCalendar> calendar = configService.findHolidayDays(year, holidayType); |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("year", year); |
| | | result.put("holidayType", holidayType); |
| | | result.put("holidayTypeLabel", HolidayConstants.HOLIDAY_TYPE_LABELS.get(holidayType)); |
| | | result.put("days", calendar); |
| | | result.put("rows", buildOverviewRows(year, holidayType, calendar)); |
| | | result.put("templateFileName", workbookService.referenceFileName()); |
| | | return result; |
| | | } |
| | | |
| | | @Transactional |
| | | public Map<String, Object> importWorkbook(MultipartFile file, int year, String holidayType, boolean allowHistoricalOverwrite) { |
| | | HolidayWorkbookService.ImportSummary summary = workbookService.importWorkbook(file, year, holidayType, allowHistoricalOverwrite); |
| | | if (summary.isConfirmationRequired()) { |
| | | return importResult(summary); |
| | | } |
| | | ImportBatch batch = new ImportBatch(); |
| | | batch.setFileName(file == null ? null : file.getOriginalFilename()); |
| | | batch.setImportType("HOLIDAY_FLOW"); |
| | | batch.setReportPeriod(year + "-" + holidayType); |
| | | batch.setTotalRows(summary.getSuccessRows() + summary.getFailRows()); |
| | | batch.setSuccessRows(summary.getSuccessRows()); |
| | | batch.setFailRows(summary.getFailRows()); |
| | | batch.setErrorDetail(joinErrors(summary.getErrors())); |
| | | batch.setCreatedAt(LocalDateTime.now()); |
| | | importBatchMapper.insert(batch); |
| | | |
| | | return importResult(summary); |
| | | } |
| | | |
| | | private Map<String, Object> importResult(HolidayWorkbookService.ImportSummary summary) { |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("passengerRows", summary.getPassengerRows()); |
| | | result.put("vehicleRows", summary.getVehicleRows()); |
| | | result.put("skippedRows", summary.getSkippedRows()); |
| | | result.put("successRows", summary.getSuccessRows()); |
| | | result.put("failRows", summary.getFailRows()); |
| | | result.put("errors", summary.getErrors()); |
| | | result.put("confirmationRequired", summary.isConfirmationRequired()); |
| | | result.put("historicalChanges", summary.getHistoricalChanges()); |
| | | return result; |
| | | } |
| | | |
| | | public byte[] exportWorkbook(int year, String holidayType) { |
| | | return workbookService.exportWorkbook(year, holidayType); |
| | | } |
| | | |
| | | public List<Map<String, Object>> buildOverviewRows(int year, String holidayType, List<HolidayCalendar> calendar) { |
| | | Map<LocalDate, Map<String, BigDecimal>> passengerByDate = loadPassengerByDate(year); |
| | | Map<LocalDate, Map<String, BigDecimal>> vehicleByDate = loadVehicleByDate(year); |
| | | Map<LocalDate, BigDecimal> comparisonBaseByDate = loadComparisonBaseByDate(year); |
| | | BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY); |
| | | Map<Integer, HolidayCalendar> calendarByDay = new LinkedHashMap<>(); |
| | | for (HolidayCalendar day : calendar) { |
| | | if (day.getDayIndex() != null) calendarByDay.put(day.getDayIndex(), day); |
| | | } |
| | | List<Map<String, Object>> rows = new ArrayList<>(); |
| | | for (HolidayCalendar day : calendar) { |
| | | DailyMetrics current = buildDailyMetrics(year, holidayType, day.getDayIndex(), day.getHolidayDate(), |
| | | passengerByDate, vehicleByDate, comparisonBaseByDate, coefficient); |
| | | DailyMetrics ringRatioBase = ringRatioBaseFromMaps(year, holidayType, day.getDayIndex(), calendarByDay, |
| | | passengerByDate, vehicleByDate, comparisonBaseByDate, coefficient); |
| | | BigDecimal nationalProvincialRingRatio = ratio(current.getNationalProvincial(), |
| | | ringRatioBase == null ? null : ringRatioBase.getNationalProvincial()); |
| | | Map<String, Object> row = new LinkedHashMap<>(); |
| | | row.put("dayIndex", day.getDayIndex()); |
| | | row.put("date", day.getHolidayDate()); |
| | | row.put("rail", current.getRail()); |
| | | row.put("civilAviation", current.getCivilAviation()); |
| | | row.put("road", current.getRoad()); |
| | | row.put("water", current.getWater()); |
| | | row.put("expressway", current.getExpressway()); |
| | | row.put("nationalProvincial", current.getNationalProvincial()); |
| | | row.put("comparisonBase", current.getComparisonBase()); |
| | | row.put("nationalProvincialRingRatio", nationalProvincialRingRatio); |
| | | row.put("coefficient", current.getCoefficient()); |
| | | row.put("nonBusinessPersonFlow", current.getNonBusinessPersonFlow()); |
| | | row.put("totalPersonFlow", current.getTotalPersonFlow()); |
| | | rows.add(row); |
| | | } |
| | | return rows; |
| | | } |
| | | |
| | | public DailyMetrics dailyMetrics(int year, String holidayType, int dayIndex) { |
| | | List<HolidayCalendar> calendar = configService.findHolidayDays(year, holidayType); |
| | | HolidayCalendar target = null; |
| | | for (HolidayCalendar day : calendar) { |
| | | if (day.getDayIndex() != null && day.getDayIndex() == dayIndex) { |
| | | target = day; |
| | | break; |
| | | } |
| | | } |
| | | if (target == null) return null; |
| | | 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); |
| | | BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY); |
| | | return buildDailyMetrics(year, holidayType, dayIndex, date, passengerByDate, vehicleByDate, |
| | | comparisonBaseByDate, coefficient); |
| | | } |
| | | |
| | | public DailyMetrics ringRatioBaseMetrics(int year, String holidayType, int dayIndex) { |
| | | if (dayIndex <= 1) return null; |
| | | List<HolidayCalendar> calendar = configService.findHolidayDays(year, holidayType); |
| | | Map<Integer, HolidayCalendar> calendarByDay = new LinkedHashMap<>(); |
| | | for (HolidayCalendar day : calendar) { |
| | | if (day.getDayIndex() != null) calendarByDay.put(day.getDayIndex(), day); |
| | | } |
| | | Map<LocalDate, Map<String, BigDecimal>> passengerByDate = loadPassengerByDate(year); |
| | | Map<LocalDate, Map<String, BigDecimal>> vehicleByDate = loadVehicleByDate(year); |
| | | Map<LocalDate, BigDecimal> comparisonBaseByDate = loadComparisonBaseByDate(year); |
| | | BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY); |
| | | return ringRatioBaseFromMaps(year, holidayType, dayIndex, calendarByDay, passengerByDate, |
| | | vehicleByDate, comparisonBaseByDate, coefficient); |
| | | } |
| | | |
| | | private DailyMetrics buildDailyMetrics(int year, String holidayType, int dayIndex, LocalDate date, |
| | | Map<LocalDate, Map<String, BigDecimal>> passengerByDate, |
| | | Map<LocalDate, Map<String, BigDecimal>> vehicleByDate, |
| | | Map<LocalDate, BigDecimal> comparisonBaseByDate, |
| | | BigDecimal coefficient) { |
| | | Map<String, BigDecimal> passenger = passengerByDate.getOrDefault(date, Collections.emptyMap()); |
| | | Map<String, BigDecimal> vehicle = vehicleByDate.getOrDefault(date, Collections.emptyMap()); |
| | | BigDecimal expressway = vehicle.get(HolidayConstants.ROAD_EXPRESSWAY); |
| | | BigDecimal nationalProvincial = vehicle.get(HolidayConstants.ROAD_NATIONAL_PROVINCIAL); |
| | | BigDecimal comparisonBase = comparisonBaseByDate.get(date); |
| | | BigDecimal nonBusiness = nonBusinessPersonFlow(expressway, nationalProvincial, coefficient); |
| | | |
| | | DailyMetrics metrics = new DailyMetrics(); |
| | | metrics.setYear(year); |
| | | metrics.setHolidayType(holidayType); |
| | | metrics.setDayIndex(dayIndex); |
| | | 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)); |
| | | metrics.setWater(passenger.get(HolidayConstants.MODE_WATER)); |
| | | metrics.setExpressway(expressway); |
| | | metrics.setNationalProvincial(nationalProvincial); |
| | | metrics.setComparisonBase(comparisonBase); |
| | | metrics.setCoefficient(coefficient); |
| | | metrics.setNonBusinessPersonFlow(nonBusiness); |
| | | recalculateDerived(metrics); |
| | | return metrics; |
| | | } |
| | | |
| | | private DailyMetrics ringRatioBaseFromMaps(int year, String holidayType, int dayIndex, |
| | | Map<Integer, HolidayCalendar> calendarByDay, |
| | | Map<LocalDate, Map<String, BigDecimal>> passengerByDate, |
| | | Map<LocalDate, Map<String, BigDecimal>> vehicleByDate, |
| | | Map<LocalDate, BigDecimal> comparisonBaseByDate, |
| | | BigDecimal coefficient) { |
| | | if (dayIndex <= 1) return null; |
| | | HolidayCalendar previous = calendarByDay.get(dayIndex - 1); |
| | | if (previous == null || previous.getHolidayDate() == null) return null; |
| | | DailyMetrics base = buildDailyMetrics(year, holidayType, previous.getDayIndex(), previous.getHolidayDate(), |
| | | passengerByDate, vehicleByDate, comparisonBaseByDate, coefficient); |
| | | BigDecimal comparisonBase = comparisonBaseByDate.get(previous.getHolidayDate()); |
| | | if (comparisonBase != null) { |
| | | base.setNationalProvincial(comparisonBase); |
| | | recalculateDerived(base); |
| | | } |
| | | return base; |
| | | } |
| | | |
| | | private void recalculateDerived(DailyMetrics metrics) { |
| | | BigDecimal nonBusiness = nonBusinessPersonFlow(metrics.getExpressway(), metrics.getNationalProvincial(), |
| | | metrics.getCoefficient()); |
| | | metrics.setNonBusinessPersonFlow(nonBusiness); |
| | | metrics.setRoadPersonFlow(sumNullable(metrics.getRoad(), nonBusiness)); |
| | | metrics.setTotalPersonFlow(totalPersonFlow(nonBusiness, metrics.getRoad(), metrics.getRail(), |
| | | metrics.getCivilAviation(), metrics.getWater())); |
| | | } |
| | | |
| | | private BigDecimal nonBusinessPersonFlow(BigDecimal expressway, BigDecimal nationalProvincial, |
| | | BigDecimal coefficient) { |
| | | if (coefficient == null || (expressway == null && nationalProvincial == null)) return null; |
| | | return HolidayNumberUtils.multiply( |
| | | HolidayNumberUtils.sumNullable(expressway, nationalProvincial), coefficient); |
| | | } |
| | | |
| | | public DailyMetrics cumulativeMetrics(int year, String holidayType, int dayIndex) { |
| | | DailyMetrics cumulative = new DailyMetrics(); |
| | | cumulative.setYear(year); |
| | | cumulative.setHolidayType(holidayType); |
| | | cumulative.setDayIndex(dayIndex); |
| | | for (int day = 1; day <= dayIndex; day++) { |
| | | DailyMetrics item = dailyMetrics(year, holidayType, day); |
| | | if (item == null) continue; |
| | | cumulative.setRail(sumNullable(cumulative.getRail(), item.getRail())); |
| | | cumulative.setCivilAviation(sumNullable(cumulative.getCivilAviation(), item.getCivilAviation())); |
| | | cumulative.setRoad(sumNullable(cumulative.getRoad(), item.getRoad())); |
| | | cumulative.setWater(sumNullable(cumulative.getWater(), item.getWater())); |
| | | cumulative.setExpressway(sumNullable(cumulative.getExpressway(), item.getExpressway())); |
| | | cumulative.setNationalProvincial(sumNullable(cumulative.getNationalProvincial(), item.getNationalProvincial())); |
| | | cumulative.setNonBusinessPersonFlow(sumNullable(cumulative.getNonBusinessPersonFlow(), item.getNonBusinessPersonFlow())); |
| | | cumulative.setRoadPersonFlow(sumNullable(cumulative.getRoadPersonFlow(), item.getRoadPersonFlow())); |
| | | cumulative.setTotalPersonFlow(sumNullable(cumulative.getTotalPersonFlow(), item.getTotalPersonFlow())); |
| | | } |
| | | return cumulative; |
| | | } |
| | | |
| | | |
| | | public DailyMetrics findMetricsByDate(LocalDate date) { |
| | | HolidayCalendar calendar = configService.findHolidayByDate(date); |
| | | if (calendar == null) return null; |
| | | return dailyMetrics(calendar.getYear(), calendar.getHolidayType(), calendar.getDayIndex()); |
| | | } |
| | | |
| | | private Map<LocalDate, Map<String, BigDecimal>> loadPassengerByDate(int year) { |
| | | Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>(); |
| | | List<HolidayPassengerFlow> rows = passengerFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayPassengerFlow>().eq(HolidayPassengerFlow::getYear, year)); |
| | | for (HolidayPassengerFlow row : rows) { |
| | | result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>()) |
| | | .put(row.getTransportMode(), row.getPassengerCount()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, Map<String, BigDecimal>> loadVehicleByDate(int year) { |
| | | Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>(); |
| | | List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>().eq(HolidayVehicleFlow::getYear, year)); |
| | | for (HolidayVehicleFlow row : rows) { |
| | | result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>()) |
| | | .put(row.getRoadCategory(), row.getVehicleCount()); |
| | | } |
| | | Map<LocalDate, BigDecimal> adjustments = loadNationalProvincialAdjustmentsByDate(year); |
| | | for (Map.Entry<LocalDate, BigDecimal> entry : adjustments.entrySet()) { |
| | | result.computeIfAbsent(entry.getKey(), key -> new LinkedHashMap<>()) |
| | | .put(HolidayConstants.ROAD_NATIONAL_PROVINCIAL, entry.getValue()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, BigDecimal> loadNationalProvincialAdjustmentsByDate(int year) { |
| | | Map<LocalDate, BigDecimal> result = new LinkedHashMap<>(); |
| | | List<HolidayVehicleAdjustment> rows = vehicleAdjustmentMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleAdjustment>().eq(HolidayVehicleAdjustment::getYear, year)); |
| | | for (HolidayVehicleAdjustment row : rows) { |
| | | if (row.getFlowDate() != null && row.getAdjustedValue() != null) { |
| | | result.put(row.getFlowDate(), row.getAdjustedValue()); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, BigDecimal> loadComparisonBaseByDate(int year) { |
| | | Map<LocalDate, BigDecimal> result = new LinkedHashMap<>(); |
| | | List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getYear, year) |
| | | .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL)); |
| | | for (HolidayVehicleFlow row : rows) { |
| | | if (row.getComparisonBase() != null) result.put(row.getFlowDate(), row.getComparisonBase()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private BigDecimal totalPersonFlow(BigDecimal nonBusinessPersonFlow, BigDecimal road, BigDecimal rail, |
| | | BigDecimal civilAviation, BigDecimal water) { |
| | | BigDecimal total = nonBusinessPersonFlow; |
| | | total = sumNullable(total, road); |
| | | total = sumNullable(total, rail); |
| | | total = sumNullable(total, civilAviation); |
| | | total = sumNullable(total, water); |
| | | return total; |
| | | } |
| | | |
| | | |
| | | private BigDecimal ratio(BigDecimal current, BigDecimal base) { |
| | | BigDecimal quotient = HolidayNumberUtils.divide(current, base); |
| | | return quotient == null ? null : HolidayNumberUtils.normalize(quotient.subtract(BigDecimal.ONE)); |
| | | } |
| | | |
| | | private BigDecimal sumNullable(BigDecimal left, BigDecimal right) { |
| | | return HolidayNumberUtils.sumNullable(left, right); |
| | | } |
| | | |
| | | private String joinErrors(List<String> errors) { |
| | | if (errors == null || errors.isEmpty()) return null; |
| | | StringBuilder builder = new StringBuilder(); |
| | | int limit = Math.min(errors.size(), 200); |
| | | for (int i = 0; i < limit; i++) { |
| | | if (i > 0) builder.append('\n'); |
| | | builder.append(errors.get(i)); |
| | | } |
| | | if (errors.size() > limit) { |
| | | builder.append("\n... ").append(errors.size() - limit).append(" more"); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | @Data |
| | | public static class DailyMetrics { |
| | | private int year; |
| | | private String holidayType; |
| | | private int dayIndex; |
| | | private LocalDate date; |
| | | private BigDecimal rail; |
| | | private BigDecimal civilAviation; |
| | | private BigDecimal road; |
| | | private BigDecimal water; |
| | | private BigDecimal expressway; |
| | | private BigDecimal nationalProvincial; |
| | | private BigDecimal comparisonBase; |
| | | private BigDecimal coefficient; |
| | | private BigDecimal nonBusinessPersonFlow; |
| | | private BigDecimal roadPersonFlow; |
| | | private BigDecimal totalPersonFlow; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import lombok.Data; |
| | | import org.jfree.chart.ChartFactory; |
| | | import org.jfree.chart.JFreeChart; |
| | | import org.jfree.chart.StandardChartTheme; |
| | | import org.jfree.chart.axis.AxisLocation; |
| | | import org.jfree.chart.axis.CategoryAxis; |
| | | import org.jfree.chart.axis.NumberAxis; |
| | | import org.jfree.chart.axis.SymbolAxis; |
| | | import org.jfree.chart.axis.ValueAxis; |
| | | import org.jfree.chart.labels.StandardPieSectionLabelGenerator; |
| | | import org.jfree.chart.plot.CategoryPlot; |
| | | import org.jfree.chart.plot.PiePlot; |
| | | import org.jfree.chart.plot.Plot; |
| | | import org.jfree.chart.plot.PieLabelRecord; |
| | | import org.jfree.chart.plot.PiePlotState; |
| | | import org.jfree.chart.text.TextFragment; |
| | | import org.jfree.chart.plot.PlotOrientation; |
| | | import org.jfree.chart.plot.XYPlot; |
| | | import org.jfree.chart.renderer.category.StackedBarRenderer; |
| | | import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; |
| | | import org.jfree.chart.ui.RectangleEdge; |
| | | import org.jfree.chart.ui.RectangleInsets; |
| | | import org.jfree.chart.ui.TextAnchor; |
| | | import org.jfree.data.DefaultKeyedValues; |
| | | import org.jfree.data.KeyedValues; |
| | | import org.jfree.data.category.CategoryDataset; |
| | | import org.jfree.data.category.DefaultCategoryDataset; |
| | | import org.jfree.data.general.DefaultPieDataset; |
| | | import org.jfree.data.xy.XYDataset; |
| | | import org.jfree.data.xy.XYSeries; |
| | | import org.jfree.data.xy.XYSeriesCollection; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.awt.BasicStroke; |
| | | import java.awt.Color; |
| | | import java.awt.Font; |
| | | import java.awt.FontMetrics; |
| | | import java.awt.Graphics2D; |
| | | import java.awt.Paint; |
| | | import java.awt.Shape; |
| | | import java.awt.image.BufferedImage; |
| | | import java.awt.geom.Ellipse2D; |
| | | import java.awt.geom.Path2D; |
| | | import java.awt.geom.Point2D; |
| | | import java.awt.geom.Rectangle2D; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.lang.reflect.Field; |
| | | import java.lang.reflect.Modifier; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.text.DecimalFormat; |
| | | import java.text.NumberFormat; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.Comparator; |
| | | import java.util.IdentityHashMap; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Service |
| | | public class HolidayTrendChartService { |
| | | |
| | | private static final Font BASE_FONT = new Font("Microsoft YaHei", Font.PLAIN, 20); |
| | | private static final Font TICK_FONT = BASE_FONT.deriveFont(Font.BOLD, 22f); |
| | | private static final Font LABEL_FONT = BASE_FONT.deriveFont(Font.BOLD, 22f); |
| | | private static final Font ITEM_LABEL_FONT = BASE_FONT.deriveFont(Font.BOLD, 20f); |
| | | private static final Font LEGEND_FONT = BASE_FONT.deriveFont(Font.BOLD, 22f); |
| | | private static final Font STACK_LABEL_FONT = BASE_FONT.deriveFont(Font.BOLD, 17f); |
| | | |
| | | private static final Color AXIS_COLOR = Color.BLACK; |
| | | private static final Color MUTED_TEXT_COLOR = new Color(102, 102, 102); |
| | | private static final double UNIT_LABEL_BAND = 58.0; |
| | | private static final double RIGHT_LABEL_BAND = 74.0; |
| | | private static final double ITEM_LABEL_GAP = 10.0; |
| | | private static final float HALO_OUTLINE_WIDTH = 4.0f; |
| | | private static final int PERCENT_ITEM_LABEL_DECIMALS = 1; |
| | | private static final double LINE_CHART_HEIGHT_SCALE = 1.20; |
| | | private static final Color ITEM_LABEL_COLOR = new Color(55, 55, 55); |
| | | private static final Color[] SERIES_COLORS = { |
| | | new Color(192, 80, 77), |
| | | new Color(79, 129, 189), |
| | | new Color(155, 187, 89), |
| | | new Color(128, 100, 162), |
| | | new Color(75, 172, 198) |
| | | }; |
| | | |
| | | static { |
| | | System.setProperty("java.awt.headless", "true"); |
| | | } |
| | | |
| | | public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis) { |
| | | return lineChart(yAxisLabel, series, percentAxis, 1100, 590); |
| | | } |
| | | |
| | | public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis, |
| | | int width, int height) { |
| | | return lineChart(yAxisLabel, series, percentAxis, 2, width, height); |
| | | } |
| | | |
| | | public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis, |
| | | int valueDecimals, int width, int height) { |
| | | XYSeriesCollection dataset = new XYSeriesCollection(); |
| | | for (int seriesIndex = 0; seriesIndex < series.size(); seriesIndex++) { |
| | | ChartSeries item = series.get(seriesIndex); |
| | | XYSeries xySeries = new XYSeries(item.getName(), false, false); |
| | | for (int i = 0; i < item.getValues().size(); i++) { |
| | | BigDecimal value = item.getValues().get(i); |
| | | if (value != null) { |
| | | xySeries.add(i, displayValue(value, percentAxis, valueDecimals).doubleValue()); |
| | | } |
| | | } |
| | | dataset.addSeries(xySeries); |
| | | } |
| | | JFreeChart chart = ChartFactory.createXYLineChart( |
| | | null, null, null, dataset, PlotOrientation.VERTICAL, true, false, false); |
| | | applyTheme(chart); |
| | | chart.setBackgroundPaint(Color.WHITE); |
| | | chart.setBorderVisible(false); |
| | | chart.getLegend().setPosition(RectangleEdge.BOTTOM); |
| | | chart.getLegend().setItemFont(LEGEND_FONT); |
| | | chart.getLegend().setFrame(new org.jfree.chart.block.BlockBorder(0.0, 0.0, 0.0, 0.0)); |
| | | chart.getLegend().setBackgroundPaint(Color.WHITE); |
| | | chart.getLegend().setItemPaint(MUTED_TEXT_COLOR); |
| | | |
| | | XYPlot plot = chart.getXYPlot(); |
| | | stylePlot(plot); |
| | | plot.setDomainGridlinesVisible(false); |
| | | plot.setRangeGridlinesVisible(false); |
| | | plot.setDomainTickBandPaint(null); |
| | | plot.setRangeTickBandPaint(null); |
| | | SymbolAxis domainAxis = new SymbolAxis(null, DAY_LABELS.toArray(new String[0])); |
| | | domainAxis.setGridBandPaint(new Color(0, 0, 0, 0)); |
| | | domainAxis.setGridBandAlternatePaint(new Color(0, 0, 0, 0)); |
| | | domainAxis.setLowerMargin(0.04); |
| | | domainAxis.setUpperMargin(0.04); |
| | | plot.setDomainAxis(domainAxis); |
| | | styleDomainAxis(plot.getDomainAxis()); |
| | | |
| | | NumberAxis rangeAxis = new NumberAxis(); |
| | | rangeAxis.setAutoRangeIncludesZero(false); |
| | | rangeAxis.setLowerMargin(0.04); |
| | | rangeAxis.setUpperMargin(0.07); |
| | | rangeAxis.setTickLabelFont(TICK_FONT); |
| | | rangeAxis.setLabelFont(LABEL_FONT); |
| | | rangeAxis.setTickLabelPaint(MUTED_TEXT_COLOR); |
| | | rangeAxis.setLabelPaint(AXIS_COLOR); |
| | | rangeAxis.setAxisLinePaint(AXIS_COLOR); |
| | | rangeAxis.setTickMarkPaint(AXIS_COLOR); |
| | | if (percentAxis) { |
| | | rangeAxis.setRange(0.0, 1.0); |
| | | rangeAxis.setAutoRangeIncludesZero(true); |
| | | } |
| | | NumberFormat valueFormat = percentAxis |
| | | ? NumberFormat.getPercentInstance() : new DecimalFormat("#,##0.##"); |
| | | valueFormat.setMaximumFractionDigits(percentAxis ? 0 : valueDecimals); |
| | | rangeAxis.setNumberFormatOverride(valueFormat); |
| | | plot.setRangeAxis(rangeAxis); |
| | | plot.setAxisOffset(new RectangleInsets(2.0, 2.0, 2.0, 2.0)); |
| | | |
| | | XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(true, true); |
| | | renderer.setDefaultShapesVisible(true); |
| | | renderer.setDefaultShapesFilled(true); |
| | | renderer.setDefaultLinesVisible(true); |
| | | renderer.setDefaultItemLabelsVisible(false); |
| | | for (int i = 0; i < series.size(); i++) { |
| | | renderer.setSeriesPaint(i, seriesColor(i)); |
| | | renderer.setSeriesFillPaint(i, seriesColor(i)); |
| | | renderer.setSeriesShape(i, seriesShape(i)); |
| | | renderer.setSeriesStroke(i, new BasicStroke(6.0f, |
| | | BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | } |
| | | plot.setRenderer(renderer); |
| | | return render(chart, width, scaleLineChartHeight(height), |
| | | yAxisLabel, true, percentAxis, valueDecimals); |
| | | } |
| | | |
| | | public byte[] pieChart(String valueSuffix, List<ChartCategory> categories, |
| | | int width, int height) { |
| | | DefaultPieDataset<String> dataset = new DefaultPieDataset<>(); |
| | | for (ChartCategory category : categories) { |
| | | dataset.setValue(category.getName(), category.getValue()); |
| | | } |
| | | PiePlot<String> piePlot = new WaterRightPiePlot<>(dataset); |
| | | JFreeChart chart = new JFreeChart(null, JFreeChart.DEFAULT_TITLE_FONT, piePlot, true); |
| | | applyTheme(chart); |
| | | chart.setBackgroundPaint(Color.WHITE); |
| | | chart.setBorderVisible(false); |
| | | chart.getLegend().setPosition(RectangleEdge.BOTTOM); |
| | | chart.getLegend().setItemFont(LEGEND_FONT); |
| | | chart.getLegend().setFrame(new org.jfree.chart.block.BlockBorder(0.0, 0.0, 0.0, 0.0)); |
| | | chart.getLegend().setBackgroundPaint(Color.WHITE); |
| | | chart.getLegend().setItemPaint(MUTED_TEXT_COLOR); |
| | | |
| | | PiePlot plot = piePlot; |
| | | plot.setBackgroundPaint(Color.WHITE); |
| | | plot.setOutlineVisible(false); |
| | | plot.setShadowPaint(null); |
| | | plot.setLabelFont(ITEM_LABEL_FONT); |
| | | plot.setLabelPaint(ITEM_LABEL_COLOR); |
| | | plot.setLabelBackgroundPaint(Color.WHITE); |
| | | plot.setLabelOutlinePaint(new Color(160, 160, 160)); |
| | | plot.setLabelShadowPaint(null); |
| | | plot.setLabelLinkPaint(Color.BLACK); |
| | | plot.setLabelLinkStroke(new BasicStroke(2.0f)); |
| | | plot.setLabelGap(0.02); |
| | | plot.setMaximumLabelWidth(0.24); |
| | | plot.setSimpleLabels(false); |
| | | plot.setInteriorGap(0.06); |
| | | plot.setStartAngle(90.0); |
| | | for (ChartCategory category : categories) { |
| | | plot.setSectionOutlinePaint(category.getName(), Color.WHITE); |
| | | plot.setSectionOutlineStroke(category.getName(), new BasicStroke(1.2f)); |
| | | } |
| | | plot.setLabelGenerator(new StandardPieSectionLabelGenerator( |
| | | "{0} {2}", new DecimalFormat("0"), new DecimalFormat("0.00%"))); |
| | | int index = 0; |
| | | for (ChartCategory category : categories) { |
| | | plot.setSectionPaint(category.getName(), seriesColor(index++)); |
| | | } |
| | | return render(chart, width, height, valueSuffix, false, false, 2); |
| | | } |
| | | |
| | | public byte[] stackedBarChart(String yAxisLabel, List<String> categories, |
| | | List<ChartSeries> series, boolean percentAxis, |
| | | int width, int height) { |
| | | DefaultCategoryDataset dataset = new DefaultCategoryDataset(); |
| | | for (ChartSeries item : series) { |
| | | for (int i = 0; i < categories.size() && i < item.getValues().size(); i++) { |
| | | BigDecimal value = item.getValues().get(i); |
| | | if (value != null) { |
| | | dataset.addValue(value, item.getName(), categories.get(i)); |
| | | } |
| | | } |
| | | } |
| | | JFreeChart chart = ChartFactory.createStackedBarChart( |
| | | null, null, null, dataset, PlotOrientation.HORIZONTAL, true, false, false); |
| | | applyTheme(chart); |
| | | chart.setBackgroundPaint(Color.WHITE); |
| | | chart.setBorderVisible(false); |
| | | chart.getLegend().setPosition(RectangleEdge.BOTTOM); |
| | | chart.getLegend().setItemFont(LEGEND_FONT); |
| | | chart.getLegend().setFrame(new org.jfree.chart.block.BlockBorder(0.0, 0.0, 0.0, 0.0)); |
| | | chart.getLegend().setBackgroundPaint(Color.WHITE); |
| | | chart.getLegend().setItemPaint(MUTED_TEXT_COLOR); |
| | | |
| | | CategoryPlot plot = chart.getCategoryPlot(); |
| | | plot.setBackgroundPaint(Color.WHITE); |
| | | plot.setDomainGridlinesVisible(false); |
| | | plot.setRangeGridlinesVisible(false); |
| | | plot.setOutlineVisible(false); |
| | | plot.setAxisOffset(new RectangleInsets(2.0, 2.0, 2.0, 2.0)); |
| | | // Horizontal stacked bars: value axis along the bottom, no vertical axis line. |
| | | plot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_LEFT); |
| | | CategoryAxis domainAxis = plot.getDomainAxis(); |
| | | domainAxis.setAxisLineVisible(false); |
| | | domainAxis.setTickMarksVisible(false); |
| | | styleDomainAxis(domainAxis); |
| | | if (series.size() >= 3) { |
| | | RectangleInsets insets = plot.getInsets(); |
| | | plot.setInsets(new RectangleInsets(insets.getTop(), insets.getLeft(), |
| | | insets.getBottom(), insets.getRight() + RIGHT_LABEL_BAND)); |
| | | } |
| | | NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); |
| | | rangeAxis.setTickLabelFont(TICK_FONT); |
| | | rangeAxis.setLabelFont(LABEL_FONT); |
| | | rangeAxis.setTickLabelPaint(MUTED_TEXT_COLOR); |
| | | rangeAxis.setLabelPaint(AXIS_COLOR); |
| | | rangeAxis.setAxisLinePaint(AXIS_COLOR); |
| | | rangeAxis.setTickMarkPaint(AXIS_COLOR); |
| | | rangeAxis.setAutoRangeIncludesZero(true); |
| | | rangeAxis.setLowerMargin(0.05); |
| | | rangeAxis.setUpperMargin(0.12); |
| | | if (percentAxis) { |
| | | rangeAxis.setRange(0.0, 1.0); |
| | | } |
| | | NumberFormat valueFormat = percentAxis |
| | | ? NumberFormat.getPercentInstance() : new DecimalFormat("#,##0.##"); |
| | | valueFormat.setMaximumFractionDigits(percentAxis ? 0 : 2); |
| | | rangeAxis.setNumberFormatOverride(valueFormat); |
| | | |
| | | StackedBarRenderer renderer = new StackedBarRenderer(); |
| | | renderer.setDefaultItemLabelsVisible(false); |
| | | renderer.setBarPainter(new org.jfree.chart.renderer.category.StandardBarPainter()); |
| | | renderer.setShadowVisible(false); |
| | | renderer.setItemMargin(0.05); |
| | | for (int i = 0; i < series.size(); i++) { |
| | | renderer.setSeriesPaint(i, seriesColor(i)); |
| | | } |
| | | plot.setRenderer(renderer); |
| | | return render(chart, width, height, yAxisLabel, true, percentAxis, 2); |
| | | } |
| | | |
| | | private static class WaterRightPiePlot<K extends Comparable<K>> extends PiePlot<K> { |
| | | private DefaultKeyedValues<K> waterKeys; |
| | | |
| | | private WaterRightPiePlot(org.jfree.data.general.PieDataset<K> dataset) { |
| | | super(dataset); |
| | | } |
| | | |
| | | @Override |
| | | protected void drawLeftLabels(KeyedValues<K> leftKeys, Graphics2D g2, |
| | | Rectangle2D plotArea, Rectangle2D linkArea, |
| | | float maxLabelWidth, PiePlotState state) { |
| | | DefaultKeyedValues<K> filtered = new DefaultKeyedValues<>(); |
| | | waterKeys = new DefaultKeyedValues<>(); |
| | | for (int i = 0; i < leftKeys.getItemCount(); i++) { |
| | | K key = leftKeys.getKey(i); |
| | | if ("æ°´è·¯".equals(String.valueOf(key))) { |
| | | waterKeys.addValue(key, leftKeys.getValue(i)); |
| | | } else { |
| | | filtered.addValue(key, leftKeys.getValue(i)); |
| | | } |
| | | } |
| | | super.drawLeftLabels(filtered, g2, plotArea, linkArea, maxLabelWidth, state); |
| | | } |
| | | |
| | | @Override |
| | | protected void drawRightLabels(KeyedValues<K> rightKeys, Graphics2D g2, |
| | | Rectangle2D plotArea, Rectangle2D linkArea, |
| | | float maxLabelWidth, PiePlotState state) { |
| | | DefaultKeyedValues<K> merged = new DefaultKeyedValues<>(); |
| | | if (waterKeys != null) { |
| | | for (int i = 0; i < waterKeys.getItemCount(); i++) { |
| | | merged.addValue(waterKeys.getKey(i), waterKeys.getValue(i)); |
| | | } |
| | | } |
| | | for (int i = 0; i < rightKeys.getItemCount(); i++) { |
| | | merged.addValue(rightKeys.getKey(i), rightKeys.getValue(i)); |
| | | } |
| | | super.drawRightLabels(merged, g2, plotArea, linkArea, maxLabelWidth, state); |
| | | } |
| | | |
| | | @Override |
| | | protected void drawLeftLabel(Graphics2D g2, PiePlotState state, PieLabelRecord record) { |
| | | recolorLabel(record); |
| | | super.drawLeftLabel(g2, state, record); |
| | | } |
| | | |
| | | @Override |
| | | protected void drawRightLabel(Graphics2D g2, PiePlotState state, PieLabelRecord record) { |
| | | recolorLabel(record); |
| | | super.drawRightLabel(g2, state, record); |
| | | } |
| | | |
| | | private void recolorLabel(PieLabelRecord record) { |
| | | Paint paint = getSectionPaint(record.getKey()); |
| | | Object label = record.getLabel(); |
| | | if (paint == null || label == null) return; |
| | | recolorPaintFields(label, paint, new IdentityHashMap<Object, Boolean>()); |
| | | } |
| | | |
| | | private void recolorPaintFields(Object node, Paint paint, |
| | | Map<Object, Boolean> visited) { |
| | | if (node == null || visited.put(node, Boolean.TRUE) != null) return; |
| | | if (node instanceof Paint || node instanceof String || node instanceof Number |
| | | || node instanceof Font) { |
| | | return; |
| | | } |
| | | if (node instanceof Iterable) { |
| | | for (Object child : (Iterable<?>) node) { |
| | | recolorPaintFields(child, paint, visited); |
| | | } |
| | | return; |
| | | } |
| | | if (!node.getClass().getName().startsWith("org.jfree.chart.text")) return; |
| | | Class<?> type = node.getClass(); |
| | | while (type != null && type != Object.class) { |
| | | for (Field field : type.getDeclaredFields()) { |
| | | if (Modifier.isStatic(field.getModifiers())) continue; |
| | | try { |
| | | field.setAccessible(true); |
| | | Object value = field.get(node); |
| | | if (value instanceof Paint && node instanceof TextFragment) { |
| | | field.set(node, paint); |
| | | } else if (value instanceof Iterable |
| | | || value != null && value.getClass().getName() |
| | | .startsWith("org.jfree.chart.text")) { |
| | | recolorPaintFields(value, paint, visited); |
| | | } |
| | | } catch (IllegalAccessException ignored) { |
| | | // Keep the default label paint if a JFreeChart field cannot be read. |
| | | } |
| | | } |
| | | type = type.getSuperclass(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private Shape seriesShape(int seriesIndex) { |
| | | if (seriesIndex % 3 == 1) { |
| | | return new Rectangle2D.Double(-6.0, -6.0, 12.0, 12.0); |
| | | } |
| | | if (seriesIndex % 3 == 2) { |
| | | Path2D.Double diamond = new Path2D.Double(); |
| | | diamond.moveTo(0.0, -7.0); |
| | | diamond.lineTo(7.0, 0.0); |
| | | diamond.lineTo(0.0, 7.0); |
| | | diamond.lineTo(-7.0, 0.0); |
| | | diamond.closePath(); |
| | | return diamond; |
| | | } |
| | | return new Ellipse2D.Double(-6.0, -6.0, 12.0, 12.0); |
| | | } |
| | | |
| | | private void stylePlot(XYPlot plot) { |
| | | plot.setBackgroundPaint(Color.WHITE); |
| | | plot.setOutlineVisible(false); |
| | | plot.setDomainGridlinesVisible(false); |
| | | plot.setRangeGridlinesVisible(false); |
| | | } |
| | | |
| | | private void styleDomainAxis(org.jfree.chart.axis.CategoryAxis axis) { |
| | | axis.setTickLabelFont(TICK_FONT); |
| | | axis.setLabelFont(LABEL_FONT); |
| | | axis.setTickLabelPaint(MUTED_TEXT_COLOR); |
| | | axis.setLabelPaint(AXIS_COLOR); |
| | | axis.setAxisLinePaint(AXIS_COLOR); |
| | | axis.setTickMarkPaint(AXIS_COLOR); |
| | | axis.setCategoryMargin(0.05); |
| | | } |
| | | |
| | | private void styleDomainAxis(org.jfree.chart.axis.ValueAxis axis) { |
| | | axis.setTickLabelFont(TICK_FONT); |
| | | axis.setLabelFont(LABEL_FONT); |
| | | axis.setTickLabelPaint(MUTED_TEXT_COLOR); |
| | | axis.setLabelPaint(AXIS_COLOR); |
| | | axis.setAxisLinePaint(AXIS_COLOR); |
| | | axis.setTickMarkPaint(AXIS_COLOR); |
| | | } |
| | | |
| | | private void applyTheme(JFreeChart chart) { |
| | | StandardChartTheme theme = new StandardChartTheme("CN"); |
| | | theme.setExtraLargeFont(BASE_FONT.deriveFont(Font.BOLD, 22f)); |
| | | theme.setLargeFont(BASE_FONT.deriveFont(Font.BOLD, 18f)); |
| | | theme.setRegularFont(BASE_FONT); |
| | | theme.setSmallFont(TICK_FONT); |
| | | theme.setChartBackgroundPaint(Color.WHITE); |
| | | theme.setPlotBackgroundPaint(Color.WHITE); |
| | | theme.setAxisLabelPaint(AXIS_COLOR); |
| | | theme.setTickLabelPaint(MUTED_TEXT_COLOR); |
| | | theme.setLegendItemPaint(MUTED_TEXT_COLOR); |
| | | theme.setLegendBackgroundPaint(Color.WHITE); |
| | | theme.apply(chart); |
| | | chart.setBackgroundPaint(Color.WHITE); |
| | | chart.setBorderVisible(false); |
| | | } |
| | | |
| | | private Color seriesColor(int index) { |
| | | return SERIES_COLORS[index % SERIES_COLORS.length]; |
| | | } |
| | | |
| | | private byte[] render(JFreeChart chart, int width, int height, String unitLabel, |
| | | boolean axisChart, boolean percentItemLabels, int valueDecimals) { |
| | | try { |
| | | reserveUnitLabelBand(chart, axisChart, unitLabel); |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | org.jfree.chart.ChartRenderingInfo info = new org.jfree.chart.ChartRenderingInfo(); |
| | | java.awt.image.BufferedImage image = |
| | | chart.createBufferedImage(width, height, info); |
| | | Graphics2D graphics = image.createGraphics(); |
| | | try { |
| | | graphics.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, |
| | | java.awt.RenderingHints.VALUE_ANTIALIAS_ON); |
| | | if (axisChart && isHorizontalStackedChart(chart)) { |
| | | drawUnitLabelTopRight(graphics, unitLabel, info.getPlotInfo().getDataArea()); |
| | | drawStackedValueAxis(graphics, info.getPlotInfo().getDataArea()); |
| | | } else if (axisChart) { |
| | | drawAxisArrows(graphics, info.getPlotInfo().getDataArea()); |
| | | drawUnitLabel(graphics, unitLabel, info.getPlotInfo().getDataArea()); |
| | | drawDateLabel(graphics, info.getPlotInfo().getDataArea()); |
| | | } |
| | | drawXYItemLabels(graphics, image, chart, |
| | | info.getPlotInfo().getDataArea(), percentItemLabels, unitLabel, valueDecimals); |
| | | drawStackedBarLabels(graphics, image, chart, |
| | | info.getPlotInfo().getDataArea(), percentItemLabels); |
| | | } finally { |
| | | graphics.dispose(); |
| | | } |
| | | javax.imageio.ImageIO.write(image, "png", out); |
| | | return out.toByteArray(); |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("èµ°å¿å¾å¾ççæå¤±è´¥: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | private void drawAxisArrows(Graphics2D graphics, Rectangle2D dataArea) { |
| | | if (dataArea == null || dataArea.getWidth() <= 0 || dataArea.getHeight() <= 0) return; |
| | | graphics.setColor(AXIS_COLOR); |
| | | graphics.setStroke(new BasicStroke(2.2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | int x = (int) Math.round(dataArea.getX()); |
| | | int y = (int) Math.round(dataArea.getY()); |
| | | int right = (int) Math.round(dataArea.getMaxX()); |
| | | int bottom = (int) Math.round(dataArea.getMaxY()); |
| | | graphics.drawLine(x, y, x, bottom); |
| | | graphics.drawLine(x, bottom, right, bottom); |
| | | drawArrowHead(graphics, right, bottom, 1, 0); |
| | | drawArrowHead(graphics, x, y, 0, -1); |
| | | } |
| | | |
| | | private void drawArrowHead(Graphics2D graphics, int x, int y, int dx, int dy) { |
| | | int length = 11; |
| | | int width = 5; |
| | | if (dx != 0) { |
| | | graphics.drawLine(x, y, x - length, y - width); |
| | | graphics.drawLine(x, y, x - length, y + width); |
| | | } else { |
| | | graphics.drawLine(x, y, x - width, y + length); |
| | | graphics.drawLine(x, y, x + width, y + length); |
| | | } |
| | | } |
| | | |
| | | private void drawUnitLabel(Graphics2D graphics, String unitLabel, Rectangle2D dataArea) { |
| | | if (unitLabel == null || unitLabel.isEmpty()) return; |
| | | graphics.setFont(LABEL_FONT); |
| | | graphics.setColor(MUTED_TEXT_COLOR); |
| | | java.awt.FontMetrics metrics = graphics.getFontMetrics(); |
| | | int centerX = (int) Math.round(dataArea.getX()); |
| | | int x = Math.max(4, centerX - metrics.stringWidth(unitLabel) / 2); |
| | | int y = (int) Math.round(Math.max(metrics.getAscent() + 2.0, dataArea.getY() - 22.0)); |
| | | graphics.drawString(unitLabel, x, y); |
| | | } |
| | | |
| | | private void drawUnitLabelTopRight(Graphics2D graphics, String unitLabel, |
| | | Rectangle2D dataArea) { |
| | | if (unitLabel == null || unitLabel.isEmpty() || dataArea == null) return; |
| | | graphics.setFont(LABEL_FONT); |
| | | graphics.setColor(MUTED_TEXT_COLOR); |
| | | FontMetrics metrics = graphics.getFontMetrics(); |
| | | int x = (int) Math.round(dataArea.getMaxX() - metrics.stringWidth(unitLabel)); |
| | | int y = (int) Math.round(Math.max(metrics.getAscent() + 2.0, dataArea.getY() - 22.0)); |
| | | graphics.drawString(unitLabel, x, y); |
| | | } |
| | | |
| | | private void reserveUnitLabelBand(JFreeChart chart, boolean axisChart, String unitLabel) { |
| | | if (!axisChart || unitLabel == null || unitLabel.isEmpty()) return; |
| | | Plot plot = chart.getPlot(); |
| | | RectangleInsets insets = plot.getInsets(); |
| | | plot.setInsets(new RectangleInsets(Math.max(insets.getTop(), UNIT_LABEL_BAND), |
| | | insets.getLeft(), insets.getBottom(), insets.getRight())); |
| | | } |
| | | |
| | | private boolean isHorizontalStackedChart(JFreeChart chart) { |
| | | return chart.getPlot() instanceof CategoryPlot |
| | | && PlotOrientation.HORIZONTAL.equals( |
| | | ((CategoryPlot) chart.getPlot()).getOrientation()); |
| | | } |
| | | |
| | | private void drawDateLabel(Graphics2D graphics, Rectangle2D dataArea) { |
| | | if (dataArea == null || dataArea.getWidth() <= 0) return; |
| | | graphics.setFont(LABEL_FONT); |
| | | graphics.setColor(MUTED_TEXT_COLOR); |
| | | int right = (int) Math.round(dataArea.getMaxX()); |
| | | int bottom = (int) Math.round(dataArea.getMaxY()); |
| | | graphics.drawString("æ¥æ", right - 50, bottom - 28); |
| | | } |
| | | |
| | | private void drawStackedValueAxis(Graphics2D graphics, Rectangle2D dataArea) { |
| | | if (dataArea == null || dataArea.getWidth() <= 0 || dataArea.getHeight() <= 0) return; |
| | | graphics.setColor(AXIS_COLOR); |
| | | graphics.setStroke(new BasicStroke(2.2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | int left = (int) Math.round(dataArea.getX()); |
| | | int right = (int) Math.round(dataArea.getMaxX()); |
| | | int bottom = (int) Math.round(dataArea.getMaxY()); |
| | | graphics.drawLine(left, bottom, right, bottom); |
| | | } |
| | | |
| | | private void drawStackedBarLabels(Graphics2D graphics, BufferedImage image, JFreeChart chart, |
| | | Rectangle2D dataArea, boolean percentAxis) { |
| | | if (!(chart.getPlot() instanceof CategoryPlot) || dataArea == null |
| | | || dataArea.getWidth() <= 0 || dataArea.getHeight() <= 0) { |
| | | return; |
| | | } |
| | | CategoryPlot plot = (CategoryPlot) chart.getPlot(); |
| | | CategoryDataset dataset = plot.getDataset(); |
| | | CategoryAxis domainAxis = plot.getDomainAxis(); |
| | | ValueAxis rangeAxis = plot.getRangeAxis(); |
| | | if (dataset == null || domainAxis == null || rangeAxis == null) return; |
| | | |
| | | graphics.setFont(STACK_LABEL_FONT); |
| | | FontMetrics metrics = graphics.getFontMetrics(); |
| | | int columns = dataset.getColumnCount(); |
| | | int rows = dataset.getRowCount(); |
| | | boolean horizontal = PlotOrientation.HORIZONTAL.equals(plot.getOrientation()); |
| | | List<List<StackCallout>> callouts = new ArrayList<>(); |
| | | for (int column = 0; column < columns; column++) callouts.add(new ArrayList<>()); |
| | | List<HorizontalStackCallout> horizontalCallouts = new ArrayList<>(); |
| | | List<Rectangle2D> occupied = new ArrayList<>(); |
| | | for (int column = 0; column < columns; column++) { |
| | | double cumulative = 0.0; |
| | | for (int row = 0; row < rows; row++) { |
| | | Number number = dataset.getValue(row, column); |
| | | if (number == null) continue; |
| | | double value = number.doubleValue(); |
| | | if (value == 0.0) continue; |
| | | String text = formatStackLabel(value, percentAxis); |
| | | if (horizontal) { |
| | | double startX = rangeAxis.valueToJava2D(cumulative, dataArea, |
| | | plot.getRangeAxisEdge()); |
| | | double endX = rangeAxis.valueToJava2D(cumulative + value, dataArea, |
| | | plot.getRangeAxisEdge()); |
| | | double segmentWidth = Math.abs(endX - startX); |
| | | double centerY = domainAxis.getCategoryMiddle(column, columns, dataArea, |
| | | plot.getDomainAxisEdge()); |
| | | int width = metrics.stringWidth(text); |
| | | if (segmentWidth >= width + 4.0) { |
| | | int textX = (int) Math.round((startX + endX - width) / 2.0); |
| | | int baseline = (int) Math.round(centerY |
| | | + (metrics.getAscent() - metrics.getDescent()) / 2.0); |
| | | graphics.setColor(Color.WHITE); |
| | | graphics.drawString(text, textX, baseline); |
| | | occupied.add(new Rectangle2D.Double(textX, baseline - metrics.getAscent(), |
| | | width, metrics.getHeight())); |
| | | } else { |
| | | horizontalCallouts.add(new HorizontalStackCallout(Math.min(startX, endX), |
| | | Math.max(startX, endX), centerY, text, seriesColor(row))); |
| | | } |
| | | } else { |
| | | double top = rangeAxis.valueToJava2D(cumulative + value, dataArea, |
| | | plot.getRangeAxisEdge()); |
| | | double bottom = rangeAxis.valueToJava2D(cumulative, dataArea, |
| | | plot.getRangeAxisEdge()); |
| | | double segmentHeight = Math.abs(bottom - top); |
| | | double x = domainAxis.getCategoryMiddle(column, columns, dataArea, |
| | | plot.getDomainAxisEdge()); |
| | | if (segmentHeight >= metrics.getHeight() + 4.0) { |
| | | int width = metrics.stringWidth(text); |
| | | int y = (int) Math.round(Math.min(top, bottom) |
| | | + (segmentHeight - metrics.getHeight()) / 2.0 |
| | | + metrics.getAscent()); |
| | | graphics.setColor(Color.WHITE); |
| | | graphics.drawString(text, (int) Math.round(x - width / 2.0), y); |
| | | occupied.add(new Rectangle2D.Double(x - width / 2.0, |
| | | y - metrics.getAscent(), width, metrics.getHeight())); |
| | | } else if (estimatePercentShare(value, percentAxis) <= 0.03 |
| | | || !percentAxis) { |
| | | callouts.get(column).add(new StackCallout(x, |
| | | (top + bottom) / 2.0, text, seriesColor(row))); |
| | | } |
| | | } |
| | | cumulative += value; |
| | | } |
| | | } |
| | | if (horizontal) { |
| | | drawHorizontalStackCallouts(graphics, horizontalCallouts, dataArea, metrics, occupied); |
| | | } else { |
| | | for (int column = 0; column < columns; column++) { |
| | | drawStackCalloutColumn(graphics, image, callouts.get(column), column, columns, |
| | | dataArea, metrics, occupied); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void drawHorizontalStackCallouts(Graphics2D graphics, |
| | | List<HorizontalStackCallout> items, |
| | | Rectangle2D dataArea, FontMetrics metrics, |
| | | List<Rectangle2D> occupied) { |
| | | if (items.isEmpty()) return; |
| | | Map<Long, List<HorizontalStackCallout>> rows = new LinkedHashMap<>(); |
| | | for (HorizontalStackCallout item : items) { |
| | | Long key = Long.valueOf(Math.round(item.getY())); |
| | | List<HorizontalStackCallout> row = rows.get(key); |
| | | if (row == null) { |
| | | row = new ArrayList<>(); |
| | | rows.put(key, row); |
| | | } |
| | | row.add(item); |
| | | } |
| | | for (List<HorizontalStackCallout> row : rows.values()) { |
| | | Collections.sort(row, new Comparator<HorizontalStackCallout>() { |
| | | @Override |
| | | public int compare(HorizontalStackCallout first, HorizontalStackCallout second) { |
| | | return Double.compare(first.getStartX(), second.getStartX()); |
| | | } |
| | | }); |
| | | int widest = 0; |
| | | double columnX = 0.0; |
| | | for (HorizontalStackCallout item : row) { |
| | | widest = Math.max(widest, metrics.stringWidth(item.getText())); |
| | | columnX = Math.max(columnX, item.getEndX() + 8.0); |
| | | } |
| | | // Keep the label column inside the reserved right margin. |
| | | columnX = Math.min(columnX, dataArea.getMaxX() + RIGHT_LABEL_BAND - widest); |
| | | int height = metrics.getHeight(); |
| | | double step = height + 3.0; |
| | | double centerY = row.get(0).getY(); |
| | | double top = centerY - (step * row.size()) / 2.0 + (step - height) / 2.0; |
| | | for (int i = 0; i < row.size(); i++) { |
| | | HorizontalStackCallout item = row.get(i); |
| | | int width = metrics.stringWidth(item.getText()); |
| | | double x = columnX; |
| | | double y = top + i * step; |
| | | graphics.setColor(item.getColor()); |
| | | graphics.setStroke(new BasicStroke(1.4f, BasicStroke.CAP_ROUND, |
| | | BasicStroke.JOIN_ROUND)); |
| | | graphics.drawLine((int) Math.round(item.getEndX()), (int) Math.round(item.getY()), |
| | | (int) Math.round(x - 3.0), (int) Math.round(y + height / 2.0)); |
| | | drawLabelWithHalo(graphics, item.getText(), |
| | | new Rectangle2D.Double(x, y, width, height), metrics, item.getColor()); |
| | | occupied.add(new Rectangle2D.Double(x - 2.0, y - 2.0, |
| | | width + 4.0, height + 4.0)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void drawStackCalloutColumn(Graphics2D graphics, BufferedImage image, |
| | | List<StackCallout> items, int column, int columnCount, |
| | | Rectangle2D dataArea, FontMetrics metrics, |
| | | List<Rectangle2D> occupied) { |
| | | if (items.isEmpty()) return; |
| | | Collections.sort(items, new Comparator<StackCallout>() { |
| | | @Override |
| | | public int compare(StackCallout first, StackCallout second) { |
| | | return Double.compare(first.getY(), second.getY()); |
| | | } |
| | | }); |
| | | double categoryWidth = dataArea.getWidth() / Math.max(1, columnCount); |
| | | double barHalf = Math.min(80.0, Math.max(18.0, categoryWidth * 0.30)); |
| | | double spacing = metrics.getHeight() + 4.0; |
| | | double totalHeight = (items.size() - 1) * spacing; |
| | | double averageY = 0.0; |
| | | for (StackCallout item : items) averageY += item.getY(); |
| | | averageY /= items.size(); |
| | | double startY = averageY - totalHeight / 2.0 - metrics.getHeight() / 2.0; |
| | | double maxStartY = dataArea.getMaxY() - metrics.getHeight() - 3.0 - totalHeight; |
| | | startY = Math.max(dataArea.getMinY() + 3.0, Math.min(startY, maxStartY)); |
| | | |
| | | double centerX = items.get(0).getX(); |
| | | double maxWidth = 0.0; |
| | | for (StackCallout item : items) { |
| | | maxWidth = Math.max(maxWidth, metrics.stringWidth(item.getText())); |
| | | } |
| | | double rightLeft = centerX + barHalf + 14.0; |
| | | double leftLeft = centerX - barHalf - 14.0 - maxWidth; |
| | | double rightPenalty = stackLanePenalty(rightLeft, startY, items, metrics, |
| | | occupied, dataArea); |
| | | double leftPenalty = stackLanePenalty(leftLeft, startY, items, metrics, |
| | | occupied, dataArea); |
| | | boolean rightSide = rightPenalty <= leftPenalty; |
| | | double laneLeft = rightSide ? rightLeft : leftLeft; |
| | | |
| | | for (int i = 0; i < items.size(); i++) { |
| | | StackCallout item = items.get(i); |
| | | int width = metrics.stringWidth(item.getText()); |
| | | int height = metrics.getHeight(); |
| | | int y = (int) Math.round(startY + i * spacing); |
| | | int textX = (int) Math.round(rightSide ? laneLeft : laneLeft + maxWidth - width); |
| | | int textY = y + metrics.getAscent(); |
| | | double lineStartX = centerX + (rightSide ? barHalf : -barHalf); |
| | | double lineEndX = rightSide ? laneLeft : laneLeft + maxWidth; |
| | | graphics.setColor(item.getColor()); |
| | | graphics.setStroke(new BasicStroke(1.8f, BasicStroke.CAP_ROUND, |
| | | BasicStroke.JOIN_ROUND)); |
| | | graphics.drawLine((int) Math.round(lineStartX), (int) Math.round(item.getY()), |
| | | (int) Math.round(lineEndX), (int) Math.round(y + height / 2.0)); |
| | | drawLabelWithHalo(graphics, item.getText(), |
| | | new Rectangle2D.Double(textX, y, width, height), metrics, item.getColor()); |
| | | occupied.add(new Rectangle2D.Double(textX - 2.0, y - 2.0, |
| | | width + 4.0, height + 4.0)); |
| | | } |
| | | } |
| | | |
| | | private double stackLanePenalty(double laneLeft, double startY, List<StackCallout> items, |
| | | FontMetrics metrics, List<Rectangle2D> occupied, |
| | | Rectangle2D dataArea) { |
| | | double penalty = 0.0; |
| | | for (int i = 0; i < items.size(); i++) { |
| | | double width = metrics.stringWidth(items.get(i).getText()); |
| | | double height = metrics.getHeight(); |
| | | Rectangle2D rectangle = new Rectangle2D.Double(laneLeft, startY + i * (height + 4.0), |
| | | width, height); |
| | | if (rectangle.getMinX() < dataArea.getMinX() |
| | | || rectangle.getMaxX() > dataArea.getMaxX() |
| | | || rectangle.getMaxY() > dataArea.getMaxY()) { |
| | | penalty += 10000.0; |
| | | } |
| | | penalty += overlapArea(rectangle, occupied) * 100.0; |
| | | } |
| | | return penalty; |
| | | } |
| | | |
| | | private double estimatePercentShare(double value, boolean percentAxis) { |
| | | return percentAxis ? value : value / 1.0; |
| | | } |
| | | |
| | | private String formatStackLabel(double value, boolean percentAxis) { |
| | | if (percentAxis) { |
| | | NumberFormat formatter = NumberFormat.getPercentInstance(); |
| | | formatter.setMaximumFractionDigits(1); |
| | | return formatter.format(value); |
| | | } |
| | | return new DecimalFormat("#,##0").format(value); |
| | | } |
| | | |
| | | private static class HorizontalStackCallout { |
| | | private final double startX; |
| | | private final double endX; |
| | | private final double y; |
| | | private final String text; |
| | | private final Color color; |
| | | |
| | | private HorizontalStackCallout(double startX, double endX, double y, |
| | | String text, Color color) { |
| | | this.startX = startX; |
| | | this.endX = endX; |
| | | this.y = y; |
| | | this.text = text; |
| | | this.color = color; |
| | | } |
| | | |
| | | private double getStartX() { return startX; } |
| | | private double getEndX() { return endX; } |
| | | private double getY() { return y; } |
| | | private String getText() { return text; } |
| | | private Color getColor() { return color; } |
| | | } |
| | | |
| | | private static class StackCallout { |
| | | private final double x; |
| | | private final double y; |
| | | private final String text; |
| | | private final Color color; |
| | | |
| | | private StackCallout(double x, double y, String text, Color color) { |
| | | this.x = x; |
| | | this.y = y; |
| | | this.text = text; |
| | | this.color = color; |
| | | } |
| | | |
| | | private double getX() { return x; } |
| | | private double getY() { return y; } |
| | | private String getText() { return text; } |
| | | private Color getColor() { return color; } |
| | | } |
| | | |
| | | private void drawXYItemLabels(Graphics2D graphics, BufferedImage image, JFreeChart chart, |
| | | Rectangle2D dataArea, boolean percentItemLabels, |
| | | String unitLabel, int valueDecimals) { |
| | | if (!(chart.getPlot() instanceof XYPlot) || dataArea == null |
| | | || dataArea.getWidth() <= 0 || dataArea.getHeight() <= 0) { |
| | | return; |
| | | } |
| | | XYPlot plot = (XYPlot) chart.getXYPlot(); |
| | | XYDataset dataset = plot.getDataset(); |
| | | if (dataset == null) return; |
| | | |
| | | graphics.setFont(ITEM_LABEL_FONT); |
| | | FontMetrics metrics = graphics.getFontMetrics(); |
| | | List<Rectangle2D> placedLabels = new ArrayList<>(); |
| | | if (unitLabel != null && !unitLabel.isEmpty()) { |
| | | placedLabels.add(new Rectangle2D.Double(0.0, 0.0, |
| | | dataArea.getMinX() + 100.0, dataArea.getMinY() + 30.0)); |
| | | } |
| | | int itemCount = 0; |
| | | for (int seriesIndex = 0; seriesIndex < dataset.getSeriesCount(); seriesIndex++) { |
| | | itemCount = Math.max(itemCount, dataset.getItemCount(seriesIndex)); |
| | | } |
| | | |
| | | for (int itemIndex = 0; itemIndex < itemCount; itemIndex++) { |
| | | List<DayLabel> labels = new ArrayList<>(); |
| | | for (int seriesIndex = 0; seriesIndex < dataset.getSeriesCount(); seriesIndex++) { |
| | | if (itemIndex >= dataset.getItemCount(seriesIndex)) continue; |
| | | Number yValue = dataset.getY(seriesIndex, itemIndex); |
| | | Point2D point = itemPoint(plot, dataset, seriesIndex, itemIndex, dataArea); |
| | | if (yValue == null || point == null) continue; |
| | | labels.add(new DayLabel(seriesIndex, point, |
| | | formatLineLabel(yValue, percentItemLabels, valueDecimals))); |
| | | } |
| | | // A day's labels are stacked in the same top-to-bottom order as its data |
| | | // points, so a label never looks attached to a neighbouring line. |
| | | Collections.sort(labels, new Comparator<DayLabel>() { |
| | | @Override |
| | | public int compare(DayLabel first, DayLabel second) { |
| | | return Double.compare(first.getPoint().getY(), second.getPoint().getY()); |
| | | } |
| | | }); |
| | | List<List<LineLabelCandidate>> candidateSets = new ArrayList<>(); |
| | | for (DayLabel label : labels) { |
| | | List<LineLabelCandidate> candidates = lineLabelCandidates( |
| | | label, metrics, dataArea); |
| | | if (candidates.isEmpty()) { |
| | | candidateSets.clear(); |
| | | break; |
| | | } |
| | | candidateSets.add(candidates); |
| | | } |
| | | if (candidateSets.isEmpty()) continue; |
| | | |
| | | // Dynamic programming keeps each label near its own point while preserving |
| | | // the same top-to-bottom order as the data points for that date. |
| | | List<int[]> backPointers = new ArrayList<>(); |
| | | List<LineLabelCandidate> firstCandidates = candidateSets.get(0); |
| | | double[] previousCosts = new double[firstCandidates.size()]; |
| | | for (int i = 0; i < firstCandidates.size(); i++) { |
| | | previousCosts[i] = lineLabelCost(firstCandidates.get(i), labels.get(0), |
| | | labels, placedLabels); |
| | | } |
| | | |
| | | for (int labelIndex = 1; labelIndex < candidateSets.size(); labelIndex++) { |
| | | List<LineLabelCandidate> previousCandidates = candidateSets.get(labelIndex - 1); |
| | | List<LineLabelCandidate> currentCandidates = candidateSets.get(labelIndex); |
| | | double[] currentCosts = new double[currentCandidates.size()]; |
| | | Arrays.fill(currentCosts, Double.MAX_VALUE); |
| | | int[] back = new int[currentCandidates.size()]; |
| | | Arrays.fill(back, -1); |
| | | for (int currentIndex = 0; currentIndex < currentCandidates.size(); currentIndex++) { |
| | | LineLabelCandidate current = currentCandidates.get(currentIndex); |
| | | double currentCost = lineLabelCost(current, labels.get(labelIndex), |
| | | labels, placedLabels); |
| | | for (int previousIndex = 0; previousIndex < previousCandidates.size(); previousIndex++) { |
| | | if (previousCosts[previousIndex] == Double.MAX_VALUE) continue; |
| | | LineLabelCandidate previous = previousCandidates.get(previousIndex); |
| | | if (previous.getRectangle().getCenterY() |
| | | > current.getRectangle().getCenterY() + 0.1) { |
| | | continue; |
| | | } |
| | | double overlapPenalty = overlapArea(previous.getRectangle(), |
| | | Collections.singletonList(current.getRectangle())) * 1000000.0; |
| | | double cost = previousCosts[previousIndex] + currentCost + overlapPenalty; |
| | | if (cost < currentCosts[currentIndex]) { |
| | | currentCosts[currentIndex] = cost; |
| | | back[currentIndex] = previousIndex; |
| | | } |
| | | } |
| | | } |
| | | previousCosts = currentCosts; |
| | | backPointers.add(back); |
| | | } |
| | | |
| | | int bestLastIndex = -1; |
| | | double bestCost = Double.MAX_VALUE; |
| | | for (int i = 0; i < previousCosts.length; i++) { |
| | | if (previousCosts[i] < bestCost) { |
| | | bestCost = previousCosts[i]; |
| | | bestLastIndex = i; |
| | | } |
| | | } |
| | | if (bestLastIndex < 0) continue; |
| | | |
| | | int[] chosenIndexes = new int[candidateSets.size()]; |
| | | chosenIndexes[chosenIndexes.length - 1] = bestLastIndex; |
| | | for (int labelIndex = chosenIndexes.length - 2; labelIndex >= 0; labelIndex--) { |
| | | int nextIndex = chosenIndexes[labelIndex + 1]; |
| | | chosenIndexes[labelIndex] = backPointers.get(labelIndex)[nextIndex]; |
| | | } |
| | | for (int labelIndex = 0; labelIndex < chosenIndexes.length; labelIndex++) { |
| | | DayLabel label = labels.get(labelIndex); |
| | | Rectangle2D chosen = candidateSets.get(labelIndex) |
| | | .get(chosenIndexes[labelIndex]).getRectangle(); |
| | | drawLabelWithHalo(graphics, label.getText(), chosen, metrics, |
| | | seriesColor(label.getSeriesIndex())); |
| | | placedLabels.add(new Rectangle2D.Double(chosen.getX() - 4.0, chosen.getY() - 4.0, |
| | | chosen.getWidth() + 8.0, chosen.getHeight() + 8.0)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static class LineLabelCandidate { |
| | | private final Rectangle2D rectangle; |
| | | |
| | | private LineLabelCandidate(Rectangle2D rectangle) { |
| | | this.rectangle = rectangle; |
| | | } |
| | | |
| | | private Rectangle2D getRectangle() { return rectangle; } |
| | | } |
| | | |
| | | private static class DayLabel { |
| | | private final int seriesIndex; |
| | | private final Point2D point; |
| | | private final String text; |
| | | |
| | | private DayLabel(int seriesIndex, Point2D point, String text) { |
| | | this.seriesIndex = seriesIndex; |
| | | this.point = point; |
| | | this.text = text; |
| | | } |
| | | |
| | | private int getSeriesIndex() { return seriesIndex; } |
| | | private Point2D getPoint() { return point; } |
| | | private String getText() { return text; } |
| | | } |
| | | |
| | | private Point2D itemPoint(XYPlot plot, XYDataset dataset, int seriesIndex, |
| | | int itemIndex, Rectangle2D dataArea) { |
| | | Number yValue = dataset.getY(seriesIndex, itemIndex); |
| | | if (yValue == null || Double.isNaN(yValue.doubleValue()) |
| | | || Double.isInfinite(yValue.doubleValue())) { |
| | | return null; |
| | | } |
| | | double x = plot.getDomainAxis().valueToJava2D(dataset.getXValue(seriesIndex, itemIndex), |
| | | dataArea, plot.getDomainAxisEdge()); |
| | | double y = plot.getRangeAxis().valueToJava2D(yValue.doubleValue(), |
| | | dataArea, plot.getRangeAxisEdge()); |
| | | return new Point2D.Double(x, y); |
| | | } |
| | | |
| | | private List<LineLabelCandidate> lineLabelCandidates(DayLabel label, FontMetrics metrics, |
| | | Rectangle2D dataArea) { |
| | | List<LineLabelCandidate> result = new ArrayList<>(); |
| | | int width = metrics.stringWidth(label.getText()); |
| | | int height = metrics.getHeight(); |
| | | double x = label.getPoint().getX(); |
| | | double y = label.getPoint().getY(); |
| | | double horizontalOffset = ITEM_LABEL_GAP + width / 2.0; |
| | | double farHorizontalOffset = horizontalOffset + 18.0; |
| | | double verticalOffset = ITEM_LABEL_GAP + height / 2.0; |
| | | double farVerticalOffset = verticalOffset + height + 6.0; |
| | | addLabelCandidate(result, x, y, width, height, 0.0, -verticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, 0.0, verticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, horizontalOffset, 0.0, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, -horizontalOffset, 0.0, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, farHorizontalOffset, 0.0, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, -farHorizontalOffset, 0.0, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | horizontalOffset, -verticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | horizontalOffset, verticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | -horizontalOffset, -verticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | -horizontalOffset, verticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | farHorizontalOffset, -verticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | farHorizontalOffset, verticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | -farHorizontalOffset, -verticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | -farHorizontalOffset, verticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, 0.0, -farVerticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, 0.0, farVerticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | horizontalOffset, -farVerticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | horizontalOffset, farVerticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | -horizontalOffset, -farVerticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | -horizontalOffset, farVerticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | farHorizontalOffset, -farVerticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | farHorizontalOffset, farVerticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | -farHorizontalOffset, -farVerticalOffset, dataArea); |
| | | addLabelCandidate(result, x, y, width, height, |
| | | -farHorizontalOffset, farVerticalOffset, dataArea); |
| | | return result; |
| | | } |
| | | |
| | | private void addLabelCandidate(List<LineLabelCandidate> result, double x, double y, |
| | | int width, int height, double dx, double dy, |
| | | Rectangle2D dataArea) { |
| | | Rectangle2D candidate = boundToDataArea(new Rectangle2D.Double( |
| | | x + dx - width / 2.0, y + dy - height / 2.0, width, height), dataArea); |
| | | if (candidate == null) return; |
| | | for (LineLabelCandidate existing : result) { |
| | | if (Math.abs(existing.getRectangle().getX() - candidate.getX()) < 0.5 |
| | | && Math.abs(existing.getRectangle().getY() - candidate.getY()) < 0.5) { |
| | | return; |
| | | } |
| | | } |
| | | result.add(new LineLabelCandidate(candidate)); |
| | | } |
| | | |
| | | private double lineLabelCost(LineLabelCandidate candidate, DayLabel label, |
| | | List<DayLabel> sameDayLabels, |
| | | List<Rectangle2D> placedLabels) { |
| | | Rectangle2D rectangle = candidate.getRectangle(); |
| | | double dx = rectangle.getCenterX() - label.getPoint().getX(); |
| | | double dy = rectangle.getCenterY() - label.getPoint().getY(); |
| | | double ownDistance = Math.sqrt(dx * dx + dy * dy); |
| | | double cost = ownDistance * 10.0 + overlapArea(rectangle, placedLabels) * 10000.0; |
| | | Rectangle2D ownMarker = new Rectangle2D.Double(label.getPoint().getX() - 9.0, |
| | | label.getPoint().getY() - 9.0, 18.0, 18.0); |
| | | if (rectangle.intersects(ownMarker)) cost += 3000.0; |
| | | for (DayLabel other : sameDayLabels) { |
| | | if (other == label) continue; |
| | | double otherDx = rectangle.getCenterX() - other.getPoint().getX(); |
| | | double otherDy = rectangle.getCenterY() - other.getPoint().getY(); |
| | | double otherDistance = Math.sqrt(otherDx * otherDx + otherDy * otherDy); |
| | | if (otherDistance < ownDistance - 1.0) { |
| | | cost += (ownDistance - otherDistance) * 5000.0; |
| | | } |
| | | Rectangle2D otherMarker = new Rectangle2D.Double(other.getPoint().getX() - 9.0, |
| | | other.getPoint().getY() - 9.0, 18.0, 18.0); |
| | | if (rectangle.intersects(otherMarker)) cost += 2000.0; |
| | | } |
| | | return cost; |
| | | } |
| | | |
| | | private Rectangle2D boundToDataArea(Rectangle2D rectangle, Rectangle2D dataArea) { |
| | | double minX = dataArea.getMinX() + 16.0; |
| | | double minY = dataArea.getMinY() + 2.0; |
| | | double maxX = dataArea.getMaxX() - rectangle.getWidth() - 2.0; |
| | | double maxY = dataArea.getMaxY() - rectangle.getHeight() - 2.0; |
| | | if (maxX < minX || maxY < minY) return null; |
| | | double x = Math.max(minX, Math.min(rectangle.getX(), maxX)); |
| | | double y = Math.max(minY, Math.min(rectangle.getY(), maxY)); |
| | | return new Rectangle2D.Double(x, y, rectangle.getWidth(), rectangle.getHeight()); |
| | | } |
| | | |
| | | private double overlapArea(Rectangle2D rectangle, List<Rectangle2D> occupied) { |
| | | double total = 0.0; |
| | | for (Rectangle2D other : occupied) { |
| | | double width = Math.min(rectangle.getMaxX(), other.getMaxX()) |
| | | - Math.max(rectangle.getMinX(), other.getMinX()); |
| | | double height = Math.min(rectangle.getMaxY(), other.getMaxY()) |
| | | - Math.max(rectangle.getMinY(), other.getMinY()); |
| | | if (width > 0.0 && height > 0.0) total += width * height; |
| | | } |
| | | return total; |
| | | } |
| | | |
| | | static int scaleLineChartHeight(int height) { |
| | | return (int) Math.round(height * LINE_CHART_HEIGHT_SCALE); |
| | | } |
| | | |
| | | static long scaleLineChartHeight(long height) { |
| | | return Math.round(height * LINE_CHART_HEIGHT_SCALE); |
| | | } |
| | | |
| | | static BigDecimal displayValue(BigDecimal value, boolean percentItemLabels, int valueDecimals) { |
| | | if (value == null) return null; |
| | | int scale = percentItemLabels ? PERCENT_ITEM_LABEL_DECIMALS + 2 : valueDecimals; |
| | | return value.setScale(scale, RoundingMode.HALF_UP); |
| | | } |
| | | |
| | | private String formatLineLabel(Number value, boolean percentItemLabels, int valueDecimals) { |
| | | if (percentItemLabels) { |
| | | NumberFormat formatter = NumberFormat.getPercentInstance(); |
| | | formatter.setMaximumFractionDigits(PERCENT_ITEM_LABEL_DECIMALS); |
| | | return formatter.format(value.doubleValue()); |
| | | } |
| | | DecimalFormat formatter = new DecimalFormat("#,##0"); |
| | | formatter.setMaximumFractionDigits(valueDecimals); |
| | | return formatter.format(value.doubleValue()); |
| | | } |
| | | |
| | | private void drawLabelWithHalo(Graphics2D graphics, String text, Rectangle2D rectangle, |
| | | FontMetrics metrics, Color color) { |
| | | int x = (int) Math.round(rectangle.getX()); |
| | | int y = (int) Math.round(rectangle.getY() + metrics.getAscent()); |
| | | // A stroked white outline keeps the digit legible even when a line runs |
| | | // underneath it, which lets labels stay right next to their own point. |
| | | java.awt.font.TextLayout layout = new java.awt.font.TextLayout( |
| | | text, graphics.getFont(), graphics.getFontRenderContext()); |
| | | java.awt.Shape outline = layout.getOutline( |
| | | java.awt.geom.AffineTransform.getTranslateInstance(x, y)); |
| | | graphics.setColor(Color.WHITE); |
| | | graphics.setStroke(new BasicStroke(HALO_OUTLINE_WIDTH, |
| | | BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); |
| | | graphics.draw(outline); |
| | | graphics.setColor(color); |
| | | graphics.fill(outline); |
| | | } |
| | | |
| | | public static final List<String> DAY_LABELS = Arrays.asList( |
| | | "10æ1æ¥", "10æ2æ¥", "10æ3æ¥", "10æ4æ¥", |
| | | "10æ5æ¥", "10æ6æ¥", "10æ7æ¥", "10æ8æ¥"); |
| | | |
| | | public static List<ChartSeries> emptySeries(int size) { |
| | | List<ChartSeries> result = new ArrayList<>(); |
| | | for (int i = 0; i < size; i++) { |
| | | result.add(new ChartSeries()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | @Data |
| | | public static class ChartSeries { |
| | | private String name; |
| | | private List<BigDecimal> values = new ArrayList<>(); |
| | | } |
| | | |
| | | @Data |
| | | public static class ChartCategory { |
| | | private String name; |
| | | private BigDecimal value; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.trafficaudit.holiday.HolidayConstants; |
| | | import com.trafficaudit.holiday.HolidayNumberUtils; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.service.HolidayStatService.DailyMetrics; |
| | | import com.trafficaudit.observation.dto.ObservationDailyOperation; |
| | | import com.trafficaudit.observation.mapper.ObservationStationFlowMapper; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.InputStream; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.function.Function; |
| | | |
| | | @Service |
| | | public class HolidayTrendService { |
| | | |
| | | private static final BigDecimal HUNDRED = new BigDecimal("100"); |
| | | |
| | | @Value("${holiday.trend-template:docs/ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾.docx}") |
| | | private String trendTemplatePath; |
| | | |
| | | @Resource |
| | | private HolidayStatService statService; |
| | | @Resource |
| | | private HolidayConfigService configService; |
| | | @Resource |
| | | private HolidayTrendChartService chartService; |
| | | @Resource |
| | | private HolidayTrendWordService wordService; |
| | | @Resource |
| | | private ObservationStationFlowMapper observationStationFlowMapper; |
| | | |
| | | public byte[] generate(int year, String holidayType) { |
| | | String holidayLabel = HolidayConstants.HOLIDAY_TYPE_LABELS.get(holidayType); |
| | | if (holidayLabel == null) { |
| | | throw new IllegalArgumentException("䏿¯æçèåæ¥ç±»å"); |
| | | } |
| | | List<Integer> years = Arrays.asList(year, year - 1, year - 2); |
| | | Map<Integer, YearData> yearData = new LinkedHashMap<>(); |
| | | for (Integer item : years) { |
| | | yearData.put(item, buildYearData(item, holidayType)); |
| | | } |
| | | YearData selected = yearData.get(year); |
| | | if (selected == null || !selected.hasAnyData()) { |
| | | throw new IllegalArgumentException("æé年份没æè¶³å¤çèµ°å¿å¾æ°æ®"); |
| | | } |
| | | |
| | | String prefix = "â" + holidayLabel + "âåæ"; |
| | | List<HolidayTrendWordService.DocumentBlock> blocks = new ArrayList<>(); |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("ä¸ãæ»ä½æ
åµ")); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | prefix + "å
¨çè·¨åºåäººåæµå¨éæ
åµ", |
| | | chartService.lineChart("ä¸äººæ¬¡", |
| | | series(years, yearData, item -> item.totalDaily), false, 1100, 715), |
| | | lineImageSize(5268595L, 3424555L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåæç´¯è®¡è·¨åºåäººåæµå¨éèµ°å¿", |
| | | chartService.lineChart("ä¸äººæ¬¡", |
| | | series(years, yearData, item -> item.totalCumulative), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåæå交éè¿è¾æ¹å¼å®¢è¿éå æ¯æ
åµ", |
| | | chartService.pieChart("人次", selected.modalShare(), 1000, 607), true, |
| | | new HolidayTrendWordService.ImageSize(5106670L, 3097530L))); |
| | | |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("äºãåç§è¿è¾æ¹å¼å®¢æµéèµ°å¿")); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåæèªé©¾åºè¡éæ
åµ", |
| | | chartService.lineChart("ä¸äººæ¬¡", |
| | | series(years, yearData, item -> item.selfDrivingPersonFlow), false, 1100, 522), |
| | | lineImageSize(5232400L, 2484120L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåæé路客è¿éæ
åµ", |
| | | chartService.lineChart("ä¸äººæ¬¡", |
| | | series(years, yearData, item -> item.rail), false, 1100, 445), |
| | | lineImageSize(5264785L, 2129155L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåææ°´è·¯å®¢è¿éæ
åµ", |
| | | chartService.lineChart("ä¸äººæ¬¡", |
| | | series(years, yearData, item -> item.water), false, 1100, 433), |
| | | lineImageSize(5274310L, 2075815L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåææ°èªå®¢è¿éæ
åµ", |
| | | chartService.lineChart("ä¸äººæ¬¡", |
| | | series(years, yearData, item -> item.civilAviation), false, 1100, 521), |
| | | lineImageSize(5271135L, 2496185L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåæé路客è¿éæ
åµ", |
| | | chartService.lineChart("ä¸äººæ¬¡", |
| | | series(years, yearData, item -> item.road), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("ä¸ã车æµéèµ°å¿")); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåæé«éå
¬è·¯è½¦æµéæ
åµ", |
| | | chartService.lineChart("ä¸è¾æ¬¡", |
| | | series(years, yearData, item -> item.expressway), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåææ®éå½çé车æµéæ
åµ", |
| | | chartService.lineChart("ä¸è¾æ¬¡", |
| | | series(years, yearData, item -> item.nationalProvincial), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåæå
¨çè½¦æµæ»éæ
åµ", |
| | | chartService.lineChart("ä¸è¾æ¬¡", |
| | | series(years, yearData, item -> item.totalVehicleFlow), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåæå
¬è·¯åºè¡ç»æ", |
| | | chartService.stackedBarChart("ä¸äººæ¬¡", |
| | | Arrays.asList((year - 2) + "å¹´", (year - 1) + "å¹´", year + "å¹´"), |
| | | roadStructureSeries(years, yearData), false, 1000, 607), true, |
| | | new HolidayTrendWordService.ImageSize(5106670L, 3097530L))); |
| | | |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("éå½ä¸ æ¯æ¥å®¢æµææåå")); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåææ¯æ¥å®¢æµææåå", |
| | | chartService.stackedBarChart("å æ¯", |
| | | HolidayTrendChartService.DAY_LABELS, |
| | | dailyShareSeries(selected), true, 1000, 607), true, |
| | | new HolidayTrendWordService.ImageSize(5106670L, 3097530L))); |
| | | |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("éå½äº æ®éå½çéè§æµç«è·¯ç½è¿è¡")); |
| | | ObservationData observation = observationData(year, holidayType); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | prefix + "æ®éå½çéè§æµç«å¹³åæºå¨è½¦é度", |
| | | chartService.lineChart("å
¬é/å°æ¶", |
| | | Collections.singletonList(series("å¹³åé度", observation.speed)), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | prefix + "æ®éå½çéè§æµç«å¹³åæ¥æ¤åº¦", |
| | | chartService.lineChart("æ¥æ¤åº¦", |
| | | Collections.singletonList(series("平忥æ¤åº¦", observation.congestion)), false, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | |
| | | File template = resolveTemplateFile(); |
| | | if (!template.exists() || !template.isFile()) { |
| | | throw new IllegalArgumentException("èµ°å¿å¾æ¨¡æ¿æä»¶ä¸åå¨: " + template.getAbsolutePath()); |
| | | } |
| | | try (InputStream input = new FileInputStream(template)) { |
| | | return wordService.generate(readAll(input), year, blocks); |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("èµ°å¿å¾çæå¤±è´¥: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | private YearData buildYearData(int year, String holidayType) { |
| | | List<DailyMetrics> metrics = new ArrayList<>(); |
| | | for (int day = 1; day <= HolidayTrendChartService.DAY_LABELS.size(); day++) { |
| | | metrics.add(statService.dailyMetrics(year, holidayType, day)); |
| | | } |
| | | YearData data = new YearData(); |
| | | data.year = year; |
| | | data.rail = values(metrics, DailyMetrics::getRail); |
| | | data.civilAviation = values(metrics, DailyMetrics::getCivilAviation); |
| | | data.road = values(metrics, DailyMetrics::getRoad); |
| | | data.water = values(metrics, DailyMetrics::getWater); |
| | | data.expressway = values(metrics, DailyMetrics::getExpressway); |
| | | data.nationalProvincial = values(metrics, DailyMetrics::getNationalProvincial); |
| | | data.selfDrivingPersonFlow = values(metrics, DailyMetrics::getNonBusinessPersonFlow); |
| | | data.selfDrivingVehicleFlow = combine(metrics, |
| | | DailyMetrics::getExpressway, DailyMetrics::getNationalProvincial); |
| | | data.totalVehicleFlow = combine(metrics, |
| | | DailyMetrics::getExpressway, DailyMetrics::getNationalProvincial); |
| | | data.totalDaily = values(metrics, DailyMetrics::getTotalPersonFlow); |
| | | data.totalCumulative = cumulative(data.totalDaily); |
| | | data.railCumulative = cumulative(data.rail); |
| | | data.civilAviationCumulative = cumulative(data.civilAviation); |
| | | data.roadCumulative = cumulative(data.road); |
| | | data.waterCumulative = cumulative(data.water); |
| | | data.selfDrivingPersonFlowCumulative = cumulative(data.selfDrivingPersonFlow); |
| | | return data; |
| | | } |
| | | |
| | | private List<HolidayTrendChartService.ChartSeries> series( |
| | | List<Integer> years, Map<Integer, YearData> data, |
| | | Function<YearData, List<BigDecimal>> selector) { |
| | | List<HolidayTrendChartService.ChartSeries> result = new ArrayList<>(); |
| | | for (Integer year : years) { |
| | | YearData item = data.get(year); |
| | | result.add(series(year + "å¹´", selector.apply(item))); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<HolidayTrendChartService.ChartSeries> yoySeries( |
| | | String label, List<Integer> years, Map<Integer, YearData> data, |
| | | Function<YearData, List<BigDecimal>> selector) { |
| | | List<HolidayTrendChartService.ChartSeries> result = new ArrayList<>(); |
| | | YearData selected = data.get(years.get(0)); |
| | | List<BigDecimal> selectedValues = selector.apply(selected); |
| | | for (int i = 1; i < years.size(); i++) { |
| | | YearData comparison = data.get(years.get(i)); |
| | | result.add(series(label + "忝" + comparison.year, |
| | | percentageChanges(selectedValues, selector.apply(comparison)))); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<HolidayTrendChartService.ChartSeries> dailyShareSeries(YearData data) { |
| | | List<HolidayTrendChartService.ChartSeries> result = new ArrayList<>(); |
| | | result.add(series("èªé©¾", share(data.selfDrivingPersonFlow, data))); |
| | | result.add(series("éè·¯", share(data.road, data))); |
| | | result.add(series("éè·¯", share(data.rail, data))); |
| | | result.add(series("æ°èª", share(data.civilAviation, data))); |
| | | result.add(series("æ°´è·¯", share(data.water, data))); |
| | | return result; |
| | | } |
| | | |
| | | private List<BigDecimal> share(List<BigDecimal> values, YearData data) { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) { |
| | | BigDecimal total = dayTotal(data, i); |
| | | BigDecimal value = valueAt(values, i); |
| | | result.add(total == null || total.signum() == 0 |
| | | ? null : HolidayNumberUtils.divide(value == null ? BigDecimal.ZERO : value, total)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private BigDecimal dayTotal(YearData data, int index) { |
| | | BigDecimal total = BigDecimal.ZERO; |
| | | boolean hasValue = false; |
| | | for (List<BigDecimal> values : Arrays.asList( |
| | | data.selfDrivingPersonFlow, data.road, data.rail, |
| | | data.civilAviation, data.water)) { |
| | | BigDecimal value = valueAt(values, index); |
| | | if (value != null) { |
| | | total = total.add(value); |
| | | hasValue = true; |
| | | } |
| | | } |
| | | return hasValue ? HolidayNumberUtils.normalize(total) : null; |
| | | } |
| | | |
| | | private List<HolidayTrendChartService.ChartSeries> roadStructureSeries( |
| | | List<Integer> years, Map<Integer, YearData> data) { |
| | | List<BigDecimal> road = new ArrayList<>(); |
| | | List<BigDecimal> selfDriving = new ArrayList<>(); |
| | | for (int i = years.size() - 1; i >= 0; i--) { |
| | | YearData item = data.get(years.get(i)); |
| | | road.add(sum(item.road)); |
| | | selfDriving.add(sum(item.selfDrivingPersonFlow)); |
| | | } |
| | | return Arrays.asList( |
| | | series("èªé©¾åºè¡é", selfDriving), |
| | | series("é路客è¿é", road)); |
| | | } |
| | | |
| | | private ObservationData observationData(int year, String holidayType) { |
| | | List<HolidayCalendar> days = configService.findHolidayDays(year, holidayType); |
| | | if (days.isEmpty()) { |
| | | return new ObservationData(nulls(), nulls()); |
| | | } |
| | | LocalDate start = days.get(0).getHolidayDate(); |
| | | LocalDate end = start.plusDays(HolidayTrendChartService.DAY_LABELS.size() - 1L); |
| | | List<ObservationDailyOperation> rows = observationStationFlowMapper.selectDailyOperations(start, end); |
| | | Map<LocalDate, ObservationDailyOperation> byDate = new LinkedHashMap<>(); |
| | | for (ObservationDailyOperation row : rows) { |
| | | byDate.put(row.getObsDate(), row); |
| | | } |
| | | List<BigDecimal> speed = new ArrayList<>(); |
| | | List<BigDecimal> congestion = new ArrayList<>(); |
| | | for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) { |
| | | ObservationDailyOperation row = byDate.get(start.plusDays(i)); |
| | | speed.add(row == null ? null : row.getAverageSpeed()); |
| | | congestion.add(row == null ? null : row.getAverageCongestion()); |
| | | } |
| | | return new ObservationData(speed, congestion); |
| | | } |
| | | |
| | | private HolidayTrendChartService.ChartSeries series(String name, List<BigDecimal> values) { |
| | | HolidayTrendChartService.ChartSeries result = new HolidayTrendChartService.ChartSeries(); |
| | | result.setName(name); |
| | | result.setValues(values == null ? nulls() : values); |
| | | return result; |
| | | } |
| | | |
| | | private List<BigDecimal> values(List<DailyMetrics> metrics, Function<DailyMetrics, BigDecimal> selector) { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | for (DailyMetrics item : metrics) { |
| | | result.add(item == null ? null : selector.apply(item)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<BigDecimal> combine(List<DailyMetrics> metrics, |
| | | Function<DailyMetrics, BigDecimal> left, |
| | | Function<DailyMetrics, BigDecimal> right) { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | for (DailyMetrics item : metrics) { |
| | | result.add(item == null ? null : HolidayNumberUtils.sumNullable(left.apply(item), right.apply(item))); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<BigDecimal> cumulative(List<BigDecimal> values) { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | BigDecimal total = null; |
| | | for (BigDecimal value : values) { |
| | | if (value != null) total = HolidayNumberUtils.sumNullable(total, value); |
| | | result.add(value == null ? null : total); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<BigDecimal> percentageChanges(List<BigDecimal> current, List<BigDecimal> previous) { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) { |
| | | BigDecimal currentValue = valueAt(current, i); |
| | | BigDecimal previousValue = valueAt(previous, i); |
| | | BigDecimal ratio = HolidayNumberUtils.divide(currentValue, previousValue); |
| | | result.add(ratio == null ? null : HolidayNumberUtils.multiply( |
| | | ratio.subtract(BigDecimal.ONE), HUNDRED)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private BigDecimal sum(List<BigDecimal> values) { |
| | | BigDecimal total = null; |
| | | for (BigDecimal value : values) { |
| | | if (value != null) total = HolidayNumberUtils.sumNullable(total, value); |
| | | } |
| | | return total; |
| | | } |
| | | |
| | | private BigDecimal valueAt(List<BigDecimal> values, int index) { |
| | | return values == null || index >= values.size() ? null : values.get(index); |
| | | } |
| | | |
| | | private List<BigDecimal> nulls() { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) result.add(null); |
| | | return result; |
| | | } |
| | | |
| | | private HolidayTrendWordService.ImageSize lineImageSize(long width, long height) { |
| | | return new HolidayTrendWordService.ImageSize( |
| | | width, HolidayTrendChartService.scaleLineChartHeight(height)); |
| | | } |
| | | |
| | | private File resolveTemplateFile() { |
| | | List<File> candidates = new ArrayList<>(); |
| | | File configured = new File(trendTemplatePath); |
| | | candidates.add(configured); |
| | | if (!configured.isAbsolute()) { |
| | | candidates.add(new File(System.getProperty("user.dir"), trendTemplatePath)); |
| | | candidates.add(new File(System.getProperty("user.dir"), "../" + trendTemplatePath)); |
| | | } |
| | | for (File candidate : candidates) { |
| | | try { |
| | | if (candidate.exists() && candidate.isFile()) return candidate.getCanonicalFile(); |
| | | } catch (Exception ignored) { |
| | | // Keep checking. |
| | | } |
| | | } |
| | | return configured; |
| | | } |
| | | |
| | | private byte[] readAll(InputStream input) throws Exception { |
| | | java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream(); |
| | | byte[] buffer = new byte[8192]; |
| | | int length; |
| | | while ((length = input.read(buffer)) > 0) { |
| | | output.write(buffer, 0, length); |
| | | } |
| | | return output.toByteArray(); |
| | | } |
| | | |
| | | private static class YearData { |
| | | private int year; |
| | | private List<BigDecimal> rail; |
| | | private List<BigDecimal> civilAviation; |
| | | private List<BigDecimal> road; |
| | | private List<BigDecimal> water; |
| | | private List<BigDecimal> expressway; |
| | | private List<BigDecimal> nationalProvincial; |
| | | private List<BigDecimal> selfDrivingPersonFlow; |
| | | private List<BigDecimal> selfDrivingVehicleFlow; |
| | | private List<BigDecimal> totalVehicleFlow; |
| | | private List<BigDecimal> totalDaily; |
| | | private List<BigDecimal> totalCumulative; |
| | | private List<BigDecimal> railCumulative; |
| | | private List<BigDecimal> civilAviationCumulative; |
| | | private List<BigDecimal> roadCumulative; |
| | | private List<BigDecimal> waterCumulative; |
| | | private List<BigDecimal> selfDrivingPersonFlowCumulative; |
| | | |
| | | private boolean hasAnyData() { |
| | | return hasValue(totalDaily) || hasValue(rail) || hasValue(road) |
| | | || hasValue(expressway) || hasValue(nationalProvincial); |
| | | } |
| | | |
| | | private boolean hasValue(List<BigDecimal> values) { |
| | | if (values == null) return false; |
| | | for (BigDecimal value : values) { |
| | | if (value != null) return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private List<HolidayTrendChartService.ChartCategory> modalShare() { |
| | | List<HolidayTrendChartService.ChartCategory> result = new ArrayList<>(); |
| | | result.add(category("èªé©¾", sum(selfDrivingPersonFlow))); |
| | | result.add(category("éè·¯", sum(road))); |
| | | result.add(category("éè·¯", sum(rail))); |
| | | result.add(category("æ°èª", sum(civilAviation))); |
| | | result.add(category("æ°´è·¯", sum(water))); |
| | | return result; |
| | | } |
| | | |
| | | private BigDecimal sum(List<BigDecimal> values) { |
| | | BigDecimal total = null; |
| | | for (BigDecimal value : values) { |
| | | if (value != null) total = HolidayNumberUtils.sumNullable(total, value); |
| | | } |
| | | return total; |
| | | } |
| | | |
| | | private HolidayTrendChartService.ChartCategory category(String name, BigDecimal value) { |
| | | HolidayTrendChartService.ChartCategory category = new HolidayTrendChartService.ChartCategory(); |
| | | category.setName(name); |
| | | category.setValue(value == null ? BigDecimal.ZERO : value); |
| | | return category; |
| | | } |
| | | } |
| | | |
| | | private static class ObservationData { |
| | | private final List<BigDecimal> speed; |
| | | private final List<BigDecimal> congestion; |
| | | |
| | | private ObservationData(List<BigDecimal> speed, List<BigDecimal> congestion) { |
| | | this.speed = speed; |
| | | this.congestion = congestion; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import org.springframework.stereotype.Service; |
| | | import org.w3c.dom.Document; |
| | | import org.w3c.dom.Element; |
| | | import org.w3c.dom.Node; |
| | | import org.w3c.dom.NodeList; |
| | | |
| | | import javax.xml.parsers.DocumentBuilderFactory; |
| | | import javax.xml.transform.OutputKeys; |
| | | import javax.xml.transform.Transformer; |
| | | import javax.xml.transform.TransformerFactory; |
| | | import javax.xml.transform.dom.DOMSource; |
| | | import javax.xml.transform.stream.StreamResult; |
| | | import java.io.ByteArrayInputStream; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.InputStream; |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.zip.ZipEntry; |
| | | import java.util.zip.ZipInputStream; |
| | | import java.util.zip.ZipOutputStream; |
| | | |
| | | @Service |
| | | public class HolidayTrendWordService { |
| | | |
| | | private static final String WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; |
| | | private static final String DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/main"; |
| | | private static final String WORD_DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"; |
| | | private static final String REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; |
| | | private static final String PACKAGE_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"; |
| | | |
| | | public byte[] generate(byte[] templateBytes, int year, List<DocumentBlock> blocks) { |
| | | try { |
| | | byte[] documentXml = readEntry(templateBytes, "word/document.xml"); |
| | | Document document = parse(documentXml); |
| | | Element body = firstElement(document.getDocumentElement(), "body"); |
| | | if (body == null) throw new IllegalArgumentException("èµ°å¿å¾æ¨¡æ¿ç¼ºå°æ£æ"); |
| | | |
| | | TemplateParts parts = TemplateParts.from(document, body); |
| | | Map<String, byte[]> media = new LinkedHashMap<>(); |
| | | List<ImageReference> imageReferences = new ArrayList<>(); |
| | | int imageIndex = 1; |
| | | for (DocumentBlock block : blocks) { |
| | | if (!(block instanceof ChartBlock)) continue; |
| | | String imageName = "image" + imageIndex++ + ".png"; |
| | | imageReferences.add(new ImageReference("rId" + (100 + imageIndex - 2), |
| | | "media/" + imageName, ((ChartBlock) block).getPng())); |
| | | media.put("word/media/" + imageName, ((ChartBlock) block).getPng()); |
| | | } |
| | | |
| | | Element sectPr = directChild(body, "sectPr"); |
| | | List<Node> removals = directChildren(body); |
| | | for (Node node : removals) { |
| | | if (node != sectPr) body.removeChild(node); |
| | | } |
| | | |
| | | appendClone(body, parts.titleLine, year + "å¹´âåä¸âåæå
¨ç综åè¿è¾å®¢æµé", sectPr); |
| | | appendClone(body, parts.subtitleLine, null, sectPr); |
| | | appendClone(body, parts.titleBlank, null, sectPr); |
| | | |
| | | int chartCursor = 0; |
| | | int chartsOnPage = 0; |
| | | for (int blockIndex = 0; blockIndex < blocks.size(); blockIndex++) { |
| | | DocumentBlock block = blocks.get(blockIndex); |
| | | if (block instanceof PageBreakBlock) { |
| | | appendPageBreak(body, sectPr); |
| | | chartsOnPage = 0; |
| | | } else if (block instanceof SectionBlock) { |
| | | appendClone(body, parts.sectionTemplate, ((SectionBlock) block).getText(), sectPr, true); |
| | | } else if (block instanceof ChartBlock) { |
| | | ChartBlock chart = (ChartBlock) block; |
| | | Element titleTemplate = chart.isCompact() |
| | | ? parts.compactTitleTemplate : parts.lineTitleTemplate; |
| | | Element imageTemplate = chart.isCompact() |
| | | ? parts.compactImageTemplate : parts.lineImageTemplate; |
| | | appendClone(body, titleTemplate, chart.getTitle(), sectPr); |
| | | Element imageParagraph = clone(imageTemplate); |
| | | ImageReference imageReference = imageReferences.get(chartCursor++); |
| | | bindImage(imageParagraph, imageReference, chart.getTitle(), chartCursor, |
| | | ((ChartBlock) block).getImageSize()); |
| | | body.insertBefore(imageParagraph, sectPr); |
| | | appendClone(body, parts.chartBlank, null, sectPr); |
| | | chartsOnPage++; |
| | | boolean firstChartPageBreak = chartCursor == 1 |
| | | && needsAutomaticPageBreak(blocks, blockIndex + 1); |
| | | if (firstChartPageBreak |
| | | || chartsOnPage >= 2 |
| | | && needsAutomaticPageBreak(blocks, blockIndex + 1)) { |
| | | appendPageBreak(body, sectPr); |
| | | chartsOnPage = 0; |
| | | } |
| | | } |
| | | } |
| | | |
| | | byte[] updatedDocument = serialize(document); |
| | | byte[] updatedRels = buildRelationships(templateBytes, imageReferences); |
| | | Map<String, byte[]> replacements = new HashMap<>(); |
| | | replacements.put("word/document.xml", updatedDocument); |
| | | replacements.put("word/_rels/document.xml.rels", updatedRels); |
| | | replacements.putAll(media); |
| | | return rewriteZip(templateBytes, replacements); |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("èµ°å¿å¾ææ¡£çæå¤±è´¥: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | private byte[] buildRelationships(byte[] templateBytes, List<ImageReference> imageReferences) throws Exception { |
| | | Document rels = parse(readEntry(templateBytes, "word/_rels/document.xml.rels")); |
| | | Element root = rels.getDocumentElement(); |
| | | NodeList children = root.getChildNodes(); |
| | | List<Node> removals = new ArrayList<>(); |
| | | for (int i = 0; i < children.getLength(); i++) { |
| | | Node node = children.item(i); |
| | | if (node instanceof Element) { |
| | | Element element = (Element) node; |
| | | if ("Relationship".equals(element.getLocalName()) |
| | | && element.getAttribute("Type").endsWith("/image")) { |
| | | removals.add(node); |
| | | } |
| | | } |
| | | } |
| | | for (Node node : removals) root.removeChild(node); |
| | | for (ImageReference reference : imageReferences) { |
| | | Element relationship = rels.createElementNS(PACKAGE_REL_NS, "Relationship"); |
| | | relationship.setAttribute("Id", reference.getRelationshipId()); |
| | | relationship.setAttribute("Type", |
| | | "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"); |
| | | relationship.setAttribute("Target", reference.getTarget()); |
| | | root.appendChild(relationship); |
| | | } |
| | | return serialize(rels); |
| | | } |
| | | |
| | | private void bindImage(Element paragraph, ImageReference reference, String title, int index, |
| | | ImageSize imageSize) { |
| | | Element blip = firstElement(paragraph, "blip"); |
| | | if (blip == null) throw new IllegalArgumentException("èµ°å¿å¾æ¨¡æ¿å¾çèç¹ç¼ºå¤±"); |
| | | blip.setAttributeNS(REL_NS, "r:embed", reference.getRelationshipId()); |
| | | Element srcRect = firstElement(paragraph, "srcRect"); |
| | | if (srcRect != null) srcRect.getParentNode().removeChild(srcRect); |
| | | resizeDrawing(paragraph, imageSize); |
| | | Element docPr = firstElement(paragraph, "docPr"); |
| | | if (docPr != null) { |
| | | docPr.setAttribute("id", String.valueOf(1000 + index)); |
| | | docPr.setAttribute("name", title); |
| | | docPr.setAttribute("descr", title); |
| | | } |
| | | } |
| | | |
| | | private void resizeDrawing(Element paragraph, ImageSize imageSize) { |
| | | if (imageSize == null) return; |
| | | NodeList extents = paragraph.getElementsByTagNameNS(WORD_DRAWING_NS, "extent"); |
| | | for (int i = 0; i < extents.getLength(); i++) { |
| | | Element extent = (Element) extents.item(i); |
| | | extent.setAttribute("cx", String.valueOf(imageSize.getWidth())); |
| | | extent.setAttribute("cy", String.valueOf(imageSize.getHeight())); |
| | | } |
| | | NodeList transforms = paragraph.getElementsByTagNameNS(DRAWING_NS, "xfrm"); |
| | | for (int i = 0; i < transforms.getLength(); i++) { |
| | | Element transform = (Element) transforms.item(i); |
| | | Element extent = firstElement(transform, "ext"); |
| | | if (extent == null) continue; |
| | | extent.setAttribute("cx", String.valueOf(imageSize.getWidth())); |
| | | extent.setAttribute("cy", String.valueOf(imageSize.getHeight())); |
| | | } |
| | | } |
| | | |
| | | private void appendClone(Element body, Element template, String text, Element before) { |
| | | appendClone(body, template, text, before, false); |
| | | } |
| | | |
| | | private void appendClone(Element body, Element template, String text, Element before, |
| | | boolean removeNumbering) { |
| | | if (template == null) return; |
| | | Element copy = clone(template); |
| | | if (removeNumbering) removeNumbering(copy); |
| | | if (text != null) setParagraphText(copy, text); |
| | | body.insertBefore(copy, before); |
| | | } |
| | | |
| | | private void removeNumbering(Element paragraph) { |
| | | NodeList numbered = paragraph.getElementsByTagNameNS(WORD_NS, "numPr"); |
| | | while (numbered.getLength() > 0) { |
| | | Node node = numbered.item(0); |
| | | node.getParentNode().removeChild(node); |
| | | } |
| | | } |
| | | |
| | | private boolean needsAutomaticPageBreak(List<DocumentBlock> blocks, int nextIndex) { |
| | | if (nextIndex >= blocks.size()) return false; |
| | | return !(blocks.get(nextIndex) instanceof PageBreakBlock); |
| | | } |
| | | |
| | | private void appendPageBreak(Element body, Element before) { |
| | | Document document = body.getOwnerDocument(); |
| | | Element paragraph = document.createElementNS(WORD_NS, "w:p"); |
| | | Element run = document.createElementNS(WORD_NS, "w:r"); |
| | | Element br = document.createElementNS(WORD_NS, "w:br"); |
| | | br.setAttributeNS(WORD_NS, "w:type", "page"); |
| | | run.appendChild(br); |
| | | paragraph.appendChild(run); |
| | | body.insertBefore(paragraph, before); |
| | | } |
| | | |
| | | private void setParagraphText(Element paragraph, String text) { |
| | | Element pPr = firstDirectChild(paragraph, "pPr"); |
| | | Element firstRun = firstDirectChild(paragraph, "r"); |
| | | Element rPr = firstRun == null ? null : firstDirectChild(firstRun, "rPr"); |
| | | List<Node> removals = new ArrayList<>(); |
| | | Node child = paragraph.getFirstChild(); |
| | | while (child != null) { |
| | | Node next = child.getNextSibling(); |
| | | if (child != pPr) removals.add(child); |
| | | child = next; |
| | | } |
| | | for (Node node : removals) paragraph.removeChild(node); |
| | | Element run = paragraph.getOwnerDocument().createElementNS(WORD_NS, "w:r"); |
| | | if (rPr != null) run.appendChild(rPr.cloneNode(true)); |
| | | Element t = paragraph.getOwnerDocument().createElementNS(WORD_NS, "w:t"); |
| | | t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve"); |
| | | t.setTextContent(text); |
| | | run.appendChild(t); |
| | | paragraph.appendChild(run); |
| | | } |
| | | |
| | | private Element clone(Element source) { |
| | | return (Element) source.cloneNode(true); |
| | | } |
| | | |
| | | private byte[] rewriteZip(byte[] source, Map<String, byte[]> replacements) throws Exception { |
| | | ByteArrayOutputStream output = new ByteArrayOutputStream(); |
| | | Map<String, Boolean> seen = new HashMap<>(); |
| | | try (ZipInputStream input = new ZipInputStream(new ByteArrayInputStream(source)); |
| | | ZipOutputStream zipOutput = new ZipOutputStream(output)) { |
| | | ZipEntry entry; |
| | | byte[] buffer = new byte[8192]; |
| | | while ((entry = input.getNextEntry()) != null) { |
| | | seen.put(entry.getName(), Boolean.TRUE); |
| | | byte[] replacement = replacements.get(entry.getName()); |
| | | if (isGeneratedChartImage(entry.getName()) && replacement == null) continue; |
| | | zipOutput.putNextEntry(new ZipEntry(entry.getName())); |
| | | if (replacement != null) { |
| | | zipOutput.write(replacement); |
| | | } else { |
| | | int length; |
| | | while ((length = input.read(buffer)) > 0) { |
| | | zipOutput.write(buffer, 0, length); |
| | | } |
| | | } |
| | | zipOutput.closeEntry(); |
| | | } |
| | | for (Map.Entry<String, byte[]> replacement : replacements.entrySet()) { |
| | | if (Boolean.TRUE.equals(seen.get(replacement.getKey()))) continue; |
| | | zipOutput.putNextEntry(new ZipEntry(replacement.getKey())); |
| | | zipOutput.write(replacement.getValue()); |
| | | zipOutput.closeEntry(); |
| | | } |
| | | } |
| | | return output.toByteArray(); |
| | | } |
| | | |
| | | private boolean isGeneratedChartImage(String entryName) { |
| | | return entryName != null && entryName.matches("word/media/image[0-9]+\\.png"); |
| | | } |
| | | |
| | | private byte[] readEntry(byte[] zipBytes, String entryName) throws Exception { |
| | | try (ZipInputStream input = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { |
| | | ZipEntry entry; |
| | | while ((entry = input.getNextEntry()) != null) { |
| | | if (entryName.equals(entry.getName())) return readAll(input); |
| | | } |
| | | } |
| | | throw new IllegalArgumentException("模æ¿ä¸ç¼ºå° " + entryName); |
| | | } |
| | | |
| | | private Document parse(byte[] xml) throws Exception { |
| | | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| | | factory.setNamespaceAware(true); |
| | | return factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml)); |
| | | } |
| | | |
| | | private byte[] serialize(Document document) throws Exception { |
| | | Transformer transformer = TransformerFactory.newInstance().newTransformer(); |
| | | transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); |
| | | transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); |
| | | ByteArrayOutputStream output = new ByteArrayOutputStream(); |
| | | transformer.transform(new DOMSource(document), new StreamResult(output)); |
| | | return output.toByteArray(); |
| | | } |
| | | |
| | | private byte[] readAll(InputStream input) throws Exception { |
| | | ByteArrayOutputStream output = new ByteArrayOutputStream(); |
| | | byte[] buffer = new byte[8192]; |
| | | int length; |
| | | while ((length = input.read(buffer)) > 0) { |
| | | output.write(buffer, 0, length); |
| | | } |
| | | return output.toByteArray(); |
| | | } |
| | | |
| | | private Element firstElement(Element parent, String localName) { |
| | | NodeList nodes = parent.getElementsByTagNameNS("*", localName); |
| | | if (nodes.getLength() > 0) return (Element) nodes.item(0); |
| | | if (DRAWING_NS.equals(parent.getNamespaceURI()) && parent.getLocalName() != null |
| | | && localName.equals(parent.getLocalName())) return parent; |
| | | NodeList children = parent.getChildNodes(); |
| | | for (int i = 0; i < children.getLength(); i++) { |
| | | Node node = children.item(i); |
| | | if (node instanceof Element) { |
| | | Element found = firstElement((Element) node, localName); |
| | | if (found != null) return found; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private Element firstDirectChild(Element parent, String localName) { |
| | | Node node = parent.getFirstChild(); |
| | | while (node != null) { |
| | | if (node instanceof Element && localName.equals(localName(node))) return (Element) node; |
| | | node = node.getNextSibling(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private Element directChild(Element parent, String localName) { |
| | | return firstDirectChild(parent, localName); |
| | | } |
| | | |
| | | private List<Node> directChildren(Element parent) { |
| | | List<Node> result = new ArrayList<>(); |
| | | Node node = parent.getFirstChild(); |
| | | while (node != null) { |
| | | result.add(node); |
| | | node = node.getNextSibling(); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private String localName(Node node) { |
| | | return node.getLocalName() == null ? node.getNodeName().replaceFirst("^.*:", "") : node.getLocalName(); |
| | | } |
| | | |
| | | public static class DocumentBlock { |
| | | } |
| | | |
| | | public static class SectionBlock extends DocumentBlock { |
| | | private final String text; |
| | | |
| | | public SectionBlock(String text) { |
| | | this.text = text; |
| | | } |
| | | |
| | | public String getText() { |
| | | return text; |
| | | } |
| | | } |
| | | |
| | | public static class PageBreakBlock extends DocumentBlock { |
| | | } |
| | | |
| | | public static class ChartBlock extends DocumentBlock { |
| | | private final String title; |
| | | private final byte[] png; |
| | | private final boolean compact; |
| | | private final ImageSize imageSize; |
| | | |
| | | public ChartBlock(String title, byte[] png) { |
| | | this(title, png, false); |
| | | } |
| | | |
| | | public ChartBlock(String title, byte[] png, boolean compact) { |
| | | this(title, png, compact, null); |
| | | } |
| | | |
| | | public ChartBlock(String title, byte[] png, boolean compact, ImageSize imageSize) { |
| | | this.title = title; |
| | | this.png = png; |
| | | this.compact = compact; |
| | | this.imageSize = imageSize; |
| | | } |
| | | |
| | | public ChartBlock(String title, byte[] png, ImageSize imageSize) { |
| | | this(title, png, false, imageSize); |
| | | } |
| | | |
| | | public String getTitle() { |
| | | return title; |
| | | } |
| | | |
| | | public byte[] getPng() { |
| | | return png; |
| | | } |
| | | |
| | | public boolean isCompact() { |
| | | return compact; |
| | | } |
| | | |
| | | public ImageSize getImageSize() { |
| | | return imageSize; |
| | | } |
| | | } |
| | | |
| | | public static class ImageSize { |
| | | private final long width; |
| | | private final long height; |
| | | |
| | | public ImageSize(long width, long height) { |
| | | this.width = width; |
| | | this.height = height; |
| | | } |
| | | |
| | | public long getWidth() { |
| | | return width; |
| | | } |
| | | |
| | | public long getHeight() { |
| | | return height; |
| | | } |
| | | } |
| | | |
| | | private static class ImageReference { |
| | | private final String relationshipId; |
| | | private final String target; |
| | | private final byte[] image; |
| | | |
| | | private ImageReference(String relationshipId, String target, byte[] image) { |
| | | this.relationshipId = relationshipId; |
| | | this.target = target; |
| | | this.image = image; |
| | | } |
| | | |
| | | private String getRelationshipId() { |
| | | return relationshipId; |
| | | } |
| | | |
| | | private String getTarget() { |
| | | return target; |
| | | } |
| | | |
| | | private byte[] getImage() { |
| | | return image; |
| | | } |
| | | } |
| | | |
| | | private static class TemplateParts { |
| | | private Element titleLine; |
| | | private Element subtitleLine; |
| | | private Element titleBlank; |
| | | private Element sectionTemplate; |
| | | private Element sectionBlank; |
| | | private Element lineTitleTemplate; |
| | | private Element lineImageTemplate; |
| | | private Element compactTitleTemplate; |
| | | private Element compactImageTemplate; |
| | | private Element chartBlank; |
| | | |
| | | private static TemplateParts from(Document document, Element body) { |
| | | TemplateParts parts = new TemplateParts(); |
| | | List<Element> paragraphs = directParagraphs(body); |
| | | parts.titleLine = byText(paragraphs, "2026å¹´âåä¸âåæå
¨ç综åè¿è¾å®¢æµé"); |
| | | parts.subtitleLine = byText(paragraphs, "èµ° å¿ å¾"); |
| | | parts.sectionTemplate = byText(paragraphs, "æ»ä½æ
åµ"); |
| | | parts.lineTitleTemplate = byText(paragraphs, "2026å¹´âåä¸âåæå
¨çè·¨åºåäººåæµå¨éæ
åµ"); |
| | | parts.lineImageTemplate = imageAfter(paragraphs, parts.lineTitleTemplate); |
| | | parts.compactTitleTemplate = byText(paragraphs, "âåä¸âåæå交éè¿è¾æ¹å¼å®¢è¿éå æ¯æ
åµ"); |
| | | parts.compactImageTemplate = imageAfter(paragraphs, parts.compactTitleTemplate); |
| | | parts.titleBlank = nextParagraph(paragraphs, parts.subtitleLine); |
| | | parts.sectionBlank = nextParagraph(paragraphs, parts.sectionTemplate); |
| | | parts.chartBlank = afterParagraph(paragraphs, parts.lineImageTemplate); |
| | | if (parts.titleLine == null || parts.subtitleLine == null || parts.sectionTemplate == null |
| | | || parts.lineTitleTemplate == null || parts.lineImageTemplate == null |
| | | || parts.compactTitleTemplate == null || parts.compactImageTemplate == null) { |
| | | throw new IllegalArgumentException("èµ°å¿å¾æ¨¡æ¿ç»æä¸å®æ´"); |
| | | } |
| | | return parts; |
| | | } |
| | | |
| | | private static List<Element> directParagraphs(Element body) { |
| | | List<Element> result = new ArrayList<>(); |
| | | Node node = body.getFirstChild(); |
| | | while (node != null) { |
| | | if (node instanceof Element && "p".equals(((Element) node).getLocalName())) { |
| | | result.add((Element) node); |
| | | } |
| | | node = node.getNextSibling(); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private static Element byText(List<Element> paragraphs, String text) { |
| | | for (Element paragraph : paragraphs) { |
| | | if (text.equals(paragraphText(paragraph))) return paragraph; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private static Element imageAfter(List<Element> paragraphs, Element anchor) { |
| | | int index = paragraphs.indexOf(anchor); |
| | | if (index < 0) return null; |
| | | for (int i = index + 1; i < paragraphs.size(); i++) { |
| | | if (firstDescendant(paragraphs.get(i), "blip") != null) return paragraphs.get(i); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private static Element nextParagraph(List<Element> paragraphs, Element anchor) { |
| | | int index = paragraphs.indexOf(anchor); |
| | | return index >= 0 && index + 1 < paragraphs.size() ? paragraphs.get(index + 1) : null; |
| | | } |
| | | |
| | | private static Element afterParagraph(List<Element> paragraphs, Element anchor) { |
| | | int index = paragraphs.indexOf(anchor); |
| | | return index >= 0 && index + 1 < paragraphs.size() ? paragraphs.get(index + 1) : null; |
| | | } |
| | | |
| | | private static Element firstDescendant(Element parent, String localName) { |
| | | NodeList nodes = parent.getElementsByTagNameNS("*", localName); |
| | | return nodes.getLength() == 0 ? null : (Element) nodes.item(0); |
| | | } |
| | | |
| | | private static String paragraphText(Element paragraph) { |
| | | StringBuilder builder = new StringBuilder(); |
| | | NodeList nodes = paragraph.getElementsByTagNameNS(WORD_NS, "t"); |
| | | for (int i = 0; i < nodes.getLength(); i++) { |
| | | builder.append(nodes.item(i).getTextContent()); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.trafficaudit.holiday.HolidayConstants; |
| | | import com.trafficaudit.holiday.HolidayNumberUtils; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleFlow; |
| | | import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayVehicleAdjustmentMapper; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.HashMap; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | @Service |
| | | public class HolidayVehicleAdjustmentService { |
| | | |
| | | private static final int REASON_MAX_LENGTH = 500; |
| | | |
| | | @Resource |
| | | private HolidayVehicleAdjustmentMapper adjustmentMapper; |
| | | @Resource |
| | | private HolidayVehicleFlowMapper vehicleFlowMapper; |
| | | |
| | | public List<HolidayVehicleAdjustment> list(Integer year) { |
| | | List<HolidayVehicleAdjustment> rows = adjustmentMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleAdjustment>() |
| | | .eq(year != null, HolidayVehicleAdjustment::getYear, year) |
| | | .orderByDesc(HolidayVehicleAdjustment::getYear) |
| | | .orderByAsc(HolidayVehicleAdjustment::getFlowDate)); |
| | | if (rows.isEmpty()) return rows; |
| | | |
| | | Set<Integer> years = new HashSet<>(); |
| | | for (HolidayVehicleAdjustment row : rows) { |
| | | if (row.getYear() != null) years.add(row.getYear()); |
| | | } |
| | | if (years.isEmpty()) return rows; |
| | | |
| | | List<HolidayVehicleFlow> sourceRows = vehicleFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL) |
| | | .eq(HolidayVehicleFlow::getVehicleType, HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER) |
| | | .in(HolidayVehicleFlow::getYear, years)); |
| | | Map<String, BigDecimal> originalValues = new HashMap<>(); |
| | | for (HolidayVehicleFlow sourceRow : sourceRows) { |
| | | if (sourceRow.getYear() == null || sourceRow.getFlowDate() == null || sourceRow.getVehicleCount() == null) { |
| | | continue; |
| | | } |
| | | originalValues.put(sourceKey(sourceRow.getYear(), sourceRow.getFlowDate()), sourceRow.getVehicleCount()); |
| | | } |
| | | for (HolidayVehicleAdjustment row : rows) { |
| | | if (row.getYear() != null && row.getFlowDate() != null) { |
| | | row.setOriginalValue(originalValues.get(sourceKey(row.getYear(), row.getFlowDate()))); |
| | | } |
| | | } |
| | | return rows; |
| | | } |
| | | |
| | | public BigDecimal findOriginalValue(Integer year, LocalDate flowDate) { |
| | | if (year == null || flowDate == null || flowDate.getYear() != year) { |
| | | throw new IllegalArgumentException("年份忥æä¸æ£ç¡®"); |
| | | } |
| | | HolidayVehicleFlow sourceRow = vehicleFlowMapper.selectOne( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getYear, year) |
| | | .eq(HolidayVehicleFlow::getFlowDate, flowDate) |
| | | .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL) |
| | | .eq(HolidayVehicleFlow::getVehicleType, HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER) |
| | | .last("limit 1")); |
| | | return sourceRow == null ? null : sourceRow.getVehicleCount(); |
| | | } |
| | | |
| | | @Transactional |
| | | public HolidayVehicleAdjustment save(Map<String, Object> params) { |
| | | Long id = longValue(params.get("id")); |
| | | Integer year = intValue(params.get("year")); |
| | | LocalDate flowDate = dateValue(params.get("flowDate")); |
| | | BigDecimal adjustedValue = decimalValue(params.get("adjustedValue")); |
| | | String reason = textValue(params.get("reason")); |
| | | |
| | | if (year == null || year < 2000 || year > 2100) { |
| | | throw new IllegalArgumentException("年份䏿£ç¡®"); |
| | | } |
| | | if (year >= LocalDate.now().getYear()) { |
| | | throw new IllegalArgumentException("å¾å¹´å½çéæ°æ®è°æ´ä»
ç»´æ¤åå²å¹´ä»½"); |
| | | } |
| | | if (flowDate == null || flowDate.getYear() != year) { |
| | | throw new IllegalArgumentException("æ¥æå¿
é¡»å±äºæé年份"); |
| | | } |
| | | if (adjustedValue == null || adjustedValue.compareTo(BigDecimal.ZERO) < 0) { |
| | | throw new IllegalArgumentException("è°æ´åçå½çé车æµéä¸è½å°äº 0"); |
| | | } |
| | | if (reason.isEmpty()) { |
| | | throw new IllegalArgumentException("è°æ´åå ä¸è½ä¸ºç©º"); |
| | | } |
| | | if (reason.length() > REASON_MAX_LENGTH) { |
| | | throw new IllegalArgumentException("è°æ´åå ä¸è½è¶
è¿ " + REASON_MAX_LENGTH + " 个å符"); |
| | | } |
| | | |
| | | HolidayVehicleAdjustment sameKey = adjustmentMapper.selectOne( |
| | | new LambdaQueryWrapper<HolidayVehicleAdjustment>() |
| | | .eq(HolidayVehicleAdjustment::getYear, year) |
| | | .eq(HolidayVehicleAdjustment::getFlowDate, flowDate) |
| | | .last("limit 1")); |
| | | HolidayVehicleAdjustment entity; |
| | | if (id == null) { |
| | | entity = sameKey == null ? new HolidayVehicleAdjustment() : sameKey; |
| | | } else { |
| | | entity = adjustmentMapper.selectById(id); |
| | | if (entity == null) { |
| | | throw new IllegalArgumentException("è°æ´è®°å½ä¸åå¨"); |
| | | } |
| | | if (sameKey != null && !sameKey.getId().equals(id)) { |
| | | throw new IllegalArgumentException("è¯¥å¹´ä»½åæ¥æå·²åå¨è°æ´è®°å½"); |
| | | } |
| | | } |
| | | |
| | | if (entity.getId() == null) { |
| | | entity.setCreatedAt(LocalDateTime.now()); |
| | | } |
| | | entity.setYear(year); |
| | | entity.setFlowDate(flowDate); |
| | | entity.setAdjustedValue(HolidayNumberUtils.normalize(adjustedValue)); |
| | | entity.setReason(reason); |
| | | entity.setUpdatedAt(LocalDateTime.now()); |
| | | if (entity.getId() == null) { |
| | | adjustmentMapper.insert(entity); |
| | | } else { |
| | | adjustmentMapper.updateById(entity); |
| | | } |
| | | return entity; |
| | | } |
| | | |
| | | @Transactional |
| | | public void delete(Long id) { |
| | | if (id == null) { |
| | | throw new IllegalArgumentException("缺å°è®°å½ id"); |
| | | } |
| | | adjustmentMapper.deleteById(id); |
| | | } |
| | | |
| | | private String sourceKey(Integer year, LocalDate flowDate) { |
| | | return year + "|" + flowDate; |
| | | } |
| | | |
| | | private Long longValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return Long.valueOf(value.toString().trim()); |
| | | } |
| | | |
| | | private Integer intValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return Integer.valueOf(value.toString().trim()); |
| | | } |
| | | |
| | | private BigDecimal decimalValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return HolidayNumberUtils.decimalValue(value); |
| | | } |
| | | |
| | | private LocalDate dateValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return LocalDate.parse(value.toString().trim()); |
| | | } |
| | | |
| | | private String textValue(Object value) { |
| | | return value == null ? "" : value.toString().trim(); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.trafficaudit.holiday.HolidayConstants; |
| | | import com.trafficaudit.holiday.HolidayNumberUtils; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerCoefficient; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerFlow; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleFlow; |
| | | import com.trafficaudit.holiday.mapper.HolidayPassengerCoefficientMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayPassengerFlowMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper; |
| | | import lombok.Data; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.poi.ss.usermodel.Cell; |
| | | import org.apache.poi.ss.usermodel.CellType; |
| | | import org.apache.poi.ss.usermodel.DataFormatter; |
| | | import org.apache.poi.ss.usermodel.FormulaEvaluator; |
| | | import org.apache.poi.ss.usermodel.Row; |
| | | import org.apache.poi.ss.usermodel.Sheet; |
| | | import org.apache.poi.ss.usermodel.Workbook; |
| | | import org.apache.poi.ss.usermodel.WorkbookFactory; |
| | | import org.apache.poi.xssf.usermodel.XSSFWorkbook; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.InputStream; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.format.DateTimeParseException; |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Slf4j |
| | | @Service |
| | | public class HolidayWorkbookService { |
| | | |
| | | private static final Map<String, String> PASSENGER_SHEET_MODE = new LinkedHashMap<>(); |
| | | private static final int FIRST_DATA_ROW_INDEX = 2; |
| | | private static final int DAY_ROW_SCAN_LIMIT = 30; |
| | | private static final int[] REFERENCE_YEARS = {2024, 2025, 2026}; |
| | | |
| | | static { |
| | | PASSENGER_SHEET_MODE.put("éè·¯", HolidayConstants.MODE_RAIL); |
| | | PASSENGER_SHEET_MODE.put("æ°èª", HolidayConstants.MODE_CIVIL_AVIATION); |
| | | PASSENGER_SHEET_MODE.put("éè·¯", HolidayConstants.MODE_ROAD); |
| | | PASSENGER_SHEET_MODE.put("æ°´è·¯", HolidayConstants.MODE_WATER); |
| | | } |
| | | |
| | | @Resource |
| | | private HolidayConfigService configService; |
| | | @Resource |
| | | private HolidayPassengerFlowMapper passengerFlowMapper; |
| | | @Resource |
| | | private HolidayVehicleFlowMapper vehicleFlowMapper; |
| | | @Resource |
| | | private HolidayPassengerCoefficientMapper coefficientMapper; |
| | | |
| | | @Value("${holiday.reference-path:docs/ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´å䏿°æ®.xlsx}") |
| | | private String referencePath; |
| | | |
| | | public File resolveReferenceFile() { |
| | | List<File> candidates = new ArrayList<>(); |
| | | File configured = new File(referencePath); |
| | | candidates.add(configured); |
| | | if (!configured.isAbsolute()) { |
| | | candidates.add(new File(System.getProperty("user.dir"), referencePath)); |
| | | candidates.add(new File(System.getProperty("user.dir"), "../" + referencePath)); |
| | | } |
| | | for (File candidate : candidates) { |
| | | try { |
| | | if (candidate.exists() && candidate.isFile()) { |
| | | return candidate.getCanonicalFile(); |
| | | } |
| | | } catch (Exception ignored) { |
| | | // Keep checking the remaining candidate paths. |
| | | } |
| | | } |
| | | return configured; |
| | | } |
| | | |
| | | public String referenceFileName() { |
| | | return resolveReferenceFile().getName(); |
| | | } |
| | | |
| | | @Transactional |
| | | public ImportSummary importWorkbook(MultipartFile file, int year, String holidayType, boolean allowHistoricalOverwrite) { |
| | | if (file == null || file.isEmpty()) { |
| | | throw new IllegalArgumentException("è¯·éæ©è¦å¯¼å
¥ç Excel æä»¶"); |
| | | } |
| | | List<HolidayCalendar> holidayDays = configService.findHolidayDays(year, holidayType); |
| | | if (holidayDays.isEmpty()) { |
| | | throw new IllegalArgumentException("æé年份åè忥尿ªé
ç½®æ¥æï¼è¯·å
å¨ç³»ç»é
ç½®ä¸ç»´æ¤è忥"); |
| | | } |
| | | Map<Integer, LocalDate> dateByDay = new LinkedHashMap<>(); |
| | | for (HolidayCalendar day : holidayDays) { |
| | | dateByDay.put(day.getDayIndex(), day.getHolidayDate()); |
| | | } |
| | | |
| | | if (!allowHistoricalOverwrite) { |
| | | ImportSummary historicalSummary = detectHistoricalChanges(file, year, dateByDay); |
| | | if (historicalSummary.isConfirmationRequired()) { |
| | | return historicalSummary; |
| | | } |
| | | } |
| | | |
| | | ImportSummary summary = new ImportSummary(); |
| | | try (InputStream in = file.getInputStream(); Workbook workbook = WorkbookFactory.create(in)) { |
| | | FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); |
| | | importPassengerSheets(workbook, evaluator, year, holidayType, dateByDay, summary); |
| | | importVehicleSheets(workbook, evaluator, year, dateByDay, summary); |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("å·¥ä½ç°¿è§£æå¤±è´¥: " + e.getMessage(), e); |
| | | } |
| | | return summary; |
| | | } |
| | | |
| | | private ImportSummary detectHistoricalChanges(MultipartFile file, int year, Map<Integer, LocalDate> dateByDay) { |
| | | ImportSummary summary = new ImportSummary(); |
| | | try (InputStream in = file.getInputStream(); Workbook workbook = WorkbookFactory.create(in)) { |
| | | FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); |
| | | int latestDay = findLatestIncomingDay(workbook, evaluator, year, dateByDay); |
| | | if (latestDay <= 1) { |
| | | return summary; |
| | | } |
| | | Map<LocalDate, Map<String, BigDecimal>> existingPassenger = loadPassengerByDate(year); |
| | | Map<LocalDate, Map<String, BigDecimal>> existingVehicle = loadVehicleByDate(year); |
| | | detectPassengerHistory(workbook, evaluator, year, dateByDay, latestDay, existingPassenger, summary); |
| | | detectVehicleHistory(workbook, evaluator, year, dateByDay, latestDay, existingVehicle, summary); |
| | | if (!summary.getHistoricalChanges().isEmpty()) { |
| | | summary.setConfirmationRequired(true); |
| | | } |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("å·¥ä½ç°¿è§£æå¤±è´¥: " + e.getMessage(), e); |
| | | } |
| | | return summary; |
| | | } |
| | | |
| | | private int findLatestIncomingDay(Workbook workbook, FormulaEvaluator evaluator, int year, Map<Integer, LocalDate> dateByDay) { |
| | | int latest = 0; |
| | | for (Integer dayIndex : dateByDay.keySet()) { |
| | | if (hasPassengerValue(workbook, evaluator, year, dayIndex) |
| | | || hasVehicleValue(workbook, evaluator, year, dayIndex)) { |
| | | latest = Math.max(latest, dayIndex); |
| | | } |
| | | } |
| | | return latest; |
| | | } |
| | | |
| | | private boolean hasPassengerValue(Workbook workbook, FormulaEvaluator evaluator, int year, int dayIndex) { |
| | | for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) { |
| | | Sheet sheet = workbook.getSheet(entry.getKey()); |
| | | if (sheet == null) continue; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7); |
| | | if (yearColumn == null) continue; |
| | | Row row = findDayRow(sheet, evaluator, dayIndex); |
| | | if (row == null) continue; |
| | | BigDecimal value = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (value != null) return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private boolean hasVehicleValue(Workbook workbook, FormulaEvaluator evaluator, int year, int dayIndex) { |
| | | Sheet expressway = workbook.getSheet("é«éå
¬è·¯"); |
| | | if (expressway != null) { |
| | | Integer yearColumn = findYearColumn(expressway, evaluator, year, 2, 7); |
| | | if (yearColumn != null) { |
| | | Row row = findDayRow(expressway, evaluator, dayIndex); |
| | | if (row != null && numberValue(row.getCell(yearColumn), evaluator) != null) return true; |
| | | } |
| | | } |
| | | Sheet nationalProvincial = workbook.getSheet("æ®éå½çé"); |
| | | if (nationalProvincial != null) { |
| | | Integer yearColumn = findYearColumn(nationalProvincial, evaluator, year, 3, 8); |
| | | if (yearColumn != null) { |
| | | Row row = findDayRow(nationalProvincial, evaluator, dayIndex); |
| | | if (row != null && numberValue(row.getCell(yearColumn), evaluator) != null) return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private void detectPassengerHistory(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, int latestDay, |
| | | Map<LocalDate, Map<String, BigDecimal>> existing, |
| | | ImportSummary summary) { |
| | | for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) { |
| | | Sheet sheet = workbook.getSheet(entry.getKey()); |
| | | if (sheet == null) continue; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7); |
| | | if (yearColumn == null) continue; |
| | | for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) { |
| | | if (day.getKey() >= latestDay) continue; |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | BigDecimal incoming = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (incoming == null) continue; |
| | | Map<String, BigDecimal> existingDay = existing.get(day.getValue()); |
| | | BigDecimal oldValue = existingDay == null ? null : existingDay.get(entry.getValue()); |
| | | if (oldValue != null && !sameValue(oldValue, incoming)) { |
| | | summary.addHistoricalChange("第" + day.getKey() + "æ¥ " + day.getValue() + " " |
| | | + entry.getKey() + "ï¼åå¼ " + formatChangeValue(oldValue) + "ï¼å¯¼å
¥å¼ " + formatChangeValue(incoming)); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void detectVehicleHistory(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, int latestDay, |
| | | Map<LocalDate, Map<String, BigDecimal>> existing, |
| | | ImportSummary summary) { |
| | | detectVehicleSheetHistory(workbook, evaluator, year, dateByDay, latestDay, existing, summary, |
| | | "é«éå
¬è·¯", HolidayConstants.ROAD_EXPRESSWAY, 2, 7); |
| | | detectVehicleSheetHistory(workbook, evaluator, year, dateByDay, latestDay, existing, summary, |
| | | "æ®éå½çé", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, 3, 8); |
| | | detectComparisonBaseHistory(workbook, evaluator, year, dateByDay, latestDay, existing, summary); |
| | | } |
| | | |
| | | private void detectVehicleSheetHistory(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, int latestDay, |
| | | Map<LocalDate, Map<String, BigDecimal>> existing, |
| | | ImportSummary summary, String sheetName, String roadCategory, |
| | | int startColumn, int endColumn) { |
| | | Sheet sheet = workbook.getSheet(sheetName); |
| | | if (sheet == null) return; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn); |
| | | if (yearColumn == null) return; |
| | | for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) { |
| | | if (day.getKey() >= latestDay) continue; |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | BigDecimal incoming = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (incoming == null) continue; |
| | | Map<String, BigDecimal> existingDay = existing.get(day.getValue()); |
| | | BigDecimal oldValue = existingDay == null ? null : existingDay.get(roadCategory); |
| | | if (oldValue != null && !sameValue(oldValue, incoming)) { |
| | | summary.addHistoricalChange("第" + day.getKey() + "æ¥ " + day.getValue() + " " |
| | | + sheetName + "ï¼åå¼ " + formatChangeValue(oldValue) + "ï¼å¯¼å
¥å¼ " + formatChangeValue(incoming)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void detectComparisonBaseHistory(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, int latestDay, |
| | | Map<LocalDate, Map<String, BigDecimal>> existingVehicle, |
| | | ImportSummary summary) { |
| | | if (!HolidayConstants.supportsComparisonBase(year)) return; |
| | | Sheet sheet = workbook.getSheet("æ®éå½çé"); |
| | | if (sheet == null) return; |
| | | Map<LocalDate, BigDecimal> existingBase = loadComparisonBaseByDate(year); |
| | | for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) { |
| | | if (day.getKey() >= latestDay) continue; |
| | | Map<String, BigDecimal> existingDay = existingVehicle.get(day.getValue()); |
| | | if (existingDay == null || existingDay.get(HolidayConstants.ROAD_NATIONAL_PROVINCIAL) == null) continue; |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | BigDecimal incoming; |
| | | try { |
| | | incoming = numberValue(row.getCell(2), evaluator); |
| | | } catch (Exception e) { |
| | | summary.addError("æ®éå½çé 第" + (row.getRowNum() + 1) + "è¡ç¯æ¯åºæï¼" + e.getMessage()); |
| | | continue; |
| | | } |
| | | BigDecimal oldValue = existingBase.get(day.getValue()); |
| | | if (!sameNullableValue(oldValue, incoming)) { |
| | | summary.addHistoricalChange("第" + day.getKey() + "æ¥ " + day.getValue() + " æ®éå½çéç¯æ¯åºæï¼åå¼ " |
| | | + nullableChangeValue(oldValue) + "ï¼å¯¼å
¥å¼ " + nullableChangeValue(incoming)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private Map<LocalDate, Map<String, BigDecimal>> loadPassengerByDate(int year) { |
| | | Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>(); |
| | | List<HolidayPassengerFlow> rows = passengerFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayPassengerFlow>().eq(HolidayPassengerFlow::getYear, year)); |
| | | for (HolidayPassengerFlow row : rows) { |
| | | result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>()) |
| | | .put(row.getTransportMode(), row.getPassengerCount()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, Map<String, BigDecimal>> loadVehicleByDate(int year) { |
| | | Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>(); |
| | | List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>().eq(HolidayVehicleFlow::getYear, year)); |
| | | for (HolidayVehicleFlow row : rows) { |
| | | result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>()) |
| | | .put(row.getRoadCategory(), row.getVehicleCount()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, BigDecimal> loadComparisonBaseByDate(int year) { |
| | | Map<LocalDate, BigDecimal> result = new LinkedHashMap<>(); |
| | | List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getYear, year) |
| | | .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL)); |
| | | for (HolidayVehicleFlow row : rows) result.put(row.getFlowDate(), row.getComparisonBase()); |
| | | return result; |
| | | } |
| | | |
| | | private boolean sameValue(BigDecimal left, BigDecimal right) { |
| | | return HolidayNumberUtils.sameValue(left, right); |
| | | } |
| | | |
| | | private boolean sameNullableValue(BigDecimal left, BigDecimal right) { |
| | | if (left == null || right == null) return left == right; |
| | | return sameValue(left, right); |
| | | } |
| | | |
| | | private String nullableChangeValue(BigDecimal value) { |
| | | return value == null ? "空" : formatChangeValue(value); |
| | | } |
| | | |
| | | private String formatChangeValue(BigDecimal value) { |
| | | return HolidayNumberUtils.changeText(value); |
| | | } |
| | | |
| | | @Transactional |
| | | public void seedReferenceDataIfNeeded() { |
| | | File reference = resolveReferenceFile(); |
| | | if (!reference.exists() || !reference.isFile()) { |
| | | log.warn("Holiday reference workbook not found: {}", reference.getAbsolutePath()); |
| | | return; |
| | | } |
| | | try (InputStream in = new FileInputStream(reference); Workbook workbook = WorkbookFactory.create(in)) { |
| | | FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); |
| | | seedNationalDayCalendar(); |
| | | ensurePreviousDayCalendarIfMissing(); |
| | | for (int year : REFERENCE_YEARS) { |
| | | if (passengerFlowMapper.selectCount(new LambdaQueryWrapper<HolidayPassengerFlow>() |
| | | .eq(HolidayPassengerFlow::getYear, year)) == 0) { |
| | | seedPassengerYear(workbook, evaluator, year); |
| | | } |
| | | if (vehicleFlowMapper.selectCount(new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getYear, year)) == 0) { |
| | | seedVehicleYear(workbook, evaluator, year); |
| | | } |
| | | if (coefficientMapper.selectCount(new LambdaQueryWrapper<HolidayPassengerCoefficient>() |
| | | .eq(HolidayPassengerCoefficient::getYear, year)) == 0) { |
| | | seedCoefficientYear(workbook, evaluator, year); |
| | | } |
| | | } |
| | | seedPreviousDayPassengerIfMissing(workbook, evaluator); |
| | | seedRoad2019IfMissing(workbook, evaluator); |
| | | } catch (Exception e) { |
| | | log.error("Failed to seed holiday reference data", e); |
| | | } |
| | | } |
| | | |
| | | public byte[] exportWorkbook(int year, String holidayType) { |
| | | File reference = resolveReferenceFile(); |
| | | if (!reference.exists() || !reference.isFile()) { |
| | | throw new IllegalArgumentException("èåæ¥æ¨¡æ¿æä»¶ä¸åå¨: " + reference.getAbsolutePath()); |
| | | } |
| | | try (InputStream in = new FileInputStream(reference); XSSFWorkbook workbook = new XSSFWorkbook(in)) { |
| | | FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); |
| | | fillPassengerSheets(workbook, evaluator, year, holidayType); |
| | | fillVehicleSheets(workbook, evaluator, year, holidayType); |
| | | fillCoefficients(workbook, evaluator, year, holidayType); |
| | | workbook.setForceFormulaRecalculation(true); |
| | | try { |
| | | evaluator.evaluateAll(); |
| | | } catch (Exception e) { |
| | | log.warn("Workbook formula recalculation did not complete; Excel will recalculate on open", e); |
| | | } |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | workbook.write(out); |
| | | return out.toByteArray(); |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("èåæ¥æ°æ®å¯¼åºå¤±è´¥: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | private void importPassengerSheets(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | 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) { |
| | | summary.addError(entry.getKey() + "ï¼æ¾ä¸å°å·¥ä½è¡¨"); |
| | | continue; |
| | | } |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7); |
| | | if (yearColumn == null) { |
| | | summary.addError(entry.getKey() + "ï¼æ¾ä¸å° " + year + " å¹´æ°æ®å"); |
| | | continue; |
| | | } |
| | | for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) { |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) { |
| | | summary.addError(entry.getKey() + " 第" + day.getKey() + "æ¥ï¼å·¥ä½è¡¨ç¼ºå°è¯¥è¡"); |
| | | continue; |
| | | } |
| | | Integer rowDay = parseDayIndex(row.getCell(0), evaluator); |
| | | if (rowDay == null || !rowDay.equals(day.getKey())) { |
| | | summary.addError(entry.getKey() + " 第" + (row.getRowNum() + 1) + "è¡ï¼ç¬¬å æ¥ä¸è忥é
ç½®ä¸ä¸è´"); |
| | | continue; |
| | | } |
| | | BigDecimal value; |
| | | try { |
| | | value = numberValue(row.getCell(yearColumn), evaluator); |
| | | } catch (Exception e) { |
| | | summary.addError(entry.getKey() + " 第" + (row.getRowNum() + 1) + "è¡ï¼" + e.getMessage()); |
| | | continue; |
| | | } |
| | | if (value == null) { |
| | | summary.incrementSkipped(); |
| | | continue; |
| | | } |
| | | upsertPassenger(year, day.getValue(), entry.getValue(), value); |
| | | summary.incrementPassenger(); |
| | | } |
| | | } |
| | | 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(); |
| | | } |
| | | } |
| | | |
| | | private void importVehicleSheets(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, ImportSummary summary) { |
| | | importVehicleSheet(workbook, evaluator, year, dateByDay, summary, |
| | | "é«éå
¬è·¯", HolidayConstants.ROAD_EXPRESSWAY, HolidayConstants.VEHICLE_SMALL_PASSENGER, 2, 7, null); |
| | | importVehicleSheet(workbook, evaluator, year, dateByDay, summary, |
| | | "æ®éå½çé", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER, 3, 8, |
| | | HolidayConstants.supportsComparisonBase(year) ? 2 : null); |
| | | } |
| | | |
| | | private void importVehicleSheet(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, ImportSummary summary, |
| | | String sheetName, String roadCategory, String vehicleType, |
| | | int startColumn, int endColumn, Integer comparisonBaseColumn) { |
| | | Sheet sheet = workbook.getSheet(sheetName); |
| | | if (sheet == null) { |
| | | summary.addError(sheetName + "ï¼æ¾ä¸å°å·¥ä½è¡¨"); |
| | | return; |
| | | } |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn); |
| | | if (yearColumn == null) { |
| | | summary.addError(sheetName + "ï¼æ¾ä¸å° " + year + " å¹´æ°æ®å"); |
| | | return; |
| | | } |
| | | for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) { |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) { |
| | | summary.addError(sheetName + " 第" + day.getKey() + "æ¥ï¼å·¥ä½è¡¨ç¼ºå°è¯¥è¡"); |
| | | continue; |
| | | } |
| | | Integer rowDay = parseDayIndex(row.getCell(0), evaluator); |
| | | if (rowDay == null || !rowDay.equals(day.getKey())) { |
| | | summary.addError(sheetName + " 第" + (row.getRowNum() + 1) + "è¡ï¼ç¬¬å æ¥ä¸è忥é
ç½®ä¸ä¸è´"); |
| | | continue; |
| | | } |
| | | BigDecimal value; |
| | | try { |
| | | value = numberValue(row.getCell(yearColumn), evaluator); |
| | | } catch (Exception e) { |
| | | summary.addError(sheetName + " 第" + (row.getRowNum() + 1) + "è¡ï¼" + e.getMessage()); |
| | | continue; |
| | | } |
| | | if (value == null) { |
| | | summary.incrementSkipped(); |
| | | continue; |
| | | } |
| | | BigDecimal comparisonBase = null; |
| | | if (comparisonBaseColumn != null) { |
| | | try { |
| | | comparisonBase = numberValue(row.getCell(comparisonBaseColumn), evaluator); |
| | | } catch (Exception e) { |
| | | summary.addError(sheetName + " 第" + (row.getRowNum() + 1) + "è¡ç¯æ¯åºæï¼" + e.getMessage()); |
| | | continue; |
| | | } |
| | | } |
| | | upsertVehicle(year, day.getValue(), roadCategory, vehicleType, value, comparisonBase); |
| | | summary.incrementVehicle(); |
| | | } |
| | | } |
| | | |
| | | private void fillPassengerSheets(XSSFWorkbook workbook, FormulaEvaluator evaluator, int selectedYear, String holidayType) { |
| | | for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) { |
| | | Sheet sheet = workbook.getSheet(entry.getKey()); |
| | | if (sheet == null) continue; |
| | | for (int year : REFERENCE_YEARS) { |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7); |
| | | if (yearColumn == null) continue; |
| | | Map<Integer, LocalDate> dayDates = dayDates(year, holidayType); |
| | | Map<LocalDate, BigDecimal> values = loadPassengerValues(year, entry.getValue()); |
| | | for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { |
| | | if (!values.containsKey(day.getValue())) continue; |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void fillVehicleSheets(XSSFWorkbook workbook, FormulaEvaluator evaluator, int selectedYear, String holidayType) { |
| | | fillVehicleSheet(workbook, evaluator, "é«éå
¬è·¯", HolidayConstants.ROAD_EXPRESSWAY, 2, 7, holidayType, null); |
| | | fillVehicleSheet(workbook, evaluator, "æ®éå½çé", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, 3, 8, holidayType, |
| | | HolidayConstants.supportsComparisonBase(selectedYear) ? 2 : null); |
| | | } |
| | | |
| | | private void fillVehicleSheet(XSSFWorkbook workbook, FormulaEvaluator evaluator, String sheetName, |
| | | String roadCategory, int startColumn, int endColumn, String holidayType, |
| | | Integer comparisonBaseColumn) { |
| | | Sheet sheet = workbook.getSheet(sheetName); |
| | | if (sheet == null) return; |
| | | for (int year : REFERENCE_YEARS) { |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn); |
| | | if (yearColumn == null) continue; |
| | | Map<Integer, LocalDate> dayDates = dayDates(year, holidayType); |
| | | Map<LocalDate, BigDecimal> values = loadVehicleValues(year, roadCategory); |
| | | for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { |
| | | if (!values.containsKey(day.getValue())) continue; |
| | | 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)) { |
| | | // Column C (comparison base) is written as an operator backup value only. The ring |
| | | // ratio in column E always compares the current day with the previous day row of |
| | | // the same year, so the base never changes the calculated percentage. |
| | | Map<LocalDate, BigDecimal> bases = loadComparisonBaseValues(year); |
| | | for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { |
| | | if (!bases.containsKey(day.getValue())) continue; |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | setNumeric(sheet, row.getRowNum(), comparisonBaseColumn, bases.get(day.getValue())); |
| | | } |
| | | Integer previousRowIndex = null; |
| | | for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) { |
| | | previousRowIndex = null; |
| | | continue; |
| | | } |
| | | if (previousRowIndex != null) { |
| | | setFormula(sheet, row.getRowNum(), 4, |
| | | ringRatioFormula(row.getRowNum() + 1, previousRowIndex + 1)); |
| | | } |
| | | previousRowIndex = row.getRowNum(); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Ring ratio = (current day value - previous day value) / previous day value, always taken |
| | | // from the active year column (D). This intentionally ignores the comparison base column. |
| | | static String ringRatioFormula(int excelRow, int previousExcelRow) { |
| | | return "=(D" + excelRow + "-D" + previousExcelRow + ")/D" + previousExcelRow; |
| | | } |
| | | |
| | | private void fillCoefficients(XSSFWorkbook workbook, FormulaEvaluator evaluator, int selectedYear, String holidayType) { |
| | | fillCoefficientSheet(workbook, evaluator, "é«éå
¬è·¯", 8, 10, holidayType); |
| | | fillCoefficientSheet(workbook, evaluator, "æ®éå½çé", 9, 11, holidayType); |
| | | } |
| | | |
| | | private void fillCoefficientSheet(XSSFWorkbook workbook, FormulaEvaluator evaluator, String sheetName, |
| | | int startColumn, int endColumn, String holidayType) { |
| | | Sheet sheet = workbook.getSheet(sheetName); |
| | | if (sheet == null) return; |
| | | for (int year : REFERENCE_YEARS) { |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn); |
| | | if (yearColumn == null) continue; |
| | | BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY); |
| | | if (coefficient == null) continue; |
| | | Map<Integer, LocalDate> dayDates = dayDates(year, holidayType); |
| | | for (Integer dayIndex : dayDates.keySet()) { |
| | | setNumeric(sheet, FIRST_DATA_ROW_INDEX + dayIndex - 1, yearColumn, coefficient); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void seedNationalDayCalendar() { |
| | | // Calendar is admin-managed after the first bootstrap. Re-adding missing day indexes on |
| | | // every start would resurrect days the admin deleted, so only seed a fresh table. |
| | | if (!configService.listHolidays(null, null).isEmpty()) { |
| | | return; |
| | | } |
| | | int[] 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) { |
| | | byDay.put(row.getDayIndex(), row); |
| | | } |
| | | for (int day = 1; day <= lengths[i]; day++) { |
| | | if (byDay.containsKey(day)) continue; |
| | | HolidayCalendar calendar = new HolidayCalendar(); |
| | | calendar.setYear(year); |
| | | calendar.setHolidayType(HolidayConstants.TYPE_NATIONAL_DAY); |
| | | calendar.setHolidayDate(LocalDate.of(year, 10, day)); |
| | | calendar.setDayIndex(day); |
| | | calendar.setCreatedAt(LocalDateTime.now()); |
| | | calendar.setUpdatedAt(LocalDateTime.now()); |
| | | configService.saveHoliday(toMap(calendar)); |
| | | } |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void seedPassengerYear(Workbook workbook, FormulaEvaluator evaluator, int year) { |
| | | Map<Integer, LocalDate> dayDates = dayDates(year, HolidayConstants.TYPE_NATIONAL_DAY); |
| | | if (dayDates.isEmpty()) return; |
| | | for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) { |
| | | Sheet sheet = workbook.getSheet(entry.getKey()); |
| | | if (sheet == null) continue; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7); |
| | | if (yearColumn == null) continue; |
| | | for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { |
| | | Row row = 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); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void seedVehicleYear(Workbook workbook, FormulaEvaluator evaluator, int year) { |
| | | Map<Integer, LocalDate> dayDates = dayDates(year, HolidayConstants.TYPE_NATIONAL_DAY); |
| | | if (dayDates.isEmpty()) return; |
| | | seedVehicleSheet(workbook, evaluator, year, dayDates, "é«éå
¬è·¯", |
| | | HolidayConstants.ROAD_EXPRESSWAY, HolidayConstants.VEHICLE_SMALL_PASSENGER, 2, 7); |
| | | seedVehicleSheet(workbook, evaluator, year, dayDates, "æ®éå½çé", |
| | | HolidayConstants.ROAD_NATIONAL_PROVINCIAL, HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER, 3, 8); |
| | | } |
| | | |
| | | private void seedVehicleSheet(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dayDates, String sheetName, |
| | | String roadCategory, String vehicleType, int startColumn, int endColumn) { |
| | | Sheet sheet = workbook.getSheet(sheetName); |
| | | if (sheet == null) return; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn); |
| | | if (yearColumn == null) return; |
| | | for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { |
| | | Row row = findDayRow(sheet, evaluator, day.getKey()); |
| | | if (row == null) continue; |
| | | BigDecimal value = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (value != null) { |
| | | upsertVehicle(year, day.getValue(), roadCategory, vehicleType, value, null); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void seedCoefficientYear(Workbook workbook, FormulaEvaluator evaluator, int year) { |
| | | List<BigDecimal> values = new ArrayList<>(); |
| | | collectCoefficientValues(workbook, evaluator, "é«éå
¬è·¯", year, 8, 10, values); |
| | | collectCoefficientValues(workbook, evaluator, "æ®éå½çé", year, 9, 11, values); |
| | | if (values.isEmpty()) return; |
| | | BigDecimal sum = BigDecimal.ZERO; |
| | | for (BigDecimal value : values) { |
| | | sum = HolidayNumberUtils.normalize(sum.add(value)); |
| | | } |
| | | BigDecimal coefficient = HolidayNumberUtils.divide(sum, BigDecimal.valueOf(values.size())); |
| | | upsertCoefficient(year, HolidayConstants.CATEGORY_HOLIDAY, coefficient); |
| | | upsertCoefficient(year, HolidayConstants.CATEGORY_NON_HOLIDAY, coefficient); |
| | | } |
| | | |
| | | private void collectCoefficientValues(Workbook workbook, FormulaEvaluator evaluator, String sheetName, |
| | | int year, int startColumn, int endColumn, List<BigDecimal> values) { |
| | | Sheet sheet = workbook.getSheet(sheetName); |
| | | if (sheet == null) return; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn); |
| | | if (yearColumn == null) return; |
| | | for (int rowIndex = FIRST_DATA_ROW_INDEX; rowIndex < FIRST_DATA_ROW_INDEX + 10; rowIndex++) { |
| | | Row row = sheet.getRow(rowIndex); |
| | | if (row == null) continue; |
| | | try { |
| | | BigDecimal value = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (value != null && value.compareTo(BigDecimal.ZERO) > 0) values.add(value); |
| | | } catch (Exception ignored) { |
| | | // Skip cells that are not numeric coefficients. |
| | | } |
| | | } |
| | | } |
| | | |
| | | private 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)) { |
| | | result.put(row.getDayIndex(), row.getHolidayDate()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, BigDecimal> loadPassengerValues(int year, String transportMode) { |
| | | Map<LocalDate, BigDecimal> result = new LinkedHashMap<>(); |
| | | List<HolidayPassengerFlow> rows = passengerFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayPassengerFlow>() |
| | | .eq(HolidayPassengerFlow::getYear, year) |
| | | .eq(HolidayPassengerFlow::getTransportMode, transportMode)); |
| | | for (HolidayPassengerFlow row : rows) result.put(row.getFlowDate(), row.getPassengerCount()); |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, BigDecimal> loadVehicleValues(int year, String roadCategory) { |
| | | Map<LocalDate, BigDecimal> result = new LinkedHashMap<>(); |
| | | List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getYear, year) |
| | | .eq(HolidayVehicleFlow::getRoadCategory, roadCategory)); |
| | | for (HolidayVehicleFlow row : rows) result.put(row.getFlowDate(), row.getVehicleCount()); |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, BigDecimal> loadComparisonBaseValues(int year) { |
| | | Map<LocalDate, BigDecimal> result = new LinkedHashMap<>(); |
| | | List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getYear, year) |
| | | .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL)); |
| | | for (HolidayVehicleFlow row : rows) { |
| | | if (row.getComparisonBase() != null) result.put(row.getFlowDate(), row.getComparisonBase()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private void upsertPassenger(int year, LocalDate date, String mode, BigDecimal value) { |
| | | HolidayPassengerFlow existing = passengerFlowMapper.selectOne(new LambdaQueryWrapper<HolidayPassengerFlow>() |
| | | .eq(HolidayPassengerFlow::getYear, year) |
| | | .eq(HolidayPassengerFlow::getFlowDate, date) |
| | | .eq(HolidayPassengerFlow::getTransportMode, mode) |
| | | .last("limit 1")); |
| | | if (existing == null) { |
| | | existing = new HolidayPassengerFlow(); |
| | | existing.setYear(year); |
| | | existing.setFlowDate(date); |
| | | existing.setTransportMode(mode); |
| | | existing.setPassengerCount(value); |
| | | existing.setCreatedAt(LocalDateTime.now()); |
| | | existing.setUpdatedAt(LocalDateTime.now()); |
| | | passengerFlowMapper.insert(existing); |
| | | } else { |
| | | existing.setPassengerCount(value); |
| | | existing.setUpdatedAt(LocalDateTime.now()); |
| | | passengerFlowMapper.updateById(existing); |
| | | } |
| | | } |
| | | |
| | | private void upsertVehicle(int year, LocalDate date, String roadCategory, String vehicleType, |
| | | BigDecimal value, BigDecimal comparisonBase) { |
| | | HolidayVehicleFlow existing = vehicleFlowMapper.selectOne(new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getYear, year) |
| | | .eq(HolidayVehicleFlow::getFlowDate, date) |
| | | .eq(HolidayVehicleFlow::getRoadCategory, roadCategory) |
| | | .eq(HolidayVehicleFlow::getVehicleType, vehicleType) |
| | | .last("limit 1")); |
| | | if (existing == null) { |
| | | existing = new HolidayVehicleFlow(); |
| | | existing.setYear(year); |
| | | existing.setFlowDate(date); |
| | | existing.setRoadCategory(roadCategory); |
| | | existing.setVehicleType(vehicleType); |
| | | existing.setVehicleCount(value); |
| | | existing.setComparisonBase(comparisonBase); |
| | | existing.setCreatedAt(LocalDateTime.now()); |
| | | existing.setUpdatedAt(LocalDateTime.now()); |
| | | vehicleFlowMapper.insert(existing); |
| | | } else { |
| | | existing.setVehicleCount(value); |
| | | existing.setComparisonBase(comparisonBase); |
| | | existing.setUpdatedAt(LocalDateTime.now()); |
| | | vehicleFlowMapper.updateById(existing); |
| | | } |
| | | } |
| | | |
| | | private void upsertCoefficient(int year, String dateCategory, BigDecimal value) { |
| | | HolidayPassengerCoefficient existing = coefficientMapper.selectOne( |
| | | new LambdaQueryWrapper<HolidayPassengerCoefficient>() |
| | | .eq(HolidayPassengerCoefficient::getYear, year) |
| | | .eq(HolidayPassengerCoefficient::getDateCategory, dateCategory) |
| | | .last("limit 1")); |
| | | if (existing == null) { |
| | | existing = new HolidayPassengerCoefficient(); |
| | | existing.setYear(year); |
| | | existing.setDateCategory(dateCategory); |
| | | existing.setCoefficient(value); |
| | | existing.setCreatedAt(LocalDateTime.now()); |
| | | existing.setUpdatedAt(LocalDateTime.now()); |
| | | coefficientMapper.insert(existing); |
| | | } else { |
| | | existing.setCoefficient(value); |
| | | existing.setUpdatedAt(LocalDateTime.now()); |
| | | coefficientMapper.updateById(existing); |
| | | } |
| | | } |
| | | |
| | | private Integer findYearColumn(Sheet sheet, FormulaEvaluator evaluator, int year, int startColumn, int endColumn) { |
| | | Row header = sheet.getRow(1); |
| | | if (header == null) return null; |
| | | String wanted = year + "å¹´"; |
| | | DataFormatter formatter = new DataFormatter(); |
| | | for (int column = startColumn; column <= endColumn; column++) { |
| | | Cell cell = header.getCell(column); |
| | | if (cell == null) continue; |
| | | String text = formatter.formatCellValue(cell, evaluator).replace("\n", "").trim(); |
| | | if (wanted.equals(text) || text.startsWith(wanted + "累计")) return column; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private Integer parseDayIndex(Cell cell, FormulaEvaluator evaluator) { |
| | | if (cell == null) return null; |
| | | DataFormatter formatter = new DataFormatter(); |
| | | String text = formatter.formatCellValue(cell, evaluator).trim(); |
| | | java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("第\\s*(\\d+)\\s*æ¥").matcher(text); |
| | | return matcher.find() ? Integer.valueOf(matcher.group(1)) : null; |
| | | } |
| | | |
| | | private BigDecimal numberValue(Cell cell, FormulaEvaluator evaluator) { |
| | | if (cell == null) return null; |
| | | CellType type = cell.getCellType(); |
| | | if (type == CellType.FORMULA) { |
| | | type = evaluator.evaluateFormulaCell(cell); |
| | | } |
| | | if (type == CellType.BLANK) return null; |
| | | if (type == CellType.NUMERIC) return HolidayNumberUtils.excelValue(cell.getNumericCellValue()); |
| | | if (type == CellType.STRING) { |
| | | String text = cell.getStringCellValue().trim().replace(",", ""); |
| | | if (text.isEmpty() || "-".equals(text) || "â".equals(text)) return null; |
| | | try { |
| | | return HolidayNumberUtils.decimalValue(text); |
| | | } catch (NumberFormatException e) { |
| | | throw new IllegalArgumentException("åå
æ ¼ä¸æ¯æææ°å: " + text); |
| | | } |
| | | } |
| | | if (type == CellType.BOOLEAN) throw new IllegalArgumentException("åå
æ ¼ä¸è½æ¯å¸å°å¼"); |
| | | throw new IllegalArgumentException("åå
æ ¼ä¸æ¯æææ°å"); |
| | | } |
| | | |
| | | private void setNumeric(Sheet sheet, int rowIndex, int columnIndex, BigDecimal value) { |
| | | if (value == null) return; |
| | | Row row = sheet.getRow(rowIndex); |
| | | if (row == null) row = sheet.createRow(rowIndex); |
| | | Cell cell = row.getCell(columnIndex); |
| | | if (cell == null) cell = row.createCell(columnIndex); |
| | | cell.setCellValue(value.doubleValue()); |
| | | } |
| | | |
| | | private void setFormula(Sheet sheet, int rowIndex, int columnIndex, String formula) { |
| | | Row row = sheet.getRow(rowIndex); |
| | | if (row == null) row = sheet.createRow(rowIndex); |
| | | Cell cell = row.getCell(columnIndex); |
| | | if (cell == null) cell = row.createCell(columnIndex); |
| | | // POI's setCellFormula rejects a leading '='; the helper accepts either form. |
| | | String normalized = formula.startsWith("=") ? formula.substring(1) : formula; |
| | | cell.setCellFormula(normalized); |
| | | } |
| | | |
| | | private Map<String, Object> toMap(HolidayCalendar calendar) { |
| | | Map<String, Object> params = new LinkedHashMap<>(); |
| | | params.put("year", calendar.getYear()); |
| | | params.put("holidayType", calendar.getHolidayType()); |
| | | params.put("holidayDate", calendar.getHolidayDate().toString()); |
| | | params.put("dayIndex", calendar.getDayIndex()); |
| | | return params; |
| | | } |
| | | |
| | | @Data |
| | | public static class ImportSummary { |
| | | private int passengerRows; |
| | | private int vehicleRows; |
| | | private int skippedRows; |
| | | private boolean confirmationRequired; |
| | | private final List<String> errors = new ArrayList<>(); |
| | | private final List<String> historicalChanges = new ArrayList<>(); |
| | | |
| | | public void incrementPassenger() { |
| | | passengerRows++; |
| | | } |
| | | |
| | | public void incrementVehicle() { |
| | | vehicleRows++; |
| | | } |
| | | |
| | | public void incrementSkipped() { |
| | | skippedRows++; |
| | | } |
| | | |
| | | public void addError(String message) { |
| | | errors.add(message); |
| | | } |
| | | |
| | | public void addHistoricalChange(String message) { |
| | | if (historicalChanges.size() < 200) { |
| | | historicalChanges.add(message); |
| | | } |
| | | } |
| | | |
| | | public int getSuccessRows() { |
| | | return passengerRows + vehicleRows; |
| | | } |
| | | |
| | | public int getFailRows() { |
| | | return errors.size(); |
| | | } |
| | | } |
| | | } |
| 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 lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | |
| | | /** Daily observation-station operation averages used by the holiday trend report. */ |
| | | @Data |
| | | public class ObservationDailyOperation { |
| | | private LocalDate obsDate; |
| | | private BigDecimal averageSpeed; |
| | | private BigDecimal averageCongestion; |
| | | private Integer stationCount; |
| | | } |
| 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.dto.ObservationDailyOperation; |
| | | import com.trafficaudit.observation.entity.ObservationStationFlow; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.apache.ibatis.annotations.Param; |
| | | import org.apache.ibatis.annotations.Select; |
| | | |
| | | import java.util.List; |
| | | import java.time.LocalDate; |
| | | |
| | | @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(); |
| | | |
| | | @Select("SELECT obs_date, COUNT(*) AS station_count, " |
| | | + "AVG(vehicle_speed) AS average_speed, " |
| | | + "AVG(congestion_degree) AS average_congestion " |
| | | + "FROM observation_station_flow " |
| | | + "WHERE obs_date BETWEEN #{startDate} AND #{endDate} " |
| | | + "GROUP BY obs_date ORDER BY obs_date ASC") |
| | | List<ObservationDailyOperation> selectDailyOperations(@Param("startDate") LocalDate startDate, |
| | | @Param("endDate") LocalDate endDate); |
| | | } |
| 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(); |
| | | } |
| | | } |
| | |
| | | .authorizeRequests() |
| | | // ç»å½åå¿
é¡»å¯ç¨çæ¥å£ |
| | | .antMatchers("/api/auth/login", "/api/auth/captcha", "/api/auth/captcha/verify", |
| | | "/api/auth/captcha-config").permitAll() |
| | | "/api/auth/captcha-config", "/api/version").permitAll() |
| | | // å端éæèµæº |
| | | .antMatchers("/", "/index.html", "/favicon.ico", "/js/**", "/css/**", "/fonts/**", "/img/**", "/static/**").permitAll() |
| | | .anyRequest().permitAll() |
| | |
| | | .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); |
| | | return http.build(); |
| | | } |
| | | } |
| | | } |
| | |
| | | "/api/auth/login", |
| | | "/api/auth/captcha", |
| | | "/api/auth/captcha/verify", |
| | | "/api/auth/captcha-config" |
| | | "/api/auth/captcha-config", |
| | | "/api/version" |
| | | ); |
| | | |
| | | @Resource |
| | |
| | | request.setAttribute("loginUser", username); |
| | | chain.doFilter(request, response); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.system.controller; |
| | | |
| | | import com.trafficaudit.common.Result; |
| | | import org.springframework.core.io.ClassPathResource; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import java.io.InputStream; |
| | | import java.net.URI; |
| | | import java.nio.file.Files; |
| | | import java.nio.file.Path; |
| | | import java.nio.file.Paths; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import java.util.Properties; |
| | | |
| | | /** |
| | | * Exposes the product version so the UI (and operators) can confirm which build |
| | | * is actually running on a server. Kept public on purpose: the login page shows |
| | | * the version before any token exists. |
| | | */ |
| | | @RestController |
| | | @RequestMapping("/api") |
| | | public class VersionController { |
| | | |
| | | private static final String VERSION_FILE = "app-version.properties"; |
| | | |
| | | @GetMapping("/version") |
| | | public Result<Map<String, Object>> version() { |
| | | Map<String, Object> info = new LinkedHashMap<>(); |
| | | info.put("version", readVersion()); |
| | | info.put("buildTime", resolveBuildTime()); |
| | | return Result.ok(info); |
| | | } |
| | | |
| | | private String readVersion() { |
| | | try (InputStream in = new ClassPathResource(VERSION_FILE).getInputStream()) { |
| | | Properties props = new Properties(); |
| | | props.load(in); |
| | | String value = props.getProperty("version"); |
| | | return value == null || value.trim().isEmpty() ? "unknown" : value.trim(); |
| | | } catch (Exception e) { |
| | | return "unknown"; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Build time of the running artifact. In a packaged run this is the jar file's |
| | | * last-modified time; when running from exploded classes (mvn spring-boot:run) |
| | | * there is no jar, so the value is null. |
| | | */ |
| | | private String resolveBuildTime() { |
| | | try { |
| | | URI location = VersionController.class.getProtectionDomain() |
| | | .getCodeSource().getLocation().toURI(); |
| | | Path path = resolveArtifactPath(location); |
| | | if (path != null && Files.isRegularFile(path)) { |
| | | return Files.getLastModifiedTime(path).toInstant().toString(); |
| | | } |
| | | } catch (Exception ignored) { |
| | | // Fall through: build time is optional information. |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * Spring Boot executable jars expose a nested classpath such as |
| | | * "jar:file:/app.jar!/BOOT-INF/classes!/". Strip the nested part before |
| | | * asking the filesystem for the jar timestamp. |
| | | */ |
| | | private Path resolveArtifactPath(URI location) { |
| | | String value = location.toString(); |
| | | int nested = value.indexOf("!/"); |
| | | if (nested >= 0) { |
| | | value = value.substring(0, nested); |
| | | } |
| | | if (value.startsWith("jar:")) { |
| | | value = value.substring(4); |
| | | } |
| | | return Paths.get(URI.create(value)); |
| | | } |
| | | } |
| New file |
| | |
| | | # Supplied from pom.xml during Maven resource filtering. |
| | | # Bump <version> in pom.xml on every release; do not hard-code a version here. |
| | | version=@project.version@ |
| | |
| | | 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", 1)); |
| | | assertEquals("-4.4%", ratio(service, "95.551398", "100", 1)); |
| | | assertEquals("-4.5%", ratio(service, "95.551398", "100", 2)); |
| | | assertEquals("-0.1%", ratio(service, "99.955", "100", 2)); |
| | | assertEquals("0%", ratio(service, "99.955", "100", 1)); |
| | | assertEquals("6.7%", ratio(service, "106.649", "100", 2)); |
| | | assertEquals("6.6%", ratio(service, "106.649", "100", 1)); |
| | | assertEquals("1.0%", ratio(service, "101", "100", 1)); |
| | | assertEquals("0%", ratio(service, "100", "100", 1)); |
| | | assertEquals("0%", ratio(service, "10004", "10000", 1)); |
| | | assertEquals("â", ratio(service, "100", null, 1)); |
| | | assertEquals("â", ratio(service, "100", "0", 1)); |
| | | } |
| | | |
| | | @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 cumulativeParagraphUsesExcelDisplayThenBulletinRounding() 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.5%ï¼æ¯2024å¹´åæä¸é1.6%ã", |
| | | text); |
| | | } |
| | | |
| | | @Test |
| | | void cumulativeAmountsUseExcelYiDisplayBeforeWanRounding() throws Exception { |
| | | HolidayBulletinService service = new HolidayBulletinService(null); |
| | | |
| | | assertEquals("3961.77", cumulativeNumber(service, "3961.76472")); |
| | | assertEquals("4189.32", cumulativeNumber(service, "4189.31472")); |
| | | assertEquals("4486.63", cumulativeNumber(service, "4486.62652")); |
| | | } |
| | | |
| | | private String cumulativeNumber(HolidayBulletinService service, String value) throws Exception { |
| | | Method method = HolidayBulletinService.class.getDeclaredMethod("numberCumulative", BigDecimal.class); |
| | | method.setAccessible(true); |
| | | BigDecimal number = value == null ? null : new BigDecimal(value); |
| | | return (String) method.invoke(service, number); |
| | | } |
| | | |
| | | 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, |
| | | int excelPercentScale) throws Exception { |
| | | Method method = HolidayBulletinService.class.getDeclaredMethod( |
| | | "ratio", BigDecimal.class, BigDecimal.class, int.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, excelPercentScale); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.trafficaudit.holiday.HolidayConstants; |
| | | import com.trafficaudit.holiday.HolidayNumberUtils; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerFlow; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleFlow; |
| | | import com.trafficaudit.holiday.mapper.HolidayPassengerFlowMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayVehicleAdjustmentMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import java.lang.reflect.Field; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNotEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | /** |
| | | * Ring-ratio denominators are built one day at a time. The Excel comparison base (column C) is |
| | | * consumed on the source day and inherited by the next day's denominator, while historical |
| | | * adjustment records change only that historical day's same-day value. |
| | | */ |
| | | class HolidayComparisonBaseTest { |
| | | |
| | | @Test |
| | | void comparisonBaseChangesOnlyTheNextDayRingRatioDenominator() throws Exception { |
| | | HolidayStatService service = serviceWithData( |
| | | 2026, |
| | | passengerRows(2026, "50", "10", "6", "3"), |
| | | vehicleRows(2026), |
| | | Collections.<HolidayVehicleAdjustment>emptyList()); |
| | | |
| | | List<Map<String, Object>> rows = service.buildOverviewRows( |
| | | 2026, HolidayConstants.TYPE_NATIONAL_DAY, calendar(2026)); |
| | | |
| | | assertEquals(new BigDecimal("323.7837"), rows.get(2).get("nationalProvincial")); |
| | | assertEquals(new BigDecimal("320.2418"), rows.get(2).get("comparisonBase")); |
| | | assertEquals(new BigDecimal("322.7695"), rows.get(3).get("nationalProvincial")); |
| | | |
| | | BigDecimal nationalRatio = (BigDecimal) rows.get(3).get("nationalProvincialRingRatio"); |
| | | assertEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("320.2418")), nationalRatio); |
| | | assertNotEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("323.7837")), nationalRatio); |
| | | |
| | | HolidayStatService.DailyMetrics base = service.ringRatioBaseMetrics( |
| | | 2026, HolidayConstants.TYPE_NATIONAL_DAY, 4); |
| | | assertNotNull(base); |
| | | assertEquals(new BigDecimal("320.2418"), base.getNationalProvincial()); |
| | | assertDecimalEquals("1008.58032", base.getNonBusinessPersonFlow()); |
| | | assertDecimalEquals("1058.58032", base.getRoadPersonFlow()); |
| | | assertDecimalEquals("1077.58032", base.getTotalPersonFlow()); |
| | | } |
| | | |
| | | @Test |
| | | void historicalAdjustmentChangesTheHistoricalSameDayValue() throws Exception { |
| | | HolidayVehicleAdjustment adjustment = new HolidayVehicleAdjustment(); |
| | | adjustment.setYear(2025); |
| | | adjustment.setFlowDate(LocalDate.of(2025, 10, 4)); |
| | | adjustment.setAdjustedValue(new BigDecimal("337.5009")); |
| | | |
| | | HolidayStatService service = serviceWithData( |
| | | 2025, |
| | | passengerRows(2025, "50", "10", "6", "3"), |
| | | Arrays.asList( |
| | | vehicle("2025-10-03", HolidayConstants.ROAD_EXPRESSWAY, "100", null), |
| | | vehicle("2025-10-03", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "362.0035", null), |
| | | vehicle("2025-10-04", HolidayConstants.ROAD_EXPRESSWAY, "110", null), |
| | | vehicle("2025-10-04", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "350.0000", null)), |
| | | Collections.singletonList(adjustment)); |
| | | |
| | | List<Map<String, Object>> rows = service.buildOverviewRows( |
| | | 2025, HolidayConstants.TYPE_NATIONAL_DAY, calendar(2025)); |
| | | |
| | | assertEquals(new BigDecimal("337.5009"), rows.get(3).get("nationalProvincial")); |
| | | } |
| | | |
| | | private static HolidayStatService serviceWithData(int year, |
| | | List<HolidayPassengerFlow> passengerRows, |
| | | List<HolidayVehicleFlow> vehicleRows, |
| | | List<HolidayVehicleAdjustment> adjustments) throws Exception { |
| | | HolidayConfigService configService = mock(HolidayConfigService.class); |
| | | HolidayPassengerFlowMapper passengerMapper = mock(HolidayPassengerFlowMapper.class); |
| | | HolidayVehicleFlowMapper vehicleMapper = mock(HolidayVehicleFlowMapper.class); |
| | | HolidayVehicleAdjustmentMapper adjustmentMapper = mock(HolidayVehicleAdjustmentMapper.class); |
| | | |
| | | when(configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY)).thenReturn(new BigDecimal("2.4")); |
| | | when(configService.findHolidayDays(year, HolidayConstants.TYPE_NATIONAL_DAY)).thenReturn(calendar(year)); |
| | | when(passengerMapper.selectList(any())).thenReturn(passengerRows); |
| | | when(vehicleMapper.selectList(any())).thenReturn(vehicleRows); |
| | | when(adjustmentMapper.selectList(any())).thenReturn(adjustments); |
| | | |
| | | HolidayStatService service = new HolidayStatService(); |
| | | inject(service, "configService", configService); |
| | | inject(service, "passengerFlowMapper", passengerMapper); |
| | | inject(service, "vehicleFlowMapper", vehicleMapper); |
| | | inject(service, "vehicleAdjustmentMapper", adjustmentMapper); |
| | | return service; |
| | | } |
| | | |
| | | private static BigDecimal ratio(BigDecimal current, BigDecimal base) { |
| | | return HolidayNumberUtils.normalize(HolidayNumberUtils.divide(current, base).subtract(BigDecimal.ONE)); |
| | | } |
| | | |
| | | private static void assertDecimalEquals(String expected, BigDecimal actual) { |
| | | assertNotNull(actual); |
| | | assertEquals(0, new BigDecimal(expected).compareTo(actual)); |
| | | } |
| | | |
| | | private static List<HolidayVehicleFlow> vehicleRows(int year) { |
| | | List<HolidayVehicleFlow> rows = new ArrayList<>(); |
| | | rows.add(vehicle(year + "-10-03", HolidayConstants.ROAD_EXPRESSWAY, "100", null)); |
| | | rows.add(vehicle(year + "-10-03", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "323.7837", "320.2418")); |
| | | rows.add(vehicle(year + "-10-04", HolidayConstants.ROAD_EXPRESSWAY, "110", null)); |
| | | rows.add(vehicle(year + "-10-04", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "322.7695", null)); |
| | | return rows; |
| | | } |
| | | |
| | | private static HolidayVehicleFlow vehicle(String date, String roadCategory, String value, String base) { |
| | | HolidayVehicleFlow row = new HolidayVehicleFlow(); |
| | | row.setYear(LocalDate.parse(date).getYear()); |
| | | row.setFlowDate(LocalDate.parse(date)); |
| | | row.setRoadCategory(roadCategory); |
| | | row.setVehicleType(HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER); |
| | | row.setVehicleCount(new BigDecimal(value)); |
| | | row.setComparisonBase(base == null ? null : new BigDecimal(base)); |
| | | return row; |
| | | } |
| | | |
| | | private static List<HolidayPassengerFlow> passengerRows(int year, String road, String rail, |
| | | String civilAviation, String water) { |
| | | List<HolidayPassengerFlow> rows = new ArrayList<>(); |
| | | rows.add(passenger(year + "-10-03", HolidayConstants.MODE_ROAD, road)); |
| | | rows.add(passenger(year + "-10-03", HolidayConstants.MODE_RAIL, rail)); |
| | | rows.add(passenger(year + "-10-03", HolidayConstants.MODE_CIVIL_AVIATION, civilAviation)); |
| | | rows.add(passenger(year + "-10-03", HolidayConstants.MODE_WATER, water)); |
| | | rows.add(passenger(year + "-10-04", HolidayConstants.MODE_ROAD, road)); |
| | | rows.add(passenger(year + "-10-04", HolidayConstants.MODE_RAIL, rail)); |
| | | rows.add(passenger(year + "-10-04", HolidayConstants.MODE_CIVIL_AVIATION, civilAviation)); |
| | | rows.add(passenger(year + "-10-04", HolidayConstants.MODE_WATER, water)); |
| | | return rows; |
| | | } |
| | | |
| | | private static HolidayPassengerFlow passenger(String date, String mode, String value) { |
| | | HolidayPassengerFlow row = new HolidayPassengerFlow(); |
| | | row.setYear(LocalDate.parse(date).getYear()); |
| | | row.setFlowDate(LocalDate.parse(date)); |
| | | row.setTransportMode(mode); |
| | | row.setPassengerCount(new BigDecimal(value)); |
| | | return row; |
| | | } |
| | | |
| | | private static List<HolidayCalendar> calendar(int year) { |
| | | List<HolidayCalendar> days = new ArrayList<>(); |
| | | for (int i = 1; i <= 4; i++) { |
| | | HolidayCalendar day = new HolidayCalendar(); |
| | | day.setYear(year); |
| | | day.setHolidayType(HolidayConstants.TYPE_NATIONAL_DAY); |
| | | day.setDayIndex(i); |
| | | day.setHolidayDate(LocalDate.of(year, 10, i)); |
| | | days.add(day); |
| | | } |
| | | return days; |
| | | } |
| | | |
| | | private static void inject(Object target, String field, Object value) throws Exception { |
| | | Field declared = target.getClass().getDeclaredField(field); |
| | | declared.setAccessible(true); |
| | | declared.set(target, value); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import javax.imageio.ImageIO; |
| | | import java.awt.image.BufferedImage; |
| | | import java.io.ByteArrayInputStream; |
| | | import java.math.BigDecimal; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertArrayEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertFalse; |
| | | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | |
| | | class HolidayTrendChartServiceTest { |
| | | |
| | | @Test |
| | | void rendersLinePieAndStackedChartsAsPng() throws Exception { |
| | | HolidayTrendChartService service = new HolidayTrendChartService(); |
| | | HolidayTrendChartService.ChartSeries first = new HolidayTrendChartService.ChartSeries(); |
| | | first.setName("2026å¹´"); |
| | | first.setValues(java.util.Arrays.asList( |
| | | java.math.BigDecimal.ONE, java.math.BigDecimal.TEN, null)); |
| | | HolidayTrendChartService.ChartSeries second = new HolidayTrendChartService.ChartSeries(); |
| | | second.setName("2025å¹´"); |
| | | second.setValues(java.util.Arrays.asList( |
| | | java.math.BigDecimal.valueOf(2), java.math.BigDecimal.valueOf(8), |
| | | java.math.BigDecimal.valueOf(4))); |
| | | |
| | | byte[] line = service.lineChart("ä¸äººæ¬¡", java.util.Arrays.asList(first, second), false); |
| | | assertPng(line); |
| | | assertNoLargeLightGrayBackground(line); |
| | | |
| | | HolidayTrendChartService.ChartCategory category = new HolidayTrendChartService.ChartCategory(); |
| | | category.setName("éè·¯"); |
| | | category.setValue(java.math.BigDecimal.TEN); |
| | | byte[] pie = service.pieChart("人次", java.util.Collections.singletonList(category), 400, 300); |
| | | assertPng(pie); |
| | | |
| | | byte[] stacked = service.stackedBarChart("å æ¯", java.util.Arrays.asList("10æ1æ¥", "10æ2æ¥"), |
| | | java.util.Arrays.asList(first, second), true, 500, 320); |
| | | assertPng(stacked); |
| | | assertNoLargeLightGrayBackground(stacked); |
| | | } |
| | | |
| | | @Test |
| | | void roundsLineDataToTheSamePrecisionAsLabels() { |
| | | assertEquals(new BigDecimal("0.49"), |
| | | HolidayTrendChartService.displayValue(new BigDecimal("0.4904"), false, 2)); |
| | | assertEquals(new BigDecimal("0.49"), |
| | | HolidayTrendChartService.displayValue(new BigDecimal("0.4911"), false, 2)); |
| | | assertEquals(new BigDecimal("0.491"), |
| | | HolidayTrendChartService.displayValue(new BigDecimal("0.4911"), false, 3)); |
| | | assertEquals(new BigDecimal("0.123"), |
| | | HolidayTrendChartService.displayValue(new BigDecimal("0.1234"), true, 2)); |
| | | } |
| | | |
| | | @Test |
| | | void rendersSameGeometryWhenValuesRoundToSameLabel() { |
| | | HolidayTrendChartService service = new HolidayTrendChartService(); |
| | | HolidayTrendChartService.ChartSeries first = new HolidayTrendChartService.ChartSeries(); |
| | | first.setName("平忥æ¤åº¦"); |
| | | first.setValues(java.util.Arrays.asList( |
| | | new BigDecimal("0.4904"), new BigDecimal("0.4923"), new BigDecimal("0.5100"))); |
| | | HolidayTrendChartService.ChartSeries second = new HolidayTrendChartService.ChartSeries(); |
| | | second.setName("平忥æ¤åº¦"); |
| | | second.setValues(java.util.Arrays.asList( |
| | | new BigDecimal("0.4911"), new BigDecimal("0.4934"), new BigDecimal("0.5100"))); |
| | | |
| | | byte[] firstChart = service.lineChart("æ¥æ¤åº¦", |
| | | java.util.Collections.singletonList(first), false, 2, 1100, 590); |
| | | byte[] secondChart = service.lineChart("æ¥æ¤åº¦", |
| | | java.util.Collections.singletonList(second), false, 2, 1100, 590); |
| | | |
| | | assertPng(firstChart); |
| | | assertPng(secondChart); |
| | | assertArrayEquals(firstChart, secondChart); |
| | | } |
| | | |
| | | @Test |
| | | void scalesLineChartHeightForBetterVerticalSeparation() throws Exception { |
| | | HolidayTrendChartService service = new HolidayTrendChartService(); |
| | | HolidayTrendChartService.ChartSeries series = new HolidayTrendChartService.ChartSeries(); |
| | | series.setName("æµè¯"); |
| | | series.setValues(java.util.Arrays.asList( |
| | | new BigDecimal("1.00"), new BigDecimal("2.00"), new BigDecimal("3.00"))); |
| | | |
| | | byte[] png = service.lineChart("ä¸äººæ¬¡", |
| | | java.util.Collections.singletonList(series), false, 1100, 590); |
| | | BufferedImage image = ImageIO.read(new ByteArrayInputStream(png)); |
| | | |
| | | assertNotNull(image); |
| | | assertEquals(HolidayTrendChartService.scaleLineChartHeight(590), image.getHeight()); |
| | | } |
| | | |
| | | @Test |
| | | void rendersHigherPrecisionLineLabelsWhenRequested() { |
| | | HolidayTrendChartService service = new HolidayTrendChartService(); |
| | | HolidayTrendChartService.ChartSeries congestion = new HolidayTrendChartService.ChartSeries(); |
| | | congestion.setName("平忥æ¤åº¦"); |
| | | congestion.setValues(java.util.Arrays.asList( |
| | | new java.math.BigDecimal("0.467029"), |
| | | new java.math.BigDecimal("0.490890"), |
| | | new java.math.BigDecimal("0.492414"))); |
| | | |
| | | byte[] twoDecimals = service.lineChart("æ¥æ¤åº¦", |
| | | java.util.Collections.singletonList(congestion), false, 2, 1100, 590); |
| | | byte[] threeDecimals = service.lineChart("æ¥æ¤åº¦", |
| | | java.util.Collections.singletonList(congestion), false, 3, 1100, 590); |
| | | |
| | | assertPng(twoDecimals); |
| | | assertPng(threeDecimals); |
| | | assertFalse(java.util.Arrays.equals(twoDecimals, threeDecimals)); |
| | | } |
| | | |
| | | private void assertNoLargeLightGrayBackground(byte[] data) throws Exception { |
| | | BufferedImage image = ImageIO.read(new ByteArrayInputStream(data)); |
| | | int matches = 0; |
| | | for (int y = 0; y < image.getHeight(); y++) { |
| | | for (int x = 0; x < image.getWidth(); x++) { |
| | | int rgb = image.getRGB(x, y) & 0xFFFFFF; |
| | | if (rgb == 0xF3F4F3) matches++; |
| | | } |
| | | } |
| | | assertTrue(matches < image.getWidth() * image.getHeight() / 20, |
| | | "chart still contains a large light-gray background area"); |
| | | } |
| | | |
| | | private void assertPng(byte[] data) { |
| | | assertNotNull(data); |
| | | assertTrue(data.length > 100); |
| | | assertTrue(data[0] == (byte) 0x89 && data[1] == 0x50 |
| | | && data[2] == 0x4e && data[3] == 0x47); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import java.io.ByteArrayInputStream; |
| | | import java.nio.file.Files; |
| | | import java.nio.file.Path; |
| | | import java.nio.file.Paths; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.List; |
| | | import java.util.zip.ZipEntry; |
| | | import java.util.zip.ZipInputStream; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | |
| | | class HolidayTrendWordServiceTest { |
| | | |
| | | @Test |
| | | void rebuildsTemplateWithSectionsChartsAndAppendix() throws Exception { |
| | | Path template = resolveTemplate(); |
| | | byte[] templateBytes = Files.readAllBytes(template); |
| | | HolidayTrendWordService service = new HolidayTrendWordService(); |
| | | |
| | | List<HolidayTrendWordService.DocumentBlock> blocks = new ArrayList<>(); |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("ä¸ãæ»ä½æ
åµ")); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock("å®¢æµæ»é", png(1), |
| | | new HolidayTrendWordService.ImageSize(5000000L, 3000000L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock("客æµå æ¯", png(2), true, |
| | | new HolidayTrendWordService.ImageSize(4000000L, 2500000L))); |
| | | blocks.add(new HolidayTrendWordService.PageBreakBlock()); |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("éå½ä¸ æ¯æ¥å®¢æµææåå")); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock("æ¯æ¥ææ", png(3), true)); |
| | | |
| | | byte[] result = service.generate(templateBytes, 2026, blocks); |
| | | List<String> entries = entries(result); |
| | | assertTrue(entries.contains("word/media/image1.png")); |
| | | assertTrue(entries.contains("word/media/image2.png")); |
| | | assertTrue(entries.contains("word/media/image3.png")); |
| | | assertEquals(3, entries.stream().filter(name -> name.endsWith(".png")).count()); |
| | | |
| | | String documentXml = readEntry(result, "word/document.xml"); |
| | | assertTrue(documentXml.contains("ä¸ãæ»ä½æ
åµ")); |
| | | assertTrue(documentXml.contains("éå½ä¸ æ¯æ¥å®¢æµææåå")); |
| | | assertTrue(documentXml.contains("å®¢æµæ»é")); |
| | | assertTrue(documentXml.contains("æ¯æ¥ææ")); |
| | | assertTrue(!paragraphHasNumbering(documentXml, "ä¸ãæ»ä½æ
åµ")); |
| | | assertTrue(!paragraphHasNumbering(documentXml, "éå½ä¸ æ¯æ¥å®¢æµææåå")); |
| | | assertTrue(documentXml.contains("cx=\"5000000\" cy=\"3000000\"")); |
| | | assertTrue(documentXml.contains("cx=\"4000000\" cy=\"2500000\"")); |
| | | assertEquals(2, pageBreakCount(documentXml)); |
| | | |
| | | String rels = readEntry(result, "word/_rels/document.xml.rels"); |
| | | assertTrue(rels.contains("rId100")); |
| | | assertTrue(rels.contains("rId102")); |
| | | } |
| | | |
| | | private Path resolveTemplate() { |
| | | Path direct = Paths.get("docs/ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾.docx"); |
| | | if (Files.exists(direct)) return direct; |
| | | return Paths.get("..", "docs", "ä¸é¡¹ç»è®¡", "éè¦è忥", "2026å¹´åä¸åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾.docx"); |
| | | } |
| | | |
| | | private boolean paragraphHasNumbering(String documentXml, String text) throws Exception { |
| | | javax.xml.parsers.DocumentBuilderFactory factory = |
| | | javax.xml.parsers.DocumentBuilderFactory.newInstance(); |
| | | factory.setNamespaceAware(true); |
| | | org.w3c.dom.Document document = factory.newDocumentBuilder().parse( |
| | | new java.io.ByteArrayInputStream(documentXml.getBytes("UTF-8"))); |
| | | org.w3c.dom.NodeList paragraphs = document.getElementsByTagNameNS( |
| | | "http://schemas.openxmlformats.org/wordprocessingml/2006/main", "p"); |
| | | for (int i = 0; i < paragraphs.getLength(); i++) { |
| | | org.w3c.dom.Element paragraph = (org.w3c.dom.Element) paragraphs.item(i); |
| | | StringBuilder builder = new StringBuilder(); |
| | | org.w3c.dom.NodeList texts = paragraph.getElementsByTagNameNS( |
| | | "http://schemas.openxmlformats.org/wordprocessingml/2006/main", "t"); |
| | | for (int j = 0; j < texts.getLength(); j++) { |
| | | builder.append(texts.item(j).getTextContent()); |
| | | } |
| | | if (text.equals(builder.toString())) { |
| | | return paragraph.getElementsByTagNameNS( |
| | | "http://schemas.openxmlformats.org/wordprocessingml/2006/main", "numPr") |
| | | .getLength() > 0; |
| | | } |
| | | } |
| | | throw new AssertionError("Paragraph not found: " + text); |
| | | } |
| | | |
| | | private int pageBreakCount(String documentXml) throws Exception { |
| | | javax.xml.parsers.DocumentBuilderFactory factory = |
| | | javax.xml.parsers.DocumentBuilderFactory.newInstance(); |
| | | factory.setNamespaceAware(true); |
| | | org.w3c.dom.Document document = factory.newDocumentBuilder().parse( |
| | | new java.io.ByteArrayInputStream(documentXml.getBytes("UTF-8"))); |
| | | org.w3c.dom.NodeList breaks = document.getElementsByTagNameNS( |
| | | "http://schemas.openxmlformats.org/wordprocessingml/2006/main", "br"); |
| | | int count = 0; |
| | | for (int i = 0; i < breaks.getLength(); i++) { |
| | | org.w3c.dom.Element br = (org.w3c.dom.Element) breaks.item(i); |
| | | if ("page".equals(br.getAttributeNS( |
| | | "http://schemas.openxmlformats.org/wordprocessingml/2006/main", "type"))) { |
| | | count++; |
| | | } |
| | | } |
| | | return count; |
| | | } |
| | | |
| | | private byte[] png(int seed) { |
| | | byte[] bytes = new byte[256]; |
| | | Arrays.fill(bytes, (byte) seed); |
| | | bytes[0] = (byte) 0x89; |
| | | bytes[1] = 0x50; |
| | | bytes[2] = 0x4e; |
| | | bytes[3] = 0x47; |
| | | return bytes; |
| | | } |
| | | |
| | | private List<String> entries(byte[] zipBytes) throws Exception { |
| | | List<String> result = new ArrayList<>(); |
| | | try (ZipInputStream input = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { |
| | | ZipEntry entry; |
| | | while ((entry = input.getNextEntry()) != null) result.add(entry.getName()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private String readEntry(byte[] zipBytes, String name) throws Exception { |
| | | try (ZipInputStream input = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { |
| | | ZipEntry entry; |
| | | while ((entry = input.getNextEntry()) != null) { |
| | | if (name.equals(entry.getName())) { |
| | | java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); |
| | | byte[] buffer = new byte[1024]; |
| | | int length; |
| | | while ((length = input.read(buffer)) > 0) out.write(buffer, 0, length); |
| | | return out.toString("UTF-8"); |
| | | } |
| | | } |
| | | } |
| | | return ""; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.trafficaudit.holiday.HolidayConstants; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleAdjustment; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleFlow; |
| | | import com.trafficaudit.holiday.mapper.HolidayVehicleAdjustmentMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import java.lang.reflect.Field; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertNull; |
| | | import static org.junit.jupiter.api.Assertions.assertThrows; |
| | | import static org.mockito.ArgumentMatchers.any; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.when; |
| | | |
| | | class HolidayVehicleAdjustmentServiceTest { |
| | | |
| | | @Test |
| | | void listAddsOriginalNationalProvincialValueAndLeavesMissingSourceBlank() throws Exception { |
| | | HolidayVehicleAdjustment adjusted = adjustment("2025-10-04", "337.5009"); |
| | | HolidayVehicleAdjustment missingSource = adjustment("2025-10-05", "330.0000"); |
| | | |
| | | HolidayVehicleAdjustmentMapper adjustmentMapper = mock(HolidayVehicleAdjustmentMapper.class); |
| | | HolidayVehicleFlowMapper vehicleFlowMapper = mock(HolidayVehicleFlowMapper.class); |
| | | when(adjustmentMapper.selectList(any())).thenReturn(Arrays.asList(adjusted, missingSource)); |
| | | when(vehicleFlowMapper.selectList(any())).thenReturn(Collections.singletonList( |
| | | vehicleFlow("2025-10-04", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "350.0000"))); |
| | | |
| | | HolidayVehicleAdjustmentService service = new HolidayVehicleAdjustmentService(); |
| | | inject(service, "adjustmentMapper", adjustmentMapper); |
| | | inject(service, "vehicleFlowMapper", vehicleFlowMapper); |
| | | |
| | | List<HolidayVehicleAdjustment> rows = service.list(2025); |
| | | |
| | | assertEquals(0, new BigDecimal("350.0000").compareTo(rows.get(0).getOriginalValue())); |
| | | assertNull(rows.get(1).getOriginalValue()); |
| | | } |
| | | |
| | | @Test |
| | | void findOriginalValueReturnsMatchedValueAndNullWhenMissing() throws Exception { |
| | | HolidayVehicleFlowMapper vehicleFlowMapper = mock(HolidayVehicleFlowMapper.class); |
| | | when(vehicleFlowMapper.selectOne(any())) |
| | | .thenReturn(vehicleFlow("2025-10-04", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, "337.9501")) |
| | | .thenReturn(null); |
| | | |
| | | HolidayVehicleAdjustmentService service = new HolidayVehicleAdjustmentService(); |
| | | inject(service, "vehicleFlowMapper", vehicleFlowMapper); |
| | | |
| | | assertEquals(0, new BigDecimal("337.9501") |
| | | .compareTo(service.findOriginalValue(2025, LocalDate.of(2025, 10, 4)))); |
| | | assertNull(service.findOriginalValue(2025, LocalDate.of(2025, 10, 5))); |
| | | assertThrows(IllegalArgumentException.class, |
| | | () -> service.findOriginalValue(2025, LocalDate.of(2024, 10, 4))); |
| | | } |
| | | private static HolidayVehicleAdjustment adjustment(String date, String adjustedValue) { |
| | | HolidayVehicleAdjustment row = new HolidayVehicleAdjustment(); |
| | | row.setYear(LocalDate.parse(date).getYear()); |
| | | row.setFlowDate(LocalDate.parse(date)); |
| | | row.setAdjustedValue(new BigDecimal(adjustedValue)); |
| | | return row; |
| | | } |
| | | |
| | | private static HolidayVehicleFlow vehicleFlow(String date, String roadCategory, String value) { |
| | | HolidayVehicleFlow row = new HolidayVehicleFlow(); |
| | | row.setYear(LocalDate.parse(date).getYear()); |
| | | row.setFlowDate(LocalDate.parse(date)); |
| | | row.setRoadCategory(roadCategory); |
| | | row.setVehicleCount(new BigDecimal(value)); |
| | | return row; |
| | | } |
| | | |
| | | private static void inject(Object target, String fieldName, Object value) throws Exception { |
| | | Field field = target.getClass().getDeclaredField(fieldName); |
| | | field.setAccessible(true); |
| | | field.set(target, value); |
| | | } |
| | | } |
| | |
| | | <template> |
| | | <el-header class="app-header"> |
| | | <span class="header-title">{{ title }}</span> |
| | | <span class="header-title"> |
| | | {{ systemName }}<span class="header-version" v-if="version">V{{ version }}</span> |
| | | <template v-if="pageName"> - {{ pageName }}</template> |
| | | </span> |
| | | <div class="header-right"> |
| | | <span class="header-user"> |
| | | <i class="el-icon-user"></i> {{ username }} |
| | |
| | | }, |
| | | data() { |
| | | return { |
| | | version: '', |
| | | pwdDialogVisible: false, |
| | | pwdSaving: false, |
| | | pwdForm: { oldPassword: '', newPassword: '', confirmPassword: '' }, |
| | |
| | | computed: { |
| | | username() { |
| | | return sessionStorage.getItem('username') || 'admin' |
| | | }, |
| | | // The system name always comes first so the version can sit right after it. |
| | | systemName() { |
| | | return this.title.split(' - ')[0] |
| | | }, |
| | | pageName() { |
| | | const parts = this.title.split(' - ') |
| | | return parts.length > 1 ? parts.slice(1).join(' - ') : '' |
| | | } |
| | | }, |
| | | created() { |
| | | api.get('/version') |
| | | .then(res => { |
| | | if (res && res.data && res.data.version) this.version = res.data.version |
| | | }) |
| | | .catch(() => {}) |
| | | }, |
| | | methods: { |
| | | openChangePwd() { |
| | |
| | | line-height: normal; |
| | | } |
| | | .header-user { opacity: 0.92; } |
| | | .header-version { |
| | | font-size: 12px; |
| | | line-height: 1.6; |
| | | margin-left: 8px; |
| | | padding: 2px 8px; |
| | | border-radius: 8px; |
| | | background: rgba(255, 255, 255, 0.18); |
| | | } |
| | | .header-link { color: #fff; font-size: 14px; padding: 0; } |
| | | .header-link:hover { color: #ffd04b; } |
| | | .header-logout { color: #fff; font-size: 14px; padding: 0; } |
| | |
| | | |
| | | // å
¨å±ç»åºï¼æ¸
é¤ç»å½æå¹¶åç»å½é¡µï¼å页é¢ä¾§è¾¹æ ãéåºç»å½ãèåå
±ç¨ï¼ |
| | | Vue.mixin({ |
| | | computed: { |
| | | operatorIsAdmin() { |
| | | return sessionStorage.getItem('username') === 'admin' |
| | | } |
| | | }, |
| | | methods: { |
| | | // el-menu åè·¯ç±å¯¼èªï¼NavigationDuplicatedï¼æ¶ Element UI 伿é«äº®åæ»ä¸ºç¹å»åæ§å¼ï¼ |
| | | // ç¹å»åå»¶è¿ä¸ä¸ªå®ä»»å¡å¼ºå¶çº æ£ï¼å®ä»»å¡æäº push ç微任å¡åè°ï¼ä¿è¯æç»é«äº®æ£ç¡®ï¼ã |
| | |
| | | { path: '/audit', name: 'AuditResult', component: () => import('@/views/AuditResult.vue') }, |
| | | { path: '/report', name: 'ReportExport', component: () => import('@/views/ReportExport.vue') }, |
| | | { path: '/rules', name: 'RuleManage', component: () => import('@/views/RuleManage.vue') }, |
| | | { path: '/users', name: 'UserManage', component: () => import('@/views/UserManage.vue') } |
| | | { path: '/users', name: 'UserManage', component: () => import('@/views/UserManage.vue') }, |
| | | { path: '/holiday-stat', name: 'HolidayStat', component: () => import('@/views/HolidayStat.vue') }, |
| | | { path: '/system-config', name: 'SystemConfig', component: () => import('@/views/SystemConfig.vue'), meta: { requireAdmin: true } } |
| | | ] |
| | | }) |
| | | |
| | |
| | | const token = sessionStorage.getItem('token') |
| | | if (to.path === '/login') { |
| | | next(token ? '/dashboard' : undefined) |
| | | } else if (!token) { |
| | | next('/login') |
| | | } else if (to.meta && to.meta.requireAdmin && sessionStorage.getItem('username') !== 'admin') { |
| | | next('/dashboard') |
| | | } else { |
| | | next(token ? undefined : '/login') |
| | | next() |
| | | } |
| | | }) |
| | | |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | </el-menu> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main :class="{ 'main-focus': focusMode }" style="display:flex;flex-direction:column;min-height:0"> |
| | | <h3 v-show="!focusMode" style="flex:none;margin:0 0 10px">å®¡æ ¸ç»æ</h3> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item></el-menu> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | | <div class="page-scroll"> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | </el-menu> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | | <div class="page-scroll"> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | </el-menu> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | | <h3>导å
¥æ°æ®æ¥ç</h3> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | </el-menu> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | | <h3>ä¼ä¸è§£é</h3> |
| New file |
| | |
| | | <template> |
| | | <el-container direction="vertical"> |
| | | <app-header title="交éç»è®¡æ¥è¡¨å®¡æ ¸ç³»ç» - ä¸é¡¹ç»è®¡"></app-header> |
| | | <el-container> |
| | | <el-aside width="200px"> |
| | | <el-menu ref="sideMenu" router :default-active="$route.path" @select="onMenuSelect"> |
| | | <el-menu-item index="/dashboard">å·¥ä½å°</el-menu-item> |
| | | <el-menu-item index="/import">æ°æ®å¯¼å
¥</el-menu-item> |
| | | <el-menu-item index="/data">æ°æ®æ¥ç</el-menu-item> |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | | <div class="page-scroll"> |
| | | <h3>ä¸é¡¹ç»è®¡ - è忥</h3> |
| | | <div class="toolbar"> |
| | | <el-select v-model="year" style="width:120px" @change="loadOverview"> |
| | | <el-option v-for="item in years" :key="item" :label="item + 'å¹´'" :value="item"></el-option> |
| | | </el-select> |
| | | <el-select v-model="holidayType" style="width:150px" @change="loadOverview"> |
| | | <el-option v-for="item in holidayTypeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option> |
| | | </el-select> |
| | | <el-button icon="el-icon-refresh" @click="loadOverview">å·æ°</el-button> |
| | | <el-button icon="el-icon-download" @click="downloadTemplate">ä¸è½½æ¨¡æ¿</el-button> |
| | | </div> |
| | | |
| | | <el-card shadow="never" class="operation-card"> |
| | | <div class="operation-row"> |
| | | <div class="operation-item"> |
| | | <span class="label">导å
¥æ°æ®</span> |
| | | <el-upload action="#" :auto-upload="false" :show-file-list="true" :limit="1" |
| | | accept=".xlsx,.xls" :on-change="onFileChange" :on-remove="onFileRemove" :file-list="fileList"> |
| | | <el-button type="primary" plain icon="el-icon-folder-opened">éæ©æä»¶</el-button> |
| | | </el-upload> |
| | | <el-button type="primary" icon="el-icon-upload2" :loading="importing" :disabled="!selectedFile" @click="importData">导å
¥</el-button> |
| | | </div> |
| | | <div class="operation-item"> |
| | | <span class="label">å¯¼åºæ°æ®</span> |
| | | <el-button type="success" icon="el-icon-download" :loading="exporting" @click="exportData">å¯¼åº Excel</el-button> |
| | | </div> |
| | | <div class="operation-item"> |
| | | <span class="label">客æµå¿«æ¥</span> |
| | | <el-date-picker v-model="bulletinDate" type="date" value-format="yyyy-MM-dd" style="width:160px" placeholder="éæ©æ¥æ"></el-date-picker> |
| | | <el-button type="warning" icon="el-icon-document" :loading="bulletinLoading" @click="downloadBulletin">ä¸è½½å¿«æ¥</el-button> |
| | | </div> |
| | | <div class="operation-item"> |
| | | <span class="label">èµ°å¿å¾</span> |
| | | <el-button type="primary" icon="el-icon-picture-outline" :loading="trendLoading" @click="downloadTrend">ä¸è½½èµ°å¿å¾</el-button> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <div v-if="importMessage" class="import-message"> |
| | | <pre>{{ importMessage }}</pre> |
| | | </div> |
| | | |
| | | <el-card shadow="never" style="margin-top:14px"> |
| | | <div slot="header">{{ year }}å¹´{{ holidayTypeLabel }}èåæ¥æ°æ®</div> |
| | | <el-table :data="rows" border stripe size="small" v-loading="loading" style="width:100%"> |
| | | <el-table-column prop="dayIndex" label="ç¬¬å æ¥" width="80" align="center"></el-table-column> |
| | | <el-table-column prop="date" label="æ¥æ" width="120" align="center"></el-table-column> |
| | | <el-table-column label="éè·¯ï¼ä¸äººæ¬¡ï¼" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.rail) }}</template></el-table-column> |
| | | <el-table-column label="æ°èªï¼ä¸äººæ¬¡ï¼" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.civilAviation) }}</template></el-table-column> |
| | | <el-table-column label="éè·¯ï¼ä¸äººæ¬¡ï¼" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.road) }}</template></el-table-column> |
| | | <el-table-column label="æ°´è·¯ï¼ä¸äººæ¬¡ï¼" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.water) }}</template></el-table-column> |
| | | <el-table-column label="é«é车æµï¼ä¸è¾æ¬¡ï¼" width="150" align="right"><template slot-scope="s">{{ formatNumber(s.row.expressway) }}</template></el-table-column> |
| | | <el-table-column label="å½çé车æµï¼ä¸è¾æ¬¡ï¼" width="160" align="right"><template slot-scope="s">{{ formatNumber(s.row.nationalProvincial) }}</template></el-table-column> |
| | | <el-table-column label="å½çéç¯æ¯åºæï¼ä¸è¾æ¬¡ï¼" width="190" align="right"><template slot-scope="s">{{ formatNumber(s.row.comparisonBase) }}</template></el-table-column> |
| | | <el-table-column label="å½çé车æµç¯æ¯" width="140" align="right"><template slot-scope="s">{{ formatRatio(s.row.nationalProvincialRingRatio) }}</template></el-table-column> |
| | | <el-table-column label="客è¿ç³»æ°" width="100" align="right"><template slot-scope="s">{{ formatCoefficient(s.row.coefficient) }}</template></el-table-column> |
| | | <el-table-column label="éè¥ä¸æ§äººåæµå¨éï¼ä¸äººæ¬¡ï¼" min-width="200" align="right"><template slot-scope="s">{{ formatNumber(s.row.nonBusinessPersonFlow) }}</template></el-table-column> |
| | | <el-table-column label="æ»äººåæµå¨éï¼ä¸äººæ¬¡ï¼" min-width="180" align="right"><template slot-scope="s">{{ formatNumber(s.row.totalPersonFlow) }}</template></el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <el-card shadow="never" style="margin-top:14px"> |
| | | <div slot="header"> |
| | | <span>å¾å¹´å½çéæ°æ®è°æ´</span> |
| | | </div> |
| | | <div class="operation-row"> |
| | | <div class="operation-item"> |
| | | <span class="label">年份</span> |
| | | <el-select v-model="adjustmentYear" style="width:120px" @change="loadAdjustments"> |
| | | <el-option v-for="item in availableAdjustmentYears" :key="item" :label="item + 'å¹´'" :value="item"></el-option> |
| | | </el-select> |
| | | </div> |
| | | <div v-if="operatorIsAdmin" class="operation-item"> |
| | | <el-button type="primary" icon="el-icon-plus" @click="openAdjustmentDialog()">æ°å¢è°æ´</el-button> |
| | | </div> |
| | | </div> |
| | | <el-table :data="adjustments" border stripe size="small" style="margin-top:12px" v-loading="adjustmentLoading"> |
| | | <el-table-column prop="year" label="年份" width="90" align="center"></el-table-column> |
| | | <el-table-column prop="flowDate" label="æ¥æ" width="130" align="center"></el-table-column> |
| | | <el-table-column label="è°æ´åå½çé车æµéï¼ä¸è¾æ¬¡ï¼" width="240" align="right"> |
| | | <template slot-scope="s">{{ formatOriginalAdjustmentValue(s.row.originalValue) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="è°æ´åå½çé车æµéï¼ä¸è¾æ¬¡ï¼" width="240" align="right"> |
| | | <template slot-scope="s">{{ formatAdjustmentValue(s.row.adjustedValue) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="reason" label="è°æ´åå " min-width="280" show-overflow-tooltip></el-table-column> |
| | | <el-table-column label="æ´æ°æ¶é´" min-width="170"><template slot-scope="s">{{ formatDateTime(s.row.updatedAt) }}</template></el-table-column> |
| | | <el-table-column v-if="operatorIsAdmin" label="æä½" width="130" align="center"> |
| | | <template slot-scope="s"> |
| | | <el-button type="text" @click="openAdjustmentDialog(s.row)">ç¼è¾</el-button> |
| | | <el-button type="text" class="danger-text" @click="deleteAdjustment(s.row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <el-dialog :title="adjustmentForm.id ? 'ç¼è¾è°æ´' : 'æ°å¢è°æ´'" :visible.sync="adjustmentDialogVisible" width="560px" v-dialog-drag> |
| | | <el-form label-width="220px"> |
| | | <el-form-item label="年份"> |
| | | <el-select v-model="adjustmentForm.year" style="width:100%" @change="onAdjustmentYearChange"> |
| | | <el-option v-for="item in availableAdjustmentYears" :key="item" :label="item + 'å¹´'" :value="item"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥æ"> |
| | | <el-date-picker v-model="adjustmentForm.flowDate" type="date" value-format="yyyy-MM-dd" |
| | | style="width:100%" :picker-options="adjustmentDatePickerOptions" placeholder="éæ©æ¥æ" |
| | | @change="onAdjustmentDateChange"></el-date-picker> |
| | | </el-form-item> |
| | | <el-form-item label="è°æ´åå½çé车æµéï¼ä¸è¾æ¬¡ï¼"> |
| | | <el-input :value="formatOriginalAdjustmentValue(adjustmentForm.originalValue)" readonly |
| | | :placeholder="adjustmentOriginalLoading ? 'æ¥è¯¢ä¸...' : ''"></el-input> |
| | | </el-form-item> |
| | | <el-form-item label="è°æ´å车æµéï¼ä¸è¾æ¬¡ï¼"> |
| | | <el-input-number v-model="adjustmentForm.adjustedValue" :min="0" :precision="4" :step="0.0001" controls-position="right" style="width:100%"></el-input-number> |
| | | </el-form-item> |
| | | <el-form-item label="è°æ´åå "> |
| | | <el-input v-model="adjustmentForm.reason" type="textarea" :rows="3" maxlength="500" show-word-limit></el-input> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div slot="footer"> |
| | | <el-button @click="adjustmentDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" :loading="adjustmentSaving" @click="saveAdjustment">ä¿å</el-button> |
| | | </div> |
| | | </el-dialog> |
| | | </el-card> |
| | | |
| | | <el-card shadow="never" style="margin-top:14px"> |
| | | <div slot="header"> |
| | | <span>è§æµç«åæ¥è°æ¥æ°æ®</span> |
| | | <span class="hint">ç¨äºåå§æ°æ®çåï¼ä¸ä¸ä¸æ¹è忥ç»è®¡æ°æ®å
³è</span> |
| | | </div> |
| | | <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> |
| | | </el-container> |
| | | </template> |
| | | |
| | | <script> |
| | | import api from '@/api' |
| | | import AppHeader from '@/components/AppHeader.vue' |
| | | |
| | | export default { |
| | | components: { AppHeader }, |
| | | name: 'HolidayStat', |
| | | data() { |
| | | return { |
| | | years: [2019, 2024, 2025, 2026, 2027, 2028, 2029, 2030], |
| | | year: 2026, |
| | | holidayType: 'NATIONAL_DAY', |
| | | holidayTypeOptions: [], |
| | | rows: [], |
| | | loading: false, |
| | | importing: false, |
| | | exporting: false, |
| | | bulletinLoading: false, |
| | | trendLoading: false, |
| | | selectedFile: null, |
| | | fileList: [], |
| | | importMessage: '', |
| | | allowHistoricalOverwrite: false, |
| | | bulletinDate: this.previousDateString(), |
| | | observationFile: null, |
| | | observationFileList: [], |
| | | observationImporting: false, |
| | | observationLoading: false, |
| | | observationMessage: '', |
| | | observationDates: [], |
| | | observationDialogVisible: false, |
| | | observationDialogDate: '', |
| | | observationRows: [], |
| | | observationRowsLoading: false, |
| | | adjustmentYear: new Date().getFullYear() - 1, |
| | | adjustments: [], |
| | | adjustmentLoading: false, |
| | | adjustmentDialogVisible: false, |
| | | adjustmentSaving: false, |
| | | adjustmentOriginalLoading: false, |
| | | adjustmentOriginalRequestId: 0, |
| | | adjustmentForm: { |
| | | id: null, |
| | | year: new Date().getFullYear() - 1, |
| | | flowDate: '', |
| | | originalValue: null, |
| | | adjustedValue: null, |
| | | reason: '' |
| | | } |
| | | } |
| | | }, |
| | | computed: { |
| | | holidayTypeLabel() { |
| | | const item = this.holidayTypeOptions.find(x => x.value === this.holidayType) |
| | | return item ? item.label : this.holidayType |
| | | }, |
| | | availableAdjustmentYears() { |
| | | const currentYear = new Date().getFullYear() |
| | | return this.years.filter(item => item < currentYear) |
| | | }, |
| | | adjustmentDatePickerOptions() { |
| | | return { |
| | | disabledDate: date => date.getFullYear() !== Number(this.adjustmentForm.year) |
| | | } |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.loadOptions() |
| | | this.loadOverview() |
| | | this.loadObservationDates() |
| | | this.loadAdjustments() |
| | | }, |
| | | methods: { |
| | | previousDateString() { |
| | | const date = new Date() |
| | | date.setDate(date.getDate() - 1) |
| | | const month = String(date.getMonth() + 1).padStart(2, '0') |
| | | const day = String(date.getDate()).padStart(2, '0') |
| | | return date.getFullYear() + '-' + month + '-' + day |
| | | }, |
| | | loadOptions() { |
| | | api.get('/holiday/config/options').then(res => { |
| | | const map = (res.data && res.data.holidayTypes) || {} |
| | | this.holidayTypeOptions = Object.keys(map).map(key => ({ value: key, label: map[key] })) |
| | | }) |
| | | }, |
| | | loadOverview() { |
| | | this.loading = true |
| | | api.get('/holiday/overview', { params: { year: this.year, holidayType: this.holidayType } }) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'å 载失败') |
| | | this.rows = (res.data && res.data.rows) || [] |
| | | }) |
| | | .catch(err => this.showError(err, 'èåæ¥æ°æ®å 载失败')) |
| | | .finally(() => { this.loading = false }) |
| | | }, |
| | | loadAdjustments() { |
| | | this.adjustmentLoading = true |
| | | api.get('/holiday/adjustments', { params: { year: this.adjustmentYear } }) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'å 载失败') |
| | | this.adjustments = res.data || [] |
| | | }) |
| | | .catch(err => this.showError(err, 'å¾å¹´å½çéæ°æ®è°æ´å 载失败')) |
| | | .finally(() => { this.adjustmentLoading = false }) |
| | | }, |
| | | openAdjustmentDialog(row) { |
| | | const source = row || {} |
| | | this.adjustmentForm = { |
| | | id: source.id || null, |
| | | year: source.year || this.adjustmentYear, |
| | | flowDate: source.flowDate || '', |
| | | originalValue: source.originalValue === undefined || source.originalValue === null |
| | | ? null : Number(source.originalValue), |
| | | adjustedValue: source.adjustedValue === undefined || source.adjustedValue === null |
| | | ? null : Number(source.adjustedValue), |
| | | reason: source.reason || '' |
| | | } |
| | | this.adjustmentDialogVisible = true |
| | | this.loadAdjustmentOriginalValue() |
| | | }, |
| | | onAdjustmentYearChange() { |
| | | this.adjustmentOriginalRequestId += 1 |
| | | this.adjustmentOriginalLoading = false |
| | | this.adjustmentForm.flowDate = '' |
| | | this.adjustmentForm.originalValue = null |
| | | }, |
| | | onAdjustmentDateChange() { |
| | | this.loadAdjustmentOriginalValue() |
| | | }, |
| | | loadAdjustmentOriginalValue() { |
| | | const year = this.adjustmentForm.year |
| | | const flowDate = this.adjustmentForm.flowDate |
| | | const requestId = ++this.adjustmentOriginalRequestId |
| | | this.adjustmentForm.originalValue = null |
| | | if (!year || !flowDate) { |
| | | this.adjustmentOriginalLoading = false |
| | | return |
| | | } |
| | | this.adjustmentOriginalLoading = true |
| | | api.get('/holiday/adjustments/source-value', { params: { year: year, flowDate: flowDate } }) |
| | | .then(res => { |
| | | if (requestId !== this.adjustmentOriginalRequestId) return |
| | | if (res.code !== 200) throw new Error(res.message || 'è°æ´åå¼å 载失败') |
| | | this.adjustmentForm.originalValue = res.data === null || res.data === undefined || res.data === '' |
| | | ? null : Number(res.data) |
| | | }) |
| | | .catch(err => { |
| | | if (requestId !== this.adjustmentOriginalRequestId) return |
| | | this.adjustmentForm.originalValue = null |
| | | this.showError(err, 'è°æ´åå¼å 载失败') |
| | | }) |
| | | .finally(() => { |
| | | if (requestId === this.adjustmentOriginalRequestId) this.adjustmentOriginalLoading = false |
| | | }) |
| | | }, |
| | | saveAdjustment() { |
| | | const form = this.adjustmentForm |
| | | if (!form.year || !form.flowDate) { |
| | | this.$message.warning('è¯·éæ©å¹´ä»½åæ¥æ') |
| | | return |
| | | } |
| | | if (form.adjustedValue === null || form.adjustedValue === undefined || Number(form.adjustedValue) < 0) { |
| | | this.$message.warning('请è¾å
¥ä¸ä½äº 0 çè°æ´å车æµé') |
| | | return |
| | | } |
| | | if (!form.reason || !form.reason.trim()) { |
| | | this.$message.warning('请填åè°æ´åå ') |
| | | return |
| | | } |
| | | this.adjustmentSaving = true |
| | | api.post('/holiday/adjustments/save', { |
| | | id: form.id, |
| | | year: form.year, |
| | | flowDate: form.flowDate, |
| | | adjustedValue: form.adjustedValue, |
| | | reason: form.reason |
| | | }) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'ä¿å失败') |
| | | this.adjustmentDialogVisible = false |
| | | this.adjustmentYear = form.year |
| | | this.loadAdjustments() |
| | | this.loadOverview() |
| | | this.$message.success('è°æ´å·²ä¿å') |
| | | }) |
| | | .catch(err => this.showError(err, 'ä¿å失败')) |
| | | .finally(() => { this.adjustmentSaving = false }) |
| | | }, |
| | | deleteAdjustment(row) { |
| | | this.$confirm('ç¡®å®å é¤ ' + row.flowDate + ' çå½çéæ°æ®è°æ´åï¼', 'å é¤ç¡®è®¤', { type: 'warning' }) |
| | | .then(() => api.post('/holiday/adjustments/delete', null, { params: { id: row.id } })) |
| | | .then(res => { |
| | | if (res && res.code !== 200) throw new Error(res.message || 'å é¤å¤±è´¥') |
| | | this.loadAdjustments() |
| | | this.loadOverview() |
| | | this.$message.success('è°æ´å·²å é¤') |
| | | }) |
| | | .catch(err => { if (err !== 'cancel' && err !== 'close') this.showError(err, 'å é¤å¤±è´¥') }) |
| | | }, |
| | | onFileChange(file) { |
| | | this.selectedFile = file.raw |
| | | this.fileList = [file] |
| | | this.importMessage = '' |
| | | }, |
| | | onFileRemove() { |
| | | this.selectedFile = null |
| | | this.fileList = [] |
| | | }, |
| | | importData(allowHistoricalOverwrite) { |
| | | if (!this.selectedFile) return |
| | | const allowOverwrite = allowHistoricalOverwrite === true || this.allowHistoricalOverwrite === true |
| | | const form = new FormData() |
| | | form.append('file', this.selectedFile) |
| | | form.append('year', this.year) |
| | | form.append('holidayType', this.holidayType) |
| | | form.append('allowHistoricalOverwrite', allowOverwrite ? 'true' : 'false') |
| | | this.importing = true |
| | | this.importMessage = '' |
| | | api.post('/holiday/import', form, { timeout: 120000, headers: { 'Content-Type': 'multipart/form-data' } }) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || '导å
¥å¤±è´¥') |
| | | const data = res.data || {} |
| | | if (data.confirmationRequired) { |
| | | const changes = data.historicalChanges || [] |
| | | this.importMessage = ['æ£æµå°å岿¥æçæ°æ®åçååï¼å°æªå¯¼å
¥ã请确认æ¯å¦å
许è¦çï¼'].concat(changes).join('\n') |
| | | this.$message.warning('æ£æµå°å岿°æ®åçååï¼è¯·ç¡®è®¤æ¯å¦å
许导å
¥') |
| | | this.confirmHistoricalImport(changes) |
| | | return |
| | | } |
| | | const lines = [ |
| | | '导å
¥å®æï¼å®¢æµ ' + (data.passengerRows || 0) + ' æ¡ï¼è½¦æµ ' + (data.vehicleRows || 0) + ' æ¡ï¼è·³è¿ç©ºç½ ' + (data.skippedRows || 0) + ' æ¡ã' |
| | | ] |
| | | if (data.errors && data.errors.length) { |
| | | lines.push('失败æç»ï¼') |
| | | data.errors.forEach(item => lines.push(item)) |
| | | } |
| | | this.importMessage = lines.join('\n') |
| | | this.allowHistoricalOverwrite = false |
| | | this.$message.success('导å
¥å®æ') |
| | | this.loadOverview() |
| | | }) |
| | | .catch(err => { |
| | | this.allowHistoricalOverwrite = false |
| | | this.showError(err, '导å
¥å¤±è´¥') |
| | | }) |
| | | .finally(() => { this.importing = false }) |
| | | }, |
| | | confirmHistoricalImport(changes) { |
| | | const rows = (changes && changes.length ? changes : ['å岿°æ®åçåå']).map(this.escapeHtml) |
| | | const html = '<div>æ£æµå°å·²å叿¥æçæ°æ®åçååãå
许导å
¥å°è¦çè¿äºå岿°æ®ã</div>' |
| | | + '<pre style="max-height:320px;overflow:auto;white-space:pre-wrap;margin-top:10px;background:#f7f7f7;padding:8px;border:1px solid #e4e7ed;">' |
| | | + rows.join('\n') + '</pre>' |
| | | this.$confirm(html, 'å岿°æ®åçåå', { |
| | | dangerouslyUseHTMLString: true, |
| | | confirmButtonText: 'å
许导å
¥', |
| | | cancelButtonText: 'ä¸å
许', |
| | | type: 'warning', |
| | | distinguishCancelAndClose: true |
| | | }).then(() => { |
| | | this.allowHistoricalOverwrite = true |
| | | this.importData(true) |
| | | }).catch(() => { |
| | | this.allowHistoricalOverwrite = false |
| | | }) |
| | | }, |
| | | escapeHtml(value) { |
| | | return String(value === null || value === undefined ? '' : value) |
| | | .replace(/&/g, '&') |
| | | .replace(/</g, '<') |
| | | .replace(/>/g, '>') |
| | | .replace(/"/g, '"') |
| | | .replace(/'/g, ''') |
| | | }, |
| | | exportData() { |
| | | this.exporting = true |
| | | api.get('/holiday/export', { params: { year: this.year, holidayType: this.holidayType }, responseType: 'blob', timeout: 120000 }) |
| | | .then(res => { this.downloadBlob(res, this.year + 'å¹´' + this.holidayTypeLabel + 'æ°æ®.xlsx'); this.$message.success('å¯¼åºæå') }) |
| | | .catch(err => this.handleBlobError(err, '导åºå¤±è´¥')) |
| | | .finally(() => { this.exporting = false }) |
| | | }, |
| | | downloadTemplate() { |
| | | const templatePath = 'ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´å䏿°æ®.xlsx' |
| | | const encodedPath = templatePath.split('/').map(encodeURIComponent).join('/') |
| | | api.get('/templates/' + encodedPath, { responseType: 'blob' }) |
| | | .then(res => { this.downloadBlob(res, '2026å¹´å䏿°æ®.xlsx'); this.$message.success('模æ¿ä¸è½½æå') }) |
| | | .catch(err => this.handleBlobError(err, '模æ¿ä¸è½½å¤±è´¥')) |
| | | }, |
| | | downloadBulletin() { |
| | | this.bulletinLoading = true |
| | | api.get('/holiday/bulletin', { params: { date: this.bulletinDate }, responseType: 'blob', timeout: 120000 }) |
| | | .then(res => { |
| | | const day = Number((this.bulletinDate || '').slice(-2)) |
| | | this.downloadBlob(res, 'æ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥ï¼10æ' + day + 'æ¥ï¼.docx') |
| | | this.$message.success('å¿«æ¥ä¸è½½æå') |
| | | }) |
| | | .catch(err => this.handleBlobError(err, 'å¿«æ¥çæå¤±è´¥')) |
| | | .finally(() => { this.bulletinLoading = false }) |
| | | }, |
| | | downloadTrend() { |
| | | this.trendLoading = true |
| | | api.get('/holiday/trend', { params: { year: this.year, holidayType: this.holidayType }, responseType: 'blob', timeout: 180000 }) |
| | | .then(res => { |
| | | this.downloadBlob(res, this.year + 'å¹´' + this.holidayTypeLabel + 'åæå
¨ç综åè¿è¾å®¢æµéèµ°å¿å¾.docx') |
| | | this.$message.success('èµ°å¿å¾ä¸è½½æå') |
| | | }) |
| | | .catch(err => this.handleBlobError(err, 'èµ°å¿å¾çæå¤±è´¥')) |
| | | .finally(() => { this.trendLoading = false }) |
| | | }, |
| | | downloadBlob(data, fileName) { |
| | | const blob = data instanceof Blob ? data : new Blob([data]) |
| | | const url = window.URL.createObjectURL(blob) |
| | | const link = document.createElement('a') |
| | | link.href = url |
| | | link.download = fileName |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | document.body.removeChild(link) |
| | | window.URL.revokeObjectURL(url) |
| | | }, |
| | | handleBlobError(err, fallback) { |
| | | if (err && err.response && err.response.data instanceof Blob) { |
| | | err.response.data.text().then(text => { |
| | | try { |
| | | const parsed = JSON.parse(text) |
| | | this.$message.error(parsed.message || fallback) |
| | | } catch (e) { |
| | | this.$message.error(fallback) |
| | | } |
| | | }) |
| | | return |
| | | } |
| | | this.showError(err, fallback) |
| | | }, |
| | | showError(err, fallback) { |
| | | if (err && err._toast) return |
| | | const data = err && err.response && err.response.data |
| | | this.$message.error((data && data.message) || (err && err.message) || fallback) |
| | | }, |
| | | 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, 'å é¤å¤±è´¥') }) |
| | | }, |
| | | formatAdjustmentValue(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return number.toFixed(4) |
| | | }, |
| | | formatOriginalAdjustmentValue(value) { |
| | | if (value === null || value === undefined || value === '') return '' |
| | | return this.formatAdjustmentValue(value) |
| | | }, |
| | | formatDateTime(value) { |
| | | if (!value) return 'â' |
| | | return String(value).replace('T', ' ').slice(0, 19) |
| | | }, |
| | | formatNumber(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | return Number(value).toFixed(2) |
| | | }, |
| | | 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 'â' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return number.toFixed(15).replace(/0+$/, '').replace(/\.$/, '') |
| | | }, |
| | | formatRatio(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return (number * 100).toFixed(2) + '%' |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; } |
| | | .operation-card { margin-top: 12px; } |
| | | .operation-row { display: flex; gap: 36px; align-items: flex-start; flex-wrap: wrap; } |
| | | .operation-item { display: flex; align-items: center; gap: 8px; } |
| | | .operation-item .label { color: #606266; font-size: 14px; } |
| | | .import-message { margin-top: 12px; padding: 10px 12px; background: #f5f7fa; border: 1px solid #e4e7ed; border-radius: 4px; } |
| | | .import-message pre { margin: 0; white-space: pre-wrap; font-family: inherit; color: #606266; } |
| | | .hint { margin-left: 10px; color: #909399; font-size: 12px; } |
| | | .danger-text { color: #f56c6c; } |
| | | </style> |
| | |
| | | <template> |
| | | <div class="login-container"> |
| | | <el-card class="login-card"> |
| | | <h2>交éç»è®¡æ¥è¡¨å®¡æ ¸ç³»ç»</h2> |
| | | <h2>交éç»è®¡æ¥è¡¨å®¡æ ¸ç³»ç»<span class="login-version" v-if="version">V{{ version }}</span></h2> |
| | | <el-form ref="form" :model="form" :rules="rules"> |
| | | <el-form-item prop="username"> |
| | | <el-input v-model="form.username" placeholder="ç¨æ·å" @keyup.enter.native="login"></el-input> |
| | |
| | | captchaId: '', |
| | | clickIds: [], |
| | | loggingIn: false, |
| | | captchaRequired: false |
| | | captchaRequired: false, |
| | | version: '' |
| | | } |
| | | }, |
| | | created() { |
| | | axios.get('/api/version', { timeout: 10000 }) |
| | | .then(res => { |
| | | const d = res.data |
| | | if (d && d.data && d.data.version) this.version = d.data.version |
| | | }) |
| | | .catch(() => {}) |
| | | // éªè¯ç æ¯å¦å¯ç¨ä¸å¾ä»¥å端 /api/auth/captcha-config 为åï¼ |
| | | // é»è®¤ä¸æ¾ç¤ºï¼åªæå端æç¡®è¿å required=true ææ¸²æéªè¯ç ç»ä»¶ã |
| | | // ä¿®å¤ï¼æ¤åé»è®¤ true ä¸è¯·æ±å¤±è´¥æ¶éé»å¿½ç¥ï¼åæ¢ç½ç»å该é
置读ä¸å°ï¼ |
| | |
| | | .captcha-skip-tip { font-size: 12px; color: #909399; line-height: 1.6; padding: 8px 10px; background: #F5F7FA; border-radius: 4px; } |
| | | .login-card { width: 400px; } |
| | | .login-card h2 { text-align: center; margin-bottom: 18px; } |
| | | .login-version { font-size: 13px; font-weight: normal; color: #909399; margin-left: 8px; vertical-align: 3px; } |
| | | .login-card .el-form-item { margin-bottom: 16px; } |
| | | </style> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| New file |
| | |
| | | <template> |
| | | <el-container direction="vertical"> |
| | | <app-header title="交éç»è®¡æ¥è¡¨å®¡æ ¸ç³»ç» - ç³»ç»é
ç½®"></app-header> |
| | | <el-container> |
| | | <el-aside width="200px"> |
| | | <el-menu ref="sideMenu" router :default-active="$route.path" @select="onMenuSelect"> |
| | | <el-menu-item index="/dashboard">å·¥ä½å°</el-menu-item> |
| | | <el-menu-item index="/import">æ°æ®å¯¼å
¥</el-menu-item> |
| | | <el-menu-item index="/data">æ°æ®æ¥ç</el-menu-item> |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | | <div class="page-scroll"> |
| | | <h3>ç³»ç»é
ç½®</h3> |
| | | <el-tabs v-model="activeTab" @tab-click="loadActiveTab"> |
| | | <el-tab-pane label="èåæ¥ç®¡ç" name="holidays"> |
| | | <div class="toolbar"> |
| | | <el-select v-model="holidayQuery.year" clearable placeholder="年份" style="width:130px" @change="loadHolidays"> |
| | | <el-option v-for="year in years" :key="year" :label="year + 'å¹´'" :value="year"></el-option> |
| | | </el-select> |
| | | <el-select v-model="holidayQuery.holidayType" clearable placeholder="è忥åç±»" style="width:150px" @change="loadHolidays"> |
| | | <el-option v-for="item in holidayTypeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option> |
| | | </el-select> |
| | | <el-button type="primary" icon="el-icon-search" @click="loadHolidays">æ¥è¯¢</el-button> |
| | | <el-button type="success" icon="el-icon-plus" @click="openHolidayDialog()">æ°å¢è忥</el-button> |
| | | </div> |
| | | <el-table :data="holidays" border stripe size="small" v-loading="loadingHolidays" style="width:100%"> |
| | | <el-table-column prop="year" label="年份" width="100" align="center"></el-table-column> |
| | | <el-table-column label="è忥åç±»" width="140"> |
| | | <template slot-scope="scope">{{ labelOf(holidayTypeOptions, scope.row.holidayType) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="holidayDate" label="æ¥æ" width="150" align="center"></el-table-column> |
| | | <el-table-column 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-button type="text" size="mini" class="danger-text" @click="removeHoliday(scope.row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="客è¿ç³»æ°ç®¡ç" name="coefficients"> |
| | | <div class="toolbar"> |
| | | <el-select v-model="coefficientQuery.year" clearable placeholder="年份" style="width:130px" @change="loadCoefficients"> |
| | | <el-option v-for="year in years" :key="year" :label="year + 'å¹´'" :value="year"></el-option> |
| | | </el-select> |
| | | <el-button type="primary" icon="el-icon-search" @click="loadCoefficients">æ¥è¯¢</el-button> |
| | | <el-button type="success" icon="el-icon-plus" @click="openCoefficientDialog()">æ°å¢å®¢è¿ç³»æ°</el-button> |
| | | </div> |
| | | <el-table :data="coefficients" border stripe size="small" v-loading="loadingCoefficients" style="width:100%"> |
| | | <el-table-column prop="year" label="年份" width="120" align="center"></el-table-column> |
| | | <el-table-column label="æ¥æåç±»" width="160"> |
| | | <template slot-scope="scope">{{ labelOf(dateCategoryOptions, scope.row.dateCategory) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="客è¿ç³»æ°" width="160" align="center"> |
| | | <template slot-scope="scope">{{ formatCoefficient(scope.row.coefficient) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="150" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-button type="text" size="mini" @click="openCoefficientDialog(scope.row)">ç¼è¾</el-button> |
| | | <el-button type="text" size="mini" class="danger-text" @click="removeCoefficient(scope.row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | </div> |
| | | |
| | | <el-dialog :title="holidayForm.id ? 'ç¼è¾è忥' : 'æ°å¢è忥'" :visible.sync="holidayDialog" width="520px" v-dialog-drag> |
| | | <el-form ref="holidayForm" :model="holidayForm" :rules="holidayRules" label-width="110px"> |
| | | <el-form-item label="年份" prop="year"> |
| | | <el-select v-model="holidayForm.year" style="width:100%" @change="onHolidayYearChange"> |
| | | <el-option v-for="year in years" :key="year" :label="year + 'å¹´'" :value="year"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="è忥åç±»" prop="holidayType"> |
| | | <el-select v-model="holidayForm.holidayType" style="width:100%"> |
| | | <el-option v-for="item in holidayTypeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥æ" prop="holidayDate"> |
| | | <el-date-picker v-model="holidayForm.holidayDate" type="date" value-format="yyyy-MM-dd" |
| | | style="width:100%" placeholder="éæ©æ¥æ"></el-date-picker> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥æç±»å" prop="dayIndex"> |
| | | <el-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"> |
| | | <el-button @click="holidayDialog = false">åæ¶</el-button> |
| | | <el-button type="primary" :loading="saving" @click="saveHoliday">ä¿å</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | |
| | | <el-dialog :title="coefficientForm.id ? 'ç¼è¾å®¢è¿ç³»æ°' : 'æ°å¢å®¢è¿ç³»æ°'" :visible.sync="coefficientDialog" width="520px" v-dialog-drag> |
| | | <el-form ref="coefficientForm" :model="coefficientForm" :rules="coefficientRules" label-width="110px"> |
| | | <el-form-item label="年份" prop="year"> |
| | | <el-select v-model="coefficientForm.year" style="width:100%"> |
| | | <el-option v-for="year in years" :key="year" :label="year + 'å¹´'" :value="year"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥æåç±»" prop="dateCategory"> |
| | | <el-select v-model="coefficientForm.dateCategory" style="width:100%"> |
| | | <el-option v-for="item in dateCategoryOptions" :key="item.value" :label="item.label" :value="item.value"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="客è¿ç³»æ°" prop="coefficient"> |
| | | <el-input-number v-model="coefficientForm.coefficient" :min="0.01" :precision="4" :step="0.1" style="width:100%"></el-input-number> |
| | | </el-form-item> |
| | | </el-form> |
| | | <span slot="footer"> |
| | | <el-button @click="coefficientDialog = false">åæ¶</el-button> |
| | | <el-button type="primary" :loading="saving" @click="saveCoefficient">ä¿å</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | </el-main> |
| | | </el-container> |
| | | </el-container> |
| | | </template> |
| | | |
| | | <script> |
| | | import api from '@/api' |
| | | import AppHeader from '@/components/AppHeader.vue' |
| | | |
| | | export default { |
| | | components: { AppHeader }, |
| | | name: 'SystemConfig', |
| | | data() { |
| | | return { |
| | | activeTab: 'holidays', |
| | | years: [2019, 2024, 2025, 2026, 2027, 2028, 2029, 2030], |
| | | holidayTypeOptions: [], |
| | | dateCategoryOptions: [], |
| | | holidayQuery: { year: 2026, holidayType: 'NATIONAL_DAY' }, |
| | | coefficientQuery: { year: 2026 }, |
| | | holidays: [], |
| | | coefficients: [], |
| | | loadingHolidays: false, |
| | | loadingCoefficients: false, |
| | | saving: false, |
| | | holidayDialog: false, |
| | | coefficientDialog: false, |
| | | holidayForm: { id: null, year: 2026, holidayType: 'NATIONAL_DAY', holidayDate: '', dayIndex: 1 }, |
| | | coefficientForm: { id: null, year: 2026, dateCategory: 'HOLIDAY', coefficient: 2.4 }, |
| | | holidayRules: { |
| | | year: [{ required: true, message: 'è¯·éæ©å¹´ä»½', trigger: 'change' }], |
| | | holidayType: [{ required: true, message: 'è¯·éæ©è忥åç±»', trigger: 'change' }], |
| | | holidayDate: [{ required: true, message: 'è¯·éæ©æ¥æ', trigger: 'change' }], |
| | | dayIndex: [{ required: true, message: 'è¯·éæ©æ¥æç±»å', trigger: 'change' }] |
| | | }, |
| | | coefficientRules: { |
| | | year: [{ required: true, message: 'è¯·éæ©å¹´ä»½', trigger: 'change' }], |
| | | dateCategory: [{ required: true, message: 'è¯·éæ©æ¥æåç±»', trigger: 'change' }], |
| | | coefficient: [{ required: true, message: '请è¾å
¥å®¢è¿ç³»æ°', trigger: 'change' }] |
| | | } |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.loadOptions() |
| | | this.loadHolidays() |
| | | this.loadCoefficients() |
| | | }, |
| | | methods: { |
| | | 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 || {} |
| | | this.holidayTypeOptions = this.mapOptions(data.holidayTypes) |
| | | this.dateCategoryOptions = this.mapOptions(data.dateCategories) |
| | | }) |
| | | }, |
| | | mapOptions(map) { |
| | | if (!map) return [] |
| | | return Object.keys(map).map(key => ({ value: key, label: map[key] })) |
| | | }, |
| | | labelOf(options, value) { |
| | | const item = (options || []).find(x => x.value === value) |
| | | return item ? item.label : value |
| | | }, |
| | | loadActiveTab() { |
| | | if (this.activeTab === 'holidays') this.loadHolidays() |
| | | else this.loadCoefficients() |
| | | }, |
| | | loadHolidays() { |
| | | this.loadingHolidays = true |
| | | api.get('/holiday/config/holidays', { params: this.holidayQuery }) |
| | | .then(res => { this.holidays = res.data || [] }) |
| | | .catch(e => { if (!e || !e._toast) this.$message.error('è忥å 载失败') }) |
| | | .finally(() => { this.loadingHolidays = false }) |
| | | }, |
| | | loadCoefficients() { |
| | | this.loadingCoefficients = true |
| | | api.get('/holiday/config/coefficients', { params: this.coefficientQuery }) |
| | | .then(res => { this.coefficients = res.data || [] }) |
| | | .catch(e => { if (!e || !e._toast) this.$message.error('客è¿ç³»æ°å 载失败') }) |
| | | .finally(() => { this.loadingCoefficients = false }) |
| | | }, |
| | | openHolidayDialog(row) { |
| | | this.holidayDialog = true |
| | | this.$nextTick(() => { |
| | | this.holidayForm = row ? Object.assign({}, row) : { id: null, year: 2026, holidayType: 'NATIONAL_DAY', holidayDate: '', dayIndex: 1 } |
| | | if (this.$refs.holidayForm) this.$refs.holidayForm.clearValidate() |
| | | }) |
| | | }, |
| | | onHolidayYearChange(year) { |
| | | if (this.holidayForm.holidayDate && Number(this.holidayForm.holidayDate.slice(0, 4)) !== year) { |
| | | this.holidayForm.holidayDate = '' |
| | | } |
| | | }, |
| | | saveHoliday() { |
| | | this.$refs.holidayForm.validate(valid => { |
| | | if (!valid) return |
| | | this.saving = true |
| | | api.post('/holiday/config/holidays/save', this.holidayForm) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'ä¿å失败') |
| | | this.$message.success('ä¿åæå') |
| | | this.holidayDialog = false |
| | | this.loadHolidays() |
| | | }) |
| | | .catch(err => this.showError(err, 'ä¿å失败')) |
| | | .finally(() => { this.saving = false }) |
| | | }) |
| | | }, |
| | | removeHoliday(row) { |
| | | this.$confirm('ç¡®å®å é¤è¯¥èåæ¥æ¥æåï¼', 'å é¤ç¡®è®¤', { type: 'warning' }) |
| | | .then(() => api.post('/holiday/config/holidays/delete', null, { params: { id: row.id } })) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'å é¤å¤±è´¥') |
| | | this.$message.success('å·²å é¤') |
| | | this.loadHolidays() |
| | | }) |
| | | .catch(err => { if (err !== 'cancel') this.showError(err, 'å é¤å¤±è´¥') }) |
| | | }, |
| | | openCoefficientDialog(row) { |
| | | this.coefficientDialog = true |
| | | this.$nextTick(() => { |
| | | this.coefficientForm = row ? Object.assign({}, row) : { id: null, year: 2026, dateCategory: 'HOLIDAY', coefficient: 2.4 } |
| | | if (this.$refs.coefficientForm) this.$refs.coefficientForm.clearValidate() |
| | | }) |
| | | }, |
| | | saveCoefficient() { |
| | | this.$refs.coefficientForm.validate(valid => { |
| | | if (!valid) return |
| | | this.saving = true |
| | | api.post('/holiday/config/coefficients/save', this.coefficientForm) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'ä¿å失败') |
| | | this.$message.success('ä¿åæå') |
| | | this.coefficientDialog = false |
| | | this.loadCoefficients() |
| | | }) |
| | | .catch(err => this.showError(err, 'ä¿å失败')) |
| | | .finally(() => { this.saving = false }) |
| | | }) |
| | | }, |
| | | removeCoefficient(row) { |
| | | this.$confirm('ç¡®å®å é¤è¯¥å®¢è¿ç³»æ°åï¼', 'å é¤ç¡®è®¤', { type: 'warning' }) |
| | | .then(() => api.post('/holiday/config/coefficients/delete', null, { params: { id: row.id } })) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'å é¤å¤±è´¥') |
| | | this.$message.success('å·²å é¤') |
| | | this.loadCoefficients() |
| | | }) |
| | | .catch(err => { if (err !== 'cancel') this.showError(err, 'å é¤å¤±è´¥') }) |
| | | }, |
| | | formatCoefficient(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return number.toFixed(15).replace(/0+$/, '').replace(/\.$/, '') |
| | | }, |
| | | showError(err, fallback) { |
| | | if (err && err._toast) return |
| | | const data = err && err.response && err.response.data |
| | | this.$message.error((data && data.message) || (err && err.message) || fallback) |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .toolbar { margin-bottom: 12px; display: flex; gap: 8px; align-items: center; } |
| | | .danger-text { color: #f56c6c; } |
| | | </style> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |