| | |
| | | ## éªè¯æ³¨æ |
| | | - æ²ç®±å
`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-04ï¼ |
| | | ### 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/ä¸é¡¹ç»è®¡/éè¦è忥/å¤ä»½/`ã |
| | |
| | | ### 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 ä½å°æ°ï¼è¾åºç´¯è®¡å¼ï¼åæ¯ä»ä¸ºãå¢é¿/ä¸é + ä¸ä½å°æ°ãï¼**ä¿æä¸æ¬¡åæ´ä¸å**ï¼æ»è®¡ç´¯è®¡åæ¯ 2025 精确 -4.4486% â `-4.4%`ï¼æå·¥ç¨¿ç -4.5% å±äºæ¬¡åæ´ï¼ä¸éç¨ï¼ã |
| | | - **å®ç°**ï¼`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ä¸äººæ¬¡`ï¼ã |
| | | - **ç¶æ**ï¼å¾
ç¨æ·ãéªæ¶éè¿ãåæäº¤/æ¨éã |
| | | |
| | |
| | | <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> |
| | |
| | | |
| | | 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; |
| | |
| | | private HolidayStatService statService; |
| | | @Resource |
| | | private HolidayBulletinService bulletinService; |
| | | |
| | | @Resource |
| | | private HolidayTrendService trendService; |
| | | |
| | | @GetMapping("/overview") |
| | | public Result<Map<String, Object>> overview(@RequestParam("year") int year, |
| | |
| | | .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 "äºä¸"; |
| | |
| | | |
| | | 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; |
| | |
| | | |
| | | String currentTotal = formatNumber(current.getTotalPersonFlow()); |
| | | String ringRatioClause = previousDay == null ? "" : "ï¼ç¯æ¯" + ratioPhrase( |
| | | current.getTotalPersonFlow(), previousDay.getTotalPersonFlow()); |
| | | 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()), |
| | | ratioPhrase(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow())); |
| | | 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(); |
| | |
| | | current, twoYearsAgo, DailyMetrics::getCivilAviation); |
| | | writeTableRow(table, 2, new String[]{ |
| | | number(current.getNonBusinessPersonFlow()), |
| | | ratio(current.getNonBusinessPersonFlow(), previousDay == null ? null : previousDay.getNonBusinessPersonFlow()), |
| | | ratio(current.getNonBusinessPersonFlow(), previousYear == null ? null : previousYear.getNonBusinessPersonFlow()), |
| | | ratio(current.getNonBusinessPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getNonBusinessPersonFlow()), |
| | | number(currentCumulative.getNonBusinessPersonFlow()), |
| | | ratio(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()), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getNonBusinessPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(currentCumulative.getNonBusinessPersonFlow(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.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()), |
| | | ratio(current.getRoad(), previousYear == null ? null : previousYear.getRoad()), |
| | | ratio(current.getRoad(), twoYearsAgo == null ? null : twoYearsAgo.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()), |
| | | ratio(currentCumulative.getRoad(), twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.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()), |
| | | ratio(current.getRoadPersonFlow(), previousYear == null ? null : previousYear.getRoadPersonFlow()), |
| | | ratio(current.getRoadPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getRoadPersonFlow()), |
| | | number(currentCumulative.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()), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratio(currentCumulative.getRoadPersonFlow(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoadPersonFlow()) |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getRoadPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT) |
| | | }); |
| | | writeTableRow(table, 5, transportRow(current.getRail(), |
| | | firstDayRailBase == null ? null : firstDayRailBase.getRail(), |
| | |
| | | twoYearsAgo == null ? null : twoYearsAgo.getRail(), |
| | | currentCumulative.getRail(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getRail(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.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())); |
| | | 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())); |
| | | writeTableRow(table, 8, transportRow(current.getTotalPersonFlow(), |
| | | previousDay == null ? null : previousDay.getTotalPersonFlow(), |
| | | previousYear == null ? null : previousYear.getTotalPersonFlow(), |
| | | twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(), |
| | | currentCumulative.getTotalPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(), |
| | | twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow())); |
| | | 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) { |
| | | BigDecimal previousYearCumulative, BigDecimal twoYearsAgoCumulative, |
| | | int ringRatioExcelScale) { |
| | | return new String[]{ |
| | | number(current), |
| | | ratio(current, previousDay), |
| | | ratio(current, previousYear), |
| | | ratio(current, twoYearsAgo), |
| | | ratio(current, previousDay, ringRatioExcelScale), |
| | | ratio(current, previousYear, EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current, twoYearsAgo, EXCEL_ONE_DECIMAL_PERCENT), |
| | | number(currentCumulative), |
| | | ratio(currentCumulative, previousYearCumulative), |
| | | ratio(currentCumulative, twoYearsAgoCumulative) |
| | | ratio(currentCumulative, previousYearCumulative, EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(currentCumulative, twoYearsAgoCumulative, EXCEL_ONE_DECIMAL_PERCENT) |
| | | }; |
| | | } |
| | | |
| | |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private String ratioPhrase(BigDecimal current, BigDecimal base) { |
| | | if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return "æ 坿¯æ°æ®"; |
| | | BigDecimal ratio = HolidayNumberUtils.divide(current, base); |
| | | BigDecimal percent = HolidayNumberUtils.multiply(ratio.subtract(BigDecimal.ONE), BigDecimal.valueOf(100)); |
| | | if (percent == null || percent.abs().compareTo(EPSILON) < 0) return "æå¹³"; |
| | | 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()) + "%"; |
| | | } |
| | | |
| | |
| | | DailyMetrics twoYearsAgoCumulative) { |
| | | BigDecimal total = currentCumulative == null ? null : currentCumulative.getTotalPersonFlow(); |
| | | return String.format("åæä»¥æ¥ï¼å
¨çå
¨ç¤¾ä¼è·¨åºåäººåæµå¨é累计%sä¸äººæ¬¡ï¼æ¯2025å¹´åæ%sï¼æ¯2024å¹´åæ%sã", |
| | | formatNumber(total), |
| | | ratioPhrase(total, previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow()), |
| | | ratioPhrase(total, twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow())); |
| | | 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) { |
| | | if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return "â"; |
| | | BigDecimal ratio = HolidayNumberUtils.divide(current, base); |
| | | if (ratio == null) return "â"; |
| | | BigDecimal percent = HolidayNumberUtils.multiply(ratio.subtract(BigDecimal.ONE), BigDecimal.valueOf(100)); |
| | | private String ratio(BigDecimal current, BigDecimal base, int excelPercentScale) { |
| | | BigDecimal percent = bulletinPercent(current, base, excelPercentScale); |
| | | if (percent == null) return "â"; |
| | | BigDecimal rounded = percent.setScale(1, RoundingMode.HALF_UP); |
| | | return rounded.signum() == 0 ? "0%" : rounded.toPlainString() + "%"; |
| | | 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); |
| 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.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 = 12.0; |
| | | 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) { |
| | | 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, value.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.12); |
| | | rangeAxis.setUpperMargin(0.16); |
| | | 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 : 2); |
| | | 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, height, yAxisLabel, true, percentAxis); |
| | | } |
| | | |
| | | 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); |
| | | } |
| | | |
| | | 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); |
| | | } |
| | | |
| | | 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) { |
| | | 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); |
| | | 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) { |
| | | 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<>(); |
| | | List<Rectangle2D> markers = 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 < dataset.getItemCount(seriesIndex); itemIndex++) { |
| | | Point2D point = itemPoint(plot, dataset, seriesIndex, itemIndex, dataArea); |
| | | if (point != null) { |
| | | markers.add(new Rectangle2D.Double(point.getX() - 9.0, |
| | | point.getY() - 9.0, 18.0, 18.0)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | 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))); |
| | | } |
| | | // 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()); |
| | | } |
| | | }); |
| | | double previousBottom = Double.NEGATIVE_INFINITY; |
| | | for (DayLabel label : labels) { |
| | | int width = metrics.stringWidth(label.getText()); |
| | | int height = metrics.getHeight(); |
| | | double x = label.getPoint().getX() - width / 2.0; |
| | | double top = Math.max(label.getPoint().getY() - ITEM_LABEL_GAP - height, |
| | | previousBottom + 3.0); |
| | | Rectangle2D chosen = null; |
| | | double bestPenalty = Double.MAX_VALUE; |
| | | for (int step = 0; step <= 4; step++) { |
| | | Rectangle2D candidate = boundToDataArea(new Rectangle2D.Double( |
| | | x, top + step * (height + 4.0), width, height), dataArea); |
| | | if (candidate == null) continue; |
| | | double penalty = overlapArea(candidate, placedLabels) * 1000.0 |
| | | + overlapArea(candidate, markers); |
| | | if (penalty < bestPenalty) { |
| | | bestPenalty = penalty; |
| | | chosen = candidate; |
| | | } |
| | | if (penalty <= 0.0) break; |
| | | } |
| | | if (chosen == null) continue; |
| | | drawLabelWithHalo(graphics, label.getText(), chosen, metrics, |
| | | seriesColor(label.getSeriesIndex())); |
| | | placedLabels.add(new Rectangle2D.Double(chosen.getX() - 5.0, chosen.getY() - 5.0, |
| | | chosen.getWidth() + 10.0, chosen.getHeight() + 10.0)); |
| | | previousBottom = chosen.getMaxY(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | 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 void addLabelCandidate(List<Rectangle2D> result, double x, double y, |
| | | int width, int height, double dx, double dy) { |
| | | result.add(new Rectangle2D.Double(x + dx - width / 2.0, |
| | | y + dy - height / 2.0, width, height)); |
| | | } |
| | | |
| | | 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; |
| | | } |
| | | |
| | | private String formatLineLabel(Number value, boolean percentItemLabels) { |
| | | if (percentItemLabels) { |
| | | NumberFormat formatter = NumberFormat.getPercentInstance(); |
| | | formatter.setMaximumFractionDigits(1); |
| | | return formatter.format(value.doubleValue()); |
| | | } |
| | | return new DecimalFormat("#,##0.##").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()); |
| | | graphics.setColor(Color.WHITE); |
| | | for (int dx = -1; dx <= 1; dx++) { |
| | | for (int dy = -1; dy <= 1; dy++) { |
| | | if (dx != 0 || dy != 0) graphics.drawString(text, x + dx, y + dy); |
| | | } |
| | | } |
| | | graphics.setColor(color); |
| | | graphics.drawString(text, x, y); |
| | | } |
| | | |
| | | 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), |
| | | new HolidayTrendWordService.ImageSize(5268595L, 3424555L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåæç´¯è®¡è·¨åºåäººåæµå¨éèµ°å¿", |
| | | chartService.lineChart("ä¸äººæ¬¡", |
| | | series(years, yearData, item -> item.totalCumulative), false, 1100, 590), |
| | | new HolidayTrendWordService.ImageSize(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), |
| | | new HolidayTrendWordService.ImageSize(5232400L, 2484120L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåæé路客è¿éæ
åµ", |
| | | chartService.lineChart("ä¸äººæ¬¡", |
| | | series(years, yearData, item -> item.rail), false, 1100, 445), |
| | | new HolidayTrendWordService.ImageSize(5264785L, 2129155L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåææ°´è·¯å®¢è¿éæ
åµ", |
| | | chartService.lineChart("ä¸äººæ¬¡", |
| | | series(years, yearData, item -> item.water), false, 1100, 433), |
| | | new HolidayTrendWordService.ImageSize(5274310L, 2075815L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåææ°èªå®¢è¿éæ
åµ", |
| | | chartService.lineChart("ä¸äººæ¬¡", |
| | | series(years, yearData, item -> item.civilAviation), false, 1100, 521), |
| | | new HolidayTrendWordService.ImageSize(5271135L, 2496185L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåæé路客è¿éæ
åµ", |
| | | chartService.lineChart("ä¸äººæ¬¡", |
| | | series(years, yearData, item -> item.road), false, 1100, 590), |
| | | new HolidayTrendWordService.ImageSize(5268595L, 2821305L))); |
| | | |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("ä¸ã车æµéèµ°å¿")); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåæé«éå
¬è·¯è½¦æµéæ
åµ", |
| | | chartService.lineChart("ä¸è¾æ¬¡", |
| | | series(years, yearData, item -> item.expressway), false, 1100, 590), |
| | | new HolidayTrendWordService.ImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåææ®éå½çé车æµéæ
åµ", |
| | | chartService.lineChart("ä¸è¾æ¬¡", |
| | | series(years, yearData, item -> item.nationalProvincial), false, 1100, 590), |
| | | new HolidayTrendWordService.ImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "â" + holidayLabel + "âåæå
¨çè½¦æµæ»éæ
åµ", |
| | | chartService.lineChart("ä¸è¾æ¬¡", |
| | | series(years, yearData, item -> item.totalVehicleFlow), false, 1100, 590), |
| | | new HolidayTrendWordService.ImageSize(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), |
| | | new HolidayTrendWordService.ImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | prefix + "æ®éå½çéè§æµç«å¹³åæ¥æ¤åº¦", |
| | | chartService.lineChart("æ¥æ¤åº¦", |
| | | Collections.singletonList(series("平忥æ¤åº¦", observation.congestion)), false, 1100, 590), |
| | | new HolidayTrendWordService.ImageSize(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 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.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; |
| | | } |
| | |
| | | |
| | | 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> { |
| | |
| | | + "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); |
| | | } |
| | |
| | | void formatsTableRatiosAsSignedPercentages() throws Exception { |
| | | HolidayBulletinService service = new HolidayBulletinService(null); |
| | | |
| | | assertEquals("-4.7%", ratio(service, "95.3", "100")); |
| | | assertEquals("1.0%", ratio(service, "101", "100")); |
| | | assertEquals("0%", ratio(service, "100", "100")); |
| | | assertEquals("0%", ratio(service, "10004", "10000")); |
| | | assertEquals("â", ratio(service, "100", null)); |
| | | assertEquals("â", ratio(service, "100", "0")); |
| | | 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 |
| | |
| | | } |
| | | |
| | | @Test |
| | | void cumulativeParagraphUsesWanUnitAndSingleRounding() throws Exception { |
| | | void cumulativeParagraphUsesExcelDisplayThenBulletinRounding() throws Exception { |
| | | HolidayBulletinService service = new HolidayBulletinService(null); |
| | | |
| | | DailyMetrics current = new DailyMetrics(); |
| | |
| | | String text = (String) method.invoke(service, current, previousYear, twoYearsAgo); |
| | | |
| | | assertEquals( |
| | | "åæä»¥æ¥ï¼å
¨çå
¨ç¤¾ä¼è·¨åºåäººåæµå¨é累计3088.63ä¸äººæ¬¡ï¼æ¯2025å¹´åæä¸é4.4%ï¼æ¯2024å¹´åæä¸é1.6%ã", |
| | | "åæä»¥æ¥ï¼å
¨çå
¨ç¤¾ä¼è·¨åºåäººåæµå¨é累计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, |
| | |
| | | return method.invoke(service, current, comparisonYear, selector); |
| | | } |
| | | |
| | | private String ratio(HolidayBulletinService service, String current, String base) throws Exception { |
| | | Method method = HolidayBulletinService.class.getDeclaredMethod("ratio", BigDecimal.class, BigDecimal.class); |
| | | 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); |
| | | return (String) method.invoke(service, currentValue, baseValue, excelPercentScale); |
| | | } |
| | | } |
| 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 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); |
| | | } |
| | | |
| | | 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 ""; |
| | | } |
| | | } |
| | |
| | | <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> |
| | | |
| | |
| | | importing: false, |
| | | exporting: false, |
| | | bulletinLoading: false, |
| | | trendLoading: false, |
| | | selectedFile: null, |
| | | fileList: [], |
| | | importMessage: '', |
| | |
| | | .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) |