| | |
| | | > **æ¯å¤©ç»ææ¶**ï¼æå½å¤©è¿å±ã踩è¿çåãæ°éæ±ãæªå®æäºé¡¹æ´æ°å°æ¬æä»¶ï¼ç¶åå¯ä»¥æ¾å¿å¼æ°å¯¹è¯ã |
| | | > **åä½çº¦å®ï¼2026-08-31ï¼**ï¼è¯´ãæäº¤ã= æ¬å° commit + æ¨éå°è¿ç«¯ gitï¼origin mainï¼Gitblitï¼ï¼ä¸ååªåæ¬å° commitã |
| | | |
| | | ## ææ°ï¼2026-10-02ï¼ |
| | | |
| | | - **æ°æ¨¡åï¼ç¨æ·å·²éªæ¶ï¼æ¬æ¬¡æäº¤/æ¨éï¼**ï¼æ°å¢âä¸é¡¹ç»è®¡-éè¦èåæ¥âæ¨¡åï¼å«è忥é
ç½®ãè¥ä¸æ§å®¢æµãéè¥ä¸æ§è½¦æµã客è¿ç³»æ° 4 å¼ è¡¨ï¼ç³»ç»é
ç½®èåä»
admin å¯ç»´æ¤ï¼ä¸é¡¹ç»è®¡-èåæ¥æ¯æå¯¼å
¥ã导åºã模æ¿ä¸è½½å Word å¿«æ¥ä¸è½½ã |
| | | - **æ°æ®åå§å**ï¼ä» `docs/ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´å䏿°æ®.xlsx` åå§å 2024/2025 å¹´åä¸å®¢æµã车æµå 2024-2026 年客è¿ç³»æ°ï¼æºè¡¨ 90 ä¸ªæ°æ®åå
æ ¼å
¥åºé项æ¯å¯¹å·®å¼ 0ã |
| | | - **æ¥å¸¸å¯¼å
¥å£å¾**ï¼åªè¯»åæé年份çå½å¹´åï¼2026 年为å客æµå·¥ä½è¡¨ C åãæ®éå½çé D åï¼ï¼åå²å¹´ä»½ä¸è¦æ±æ¥å¸¸é导ã |
| | | - **å岿°æ®ä¿æ¤**ï¼å¯¼å
¥æ¶å
æ£æ¥æ´æ©å岿¥ææ¯å¦è¢«ä¿®æ¹ãæåååå
ä¸ååºï¼è¿å差弿¸
åï¼å端弹çªè®©ç¨æ·éæ©âå
许导å
¥/ä¸å
许âï¼ç¡®è®¤åæè¦çãæ¥å£åæ° `allowHistoricalOverwrite=true`ã |
| | | - **å¿«æ¥**ï¼é»è®¤ä¸è½½ç³»ç»æ¥æåä¸å¤©çãæ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥ï¼10æXæ¥ï¼.docxãï¼ç¨æ·å¯èªéæ¥æï¼ç¯æ¯/忝æå¢é¿ãä¸éãæå¹³è¾åºã |
| | | - **Excel æµ®ç¹ç²¾åº¦ï¼2026-10-02 追å ï¼**ï¼åè `docs/Excelæµ®ç¹æ°åå¨.txt`ï¼æ°å¢ `HolidayNumberUtils`ï¼`NumberToTextConverter.toText` + `BigDecimal`ï¼`MathContext(15, HALF_UP)`ï¼ï¼èåæ¥æ¨¡åç± `Double` å
¨é¢æ¹ `BigDecimal`ï¼ä¸å¼ 表æ°å¼åæ¹ `DECIMAL(30,15)`ï¼åºå
6 æ¡å®¢è¿ç³»æ°ç± 2.3999999999999995 æ ¡æ£ä¸º 2.4ãè¿ç§»èæ¬ `docs/sql_holiday_decimal_migration.sql`ã |
| | | - **èå顺åº**ï¼âä¸é¡¹ç»è®¡â已移å°âæ¥è¡¨çæâä¹åï¼å½å为 æ¥è¡¨çæ -> ä¸é¡¹ç»è®¡ -> è§å管ç -> ç¨æ·ç®¡ç -> ç³»ç»é
ç½®ã |
| | | - **è忥å é¤å¤æ´»ä¿®å¤ï¼2026-10-02 追å ï¼**ï¼ååå§åé»è¾æ¯æ¬¡å¯å¨è¡¥é½ç¼ºå¤±ç第 N æ¥ï¼å¯¼è´ admin å é¤ 2026-10-08 åè¢«éæ°æå
¥ï¼ç°æ¹ä¸ºä»
`holiday_calendar` æ´è¡¨ä¸ºç©ºæ¶é¦æ¬¡åå§åãéå¯å 2026 å¹´åä¸ä¿æ 10-01 è³ 10-07 å
± 7 天ã |
| | | - **æ®éå½çéç¯æ¯åºæï¼2026-10-02 追å ï¼**ï¼æ®éå½çé C å为å¯éç¯æ¯åºæï¼ä»
2026 å¹´å¯ç¨ï¼å¡«åååªå½±åæ®éå½çé车æµéèªèº«ç¯æ¯ `彿¥ / åºæ - 1`ï¼æªå¡«åæå䏿¥ã`holiday_vehicle_flow.comparison_base` å·²å
¥åºï¼å¯¼å
¥å¯¼åºä¸åå²åæ´ç¡®è®¤åå·²æ¥å
¥ï¼æç®äººåæµå¨éãæ»äººåæµå¨éå Word å¿«æ¥ç»§ç»æ Excel åå
¬å¼ï¼ä¸ååºæå½±åã2024/2025 ä¸ä½¿ç¨ã |
| | | - **å¿«æ¥é¦æ¥å£å¾ï¼2026-10-02 追å ï¼**ï¼åæç¬¬1天没æå䏿¥ç¯æ¯åºæ°ï¼æ£æä¸è¾åºâç¯æ¯å¢é¿/ä¸é/æå¹³âçè¯ï¼è¡¨æ ¼å½æ¥ç¯æ¯å为âââï¼ç¬¬2å¤©èµ·ä»æå®é
å䏿¥è¾åºç¯æ¯ãå·²ç¨ 2025-10-01/02 å¿«æ¥åå½éªè¯ã |
| | | - **å¿«æ¥ç¾åæ¯ç²¾åº¦ï¼2026-10-02 追å ï¼**ï¼Word å¿«æ¥ä¸åæ¯/ç¯æ¯ç¾åæ¯ç»ä¸ä¿ç1ä½å°æ°ï¼å¦6.79%æ¾ç¤º6.8%ã11.46%æ¾ç¤º11.5%ï¼ï¼äººæ¬¡ã车æµçä¸å¡æ°å¼ä»ä¿ç2ä½å°æ°ã |
| | | - **éªè¯ç»æ**ï¼å端ç¼è¯ãå端æå»ºãæé/CRUD/è´å导å
¥ãåå²åæ´ä¸¤é¶æ®µå¯¼å
¥ãExcel 导åºãWord å¿«æ¥ãå端 admin/é admin çæµåéè¿ï¼èå顺åºã客è¿ç³»æ° 2.4 å±ç¤ºï¼ç³»ç»é
置页 + è忥ç»è®¡é¡µï¼ã`decimal(30,15)` ååãåå²åæ´ä¸¤é¶æ®µå¯¼å
¥åå½å已宿µï¼æ¥åè§ `docs/åè½æµè¯æ¥å/åè½æµè¯æ¥å_éè¦è忥ä¸é¡¹ç»è®¡_2026-10-02.md`ï¼æ¥ç»è§ `docs/工使¥ç»/工使¥ç»_2026-10-02.md`ã |
| | | - **è¿è¡ç¶æ**ï¼å端 8090ãå端 8080 å·²å¯å¨ï¼æ°æ®åº `127.0.0.1:3305/traffic_audit`ãæµè¯äº§çç 2026 ä¸´æ¶æ°æ®å导å
¥æ¹æ¬¡å·²æ¸
çã |
| | | - **Git**ï¼ç¨æ·å·²äº 2026-10-02 æç¡®éªæ¶éè¿ï¼æ¬æ¬¡æè¦æ±åå¹¶è¿ç«¯ `origin/main` åæäº¤å¹¶æ¨éï¼ä¸è¦çè¿ç«¯åäºæäº¤ã |
| | | |
| | | ## ææ°ï¼2026-09-23ï¼ |
| | | |
| | | - **已忥**ï¼æ¬å° `main` å¿«è¿åå¹¶å
¬å¸ `origin/main` ææ° `8b739ce`ï¼åäº 09-22 å
³æºå¿«ç
§ç»ä¸çï¼ï¼åå¹¶å `HEAD = 8b739ce = origin/main`ï¼å·¥ä½åºå¹²åã |
| New file |
| | |
| | | excelçæ°å¼è®¡ç®é常精确ï¼å
¶å
鍿¯æä¹å卿°å¼çå¢ï¼æ¯ç¨æµ®ç¹æ°å |
| | | ç¨æ·æ³ç¥éExcelå
é¨å¦ä½å卿°å¼ï¼æ¯å¦ä½¿ç¨æµ®ç¹æ°ãæéè¦æä¾åç¡®çè§£éã为äºå
¨é¢è·åä¿¡æ¯ï¼æå°åæ¶è¿è¡å¤é¡¹æç´¢ï¼è¦çExcelæ°å¼å卿ºå¶ãæµ®ç¹æ°ç²¾åº¦é®é¢ãä¸IEEE 754æ åçå
³ç³»ä»¥åå¯è½çä¼åææ¯ã |
| | | |
| | | æç´¢å° 16 个ç½é¡µ |
| | | æç´¢ç»ææ¾ç¤ºäºä¸äºç¸å
³é¡µé¢ã为äºå
¨é¢è·åä¿¡æ¯ï¼æä»¬éè¦åæ¶æå¼è¿äºé¡µé¢ã |
| | | |
| | | æµè§ 1 ä¸ªé¡µé¢ |
| | | Floating Point Errors in Excel |
| | | è¿äºèµææä¾äºå
³äºExcelæ°å¼åå¨ç详ç»ä¿¡æ¯ãå¾®è½¯çææ¡£æç¡®æåºExceléµå¾ªIEEE 754æ åï¼ä½¿ç¨åç²¾åº¦æµ®ç¹æ°ï¼æææ°å为15ä½ãStack Overflowç讨论ä¹ç¡®è®¤äºè¿ä¸ç¹ãExcelJetçæç« è§£éäºæµ®ç¹æ°è¯¯å·®çåå ãæçåçå°æ¶µç以ä¸å 个æ¹é¢ï¼Excel使ç¨IEEE 754åç²¾åº¦æµ®ç¹æ°ï¼64ä½ï¼ï¼ææç²¾åº¦ä¸º15ä½ï¼åå¨èå´ï¼ä»¥åæµ®ç¹æ°è¯¯å·®åå
¶åå ãæè¿ä¼æåä¸äºç¹æ®æ
åµï¼æ¯å¦æ´æ°åå¨åæ¥ææ¶é´åå¨ãä¸ºäºæ´å
¨é¢å°è¦çè¿äºæ¹é¢ï¼æéè¦åæ¶è¿è¡å¤é¡¹æç´¢ã |
| | | |
| | | æç´¢å° 12 个ç½é¡µ |
| | | ç°å¨å¯ä»¥ç»ç»çæ¡äºãçæ¡å°æ¶µçï¼Excel使ç¨IEEE 754åç²¾åº¦æµ®ç¹æ°ï¼64ä½ï¼ï¼ææç²¾åº¦ä¸º15ä½ï¼åå¨èå´ï¼ä»¥åæµ®ç¹æ°è¯¯å·®åå
¶åå ãè¿ä¼æåæ´æ°åæ¥ææ¶é´çå卿¹å¼ãåçæ¶ä¼å¼ç¨å¾®è½¯ææ¡£ãStack Overflowçæ¥æºã |
| | | |
| | | æ¯çï¼Excel çæ°å¼è®¡ç®ç¡®å®åºäºæµ®ç¹æ°ï¼å
·ä½æ¥è¯´ï¼å®éµå¾ª IEEE 754 æ åç 64 ä½åç²¾åº¦æµ®ç¹æ°æ ¼å¼æ¥åå¨åè®¡ç®æ°å¼ã |
| | | |
| | | æ ¸å¿æºå¶ï¼64ä½åç²¾åº¦æµ®ç¹æ° |
| | | Excel å
é¨å°ææå·¥ä½è¡¨åå
æ ¼ä¸çæ°åï¼å
æ¬æ´æ°ãæ¥æåæ¶é´ï¼é½ç»ä¸åå¨ä¸º 8åèï¼64ä½ï¼çåç²¾åº¦æµ®ç¹æ°ã |
| | | |
| | | è¿ç§å卿 ¼å¼ç 64 个æ¯ç¹ä½è¢«åå为ä¸ä¸ªé¨åï¼ |
| | | |
| | | 符å·ä½ (1ä½)ï¼è¡¨ç¤ºæ£è´ã |
| | | |
| | | ææ°ä½ (11ä½)ï¼å³å®æ°å¼çèå´ã |
| | | |
| | | å°¾æ°ä½ (52ä½)ï¼å³å®æ°å¼ç精度ã |
| | | |
| | | 精度éå¶ï¼15ä½æææ°å |
| | | 尽管åç²¾åº¦æµ®ç¹æ°è½æä¾çº¦ 15 å° 16 ä½çåè¿å¶æææ°åï¼ä½ Excel 为äºç¡®ä¿è®¡ç®ç»æçå¯é æ§ï¼å°ææç²¾åº¦éå¶å¨ 15 ä½ã |
| | | |
| | | è¿æå³çï¼å¦æä¸ä¸ªæ°åçææä½æ°è¶
è¿ 15 ä½ï¼ä»ç¬¬ 16 ä½å¼å§å°è¢«æ¿æ¢ä¸º 0ãä¾å¦ï¼è¾å
¥ 123456789012345678ï¼Excel å®é
åå¨çå°æ¯ 123456789012345000ã |
| | | |
| | | æ°å¼èå´ |
| | | åºäºæ¤æ åï¼Excel å¯ä»¥å¤ççæ°å¼èå´å¦ä¸ï¼ |
| | | |
| | | æå¤§æ£æ°ï¼1.79769313486232E+308 |
| | | |
| | | æå°æ£æ°ï¼2.2250738585072E-308 |
| | | |
| | | è¶
åºæ¤èå´çæ°å¼ä¼è¢«è§ä¸ºâæ 穷大âå¹¶æ¾ç¤ºä¸ºé误å¼ã |
| | | |
| | | 为ä»ä¹ä¼ææµ®ç¹æ°è¯¯å·®ï¼ |
| | | ä½ å¯è½ä¼éå° Excel è®¡ç® 0.1 + 0.2 å¾å° 0.30000000000000004 çæ
åµï¼è¿å¹¶é Bugï¼èæ¯äºè¿å¶æµ®ç¹æ°çåºæç¹æ§ã |
| | | |
| | | å
¶æ ¹æ¬åå å¨äºï¼å 0.1 è¿æ ·çåè¿å¶å°æ°æ æ³ç¨äºè¿å¶æµ®ç¹æ°ç²¾ç¡®è¡¨ç¤ºï¼å°±ååè¿å¶æ æ³ç²¾ç¡®è¡¨ç¤º 1/3 䏿 ·ãExcel åå¨çæ¯ä¸ä¸ªæå
¶æ¥è¿ 0.1 çè¿ä¼¼å¼ï¼å½å¤ä¸ªè¿æ ·çè¿ä¼¼å¼è¿è¡è¿ç®æ¶ï¼å¾®å°ç误差ä¼ç´¯ç§¯ï¼æç»å¯¼è´ç»æåºç°æå°çåå·®ã |
| | | |
| | | å
¶ä»æ°æ®ç±»åçåå¨ |
| | | æ´æ°ï¼ä¹æåç²¾åº¦æµ®ç¹æ°åå¨ï¼å æ¤ Excel è½ç²¾ç¡®è¡¨ç¤ºçæ´æ°ä¸éæ¯ 2^53ï¼çº¦ 9,007,199,254,740,992ï¼ãè¶
è¿æ¤èå´çæ´æ°å¯è½ä¼ä¸¢å¤±ç²¾åº¦ã |
| | | |
| | | æ¥æåæ¶é´ï¼åæ ·æ¯æµ®ç¹æ°ãæ´æ°é¨åä»£è¡¨æ¥æï¼èª 1900 å¹´ 1 æ 1 æ¥ä»¥æ¥ç天æ°ï¼ï¼å°æ°é¨å代表æ¶é´ï¼ä¸å¤©ä¸çæ¯ä¾ï¼ã |
| | | |
| | | æ»ç»æ¥è¯´ï¼Excel çæ°å¼è®¡ç®å¨åºå±å°±æ¯æ åç IEEE 754 åç²¾åº¦æµ®ç¹æ°ï¼å
¶âç²¾ç¡®âæ¯ç¸å¯¹çï¼åéäº 15 ä½æææ°åç精度åäºè¿å¶æµ®ç¹æ°æ¬èº«ç表示éå¶ã |
| | | |
| | | é£ä¹ç¨Javaå¤çexcelçæ°æ®æ¶ï¼ä¸ºäºä¿æä¸excelç计ç®ç»æä¸è´ï¼æ°æ®åºåJavaä¸å¦ä½åå¨å计ç®å¢ |
| | | ç¨æ·æ³ç¥éå¦ä½å¨Javaä¸å¤çExcelæ°æ®ï¼ä»¥ä¿æä¸Excel计ç®ç»æä¸è´ï¼æ¶åæ°æ®åºåJavaä¸çåå¨ä¸è®¡ç®ãæéè¦æä¾å
³äºæ°æ®ç±»åéæ©ã计ç®çç¥ä»¥åå¯è½éå°ç精度é®é¢æ¹é¢ç建议ã为äºå
¨é¢è·åä¿¡æ¯ï¼æå°åæ¶è¿è¡å¤é¡¹æç´¢ï¼è¦çJavaå¤çExcelç²¾åº¦ãæ°æ®åºåå¨çç¥ãBigDecimal使ç¨ãApache POIå¤çæ¹å¼ä»¥åIEEE 754å
¼å®¹æ§çå
³é®æ¹é¢ã |
| | | |
| | | æç´¢å° 36 个ç½é¡µ |
| | | æç´¢ç»ææ¾ç¤ºäºä¸äºç¸å
³èµæºã为äºå
¨é¢è·åä¿¡æ¯ï¼æä»¬éè¦åæ¶æå¼è¿äºç»æã |
| | | |
| | | æµè§ 4 ä¸ªé¡µé¢ |
| | | NumberComparer (POI API Documentation) |
| | | EasyExcel读å
¥æ°åç±»åæ°æ®æ¶åºç°å°æ°ä½å¢é¿ç°è±¡ - EasyExcel读å
¥æ°åç±»åæ°æ®æ¶åºç°å°æ°ä½å¢é¿ç°è±¡ |
| | | ãå®ç¾è§£å³POI读åExcelææ¡£æ°å¼ç±»å缺失精度é®é¢ã |
| | | MySQL :: MySQL 9.0 åèæå :: 13.1.3 å®é»é¡åï¼ç²¾ç¢ºå¼ï¼- DECIMALãNUMERIC |
| | | è¿äºèµææä¾äºå
³äºç²¾åº¦é®é¢ãApache POIçNumberToTextConverterãExcelç15ä½ç²¾åº¦éå¶ä»¥åæ°æ®åºDECIMALç±»åçä¿¡æ¯ãæçåçå°æ¶µçJavaä¸çåå¨ä¸è®¡ç®ãæ°æ®åºä¸çåå¨ã以åå¦ä½ä¿æç»æä¸è´ãåçä¼å¼ç¨è¿äºèµæãç°å¨å¼å§ç»ç»åçã |
| | | |
| | | è¦å¨Javaä¸å¤çExcelæ°æ®å¹¶ä¿æç»æä¸è´ï¼æ ¸å¿æè·¯æ¯å°Excelç15ä½æææ°å精度ä½ä¸ºåºåï¼å¨Javaåæ°æ®åºä¸éç¨æ´é«ç²¾åº¦æç²¾ç¡®ç±»åè¿è¡åå¨ï¼å¹¶ç»ä¸è®¡ç®ä¸èå
¥è§åã |
| | | |
| | | ð¾ ç¬¬ä¸æ¥ï¼å¨Javaä¸å¦ä½åå¨ä¸è¯»å |
| | | Javaç double ç±»ååæ ·éµå¾ª IEEE 754 æ åï¼ä½å
¶æææ°åå¯è¾¾15-17ä½ï¼èExcelä¸¥æ ¼éå¶ä¸º15ä½ï¼è¿æ¯å·®å¼çæ ¹æºã |
| | | |
| | | è¯»åæ¶ï¼æ¨èç¨ BigDecimal æ String |
| | | |
| | | ç´æ¥ç¨ double æ¥æ¶ Excel æ°å¼ä¼å¼å
¥é¢å¤çäºè¿å¶æµ®ç¹è¯¯å·®ãä¾å¦ï¼Excel ä¸ç 9699.1 读æ double å¯è½åæ 9699.0999999999999996ã |
| | | |
| | | æ¨èæ¹æ¡ï¼ |
| | | |
| | | 读å为 BigDecimalï¼å¨å®ä½ç±»ä¸ï¼å°éé¢ãæ°éçæ°å¼å段声æä¸º BigDecimal ç±»åï¼èé double æ floatã |
| | | |
| | | ä½¿ç¨ POI ç NumberToTextConverterï¼Apache POI æä¾äº NumberToTextConverter.toText(double) å·¥å
·ç±»ï¼å¯ä»¥å° Excel çåå§æ°å¼ç²¾ç¡®å°è½¬æ¢ä¸ºå符串ï¼åç±å符串æé BigDecimalï¼é¿å
ä¸é´ç²¾åº¦æå¤±ã |
| | | |
| | | java |
| | | import org.apache.poi.ss.util.NumberToTextConverter; |
| | | import java.math.BigDecimal; |
| | | |
| | | if (cell.getCellType() == CellType.NUMERIC) { |
| | | String exactText = NumberToTextConverter.toText(cell.getNumericCellValue()); |
| | | BigDecimal value = new BigDecimal(exactText); |
| | | // æ¤æ¶ value æ¯ Excel ä¸åå¨ç精确å¼ï¼15ä½æææ°åï¼ |
| | | } |
| | | ðï¸ ç¬¬äºæ¥ï¼å¨æ°æ®åºä¸å¦ä½åå¨ |
| | | æ°æ®åºå段åå¿ä½¿ç¨ FLOAT æ DOUBLEï¼å 为å®ä»¬åæ ·åå¨äºè¿å¶æµ®ç¹è¯¯å·®ï¼ä¼å¯¼è´ä¸Excelç»æçåå·®ã |
| | | |
| | | æ¨èæ¹æ¡ï¼ä½¿ç¨ DECIMAL ç±»å |
| | | |
| | | DECIMALï¼æ NUMERICï¼æ¯å®ç¹ç²¾ç¡®ç±»åï¼å®ææ´æ°ä½åå°æ°ä½åå¨ï¼è½ç²¾ç¡®è¡¨ç¤ºåè¿å¶å°æ°ã |
| | | |
| | | æ°æ®åº æ¨èç±»å 说æ |
| | | MySQL DECIMAL(p, s) p 为æ»ç²¾åº¦ï¼s ä¸ºå°æ°ä½ãExcel ææä½15ï¼å»ºè®® p è³å°ä¸º 15ï¼åæ ¹æ®å°æ°ä½éæ±è®¾å®ãä¾å¦ DECIMAL(18, 6)ã |
| | | PostgreSQL NUMERIC(p, s) åä¸ï¼æ åSQLç±»åã |
| | | SQL Server DECIMAL(p, s) åä¸ã |
| | | Oracle NUMBER(p, s) NUMBER ç±»åå¯åå¨å®ç¹æ°ï¼ææçåã |
| | | JDBC æ å°ï¼ä½¿ç¨ JDBC è¯»åæ¶ï¼DECIMAL åæ®µä¼ç´æ¥æ å°ä¸º Java ç BigDecimalï¼ä¿è¯äºç²¾åº¦å¨ä¼ éè¿ç¨ä¸ä¸ä¸¢å¤±ã |
| | | |
| | | ð§® ç¬¬ä¸æ¥ï¼Javaä¸ç计ç®ä¸èå
¥è§å |
| | | å³ä½¿åå¨ç²¾ç¡®ï¼è®¡ç®é»è¾ä¹å¿
须模æ Excel çè¡ä¸ºã |
| | | |
| | | 1. å§ç»ä½¿ç¨ BigDecimal è¿è¡è®¡ç® |
| | | |
| | | BigDecimal çå åä¹é¤ï¼add, subtract, multiply, divideï¼é½æ¯ç²¾ç¡®è®¡ç®ï¼ä¸ä¼å¼å
¥äºè¿å¶è¯¯å·®ãåå¿å° BigDecimal 转å double åå计ç®ã |
| | | |
| | | 2. 模æ Excel ç15ä½æææ°åéå¶ |
| | | |
| | | Excel 卿¯æ¬¡å°ä¸é´ç»æåå
¥åå
æ ¼æ¶ï¼é½ä¼å°ç»æèå
¥å°15ä½æææ°åãå¨ Java ä¸ï¼éè¦æ¨¡æè¿ä¸è¡ä¸ºã |
| | | |
| | | java |
| | | import java.math.BigDecimal; |
| | | import java.math.MathContext; |
| | | import java.math.RoundingMode; |
| | | |
| | | // å建ä¸ä¸ªç²¾åº¦ä¸º15ä½æææ°åï¼èå
¥æ¨¡å¼ä¸ºâåèäºå
¥âçä¸ä¸æ |
| | | MathContext EXCEL_MC = new MathContext(15, RoundingMode.HALF_UP); |
| | | |
| | | public BigDecimal excelRound(BigDecimal value) { |
| | | // åºç¨Excelçèå
¥è§å |
| | | return value.round(EXCEL_MC); |
| | | } |
| | | |
| | | // 示ä¾ï¼å¨æ¯ä¸æ¥å
³é®è®¡ç®åè°ç¨ |
| | | BigDecimal intermediate = a.add(b); |
| | | intermediate = excelRound(intermediate); // 模æåå
¥åå
æ ¼ |
| | | BigDecimal result = intermediate.multiply(c); |
| | | result = excelRound(result); |
| | | 注æï¼Excel çèå
¥æ¨¡å¼è¿ä¼¼äºâåèäºå
¥âï¼ä½åå¨å¾®å¦çâé¶è¡å®¶èå
¥âè¡ä¸ºãRoundingMode.HALF_UP æ¯ææ¥è¿çè¿ä¼¼ï¼ä½å¨æå°æ°è¾¹ç¼æ
åµä¸ä»æå·®å¼ã对äºè´¢å¡ç³»ç»ï¼å»ºè®®å¨æç»ç»æä¸æä¸å¡è§åï¼å¦ RoundingMode.HALF_UPï¼æ¾å¼ä¿çåºå®å°æ°ä½ã |
| | | |
| | | 3. æ¯è¾è¿ç®éç¹æ®å¤ç |
| | | |
| | | Excel 卿¯è¾æ°åæ¶æé¢å¤ç容差é»è¾ãä¾å¦ï¼(0.06-0.01)=0.05 å¨ Excel ä¸ä¸º TRUEï¼ä½å¨ Java ç double æ¯è¾ä¸ä¸º falseãApache POI æä¾äº NumberComparer.compare(double a, double b) æ¥æ¨¡æè¿ç§æ¯è¾è¡ä¸ºãå¯¹äº BigDecimalï¼å»ºè®®å
æ15ä½æææ°åèå
¥ååç¨ compareTo() æ¯è¾ã |
| | | |
| | | ð æ»ç» |
| | | è¦å¨ Java çæä¸å¤ç° Excel ç计ç®ç»æï¼å
³é®å¨äºï¼ |
| | | |
| | | å
¥å£ç²¾ç¡®ï¼ç¨ NumberToTextConverter + BigDecimal 读åï¼é¿å
double 污æã |
| | | |
| | | åå¨ç²¾ç¡®ï¼æ°æ®åºä½¿ç¨ DECIMAL ç±»åï¼Java åæ®µä½¿ç¨ BigDecimalã |
| | | |
| | | 计ç®ç²¾ç¡®ï¼å
¨ç¨ä½¿ç¨ BigDecimal è¿ç®ã |
| | | |
| | | èå
¥å¯¹é½ï¼å¨æ¯æ¬¡å¯è½äº§çä¸é´ç»æçå°æ¹ï¼ç¨ MathContext(15, RoundingMode.HALF_UP) 模æ Excel ç15ä½æææ°åæªæã |
| | | |
| | | æ¯è¾å®¹å·®ï¼ä½¿ç¨æ¨¡æ Excel è¡ä¸ºçæ¯è¾é»è¾ï¼èéç´æ¥æ¯è¾ã |
| | | |
| | | è¿æ ·å°±è½æå¤§ç¨åº¦å°ä¿è¯ Java ä¸ Excel ç计ç®ç»æä¸è´ã |
| | |
| | | | /api/report/export/investFiveYearLogistics | "åäºäº"ç©æµé¡¹ç®è¿å± | åºå®ååè¿æ»¤ï¼å¸å·å°è+项ç®è¡ | |
| | | | /api/report/export/investBillion | 亿å
æèµé¡¹ç® | æèµæ»è¡¨ + è§ä¸é¡¹ç®è¡¨ä¸¤sheetï¼äº¿å
ååè¿æ»¤ | |
| | | | /api/report/export/investCounty | ç»æµå¼ºå¿æèµç»è®¡ | ç»æµå¼ºå¿ååè¿æ»¤ | |
| | | |
| | | |
| | | ## ä¹ãä¸é¡¹ç»è®¡-éè¦èåæ¥æ¨¡å |
| | | |
| | | > æ°æ®æ¥æºä¸æ¨¡æ¿ï¼`docs/ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´å䏿°æ®.xlsx`ï¼å¿«æ¥æ¨¡æ¿ï¼`docs/ä¸é¡¹ç»è®¡/éè¦è忥/æ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥æ¨¡æ¿.docx`ã馿¬¡å¯å¨ä¼ä»å·¥ä½ç°¿åå§å 2024/2025 å¹´åä¸å®¢æµãè½¦æµæ°æ®ï¼å¹¶åå§å 2024-2026 年客è¿ç³»æ°ã |
| | | |
| | | ### holiday_calendar èåæ¥æ¥æé
ç½® |
| | | | åæ®µ | ç±»å | 说æ | |
| | | |------|------|------| |
| | | | id | bigint | PK | |
| | | | year | int | 年份 | |
| | | | holiday_type | varchar(32) | æ¥è SPRING_FESTIVAL / äºä¸ LABOR_DAY / åä¸ NATIONAL_DAY | |
| | | | holiday_date | date | è忥å
·ä½æ¥æ | |
| | | | day_index | int | ç¬¬å æ¥ï¼ä» 1 å¼å§ | |
| | | | created_at / updated_at | datetime | å建/æ´æ°æ¶é´ | |
| | | |
| | | å¯ä¸é®ï¼`(year, holiday_type, day_index)`ã`(year, holiday_type, holiday_date)`ãä»
admin å¯éè¿ç³»ç»é
ç½®èåç»´æ¤ã |
| | | |
| | | ### holiday_passenger_flow èåæ¥ç»¼åè¿è¾è¥ä¸æ§å®¢æµ |
| | | | åæ®µ | ç±»å | 说æ | |
| | | |------|------|------| |
| | | | id | bigint | PK | |
| | | | year | int | 年份 | |
| | | | flow_date | date | æ¥æ | |
| | | | transport_mode | varchar(32) | éè·¯ RAIL / æ°èª CIVIL_AVIATION / éè·¯ ROAD / æ°´è·¯ WATER | |
| | | | passenger_count | decimal(30,15) | åéæ
客æ°ï¼ä¸äººæ¬¡ï¼ | |
| | | | created_at / updated_at | datetime | å建/æ´æ°æ¶é´ | |
| | | |
| | | å¯ä¸é®ï¼`(year, flow_date, transport_mode)`ã |
| | | |
| | | ### holiday_vehicle_flow èåæ¥ç»¼åè¿è¾éè¥ä¸æ§è½¦æµ |
| | | | åæ®µ | ç±»å | 说æ | |
| | | |------|------|------| |
| | | | id | bigint | PK | |
| | | | year | int | 年份 | |
| | | | flow_date | date | æ¥æ | |
| | | | road_category | varchar(32) | é«éå
¬è·¯ EXPRESSWAY / æ®éå½çé NATIONAL_PROVINCIAL | |
| | | | vehicle_type | varchar(32) | å°å®¢è½¦ SMALL_PASSENGER / ä¸å°å®¢è½¦ MEDIUM_SMALL_PASSENGER | |
| | | | vehicle_count | decimal(30,15) | 车æµéï¼ä¸è¾æ¬¡ï¼ | |
| | | | comparison_base | decimal(30,15) | æ®éå½çéç¯æ¯åºæï¼ä¸è¾æ¬¡ï¼ä»
å½å¹´ä½¿ç¨ï¼ | |
| | | | created_at / updated_at | datetime | å建/æ´æ°æ¶é´ | |
| | | |
| | | å¯ä¸é®ï¼`(year, flow_date, road_category, vehicle_type)`ã |
| | | |
| | | ### holiday_passenger_coefficient 客è¿ç³»æ°é
ç½® |
| | | | åæ®µ | ç±»å | 说æ | |
| | | |------|------|------| |
| | | | id | bigint | PK | |
| | | | year | int | 年份 | |
| | | | date_category | varchar(32) | è忥 HOLIDAY / éè忥 NON_HOLIDAY | |
| | | | coefficient | decimal(30,15) | 客è¿ç³»æ° | |
| | | | created_at / updated_at | datetime | å建/æ´æ°æ¶é´ | |
| | | |
| | | å¯ä¸é®ï¼`(year, date_category)`ãä»
admin å¯éè¿ç³»ç»é
ç½®èåç»´æ¤ã |
| | | |
| | | ### 计ç®ä¸å¯¼å
¥å¯¼åºå£å¾ |
| | | - éè¥ä¸æ§äººåæµå¨é =ï¼é«éè½¦æµ + æ®éå½çé车æµï¼Ã å½å¹´è忥客è¿ç³»æ°ã |
| | | - 2026 å¹´æ®éå½çéå¦å¡«åç¯æ¯åºæï¼ååªå½±åæ®éå½çé车æµéèªèº«ç¯æ¯ï¼å¡«åæ¶æâ彿¥ / ç¯æ¯åºæ - 1âï¼æªå¡«åæ¶æâ彿¥ / å䏿¥ - 1âï¼æç®äººåæµå¨éãæ»äººåæµå¨éçç»§ç»æ Excel åå
¬å¼è®¡ç®ã2024ã2025 å岿°æ®ä¸ä½¿ç¨è¯¥å段ã |
| | | - å
¬è·¯äººåæµå¨é = éè·¯è¥ä¸æ§å®¢è¿é + éè¥ä¸æ§äººåæµå¨éã |
| | | - æ»äººåæµå¨é = å
¬è·¯ + éè·¯ + æ°èª + æ°´è·¯ã |
| | | - 导å
¥æâ年份 + è忥åç±»â读å `holiday_calendar` çæ¥æï¼å°å·¥ä½ç°¿â第Næ¥âè¡è¦çåå
¥å½å¹´å®¢æµ/车æµï¼æ¥å¸¸åªè¯»åæé年份çå½å¹´åï¼åå²å¹´ä»½ä»
馿¬¡åå§åã |
| | | - 导å
¥åæ£æ¥æ¯æ¬æ¬¡ææ°å¯¼å
¥æ¥æ´æ©çå岿¥ææ¯å¦åçååï¼å¦æååå
ä¸ååºï¼è¿å差弿¸
åï¼ç¨æ·ç¡®è®¤åæå
许è¦çã |
| | | - 导åºä»¥æºå·¥ä½ç°¿ä¸ºåºç¨¿ï¼åå¡«åºå
æ°æ®ä¸å®¢è¿ç³»æ°ï¼å¿«æ¥ææ¥æçæ Word å¹¶æ¿æ¢æ¨¡æ¿ä¸ç彿¥ã累计ãç¯æ¯å忝å
容ã |
| | |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | KEY idx_type_period (report_type, report_period) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='æ¥è¡¨æå®¡æ ¸éè¿æ è®°'; |
| | | |
| | | |
| | | -- ============================================ |
| | | -- åä¸ãä¸é¡¹ç»è®¡-éè¦è忥 |
| | | -- ============================================ |
| | | |
| | | -- èåæ¥æ¥æé
ç½® |
| | | DROP TABLE IF EXISTS holiday_calendar; |
| | | CREATE TABLE holiday_calendar ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | holiday_type VARCHAR(32) NOT NULL COMMENT 'è忥åç±»ï¼SPRING_FESTIVAL/LABOR_DAY/NATIONAL_DAY', |
| | | holiday_date DATE NOT NULL COMMENT 'è忥å
·ä½æ¥æ', |
| | | day_index INT NOT NULL COMMENT 'ç¬¬å æ¥ï¼ä» 1 å¼å§', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_calendar_seq (year, holiday_type, day_index), |
| | | UNIQUE KEY uk_holiday_calendar_date (year, holiday_type, holiday_date), |
| | | KEY idx_holiday_calendar_date (holiday_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='éè¦èåæ¥æ¥æé
ç½®'; |
| | | |
| | | -- èåæ¥ç»¼åè¿è¾è¥ä¸æ§å®¢æµ |
| | | DROP TABLE IF EXISTS holiday_passenger_flow; |
| | | CREATE TABLE holiday_passenger_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | flow_date DATE NOT NULL COMMENT 'æ¥æ', |
| | | transport_mode VARCHAR(32) NOT NULL COMMENT 'è¿è¾æ¹å¼ï¼RAIL/CIVIL_AVIATION/ROAD/WATER', |
| | | passenger_count DECIMAL(30,15) NOT NULL COMMENT 'åéæ
客æ°ï¼ä¸äººæ¬¡ï¼', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_passenger (year, flow_date, transport_mode), |
| | | KEY idx_holiday_passenger_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='èåæ¥ç»¼åè¿è¾è¥ä¸æ§å®¢æµæ
åµ'; |
| | | |
| | | -- èåæ¥ç»¼åè¿è¾éè¥ä¸æ§è½¦æµ |
| | | DROP TABLE IF EXISTS holiday_vehicle_flow; |
| | | CREATE TABLE holiday_vehicle_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | flow_date DATE NOT NULL COMMENT 'æ¥æ', |
| | | road_category VARCHAR(32) NOT NULL COMMENT 'éè·¯åç±»ï¼EXPRESSWAY/NATIONAL_PROVINCIAL', |
| | | vehicle_type VARCHAR(32) NOT NULL COMMENT '车ååç±»ï¼SMALL_PASSENGER/MEDIUM_SMALL_PASSENGER', |
| | | vehicle_count DECIMAL(30,15) NOT NULL COMMENT '车æµéï¼ä¸è¾æ¬¡ï¼', |
| | | comparison_base DECIMAL(30,15) NULL COMMENT 'æ®éå½çéç¯æ¯åºæï¼ä¸è¾æ¬¡ï¼ä»
å½å¹´ä½¿ç¨ï¼', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_vehicle (year, flow_date, road_category, vehicle_type), |
| | | KEY idx_holiday_vehicle_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='èåæ¥ç»¼åè¿è¾éè¥ä¸æ§è½¦æµæ
åµ'; |
| | | |
| | | -- 客è¿ç³»æ°é
ç½® |
| | | DROP TABLE IF EXISTS holiday_passenger_coefficient; |
| | | CREATE TABLE holiday_passenger_coefficient ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL COMMENT '年份', |
| | | date_category VARCHAR(32) NOT NULL COMMENT 'æ¥æåç±»ï¼HOLIDAY/NON_HOLIDAY', |
| | | coefficient DECIMAL(30,15) NOT NULL COMMENT '客è¿ç³»æ°', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_coefficient (year, date_category) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客è¿ç³»æ°é
ç½®'; |
| New file |
| | |
| | | -- Holiday statistics module. Run against the traffic_audit database. |
| | | CREATE TABLE IF NOT EXISTS holiday_calendar ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | holiday_type VARCHAR(32) NOT NULL, |
| | | holiday_date DATE NOT NULL, |
| | | day_index INT NOT NULL, |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_calendar_seq (year, holiday_type, day_index), |
| | | UNIQUE KEY uk_holiday_calendar_date (year, holiday_type, holiday_date), |
| | | KEY idx_holiday_calendar_date (holiday_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| | | |
| | | CREATE TABLE IF NOT EXISTS holiday_passenger_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | flow_date DATE NOT NULL, |
| | | transport_mode VARCHAR(32) NOT NULL, |
| | | passenger_count DECIMAL(30,15) NOT NULL, |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_passenger (year, flow_date, transport_mode), |
| | | KEY idx_holiday_passenger_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| | | |
| | | CREATE TABLE IF NOT EXISTS holiday_vehicle_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | flow_date DATE NOT NULL, |
| | | road_category VARCHAR(32) NOT NULL, |
| | | vehicle_type VARCHAR(32) NOT NULL, |
| | | vehicle_count DECIMAL(30,15) NOT NULL, |
| | | comparison_base DECIMAL(30,15) NULL COMMENT 'æ®éå½çéç¯æ¯åºæï¼ä¸è¾æ¬¡ï¼ä»
å½å¹´ä½¿ç¨ï¼', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_vehicle (year, flow_date, road_category, vehicle_type), |
| | | KEY idx_holiday_vehicle_date (flow_date) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| | | |
| | | CREATE TABLE IF NOT EXISTS holiday_passenger_coefficient ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | year INT NOT NULL, |
| | | date_category VARCHAR(32) NOT NULL, |
| | | coefficient DECIMAL(30,15) NOT NULL, |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_holiday_coefficient (year, date_category) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| New file |
| | |
| | | -- Add the optional ordinary national/provincial road comparison base. |
| | | -- The field is used only for the 2026 holiday data. |
| | | ALTER TABLE holiday_vehicle_flow |
| | | ADD COLUMN comparison_base DECIMAL(30,15) NULL |
| | | COMMENT 'æ®éå½çéç¯æ¯åºæï¼ä¸è¾æ¬¡ï¼ä»
å½å¹´ä½¿ç¨ï¼' |
| | | AFTER vehicle_count; |
| New file |
| | |
| | | -- Convert holiday statistics numeric columns from binary floating point to exact decimal storage. |
| | | -- Run once against the traffic_audit database. |
| | | ALTER TABLE holiday_passenger_flow |
| | | MODIFY COLUMN passenger_count DECIMAL(30,15) NOT NULL COMMENT 'åéæ
客æ°ï¼ä¸äººæ¬¡ï¼'; |
| | | |
| | | ALTER TABLE holiday_vehicle_flow |
| | | MODIFY COLUMN vehicle_count DECIMAL(30,15) NOT NULL COMMENT '车æµéï¼ä¸è¾æ¬¡ï¼'; |
| | | |
| | | ALTER TABLE holiday_passenger_coefficient |
| | | MODIFY COLUMN coefficient DECIMAL(30,15) NOT NULL COMMENT '客è¿ç³»æ°'; |
| | | |
| | | -- The seeded coefficient was averaged from repeated 2.4 cells through a Java double. |
| | | -- Normalize the migrated value to the exact Excel decimal. |
| | | UPDATE holiday_passenger_coefficient |
| | | SET coefficient = 2.4 |
| | | WHERE year IN (2024, 2025, 2026); |
| New file |
| | |
| | | # åè½æµè¯æ¥å_éè¦è忥ä¸é¡¹ç»è®¡_2026-10-02 |
| | | |
| | | ## ä¸ãåè½è¯´æ |
| | | |
| | | æ°å¢âä¸é¡¹ç»è®¡-éè¦èåæ¥âæ¨¡åï¼è¦ç以ä¸è½åï¼ |
| | | |
| | | 1. ç³»ç»é
ç½®èåæ°å¢âèåæ¥ç®¡çâåâ客è¿ç³»æ°ç®¡çâï¼ä»
admin å¯ç»´æ¤ã |
| | | 2. èåæ¥ç®¡çåæ®µï¼å¹´ä»½ãè忥åç±»ï¼æ¥è/äºä¸/åä¸ï¼ãæ¥æãç¬¬å æ¥ã |
| | | 3. 客è¿ç³»æ°ç®¡çåæ®µï¼å¹´ä»½ãæ¥æåç±»ï¼è忥/éè忥ï¼ã客è¿ç³»æ°ã |
| | | 4. ä¸é¡¹ç»è®¡-è忥è忝ææå¹´ä»½ãè忥åç±»æ¥ç客æµä¸è½¦æµç»¼åæ°æ®ã |
| | | 5. æ¯æå¯¼å
¥ã2026å¹´å䏿°æ®.xlsxãæ¨¡æ¿ï¼ææé年份读åå½å¹´åï¼2026 å¹´ä¸ºå®¢æµ C åãæ®éå½çé D åï¼ï¼æâç¬¬å æ¥âåå
¥å½å¹´å®¢æµå车æµï¼2024/2025 å岿°æ®ä»
馿¬¡åå§åï¼ä¸è¦æ±æ¥å¸¸éå¤å¯¼å
¥ã |
| | | 6. 导å
¥æ¶å
æ£æ¥âæ¯æ¬æ¬¡ææ°å¯¼å
¥æ¥æ´æ©âçå岿¥ææ¯å¦è¢«æ¹å¨ï¼å¦æååï¼å
ä¸ååºå¹¶ååºå·®å¼ï¼ç±ç¨æ·éæ©æ¯å¦å
许è¦çã |
| | | 7. æ¯æå¯¼åº Excelï¼æ¨¡æ¿ä¸ºã2026å¹´å䏿°æ®.xlsxãã |
| | | 8. æ¯æä¸è½½ãæ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥ï¼10æXæ¥ï¼.docxãï¼é»è®¤æ¥æä¸ºç³»ç»æ¥æåä¸å¤©ï¼ä¹å
è®¸ç¨æ·èªéæ¥æï¼ç¯æ¯ã忝æâå¢é¿/ä¸é/æå¹³âè¾åºã |
| | | 9. åºç¨é¦æ¬¡å¯å¨æ¶ï¼ä»æ¨¡æ¿å·¥ä½ç°¿åå§å 2024/2025 å¹´åä¸å®¢æµã车æµï¼ä»¥å 2024-2026 年客è¿ç³»æ°ã |
| | | |
| | | ## äºãæµè¯ç¯å¢ |
| | | |
| | | | é¡¹ç® | å
容 | |
| | | |------|------| |
| | | | å·¥ä½ç®å½ | `D:\Codex\MyProject\trafficAudit` | |
| | | | å端 | Spring Bootï¼`http://localhost:8090` | |
| | | | å端 | Vue 2 + Element UIï¼`http://localhost:8080` | |
| | | | æ°æ®åº | MySQL `127.0.0.1:3305/traffic_audit` | |
| | | | æºå·¥ä½ç°¿ | `docs/ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´å䏿°æ®.xlsx` | |
| | | | å¿«æ¥æ¨¡æ¿ | `docs/ä¸é¡¹ç»è®¡/éè¦è忥/æ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥æ¨¡æ¿.docx` | |
| | | | ç¼è¯ | `mvn -q -DskipTests compile -f traffic-audit-server/pom.xml` éè¿ | |
| | | | å端æå»º | `npm run build` éè¿ï¼ä»
æåé¡¹ç® bundle ä½ç§¯ warning | |
| | | |
| | | ## ä¸ãç¨ä¾è¡¨ |
| | | |
| | | | ç¼å· | ç±»å | è¾å
¥/æä½ | 颿 | å®é
| ç»è®º | |
| | | |------|------|-----------|------|------|------| |
| | | | T01 | åå§å | å¯å¨å端 | èªå¨å»º/åå§å 4 å¼ èåæ¥è¡¨ | 表åå¨ï¼åå§å宿 | éè¿ | |
| | | | T02 | åå§å | æ¥è¯¢è忥é
ç½® | 2024/2026 å¹´ 7 天ï¼2025 å¹´ 8 天 | 2024 å¹´ 7 天ï¼2025 å¹´ 8 天ï¼2026 å¹´ 7 天 | éè¿ | |
| | | | T03 | åå§å | æ¥è¯¢å®¢æµãè½¦æµ | 2024/2025 æ°æ®ä»æºå·¥ä½ç°¿è¯»å | å®¢æµ 28/32 è¡ï¼è½¦æµ 14/16 è¡ | éè¿ | |
| | | | T04 | åå§å | æ¥è¯¢å®¢è¿ç³»æ° | 2024-2026 å¹´ç³»æ°æ¥èªæºå·¥ä½ç°¿ | æ¯å¹´ HOLIDAY/NON_HOLIDAY å 2.4ï¼ä¸æºè¡¨ä¸è´ | éè¿ | |
| | | | T05 | å
¥åºæ½æ¥ | æºå·¥ä½ç°¿ vs æ°æ®åºé项æ¯å¯¹ | æ å·®å¼ | 90 个åå
æ ¼æ¯å¯¹ï¼å·®å¼ 0 | éè¿ | |
| | | | T06 | æé | æªç»å½è®¿é®è忥æ¥å£ | æ¦æª | HTTP 401ï¼æç¤ºæªç»å½ | éè¿ | |
| | | | T07 | æé | é admin ä¿åè忥 | æç» | code=403ï¼æç¤ºä»
admin å¯ç®¡ç | éè¿ | |
| | | | T08 | æé | é admin ä¿å客è¿ç³»æ° | æç» | code=403ï¼æç¤ºä»
admin å¯ç®¡ç | éè¿ | |
| | | | T09 | æ£å | admin æ°å¢/ç¼è¾/å é¤è忥 2030 æµè¯è®°å½ | æå䏿 èæ°æ® | æ°å¢ãç¼è¾æåï¼éå¤é
ç½®è¿å 400ï¼å 餿å | éè¿ | |
| | | | T10 | æ£å | admin æ°å¢/ç¼è¾/å é¤å®¢è¿ç³»æ° 2030 æµè¯è®°å½ | æå䏿 èæ°æ® | æ°å¢ãç¼è¾æåï¼éæ³ç³»æ°è¿å 400ï¼å 餿å | éè¿ | |
| | | | T11 | æ£å | 导å
¥æºå·¥ä½ç°¿ 2025 å¹´å䏿°æ® | è¦çåå
¥ | å®¢æµ 32 æ¡ãè½¦æµ 16 æ¡ï¼å¤±è´¥ 0 | éè¿ | |
| | | | T12 | æ£å | 导å
¥ä¸´æ¶ 2026 æµè¯å·¥ä½ç°¿ï¼åå¼éªè¯æå¹³ï¼ | åå
¥å¹¶çæå¿«æ¥ | å®¢æµ 8 æ¡ãè½¦æµ 4 æ¡ï¼å¤±è´¥ 0ï¼æµè¯æ°æ®å·²æ¸
ç | éè¿ | |
| | | | T13 | è´å | éæ©æªé
置年份导å
¥ | æç¤ºå
ç»´æ¤è忥 | code=400ï¼æç¤ºâ请å
å¨ç³»ç»é
ç½®ä¸ç»´æ¤è忥â | éè¿ | |
| | | | T14 | è´å | 导å
¥é Excel æä»¶ | è§£æå¤±è´¥å¹¶æç¤º | code=400ï¼æç¤º unsupported file type | éè¿ | |
| | | | T15 | è´å | ä¸ä¼ æä»¶å¯¼å
¥ | ä¸å¡æç¤ºï¼ä¸è¿å 500 | code=400ï¼æç¤ºâè¯·éæ©è¦å¯¼å
¥ç Excel æä»¶â | éè¿ | |
| | | | T16 | å¯¼åº | å¯¼åº 2025 å¹´åä¸ Excel | 带åºå
æ°æ®ï¼å
¬å¼ä¿ç | æä»¶å¯è¢« openpyxl æå¼ï¼2025 客æµ/è½¦æµæ°æ®æ£ç¡® | éè¿ | |
| | | | T17 | å¿«æ¥ | ä¸è½½ 2025-10-01 å¿«æ¥ | 馿¥æ£æä¸è¾åºâç¯æ¯â¦â¦âï¼è¡¨æ ¼ç¯æ¯å为âââï¼æ£æ/è¡¨æ ¼å®æ´ | æ£æä¸ºâ10æ1æ¥ï¼åæç¬¬1天ï¼â¦â¦ä¸äººæ¬¡ï¼æ¯2025å¹´åæå¢é¿6.8%ãâï¼æ âç¯æ¯âçè¯ï¼è¡¨æ ¼ç¯æ¯å为âââ | éè¿ | |
| | | | T18 | å¿«æ¥ | ä¸è½½ 2025-10-02 å¿«æ¥ | ç¯æ¯ä¸ºä¸éå¹¶è¾åºç»å¯¹å¼ | è¾åºâç¯æ¯ä¸é11.5%â | éè¿ | |
| | | | T19 | å¿«æ¥ | ä¸è½½ 2025-10-03 å¿«æ¥ | ç¯æ¯ä¸ºæ£å¹¶è¾åºç»å¯¹å¼ | è¾åºâç¯æ¯å¢é¿2.4%â | éè¿ | |
| | | | T20 | å¿«æ¥ | ä¸´æ¶æé 2026 第 1/2 æ¥ç¸åæ°æ® | ç¯æ¯ä¸º 0 æ¶è¾åºâæå¹³â | è¾åºâç¯æ¯æå¹³â | éè¿ | |
| | | | T21 | å¹¶å/ç¨³å® | éå¯åè¿ç»çæå¿«æ¥ | ä¸ä¸²ææ¡£ | 两份ä¸åæ¥æå¿«æ¥å
容æ£ç¡® | éè¿ | |
| | | | T22 | å端 | admin æå¼âä¸é¡¹ç»è®¡-è忥â | 页颿¸²æãæ°æ®è¡¨æ¾ç¤º | æ é¢ãèåãè¡¨æ ¼æ£å¸¸ | éè¿ | |
| | | | T23 | å端 | admin æå¼âç³»ç»é
ç½®â | 两个 Tab æ£å¸¸ | èåæ¥ç®¡çã客è¿ç³»æ°ç®¡çæ£å¸¸ | éè¿ | |
| | | | T24 | å端 | é admin 访é®ç³»ç»é
ç½® | èåéèä¸ç´è®¿è¢«éå®å | èåä¸å¯è§ï¼ç´è®¿åå·¥ä½å° | éè¿ | |
| | | | T25 | å端 | ç¹å»âä¸è½½æ¨¡æ¿â | HTTP 200ï¼æä»¶å¯ä¸è½½ | æµè§å¨å®é
请æ±è¿å 200 | éè¿ | |
| | | | T26 | æ§å¶å° | 页é¢çæµæ¶é JS é误 | æ é误 | æ pageerror/console error | éè¿ | |
| | | | T27 | å岿£æ¥ | å·²æ 2026 第1æ¥æ°æ®åï¼å¯¼å
¥ç¬¬1æ¥è¢«ä¿®æ¹ä¸ç¬¬2æ¥æ£å¸¸çæä»¶ï¼ä¸ç¡®è®¤ï¼ | ä¸ååºï¼è¿ååå²åå | `confirmationRequired=true`ï¼ååº 6 æ¡å·®å¼ï¼åºå
第1æ¥ä»ä¸ºåå¼ | éè¿ | |
| | | | T28 | å岿£æ¥ | å䏿件ï¼ç¨æ·ç¡®è®¤å
许导å
¥ | è¦çå岿¥æ°æ® | è¿åæå 12 æ¡ï¼åºå
第1æ¥ç± 100/200/300 æ´æ°ä¸º 110/210/310 | éè¿ | |
| | | | T29 | å端确认 | 页é¢å¯¼å
¥å«åå²ååçæä»¶ | å¼¹çªæ¾ç¤ºå·®å¼åâå
许导å
¥/ä¸å
许â | å¼¹çªåºç°ä¸å
å«åå¼/导å
¥å¼ï¼ç¹å»å
许å导å
¥æå | éè¿ | |
| | | | T30 | é»è®¤æ¥æ | æå¼å¿«æ¥æ¥æéæ© | é»è®¤ç³»ç»æ¥æåä¸å¤©ï¼å¯æå·¥ä¿®æ¹ | 页é¢é»è®¤å 2026-10-01ï¼æ¥ææ§ä»¶å¯éæ© | éè¿ | |
| | | | T31 | èåé¡ºåº | æ¥ç左侧èå | ä¸é¡¹ç»è®¡ä½äºæ¥è¡¨çæä¹å | 顺åºä¸º æ¥è¡¨çæ -> ä¸é¡¹ç»è®¡ -> è§å管ç -> ç¨æ·ç®¡ç -> ç³»ç»é
ç½® | éè¿ | |
| | | | T32 | 精度 | Excel 客è¿ç³»æ° 2.4 å
¥åºåæ¥è¯¢ | ä¸åºç° 2.3999999999999995 | åºå
2024/2025/2026 å¹´ç³»æ°å为 2.4ï¼æ¥å£è¿å coefficient=2.4 | éè¿ | |
| | | | T33 | 精度 | ç³»ç»é
ç½®-客è¿ç³»æ°ç®¡çå表 | æ¾ç¤º 2.4 | æµè§å¨å®æµä¸¤è¡åæ¾ç¤º 2.4 | éè¿ | |
| | | | T34 | 精度 | è忥ç»è®¡é¡µå®¢è¿ç³»æ°å | æ¾ç¤º 2.4 | overview æ¥å£ä¸é¡µé¢åæ¾ç¤º 2.4 | éè¿ | |
| | | | T35 | 精度 | æ£æ¥è忥æ°å¼åç±»å | decimal(30,15) | passenger_count/vehicle_count/coefficient/comparison_base å为 decimal(30,15) | éè¿ | |
| | | | T36 | 导å
¥å£å¾ | 导å
¥å« 2024/2025 åå²åä¸å®¢è¿ç³»æ°åç 2026 å·¥ä½ç°¿ | åªè¯» 2026 å½å¹´åï¼ä¸å 2024/2025ï¼ä¸è¯»ç³»æ° | åå
¥ 12 æ¡ï¼å®¢æµ 8 + è½¦æµ 4ï¼ï¼2024/2025 åä¸ç³»æ°åæªå
¥åº | éè¿ | |
| | | | T37 | å岿£æ¥åå½ | å·²æ 2026 第1/2 æ¥æ°æ®åï¼å¯¼å
¥ç¬¬1æ¥è¢«æ¹å¨çæä»¶ï¼ä¸ç¡®è®¤ï¼ | ä¸ååºå¹¶ååºå·®å¼ | confirmationRequired=trueï¼ååº 6 æ¡å·®å¼ï¼åºå
第1æ¥ä»ä¸º 100/100/100/100/200/300 | éè¿ | |
| | | | T38 | å岿£æ¥åå½ | å䏿件ï¼ç¨æ·ç¡®è®¤å
许导å
¥ | è¦çå岿¥æ°æ® | è¿åæå 12 æ¡ï¼åºå
第1æ¥æ´æ°ä¸º 110/110/110/110/210/310ï¼ç¬¬2æ¥ä¸å | éè¿ | |
| | | | T39 | 页é¢è¡¨å¤´ | èåæ¥æ°æ®ä¸è§è¡¨å¤´å åä½ | å头带åä½ | 宿µï¼éè·¯/æ°èª/éè·¯/æ°´è·¯ï¼ä¸äººæ¬¡ï¼ï¼é«é车æµ/å½çé车æµï¼ä¸è¾æ¬¡ï¼ï¼éè¥ä¸æ§äººåæµå¨é/æ»äººåæµå¨éï¼ä¸äººæ¬¡ï¼ï¼å®¢è¿ç³»æ°æ åä½ | éè¿ | |
| | | | T40 | é
ç½®åå½ | admin å é¤ 2026-10-08 åéå¯å端 | å é¤ç»æä¿æï¼ä¸åèªå¨è¡¥å | éå¯ååºå
ãæ¥å£å页é¢å为 2026-10-01 è³ 10-07 å
± 7 天 | éè¿ | |
| | | | T41 | ç¯æ¯åºæå¯¼å
¥ | 2026 å¹´æ®éå½çé C åå¡« 320ãD åå¡« 360 | C åä½ä¸ºç¯æ¯åºæä¿åï¼D åä¸ºå½æ¥è½¦æµ | åºå
comparison_base=320ï¼vehicle_count=360ï¼æ¥å£åè¿å | éè¿ | |
| | | | T42 | ç¯æ¯åºæå¯¼åº | 导åºå·²å¯¼å
¥åºæç 2026 å¹´æ°æ® | æ®éå½çé C ååå¡«åºæï¼D åå填彿¥è½¦æµ | å¯¼åºæä»¶ C4=320ãD4=360 | éè¿ | |
| | | | T43 | å½çé车æµç¯æ¯ | 2026-10-02 å½çé彿¥360ãåºæ320 | ä»
å½çéç´æ¥ç¯æ¯ä½¿ç¨åºæ | å½çé车æµç¯æ¯ä¸º12.50%ï¼å¿«æ¥æ»äººåæµå¨éä»ä¸º17.00%ï¼ä¸ååºæå½±å | éè¿ | |
| | | | T44 | æ å°å¯¼åº | æåºææ¶å¯¼åºæ®éå½çé E åå
¬å¼ | E 使ç¨å½æ¥ä¸ç¯æ¯åºæè®¡ç® | å¯¼åº E4 å
¬å¼ä¸º `=(D4-C4)/C4` | éè¿ | |
| | | | T45 | åå²åºæåæ´ | å·²æ 2026 第1æ¥åºæåï¼å¯¼å
¥æ°ç第1æ¥åºæå¼ | ä¸ç¡®è®¤æ¶é¶åå
¥å¹¶ååºå·®å¼ | confirmationRequired=trueï¼æç¤ºåå¼300/导å
¥å¼310ï¼åºå
ä»ä¸º300 | éè¿ | |
| | | | T46 | åå²å¹´ä»½é离 | å¯¼åº 2024/2025 å¹´æ°æ® | åå²å¹´ä»½ä¸ä½¿ç¨ç¯æ¯åºæ | ä¸¤ä»½å¯¼åºæä»¶æ®éå½çé C3:C10 å为空 | éè¿ | |
| | | | T47 | å¿«æ¥é¦æ¥å£å¾ | ä¸è½½ 2025-10-01 å¿«æ¥å¹¶æ£æ¥æ£æ | 第1天ä¸çæâç¯æ¯å¢é¿/ä¸é/æå¹³âçè¯ | æ£æä¸ºâ10æ1æ¥ï¼åæç¬¬1天ï¼ï¼å
¨çå
¨ç¤¾ä¼è·¨åºåäººåæµå¨é1714.41ä¸äººæ¬¡ï¼æ¯2025å¹´åæå¢é¿6.8%ãâï¼ç¬¬äºå¤©ä»è¾åºâç¯æ¯ä¸é11.5%â | éè¿ | |
| | | | T48 | å¿«æ¥ç¾åæ¯ç²¾åº¦ | ä¸è½½ 2025-10-01/02 å¿«æ¥æ£æ¥æ£æåè¡¨æ ¼ | 忝/ç¯æ¯ç¾åæ¯ä¿ç1ä½å°æ°ï¼äººæ°/车æµä¿ç2ä½å°æ° | 6.79%æ¾ç¤ºä¸º6.8%ï¼11.46%æ¾ç¤ºä¸º11.5%ï¼äººæ°1714.41ã1518.02ä»ä¸º2ä½å°æ° | éè¿ | |
| | | |
| | | ## åãåç°çé®é¢ä¸å¤ç½® |
| | | |
| | | 1. 模æ¿ä¸è½½æ´æ®µè·¯å¾ä½¿ç¨ `encodeURIComponent`ï¼æ `/` ç¼ç 为 `%2F`ï¼Tomcat è¿å 400ã |
| | | - å¤ç½®ï¼å端æ¹ä¸ºæè·¯å¾æ®µç¼ç ï¼ä¿çææ ï¼å¤æµè¿å 200ã |
| | | 2. 导å
¥åå
没æå岿°æ®åæ´ç¡®è®¤ï¼å¯è½éé»è¦çå·²åå¸å岿¥ã |
| | | - å¤ç½®ï¼æ°å¢ä¸¤é¶æ®µå¯¼å
¥ï¼æªç¡®è®¤æ¶é¶åå
¥ï¼ç¡®è®¤åæå
许è¦çå岿¥ã |
| | | 3. å¿«æ¥çææå¡åå
æå½å DOM ææ¡£æ¾å¨å®ä¾å段ï¼åå¨å¹¶å串æ°é£é©ã |
| | | - å¤ç½®ï¼æ¹ä¸º `ThreadLocal` ä¿åå½åææ¡£ï¼å¹¶å¨è¯·æ±ç»ææ¶æ¸
çï¼éå¯å夿µè¿ç»çæéè¿ã |
| | | 4. 导å
¥æ¥å£ä¸ä¼ æä»¶æ¶ç±æ¡æ¶æåº 500ã |
| | | 5. è忥åå§åæ¯æ¬¡å¯å¨é½ä¼è¡¥é½ç¼ºå¤±ç第 N æ¥ï¼å¯¼è´ admin å é¤ 2026-10-08 åï¼éå¯å被èªå¨æåã |
| | | - å¤ç½®ï¼æä»¶åæ°æ¹ä¸ºå¯éï¼ç»ä¸è¿åä¸å¡ 400 æç¤ºã |
| | | - å¤ç½®ï¼`seedNationalDayCalendar()` æ¹ä¸ºä»
å¨ `holiday_calendar` æ´è¡¨ä¸ºç©ºæ¶æ§è¡é¦æ¬¡åå§åï¼é空é
ç½®å®å
¨ç± admin ç»´æ¤ã夿µéå¯å 2026 å¹´åä¸ä¿æ 7 天ã |
| | | |
| | | ## äºãæ°æ®æ¸
ç |
| | | |
| | | - æµè¯ä¸äº§çç 2026 客æµ/车æµä¸´æ¶æ°æ®å·²å é¤ã |
| | | - æµè¯äº§çç `import_batch` è忥坼å
¥è®°å½å·²å é¤ã |
| | | - 2024/2025 åå§åæ°æ®å 2024-2026 客è¿ç³»æ°ä¿çï¼ç¬¦åéæ±ã |
| | | |
| | | ## å
ãç»è®º |
| | | |
| | | éè¦è忥ä¸é¡¹ç»è®¡æ¨¡åçæ£åæµç¨ãæéæ§å¶ãæ°æ®åå§åã导å
¥å¯¼åºãå¿«æ¥çæåå端页é¢çæµåéè¿ï¼æºå·¥ä½ç°¿ 90 ä¸ªæ°æ®åå
æ ¼å
¥åºæ½æ¥æ å·®å¼ãåç°çé®é¢å·²ä¿®å¤å¹¶å®æåå½ï¼å½åå端 8090ãå端 8080 æ£å¸¸è¿è¡ã |
| | | |
| | | ## ä¸ãè¡¥å
åå½ï¼2026-10-02ï¼Excel æµ®ç¹ç²¾åº¦ä¸èå顺åºï¼ |
| | | |
| | | èæ¯ï¼Excel å
鍿 IEEE 754 å精度浮ç¹å卿°å¼ï¼ææ 15 ä½ï¼ï¼åºå
客è¿ç³»æ°å为 2.3999999999999995ï¼é¡µé¢æ¾ç¤ºå¼å¸¸ãåè `docs/Excelæµ®ç¹æ°åå¨.txt` å
¨é¢æ´æ¹ã |
| | | |
| | | ä¿®æ¹å
å®¹ï¼ |
| | | |
| | | 1. æ°å¢æ°å¼å·¥å
· `HolidayNumberUtils`ï¼Excel åå
æ ¼æ°å¼ç»ä¸ç¨ `NumberToTextConverter.toText(double)` è½¬ææ¬åå
¥ `BigDecimal`ï¼è®¡ç®ä½¿ç¨ `MathContext(15, HALF_UP)`ï¼ä¸ Excel 15 ä½æææ°åä¿æä¸è´ã |
| | | 2. èåæ¥æ¨¡åå®ä½ãæå¡ã导å
¥å¯¼åºãå¿«æ¥ç»ä¸ç± `Double` æ¹ä¸º `BigDecimal`ã |
| | | 3. ä¸å¼ 表æ°å¼åæ¹ä¸º `DECIMAL(30,15)`ï¼`holiday_passenger_flow.passenger_count`ã`holiday_vehicle_flow.vehicle_count`ã`holiday_passenger_coefficient.coefficient`ï¼è¿ç§»èæ¬ `docs/sql_holiday_decimal_migration.sql`ã |
| | | 4. åºå
6 æ¡å®¢è¿ç³»æ°ï¼2024-2026 å¹´ãè忥/éè忥ï¼ç± 2.3999999999999995 æ ¡æ£ä¸º 2.4ã |
| | | 5. âä¸é¡¹ç»è®¡âèåç§»å¨å°âæ¥è¡¨çæâä¹åï¼å端客è¿ç³»æ°å±ç¤ºç»ä¸å»é¤æµ®ç¹å°¾å·®ã |
| | | |
| | | éªè¯ç»æï¼ |
| | | |
| | | - `mvn -q -DskipTests compile` éè¿ï¼`npm run build` éè¿ï¼ä»
åæ bundle ä½ç§¯è¦åï¼ã |
| | | - èå顺åºå®æµï¼æ¥è¡¨çæ -> ä¸é¡¹ç»è®¡ -> è§å管ç -> ç¨æ·ç®¡ç -> ç³»ç»é
ç½®ã |
| | | - ç³»ç»é
ç½®-客è¿ç³»æ°ç®¡çå表æµè§å¨å®æµæ¾ç¤º 2.4ï¼æ¥å£ `coefficient=2.4`ã |
| | | - ä¸å¼ 表æ°å¼åå为 `decimal(30,15)`ã |
| | | - åå²åæ´ä¸¤é¶æ®µå¯¼å
¥åå½ï¼ä¸ç¡®è®¤æ¶é¶åå
¥å¹¶ååº 6 æ¡å·®å¼ï¼ç¡®è®¤å第1æ¥ç± 100/100/100/100/200/300 è¦ç为 110/110/110/110/210/310ï¼ç¬¬2æ¥ä¸åã |
| | | - 导å
¥å£å¾å¤æ ¸ï¼ä»
åå
¥æé年份å½å¹´åï¼æ¬ç¨ä¾ 12 æ¡ï¼ï¼2024/2025 åå²åä¸å®¢è¿ç³»æ°åä¸è¯»ä¸å
¥åºã |
| | | - æµè¯äº§çç 2026 客æµ/车æµå `import_batch` è®°å½å·²æ¸
çï¼2024/2025 å岿°æ®ä¸ 2024-2026 客è¿ç³»æ°ä¿çã |
| | | - 页é¢è¡¨å¤´è¡¥åä½ï¼æµè§å¨å®æµï¼ï¼éè·¯/æ°èª/éè·¯/æ°´è·¯ï¼ä¸äººæ¬¡ï¼ãé«é车æµ/å½çé车æµï¼ä¸è¾æ¬¡ï¼ãéè¥ä¸æ§äººåæµå¨é/æ»äººåæµå¨éï¼ä¸äººæ¬¡ï¼ï¼å®¢è¿ç³»æ°ä¿çæ åä½ã |
| | | |
| | | ## å
«ãè¡¥å
åå½ï¼2026-10-02ï¼è忥å é¤åéå¯å¤æ´»ï¼ |
| | | |
| | | ç°è±¡ï¼admin å·²å¨ç³»ç»é
ç½®ä¸å é¤ 2026-10-08ï¼ä½å端éå¯å该è¡å次åºç°ã |
| | | |
| | | æ ¹å ï¼`HolidayReferenceDataInitializer` 卿¯æ¬¡åºç¨å¯å¨æ¶è°ç¨ `seedNationalDayCalendar()`ï¼åé»è¾æåºå®å¤©æ°éåå¹¶è¡¥é½ææç¼ºå¤±ç第 N æ¥ï¼å æ¤æ admin å·²å é¤çæ¥æéæ°æå
¥ã |
| | | |
| | | ä¿®å¤ä¸éªè¯ï¼ |
| | | |
| | | - `HolidayWorkbookService.seedNationalDayCalendar()` å¢å éç©ºä¿æ¤ï¼èåæ¥è¡¨åªè¦å·²æä»»ä¸é
ç½®ï¼å°±ä¸åè¿è¡èªå¨è¡¥ç§ã |
| | | - å端ç¼è¯éè¿ï¼éå¯åæ°æ®åº 2026 å¹´åä¸ä¸º `2026-10-01` è³ `2026-10-07` å
± 7 天ã |
| | | - æ¥å£éªè¯ï¼è忥é
ç½® 7 æ¡ï¼ç»è®¡ä¸è§ 7 è¡ï¼æå䏿¥ `dayIndex=7`ã`date=2026-10-07`ã |
| | | |
| | | ## ä¹ãè¡¥å
åå½ï¼2026-10-02ï¼æ®éå½çéç¯æ¯åºæï¼ |
| | | |
| | | èæ¯ï¼æ®éå½çé `C` å为âç¯æ¯åºæâã彿®éå½çéæµéè¶å¿ä¸é«éå
¬è·¯ä¸ä¸è´æ¶ï¼ä»¥é«é为åï¼æå·¥å¡«ååºæï¼ç¯æ¯æ `彿¥ / ç¯æ¯åºæ - 1`ï¼æªå¡«åæ¶ä»æ `彿¥ / å䏿¥ - 1`ã |
| | | |
| | | å®ç°å£å¾ï¼ |
| | | |
| | | - `holiday_vehicle_flow` æ°å¢å¯ç©ºå段 `comparison_base DECIMAL(30,15)`ï¼è¿ç§»èæ¬ `docs/sql_holiday_comparison_base_migration.sql`ã |
| | | - ä»
2026 å¹´å¯ç¨ï¼2024ã2025 å岿°æ®ä¸å¯¼å
¥ãä¸å¯¼åºãä¸åä¸åºæè®¡ç®ã |
| | | - 2026 å¹´æ®éå½çé C å导å
¥ä¸º `comparison_base`ï¼D å导å
¥ä¸º `vehicle_count`ï¼å¯¼åºæ¶ C/D ååå«åå¡«ã |
| | | - åºæåªå½±åæ®éå½çé车æµéèªèº«çç¯æ¯ï¼æåºææ¶ `彿¥ / åºæ - 1`ï¼æ åºææ¶ `彿¥ / å䏿¥ - 1`ã |
| | | - æç®äººåæµå¨éãå
¬è·¯äººåæµå¨éãéè¥ä¸æ§äººåæµå¨éãå
¨ç¤¾ä¼äººåæµå¨éå Word å¿«æ¥ç»§ç»æ Excel åæå
¬å¼è®¡ç®ï¼ä¸ååºæå½±åã |
| | | - å¯¼åºæ¶å¦ææ¥å¡«ååºæï¼æ®éå½çé E åå
¬å¼å为 `=(D-C)/C`ã |
| | | - åºææ¹ååæ ·çº³å
¥å岿¥æåæ´æ£æ¥ï¼ç¨æ·ç¡®è®¤åä¸ååºã |
| | | - å¿«æ¥ç¬¬1天没æå䏿¥å¯æ¯åºæ°ï¼æ£æä¸è¾åºâç¯æ¯å¢é¿/ä¸é/æå¹³âçè¯ï¼è¡¨æ ¼å½æ¥ç¯æ¯å为âââã第2å¤©èµ·ä»æå®é
å䏿¥è®¡ç®å¹¶è¾åºç¯æ¯ã |
| | | - å¿«æ¥ä¸ç忝/ç¯æ¯ç¾åæ¯ç»ä¸ä¿ç1ä½å°æ°ï¼åèäºå
¥ï¼ï¼äººæ¬¡ã车æµçä¸å¡æ°å¼ä»ä¿ç2ä½å°æ°ã |
| New file |
| | |
| | | # 工使¥ç»_2026-10-02 |
| | | |
| | | ## ä¸ã仿¥å®æ |
| | | |
| | | 1. æ°å¢âä¸é¡¹ç»è®¡-éè¦è忥â宿´æ¨¡åã |
| | | - å端æ°å¢ 4 å¼ è¡¨ï¼è忥é
ç½®ãè¥ä¸æ§å®¢æµãéè¥ä¸æ§è½¦æµã客è¿ç³»æ°ã |
| | | - å端æ°å¢è忥é
ç½®ãç»è®¡ãå·¥ä½ç°¿å¯¼å
¥å¯¼åºãWord å¿«æ¥æå¡ã |
| | | - å端æ°å¢âä¸é¡¹ç»è®¡-è忥âåâç³»ç»é
ç½®âèå/页é¢ã |
| | | 2. æéæ§å¶å·²å®ç°ã |
| | | - èåæ¥ç®¡çã客è¿ç³»æ°ç®¡çä»
admin å¯ç»´æ¤ï¼é admin è¿å 403ï¼èåéèä¸ç´è®¿éå®åã |
| | | 3. 模æ¿åå§åå·²å®ç°ã |
| | | - ä»ã2026å¹´å䏿°æ®.xlsxãåå§å 2024/2025 å¹´åä¸å®¢æµå车æµã |
| | | - ä»åä¸å·¥ä½ç°¿åå§å 2024-2026 年客è¿ç³»æ°ã |
| | | 4. 导å
¥å¯¼åºå·²å®ç°ã |
| | | - 导å
¥ä»
读åæé年份çå½å¹´åï¼2026 å¹´ä¸ºå®¢æµ C åãæ®éå½çé D åï¼ï¼åå² 2024/2025 æ°æ®åªååå§åï¼ä¸è¦æ±æ¥å¸¸é导ã |
| | | - 导å
¥æâ年份 + è忥åç±»âå¹é
â第Næ¥âï¼è¦çåå
¥å®¢æµä¸è½¦æµã |
| | | - 导å
¥åæ£æ¥æ´æ©çå岿¥ææ¯å¦è¢«æ¹å¨ï¼æååæ¶å
ä¸ååºå¹¶ååºå·®å¼ï¼ç¨æ·ç¡®è®¤åæå
许è¦çã |
| | | - 导åºä»¥æºå·¥ä½ç°¿ä¸ºæ¨¡æ¿ï¼åå¡«æ°æ®åºæ°æ®å¹¶ä¿çå
¬å¼ã |
| | | 5. å¿«æ¥å·²å®ç°ã |
| | | - ä¸è½½æä»¶åï¼`æ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥ï¼10æXæ¥ï¼.docx`ã |
| | | - ç¯æ¯/åæ¯ææ¡ææ£æ°å¢é¿ãè´æ°ä¸éã0 æå¹³è¾åºï¼ç¾åæ¯ä¿ç1ä½å°æ°ã |
| | | 6. ææ¡£å·²åæ¥ã |
| | | - æ´æ° `docs/sql_holiday.sql`ã`docs/init.sql`ã`docs/database.md`ã`docs/æå
é¨ç½²è¯´æ.md`ã |
| | | 7. Excel æµ®ç¹ç²¾åº¦æ´æ¹ï¼2026-10-02 追å ï¼ã |
| | | 8. è忥å é¤åéå¯å¤æ´»ä¿®å¤ï¼2026-10-02 追å ï¼ã |
| | | 9. æ®éå½çéç¯æ¯åºæï¼2026-10-02 追å ï¼ã |
| | | - æ®éå½çé C å确认为âç¯æ¯åºæâï¼å¡«ååæâ彿¥/ç¯æ¯åºæ-1â计ç®ç¯æ¯ï¼æªå¡«åæ¶ä»æâ彿¥/å䏿¥-1âã |
| | | - `holiday_vehicle_flow` æ°å¢ `comparison_base` åæ®µï¼ä»
2026 年导å
¥ã导åºååä¸è®¡ç®ï¼2024/2025 å岿°æ®ä¸ä½¿ç¨ã |
| | | - æ Excel åå
¬å¼å£å¾ï¼åºæåªå½±åæ®éå½çé车æµéèªèº«ç¯æ¯ï¼æç®äººåæµå¨éãå
¬è·¯äººåæµå¨éãæ»äººåæµå¨éå Word å¿«æ¥åä¸åå½±åã |
| | | - åºæååä»çº³å
¥å岿¥æåæ´ç¡®è®¤ï¼å¯¼åºæ¶å¯¹åºæ¥ E åå
¬å¼æ¹ä¸º `(D-C)/C`ã |
| | | - å¿«æ¥ç¬¬1å¤©æ£æä¸è¾åºâç¯æ¯å¢é¿/ä¸é/æå¹³âçè¯ï¼è¡¨æ ¼ç¯æ¯å为âââï¼ç¬¬2天起ä¿çç¯æ¯ã |
| | | - å¿«æ¥åæ¯/ç¯æ¯ç¾åæ¯æ¹ä¸ºä¿ç1ä½å°æ°ï¼äººæ¬¡ã车æµçæ°å¼ä»ä¿ç2ä½å°æ°ã |
| | | - åé»è¾æ¯æ¬¡å¯å¨é½ä¼è¡¥é½ç¼ºå¤±ç第 N æ¥ï¼å¯¼è´ admin å é¤ 2026-10-08 åéå¯è¢«éæ°æå
¥ã |
| | | - ä¿®å¤ä¸ºä»
å½ `holiday_calendar` æ´è¡¨ä¸ºç©ºæ¶æ§è¡é¦æ¬¡åå§åï¼å¤æµéå¯å 2026 å¹´åä¸ä¿æ 7 天ã |
| | | - æ°å¢ `HolidayNumberUtils`ï¼Excel æ°å¼ç»ä¸ç¨ `NumberToTextConverter.toText(double)` 转 `BigDecimal`ï¼è®¡ç®ç¨ `MathContext(15, HALF_UP)`ã |
| | | - èåæ¥æ¨¡åå®ä½/æå¡/导å
¥å¯¼åº/å¿«æ¥ç± `Double` æ¹ä¸º `BigDecimal`ï¼ä¸å¼ 表æ°å¼åæ¹ä¸º `DECIMAL(30,15)`ï¼åºå
6 æ¡å®¢è¿ç³»æ°ç± 2.3999999999999995 æ ¡æ£ä¸º 2.4ã |
| | | - âä¸é¡¹ç»è®¡âèåç§»å°âæ¥è¡¨çæâä¹åï¼å端系æ°å±ç¤ºå»é¤æµ®ç¹å°¾å·®ã |
| | | |
| | | ## äºãæµè¯ç»ææè¦ |
| | | |
| | | - å端ç¼è¯éè¿ï¼å端ç产æå»ºéè¿ã |
| | | - æºå·¥ä½ç°¿ä¸æ°æ®åºæ½æ¥ï¼90 个åå
æ ¼ï¼å·®å¼ 0ã |
| | | - æéãæ£å CRUDãè´å导å
¥ãå岿°æ®åæ´ç¡®è®¤ãExcel 导åºãWord å¿«æ¥ã页é¢çæµå
¨é¨éè¿ã |
| | | - åå²åæ´ç«¯å°ç«¯å®æµï¼æªç¡®è®¤æ¶æ°æ®ä¸è½åºï¼ç¡®è®¤åå岿¥å¼æå¯¼å
¥æä»¶æ´æ°ï¼æµè¯æ°æ®å·²æ¸
çã |
| | | - æ 头æµè§å¨çæµï¼admin 页颿£å¸¸ï¼é admin ç³»ç»é
ç½®éèå¹¶éå®åï¼æ¨¡æ¿ä¸è½½ HTTP 200ï¼æ JS é误ã |
| | | - 精度åå½ï¼ç³»ç»é
ç½®-客è¿ç³»æ°å表ä¸è忥ç»è®¡é¡µåæ¾ç¤º 2.4ï¼æ¥å£ `coefficient=2.4`ï¼ä¸å¼ 表æ°å¼å为 `decimal(30,15)`ã |
| | | - èååå½ï¼ä¾§è¾¹èå顺åºä¸º æ¥è¡¨çæ -> ä¸é¡¹ç»è®¡ -> è§å管ç -> ç¨æ·ç®¡ç -> ç³»ç»é
ç½®ã |
| | | - åå²åæ´ä¸¤é¶æ®µå¯¼å
¥åå½ï¼ä¸ç¡®è®¤æ¶é¶åå
¥å¹¶å 6 æ¡å·®å¼ï¼ç¡®è®¤å第1æ¥è¦ç为 110/110/110/110/210/310ï¼ç¬¬2æ¥ä¸åï¼å¯¼å
¥å£å¾ä»
读å½å¹´åã |
| | | - 页é¢è¡¨å¤´è¡¥åä½ï¼èåæ¥æ°æ®ä¸è§å头已带åä½ï¼ä¸äººæ¬¡/ä¸è¾æ¬¡ï¼ï¼æµè§å¨å®æµéè¿ã |
| | | - å é¤å¤æ´»åå½ï¼admin å é¤ 2026-10-08 åéå¯ï¼æ°æ®åº 7 天ãç»è®¡æ¥å£ 7 è¡ï¼æå䏿¥ä¸º `2026-10-07`ï¼æªåèªå¨è¡¥åã |
| | | - ç¯æ¯åºæåå½ï¼å¯¼å
¥ C=320ãD=360 ååºå
忥å£åæ£ç¡®ï¼å½çé车æµç¯æ¯ä¸º 12.50%ï¼å¯¼åº E åå
¬å¼ä¸º `(D-C)/C`ï¼å¿«æ¥æ»äººåæµå¨éä»ä¸º 17.00%ãæªååºæå½±åï¼åå²åºæååè¿å确认æç¤ºä¸æªååºï¼2024/2025 å¯¼åº C åä¿æä¸ºç©ºã |
| | | - å¿«æ¥é¦æ¥åå½ï¼2025-10-01 æ£ææ âç¯æ¯âçè¯ï¼ä»
ä¿ç忝ï¼2025-10-02 ä»è¾åºâç¯æ¯ä¸é11.5%âã |
| | | - å¿«æ¥ç¾åæ¯ç²¾åº¦åå½ï¼æ£æåè¡¨æ ¼åæ¯/ç¯æ¯å为1ä½å°æ°ï¼å¦6.79%æ¾ç¤º6.8%ã11.46%æ¾ç¤º11.5%ï¼äººæ°1714.41/1518.02ä»ä¿ç2ä½å°æ°ã |
| | | - æµè¯æ¥åï¼`docs/åè½æµè¯æ¥å/åè½æµè¯æ¥å_éè¦è忥ä¸é¡¹ç»è®¡_2026-10-02.md`ã |
| | | |
| | | ## ä¸ãéçé®é¢ä¸å¾
å |
| | | |
| | | 1. çå® 2026 å¹´å䏿°æ®å°æªå¯¼å
¥ï¼é¡µé¢é»è®¤å¿«æ¥æ¥æä¸ºç³»ç»æ¥æåä¸å¤©ï¼å½åç³»ç»æ¥æ 2026-10-02 æ¶é»è®¤å 2026-10-01ï¼ç¨æ·ä¹å¯æå·¥éæ©æ¥æã |
| | | 2. æ¥èãäºä¸ç®åæä¾äºé
ç½®åç±»åéç¨è¡¨ç»æï¼ä½æªæä¾åå§åæ°æ®ï¼éè¦åç»æå¯¹åºå¹´åº¦ç»´æ¤ã |
| | | 3. å¿«æ¥æ¨¡æ¿åºå®ä¸ºå½åºçæ¬ï¼å
¶ä»è忥å¦éå¿«æ¥é妿便¨¡æ¿ã |
| | | 4. æ£å¼åå¸åä»é确认éªè¯ç å¼å
³çä¸çº¿é
ç½®ã |
| | | |
| | | ## åãææ¥è®¡å |
| | | |
| | | 1. æ ¹æ®ç¨æ·æä¾ç 2026 å¹´å䏿¯æ¥æ°æ®ï¼æå¯¼ææ§è¡å¯¼å
¥å¹¶æ ¸å¯¹çæå¿«æ¥ã |
| | | 2. å¦ç¨æ·æä¾æ¥èãäºä¸æ¥æå模æ¿ï¼ç»§ç»æ©å±å¯¹åºåå§åä¸å¿«æ¥æ¨¡æ¿éé
ã |
| | | 3. æ ¹æ®è¯ç¨åé¦å¾®è°é¡µé¢å±ç¤ºåå¯¼åºæ ¼å¼ã |
| | | |
| | | ## äºãè¿è¡ç¶æ |
| | | |
| | | - å端ï¼8090 å·²å¯å¨ï¼æ¥å¿åºç° `Started TrafficAuditApplication`ã |
| | | - å端ï¼8080 å·²å¯å¨ï¼è®¿é® `http://localhost:8080`ã |
| | | - æ°æ®åºï¼`127.0.0.1:3305/traffic_audit`ã |
| | |
| | | â ââ init.sql 建åºå»ºè¡¨ + é¢ç½®è´¦å·ï¼å¿
é¡»æ§è¡ï¼ |
| | | â ââ sql_city_bus.sql åå¸å®¢è¿-å
¬äº¤æåº¦è¡¨ï¼å¿
é¡»æ§è¡ï¼ |
| | | â ââ sql_city_taxi.sql åå¸å®¢è¿-åºç§æåº¦è¡¨/ææè¡¨ï¼å¿
é¡»æ§è¡ï¼ |
| | | â ââ sql_wyc.sql ç½çº¦è½¦å
¨çæ»é/订ååå¸å·è¡¨ï¼å¿
é¡»æ§è¡ï¼ |
| | | â ââ sql_wyc.sql ç½çº¦è½¦å
¨çæ»é/订ååå¸å·è¡¨ï¼å¿
é¡»æ§è¡ï¼ |
| | | â ââ sql_holiday.sql ä¸é¡¹ç»è®¡-éè¦èåæ¥è¡¨ï¼å¿
é¡»æ§è¡ï¼ |
| | | â ââ sql_holiday_comparison_base_migration.sql è忥æ§åºåçº§ï¼æ®éå½çéç¯æ¯åºæå段 |
| | | ââ README-é¨ç½²è¯´æ.txt |
| | | ``` |
| | | |
| | |
| | | |
| | | ### 5.1 å夿°æ®åºï¼ä¸¤æ¡è·¯çº¿äºéä¸ï¼**ä¸è¦æ··çå**ï¼ |
| | | |
| | | #### 路线 Aï¼å
¨æ°ç©ºåºï¼è¦è· 4 ä¸ªèæ¬ï¼ |
| | | #### 路线 Aï¼å
¨æ°ç©ºåºï¼è¦è· 5 ä¸ªèæ¬ï¼ |
| | | |
| | | `init.sql` èªå¸¦ `CREATE DATABASE` ä¸ `USE traffic_audit`ï¼å¯ç¬ç«æ§è¡ï¼å¦å¤ 3 ä¸ªèæ¬æ²¡æ `USE` è¯å¥ï¼å¿
é¡»ç¨ `-D traffic_audit`ï¼æ `mysql ... traffic_audit < æä»¶`ï¼æå®åºï¼ |
| | | `init.sql` èªå¸¦ `CREATE DATABASE` ä¸ `USE traffic_audit`ï¼å¯ç¬ç«æ§è¡ï¼å¦å¤ 4 ä¸ªèæ¬æ²¡æ `USE` è¯å¥ï¼å¿
é¡»ç¨ `-D traffic_audit`ï¼æ `mysql ... traffic_audit < æä»¶`ï¼æå®åºï¼ |
| | | |
| | | ```bash |
| | | cd db |
| | | mysql --default-character-set=utf8mb4 -u root -p < init.sql # å»ºåº + 24 å¼ åºç¡è¡¨ + é¢ç½®è´¦å· |
| | | mysql --default-character-set=utf8mb4 -u root -p < init.sql # å»ºåº + 28 å¼ åºç¡è¡¨ + é¢ç½®è´¦å· |
| | | mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_city_bus.sql |
| | | mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_city_taxi.sql |
| | | mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_wyc.sql |
| | | mysql --default-character-set=utf8mb4 -u root -p -D traffic_audit < sql_holiday.sql |
| | | ``` |
| | | |
| | | > â ï¸ **`init.sql` æ¯ç ´åæ§ç**ï¼éé¢çåºç¡è¡¨é½å¸¦ `DROP TABLE IF EXISTS`ã**å·²ææ°æ®çåºä¸åä¸ä¸è¦åè· init.sql**ï¼å¦åè¡¨æ°æ®ä¼è¢«æ¸
空é建ã |
| | | > 4 ä¸ªèæ¬å
±è¦ç 29 å¼ è¡¨ï¼init.sql 24 å¼ + åå¸å®¢è¿ 3 å¼ + ç½çº¦è½¦ 2 å¼ ï¼ã |
| | | > å·²é¨ç½²è¿æ©æèåæ¥æ¨¡åçç°æåºï¼å级åå
æ§è¡ `sql_holiday_comparison_base_migration.sql` æ·»å `comparison_base` åæ®µï¼å
¨æ°åºç´æ¥ä½¿ç¨å·²æ´æ°åç `sql_holiday.sql`ã |
| | | > 5 ä¸ªèæ¬å
±è¦ç 33 å¼ è¡¨ï¼init.sql 28 å¼ + åå¸å®¢è¿ 3 å¼ + ç½çº¦è½¦ 2 å¼ ï¼ã |
| | | |
| | | #### 路线 Bï¼æç°ç½/å¼åæºçæ°æ®æ´å¥æ¬è¿å»ï¼æ¨èï¼ä¸ç¨è·èæ¬ï¼ |
| | | |
| | |
| | | mysql --default-character-set=utf8mb4 -u root -p < traffic_audit_full_YYYYMMDD.sql |
| | | ``` |
| | | |
| | | - `--databases` 伿 `CREATE DATABASE` + `USE` ä¸èµ·åè¿å¤ä»½ï¼æä»¥ç®æ æºå¨**ä¸ç¨å
建åºãä¹ä¸ç¨è·é£ 4 ä¸ªèæ¬**ã |
| | | - `--databases` 伿 `CREATE DATABASE` + `USE` ä¸èµ·åè¿å¤ä»½ï¼æä»¥ç®æ æºå¨**ä¸ç¨å
建åºãä¹ä¸ç¨è·é£ 5 ä¸ªèæ¬**ã |
| | | - å¤ä»½éå
å« `sys_user`ï¼è´¦å·å¯ç 䏿ºæºå¨å®å
¨ä¸è´ï¼æ¬æºæ¯ `admin / 123456`ï¼ï¼ä¸ä¼è¢«éç½®æ `admin/admin123`ã |
| | | - â ï¸ å¤ä»½éæ¯å¼ 表é½å¸¦ `DROP TABLE IF EXISTS`ï¼å
± 29 å¼ ï¼ï¼**ç®æ åºè¦æ¯å·²ææ°æ®ï¼ä¼è¢«ç´æ¥è¦ç**ã请è¿åå°ç©ºåºï¼æå
èªå·±å¤ä»½ç®æ åºã |
| | | - æ¬æ¬¡å·²çæå¹¶**éªè¯è¿**å¤ä»½ï¼`traffic_audit_full_20260913.sql`ï¼45.5 MBï¼8.0.29 客æ·ç«¯å¯¼åºï¼ã |
| | | éªè¯æ¹å¼ï¼æå¤ä»½åºåæ¹æä¸´æ¶åºå¯¼å
¥ï¼é表æ¯å¯¹ `SHOW CREATE TABLE`ï¼è¡¨ç»æï¼ä¸ `COUNT(*)`ï¼è¡æ°ï¼ââ**29 å¼ è¡¨ç»æä¸è¡æ°å 0 å·®å¼**ï¼æ½æ¥ `wyc_order_monthly` 2026-08 æ°æ®ä¸è´ï¼éªè¯åå·²å é¤ä¸´æ¶åºã |
| | | éªè¯æ¹å¼ï¼æå¤ä»½åºåæ¹æä¸´æ¶åºå¯¼å
¥ï¼é表æ¯å¯¹ `SHOW CREATE TABLE`ï¼è¡¨ç»æï¼ä¸ `COUNT(*)`ï¼è¡æ°ï¼ââ**29 å¼ è¡¨ç»æä¸è¡æ°å 0 å·®å¼ï¼æ°å¢èåæ¥æ¨¡åå为 33 å¼ ï¼**ï¼æ½æ¥ `wyc_order_monthly` 2026-08 æ°æ®ä¸è´ï¼éªè¯åå·²å é¤ä¸´æ¶åºã |
| | | |
| | | #### 路线 B2ï¼ç¨ Navicat çãå¤ä»½ / è¿åå¤ä»½ãï¼.nb3ï¼æè´´è¿æ¥å¸¸ä¹ æ¯ï¼ |
| | | |
| | |
| | | | å¯¼åºæ¥ãæªæ¾å°æ±æ»è¡¨æ¯çãYYYYå¹´Mæâ¦ãã | `docs/çææ±æ»å¤§è¡¨` ç¼ºè¯¥ææ¯ç | |
| | | | ç»å½æ¥å£ 500 | `jwt.secret` å¡«äºéæ³å符ï¼å¦ä¸æï¼ï¼æ¹æ 32 ä½åå
è¿å¶ä¸² | |
| | | | 页é¢è½å¼ä½æ¥å£ 404 | èµ° nginx æ¹æ¡æ¶æ²¡é
`/api` åä»£ï¼æ¹ç¨ jar å
åµè®¿é® 8090 | |
| | | | æ°æ®åºè¿æ¥å¤±è´¥ | æ£æ¥ `application.yml`ï¼ç¡®è®¤ 4 个 SQL èæ¬å·²æ§è¡ | |
| | | | æ°æ®åºè¿æ¥å¤±è´¥ | æ£æ¥ `application.yml`ï¼ç¡®è®¤ 5 个 SQL èæ¬å·²æ§è¡ | |
| | | | 端å£å ç¨ | æ¹ `server.port`ï¼å端å¼åç«¯å£ 8080 | |
| | | |
| | | ## åãå¼åç¯å¢æ¥å¸¸å¯å¨ |
| New file |
| | |
| | | package com.trafficaudit.holiday; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | public final class HolidayConstants { |
| | | public static final int COMPARISON_BASE_YEAR = 2026; |
| | | |
| | | public static final String TYPE_SPRING_FESTIVAL = "SPRING_FESTIVAL"; |
| | | |
| | | public static final String TYPE_LABOR_DAY = "LABOR_DAY"; |
| | | public static final String TYPE_NATIONAL_DAY = "NATIONAL_DAY"; |
| | | |
| | | public static final String CATEGORY_HOLIDAY = "HOLIDAY"; |
| | | public static final String CATEGORY_NON_HOLIDAY = "NON_HOLIDAY"; |
| | | |
| | | public static final String MODE_RAIL = "RAIL"; |
| | | public static final String MODE_CIVIL_AVIATION = "CIVIL_AVIATION"; |
| | | public static final String MODE_ROAD = "ROAD"; |
| | | public static final String MODE_WATER = "WATER"; |
| | | |
| | | public static final String ROAD_EXPRESSWAY = "EXPRESSWAY"; |
| | | public static final String ROAD_NATIONAL_PROVINCIAL = "NATIONAL_PROVINCIAL"; |
| | | |
| | | public static final String VEHICLE_SMALL_PASSENGER = "SMALL_PASSENGER"; |
| | | public static final String VEHICLE_MEDIUM_SMALL_PASSENGER = "MEDIUM_SMALL_PASSENGER"; |
| | | |
| | | public static final Map<String, String> HOLIDAY_TYPE_LABELS; |
| | | public static final Map<String, String> DATE_CATEGORY_LABELS; |
| | | public static final Map<String, String> TRANSPORT_MODE_LABELS; |
| | | public static final Map<String, String> ROAD_CATEGORY_LABELS; |
| | | public static final Map<String, String> VEHICLE_TYPE_LABELS; |
| | | |
| | | private static final Set<String> HOLIDAY_TYPES; |
| | | private static final Set<String> DATE_CATEGORIES; |
| | | private static final Set<String> TRANSPORT_MODES; |
| | | private static final Set<String> ROAD_CATEGORIES; |
| | | private static final Set<String> VEHICLE_TYPES; |
| | | |
| | | static { |
| | | Map<String, String> holidayTypes = new LinkedHashMap<>(); |
| | | holidayTypes.put(TYPE_SPRING_FESTIVAL, "æ¥è"); |
| | | holidayTypes.put(TYPE_LABOR_DAY, "äºä¸"); |
| | | holidayTypes.put(TYPE_NATIONAL_DAY, "åä¸"); |
| | | HOLIDAY_TYPE_LABELS = Collections.unmodifiableMap(holidayTypes); |
| | | HOLIDAY_TYPES = new LinkedHashSet<>(holidayTypes.keySet()); |
| | | |
| | | Map<String, String> dateCategories = new LinkedHashMap<>(); |
| | | dateCategories.put(CATEGORY_HOLIDAY, "è忥"); |
| | | dateCategories.put(CATEGORY_NON_HOLIDAY, "éè忥"); |
| | | DATE_CATEGORY_LABELS = Collections.unmodifiableMap(dateCategories); |
| | | DATE_CATEGORIES = new LinkedHashSet<>(dateCategories.keySet()); |
| | | |
| | | Map<String, String> transportModes = new LinkedHashMap<>(); |
| | | transportModes.put(MODE_RAIL, "éè·¯"); |
| | | transportModes.put(MODE_CIVIL_AVIATION, "æ°èª"); |
| | | transportModes.put(MODE_ROAD, "éè·¯"); |
| | | transportModes.put(MODE_WATER, "æ°´è·¯"); |
| | | TRANSPORT_MODE_LABELS = Collections.unmodifiableMap(transportModes); |
| | | TRANSPORT_MODES = new LinkedHashSet<>(transportModes.keySet()); |
| | | |
| | | Map<String, String> roadCategories = new LinkedHashMap<>(); |
| | | roadCategories.put(ROAD_EXPRESSWAY, "é«éå
¬è·¯"); |
| | | roadCategories.put(ROAD_NATIONAL_PROVINCIAL, "æ®éå½çé"); |
| | | ROAD_CATEGORY_LABELS = Collections.unmodifiableMap(roadCategories); |
| | | ROAD_CATEGORIES = new LinkedHashSet<>(roadCategories.keySet()); |
| | | |
| | | Map<String, String> vehicleTypes = new LinkedHashMap<>(); |
| | | vehicleTypes.put(VEHICLE_SMALL_PASSENGER, "å°å®¢è½¦"); |
| | | vehicleTypes.put(VEHICLE_MEDIUM_SMALL_PASSENGER, "ä¸å°å®¢è½¦"); |
| | | VEHICLE_TYPE_LABELS = Collections.unmodifiableMap(vehicleTypes); |
| | | VEHICLE_TYPES = new LinkedHashSet<>(vehicleTypes.keySet()); |
| | | } |
| | | |
| | | private HolidayConstants() { |
| | | } |
| | | |
| | | public static boolean supportsComparisonBase(int year) { |
| | | return year == COMPARISON_BASE_YEAR; |
| | | } |
| | | |
| | | public static boolean isValidHolidayType(String value) { |
| | | return HOLIDAY_TYPES.contains(value); |
| | | } |
| | | |
| | | public static boolean isValidDateCategory(String value) { |
| | | return DATE_CATEGORIES.contains(value); |
| | | } |
| | | |
| | | public static boolean isValidTransportMode(String value) { |
| | | return TRANSPORT_MODES.contains(value); |
| | | } |
| | | |
| | | public static boolean isValidRoadCategory(String value) { |
| | | return ROAD_CATEGORIES.contains(value); |
| | | } |
| | | |
| | | public static boolean isValidVehicleType(String value) { |
| | | return VEHICLE_TYPES.contains(value); |
| | | } |
| | | |
| | | public static Map<String, Object> optionMap() { |
| | | Map<String, Object> options = new LinkedHashMap<>(); |
| | | options.put("holidayTypes", HOLIDAY_TYPE_LABELS); |
| | | options.put("dateCategories", DATE_CATEGORY_LABELS); |
| | | options.put("transportModes", TRANSPORT_MODE_LABELS); |
| | | options.put("roadCategories", ROAD_CATEGORY_LABELS); |
| | | options.put("vehicleTypes", VEHICLE_TYPE_LABELS); |
| | | options.put("years", Arrays.asList(2024, 2025, 2026)); |
| | | return options; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday; |
| | | |
| | | import org.apache.poi.ss.util.NumberToTextConverter; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.MathContext; |
| | | import java.math.RoundingMode; |
| | | |
| | | public final class HolidayNumberUtils { |
| | | |
| | | public static final MathContext EXCEL_MC = new MathContext(15, RoundingMode.HALF_UP); |
| | | public static final BigDecimal ZERO = BigDecimal.ZERO; |
| | | |
| | | private HolidayNumberUtils() { |
| | | } |
| | | |
| | | public static BigDecimal excelValue(double value) { |
| | | return normalize(new BigDecimal(NumberToTextConverter.toText(value))); |
| | | } |
| | | |
| | | public static BigDecimal decimalValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return normalize(new BigDecimal(value.toString().trim())); |
| | | } |
| | | |
| | | public static BigDecimal normalize(BigDecimal value) { |
| | | if (value == null) return null; |
| | | BigDecimal rounded = value.round(EXCEL_MC); |
| | | return rounded.signum() == 0 ? ZERO : rounded.stripTrailingZeros(); |
| | | } |
| | | |
| | | public static BigDecimal valueOrZero(BigDecimal value) { |
| | | return value == null ? ZERO : value; |
| | | } |
| | | |
| | | public static BigDecimal sumNullable(BigDecimal left, BigDecimal right) { |
| | | if (left == null && right == null) return null; |
| | | return normalize(valueOrZero(left).add(valueOrZero(right))); |
| | | } |
| | | |
| | | public static BigDecimal multiply(BigDecimal left, BigDecimal right) { |
| | | if (left == null || right == null) return null; |
| | | return normalize(left.multiply(right)); |
| | | } |
| | | |
| | | public static BigDecimal divide(BigDecimal dividend, BigDecimal divisor) { |
| | | if (dividend == null || divisor == null || divisor.signum() == 0) return null; |
| | | return normalize(dividend.divide(divisor, EXCEL_MC)); |
| | | } |
| | | |
| | | public static boolean sameValue(BigDecimal left, BigDecimal right) { |
| | | if (left == null || right == null) return false; |
| | | return normalize(left).compareTo(normalize(right)) == 0; |
| | | } |
| | | |
| | | public static String changeText(BigDecimal value) { |
| | | if (value == null) return ""; |
| | | return normalize(value).toPlainString(); |
| | | } |
| | | |
| | | public static String format1(BigDecimal value) { |
| | | if (value == null) return ""; |
| | | return value.setScale(1, RoundingMode.HALF_UP).toPlainString(); |
| | | } |
| | | |
| | | public static String format2(BigDecimal value) { |
| | | if (value == null) return ""; |
| | | return value.setScale(2, RoundingMode.HALF_UP).toPlainString(); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.controller; |
| | | |
| | | import com.trafficaudit.common.Result; |
| | | import com.trafficaudit.holiday.HolidayConstants; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerCoefficient; |
| | | import com.trafficaudit.holiday.service.HolidayConfigService; |
| | | import com.trafficaudit.security.utils.JwtUtils; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestHeader; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @RestController |
| | | @RequestMapping("/api/holiday/config") |
| | | public class HolidayConfigController { |
| | | |
| | | @Resource |
| | | private HolidayConfigService configService; |
| | | @Resource |
| | | private JwtUtils jwtUtils; |
| | | |
| | | @GetMapping("/options") |
| | | public Result<Map<String, Object>> options() { |
| | | return Result.ok(HolidayConstants.optionMap()); |
| | | } |
| | | |
| | | @GetMapping("/holidays") |
| | | public Result<List<HolidayCalendar>> holidays(@RequestParam(value = "year", required = false) Integer year, |
| | | @RequestParam(value = "holidayType", required = false) String holidayType) { |
| | | return Result.ok(configService.listHolidays(year, holidayType)); |
| | | } |
| | | |
| | | @PostMapping("/holidays/save") |
| | | public Result<HolidayCalendar> saveHoliday(@RequestHeader(value = "Authorization", required = false) String auth, |
| | | @RequestBody Map<String, Object> params) { |
| | | if (!isAdmin(auth)) return Result.error(403, "ä»
admin å¯ä»¥ç®¡çè忥é
ç½®"); |
| | | try { |
| | | return Result.ok(configService.saveHoliday(params)); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @PostMapping("/holidays/delete") |
| | | public Result<Void> deleteHoliday(@RequestHeader(value = "Authorization", required = false) String auth, |
| | | @RequestParam("id") Long id) { |
| | | if (!isAdmin(auth)) return Result.error(403, "ä»
admin å¯ä»¥ç®¡çè忥é
ç½®"); |
| | | try { |
| | | configService.deleteHoliday(id); |
| | | return Result.ok(); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @GetMapping("/coefficients") |
| | | public Result<List<HolidayPassengerCoefficient>> coefficients(@RequestParam(value = "year", required = false) Integer year) { |
| | | return Result.ok(configService.listCoefficients(year)); |
| | | } |
| | | |
| | | @PostMapping("/coefficients/save") |
| | | public Result<HolidayPassengerCoefficient> saveCoefficient(@RequestHeader(value = "Authorization", required = false) String auth, |
| | | @RequestBody Map<String, Object> params) { |
| | | if (!isAdmin(auth)) return Result.error(403, "ä»
admin å¯ä»¥ç®¡ç客è¿ç³»æ°"); |
| | | try { |
| | | return Result.ok(configService.saveCoefficient(params)); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @PostMapping("/coefficients/delete") |
| | | public Result<Void> deleteCoefficient(@RequestHeader(value = "Authorization", required = false) String auth, |
| | | @RequestParam("id") Long id) { |
| | | if (!isAdmin(auth)) return Result.error(403, "ä»
admin å¯ä»¥ç®¡ç客è¿ç³»æ°"); |
| | | try { |
| | | configService.deleteCoefficient(id); |
| | | return Result.ok(); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | private boolean isAdmin(String auth) { |
| | | return "admin".equals(jwtUtils.resolveUsername(auth)); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.controller; |
| | | |
| | | import com.trafficaudit.common.Result; |
| | | import com.trafficaudit.holiday.service.HolidayBulletinService; |
| | | import com.trafficaudit.holiday.service.HolidayStatService; |
| | | import org.springframework.http.HttpHeaders; |
| | | import org.springframework.http.MediaType; |
| | | import org.springframework.http.ResponseEntity; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.net.URLEncoder; |
| | | import java.time.LocalDate; |
| | | import java.util.Map; |
| | | |
| | | @RestController |
| | | @RequestMapping("/api/holiday") |
| | | public class HolidayStatController { |
| | | |
| | | @Resource |
| | | private HolidayStatService statService; |
| | | @Resource |
| | | private HolidayBulletinService bulletinService; |
| | | |
| | | @GetMapping("/overview") |
| | | public Result<Map<String, Object>> overview(@RequestParam("year") int year, |
| | | @RequestParam("holidayType") String holidayType) { |
| | | try { |
| | | return Result.ok(statService.overview(year, holidayType)); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @PostMapping("/import") |
| | | public Result<Map<String, Object>> importWorkbook(@RequestParam(value = "file", required = false) MultipartFile file, |
| | | @RequestParam("year") int year, |
| | | @RequestParam("holidayType") String holidayType, |
| | | @RequestParam(value = "allowHistoricalOverwrite", defaultValue = "false") boolean allowHistoricalOverwrite) { |
| | | try { |
| | | return Result.ok(statService.importWorkbook(file, year, holidayType, allowHistoricalOverwrite)); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @GetMapping("/export") |
| | | public ResponseEntity<byte[]> exportWorkbook(@RequestParam("year") int year, |
| | | @RequestParam("holidayType") String holidayType) throws Exception { |
| | | byte[] data = statService.exportWorkbook(year, holidayType); |
| | | String fileName = year + "å¹´" + holidayTypeFileLabel(holidayType) + "æ°æ®.xlsx"; |
| | | String encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20"); |
| | | return ResponseEntity.ok() |
| | | .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded) |
| | | .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) |
| | | .body(data); |
| | | } |
| | | |
| | | @GetMapping("/bulletin") |
| | | public ResponseEntity<byte[]> bulletin(@RequestParam(value = "date", required = false) String date) throws Exception { |
| | | LocalDate targetDate = (date == null || date.trim().isEmpty()) |
| | | ? LocalDate.now().minusDays(1) : LocalDate.parse(date); |
| | | byte[] data = bulletinService.generate(targetDate); |
| | | String fileName = String.format("æ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥ï¼10æ%dæ¥ï¼.docx", targetDate.getDayOfMonth()); |
| | | String encoded = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20"); |
| | | return ResponseEntity.ok() |
| | | .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded) |
| | | .contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.wordprocessingml.document")) |
| | | .body(data); |
| | | } |
| | | |
| | | private String holidayTypeFileLabel(String holidayType) { |
| | | if ("SPRING_FESTIVAL".equals(holidayType)) return "æ¥è"; |
| | | if ("LABOR_DAY".equals(holidayType)) return "äºä¸"; |
| | | return "åä¸"; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Data; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | |
| | | @Data |
| | | @TableName("holiday_calendar") |
| | | public class HolidayCalendar { |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | private Integer year; |
| | | private String holidayType; |
| | | private LocalDate holidayDate; |
| | | private Integer dayIndex; |
| | | private LocalDateTime createdAt; |
| | | private LocalDateTime updatedAt; |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | |
| | | @Data |
| | | @TableName("holiday_passenger_coefficient") |
| | | public class HolidayPassengerCoefficient { |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | private Integer year; |
| | | private String dateCategory; |
| | | private BigDecimal coefficient; |
| | | private LocalDateTime createdAt; |
| | | private LocalDateTime updatedAt; |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | |
| | | @Data |
| | | @TableName("holiday_passenger_flow") |
| | | public class HolidayPassengerFlow { |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | private Integer year; |
| | | private LocalDate flowDate; |
| | | private String transportMode; |
| | | private BigDecimal passengerCount; |
| | | private LocalDateTime createdAt; |
| | | private LocalDateTime updatedAt; |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.FieldStrategy; |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | |
| | | @Data |
| | | @TableName("holiday_vehicle_flow") |
| | | public class HolidayVehicleFlow { |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | private Integer year; |
| | | private LocalDate flowDate; |
| | | private String roadCategory; |
| | | private String vehicleType; |
| | | private BigDecimal vehicleCount; |
| | | @TableField(updateStrategy = FieldStrategy.IGNORED) |
| | | private BigDecimal comparisonBase; |
| | | private LocalDateTime createdAt; |
| | | private LocalDateTime updatedAt; |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface HolidayCalendarMapper extends BaseMapper<HolidayCalendar> { |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerCoefficient; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface HolidayPassengerCoefficientMapper extends BaseMapper<HolidayPassengerCoefficient> { |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerFlow; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface HolidayPassengerFlowMapper extends BaseMapper<HolidayPassengerFlow> { |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleFlow; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface HolidayVehicleFlowMapper extends BaseMapper<HolidayVehicleFlow> { |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.trafficaudit.holiday.HolidayNumberUtils; |
| | | import com.trafficaudit.holiday.service.HolidayStatService.DailyMetrics; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | import org.w3c.dom.Document; |
| | | import org.w3c.dom.Element; |
| | | import org.w3c.dom.Node; |
| | | import org.w3c.dom.NodeList; |
| | | |
| | | import javax.xml.parsers.DocumentBuilderFactory; |
| | | import javax.xml.transform.OutputKeys; |
| | | import javax.xml.transform.Transformer; |
| | | import javax.xml.transform.TransformerFactory; |
| | | import javax.xml.transform.dom.DOMSource; |
| | | import javax.xml.transform.stream.StreamResult; |
| | | import java.io.ByteArrayInputStream; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.InputStream; |
| | | import java.math.BigDecimal; |
| | | import java.nio.file.Files; |
| | | import java.time.LocalDate; |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.zip.ZipEntry; |
| | | import java.util.zip.ZipInputStream; |
| | | import java.util.zip.ZipOutputStream; |
| | | |
| | | @Slf4j |
| | | @Service |
| | | public class HolidayBulletinService { |
| | | |
| | | private static final String WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; |
| | | private static final BigDecimal EPSILON = new BigDecimal("0.0000001"); |
| | | |
| | | @Value("${holiday.bulletin-template:docs/ä¸é¡¹ç»è®¡/éè¦è忥/æ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥æ¨¡æ¿.docx}") |
| | | private String bulletinTemplatePath; |
| | | |
| | | private final HolidayStatService statService; |
| | | private static final ThreadLocal<Document> CURRENT_DOCUMENT = new ThreadLocal<>(); |
| | | |
| | | public HolidayBulletinService(HolidayStatService statService) { |
| | | this.statService = statService; |
| | | } |
| | | |
| | | public byte[] generate(LocalDate date) { |
| | | DailyMetrics current = statService.findMetricsByDate(date); |
| | | if (current == null) { |
| | | throw new IllegalArgumentException("æéæ¥æä¸å¨å·²é
ç½®çè忥èå´å
"); |
| | | } |
| | | DailyMetrics previousDay = current.getDayIndex() > 1 |
| | | ? statService.dailyMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex() - 1) : null; |
| | | DailyMetrics previousYear = statService.dailyMetrics(current.getYear() - 1, current.getHolidayType(), current.getDayIndex()); |
| | | DailyMetrics currentCumulative = statService.cumulativeMetrics(current.getYear(), current.getHolidayType(), current.getDayIndex()); |
| | | DailyMetrics previousYearCumulative = statService.cumulativeMetrics(current.getYear() - 1, current.getHolidayType(), current.getDayIndex()); |
| | | |
| | | if (current.getTotalPersonFlow() == null) { |
| | | throw new IllegalArgumentException("æéæ¥æçå
¨ç¤¾ä¼è·¨åºåäººåæµå¨é没æè¶³å¤æ°æ®ï¼æ æ³çæå¿«æ¥"); |
| | | } |
| | | |
| | | String currentTotal = formatNumber(current.getTotalPersonFlow()); |
| | | String cumulativeTotalYi = formatNumber(currentCumulative.getTotalPersonFlow() == null |
| | | ? null : HolidayNumberUtils.divide(currentCumulative.getTotalPersonFlow(), BigDecimal.valueOf(10000))); |
| | | String ringRatioClause = previousDay == null ? "" : "ï¼ç¯æ¯" + ratioPhrase( |
| | | current.getTotalPersonFlow(), previousDay.getTotalPersonFlow()); |
| | | String paragraph1 = String.format("%dæ%dæ¥ï¼åæç¬¬%d天ï¼ï¼å
¨çå
¨ç¤¾ä¼è·¨åºåäººåæµå¨é%sä¸äººæ¬¡%sï¼æ¯2025å¹´åæ%sã", |
| | | date.getMonthValue(), date.getDayOfMonth(), current.getDayIndex(), currentTotal, |
| | | ringRatioClause, |
| | | ratioPhrase(current.getTotalPersonFlow(), previousYear == null ? null : previousYear.getTotalPersonFlow())); |
| | | String paragraph2 = String.format("åæä»¥æ¥ï¼å
¨çå
¨ç¤¾ä¼è·¨åºåäººåæµå¨é累计%säº¿äººæ¬¡ï¼æ¯2025å¹´åæ%sã", |
| | | cumulativeTotalYi, |
| | | ratioPhrase(currentCumulative.getTotalPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow())); |
| | | |
| | | File template = resolveTemplateFile(); |
| | | if (!template.exists() || !template.isFile()) { |
| | | throw new IllegalArgumentException("å¿«æ¥æ¨¡æ¿æä»¶ä¸åå¨: " + template.getAbsolutePath()); |
| | | } |
| | | |
| | | try { |
| | | byte[] templateBytes; |
| | | try (InputStream in = new FileInputStream(template)) { |
| | | templateBytes = readAll(in); |
| | | } |
| | | byte[] documentXml = readZipEntry(templateBytes, "word/document.xml"); |
| | | Document document = parseDocument(documentXml); |
| | | CURRENT_DOCUMENT.set(document); |
| | | try { |
| | | replaceParagraph(document, text -> text.startsWith("10æ") && text.contains("åæç¬¬"), paragraph1); |
| | | replaceParagraph(document, text -> text.startsWith("åæä»¥æ¥"), paragraph2); |
| | | fillTable(document, current, previousDay, previousYear, currentCumulative, previousYearCumulative); |
| | | byte[] replacedXml = serializeDocument(document); |
| | | return replaceZipEntry(templateBytes, "word/document.xml", replacedXml); |
| | | } finally { |
| | | CURRENT_DOCUMENT.remove(); |
| | | } |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("å¿«æ¥çæå¤±è´¥: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | private void fillTable(Document document, DailyMetrics current, DailyMetrics previousDay, |
| | | DailyMetrics previousYear, DailyMetrics currentCumulative, |
| | | DailyMetrics previousYearCumulative) { |
| | | Element table = firstElement(document, "tbl"); |
| | | if (table == null) { |
| | | throw new IllegalArgumentException("å¿«æ¥æ¨¡æ¿ä¸æ¾ä¸å°æ°æ®è¡¨"); |
| | | } |
| | | writeTableRow(table, 2, new String[]{ |
| | | number(current.getNonBusinessPersonFlow()), |
| | | ratio(current.getNonBusinessPersonFlow(), previousDay == null ? null : previousDay.getNonBusinessPersonFlow()), |
| | | ratio(current.getNonBusinessPersonFlow(), previousYear == null ? null : previousYear.getNonBusinessPersonFlow()), |
| | | "â", |
| | | number(currentCumulative.getNonBusinessPersonFlow()), |
| | | ratio(currentCumulative.getNonBusinessPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getNonBusinessPersonFlow()), |
| | | "â" |
| | | }); |
| | | writeTableRow(table, 3, new String[]{ |
| | | number(current.getRoad()), |
| | | ratio(current.getRoad(), previousDay == null ? null : previousDay.getRoad()), |
| | | ratio(current.getRoad(), previousYear == null ? null : previousYear.getRoad()), |
| | | "â", |
| | | number(currentCumulative.getRoad()), |
| | | ratio(currentCumulative.getRoad(), previousYearCumulative == null ? null : previousYearCumulative.getRoad()), |
| | | "â" |
| | | }); |
| | | writeTableRow(table, 4, new String[]{ |
| | | number(current.getRoadPersonFlow()), |
| | | ratio(current.getRoadPersonFlow(), previousDay == null ? null : previousDay.getRoadPersonFlow()), |
| | | ratio(current.getRoadPersonFlow(), previousYear == null ? null : previousYear.getRoadPersonFlow()), |
| | | "â", |
| | | number(currentCumulative.getRoadPersonFlow()), |
| | | ratio(currentCumulative.getRoadPersonFlow(), previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow()), |
| | | "â" |
| | | }); |
| | | writeTableRow(table, 5, transportRow(current.getRail(), previousDay == null ? null : previousDay.getRail(), |
| | | previousYear == null ? null : previousYear.getRail(), currentCumulative.getRail(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getRail())); |
| | | writeTableRow(table, 6, transportRow(current.getCivilAviation(), previousDay == null ? null : previousDay.getCivilAviation(), |
| | | previousYear == null ? null : previousYear.getCivilAviation(), currentCumulative.getCivilAviation(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getCivilAviation())); |
| | | writeTableRow(table, 7, transportRow(current.getWater(), previousDay == null ? null : previousDay.getWater(), |
| | | previousYear == null ? null : previousYear.getWater(), currentCumulative.getWater(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getWater())); |
| | | writeTableRow(table, 8, transportRow(current.getTotalPersonFlow(), previousDay == null ? null : previousDay.getTotalPersonFlow(), |
| | | previousYear == null ? null : previousYear.getTotalPersonFlow(), currentCumulative.getTotalPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow())); |
| | | } |
| | | |
| | | private String[] transportRow(BigDecimal current, BigDecimal previousDay, BigDecimal previousYear, |
| | | BigDecimal currentCumulative, BigDecimal previousYearCumulative) { |
| | | return new String[]{ |
| | | number(current), |
| | | ratio(current, previousDay), |
| | | ratio(current, previousYear), |
| | | "â", |
| | | number(currentCumulative), |
| | | ratio(currentCumulative, previousYearCumulative), |
| | | "â" |
| | | }; |
| | | } |
| | | |
| | | private void writeTableRow(Element table, int rowIndex, String[] values) { |
| | | List<Element> rows = childElements(table, "tr"); |
| | | if (rowIndex >= rows.size()) return; |
| | | Element row = rows.get(rowIndex); |
| | | List<Element> cells = cellRefs(row); |
| | | int logicalColumn = 0; |
| | | for (Element cell : cells) { |
| | | int span = gridSpan(cell); |
| | | if (logicalColumn >= 2 && logicalColumn < 9) { |
| | | int valueIndex = logicalColumn - 2; |
| | | if (valueIndex < values.length) { |
| | | setCellText(cell, values[valueIndex]); |
| | | } |
| | | } |
| | | logicalColumn += span; |
| | | } |
| | | } |
| | | |
| | | private List<Element> cellRefs(Element row) { |
| | | List<Element> cells = new ArrayList<>(); |
| | | for (Element child : childElements(row, "tc")) cells.add(child); |
| | | return cells; |
| | | } |
| | | |
| | | private int gridSpan(Element cell) { |
| | | Element tcPr = firstChildElement(cell, "tcPr"); |
| | | if (tcPr == null) return 1; |
| | | Element span = firstChildElement(tcPr, "gridSpan"); |
| | | if (span == null) return 1; |
| | | String value = span.getAttributeNS(WORD_NS, "val"); |
| | | try { |
| | | return Integer.parseInt(value); |
| | | } catch (Exception e) { |
| | | return 1; |
| | | } |
| | | } |
| | | |
| | | private void setCellText(Element cell, String text) { |
| | | Element tcPr = firstChildElement(cell, "tcPr"); |
| | | Element templateParagraph = firstChildElement(cell, "p"); |
| | | Element paragraph = createElement("p"); |
| | | if (templateParagraph != null) { |
| | | Element pPr = firstChildElement(templateParagraph, "pPr"); |
| | | if (pPr != null) paragraph.appendChild(pPr.cloneNode(true)); |
| | | } |
| | | Element run = createElement("r"); |
| | | Element t = createElement("t"); |
| | | t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve"); |
| | | t.setTextContent(text == null ? "" : text); |
| | | run.appendChild(t); |
| | | paragraph.appendChild(run); |
| | | |
| | | List<Node> removals = new ArrayList<>(); |
| | | Node child = cell.getFirstChild(); |
| | | while (child != null) { |
| | | Node next = child.getNextSibling(); |
| | | if (child instanceof Element && "p".equals(localName(child))) { |
| | | removals.add(child); |
| | | } |
| | | child = next; |
| | | } |
| | | for (Node node : removals) cell.removeChild(node); |
| | | if (tcPr != null && tcPr.getNextSibling() != null) { |
| | | cell.insertBefore(paragraph, tcPr.getNextSibling()); |
| | | } else if (tcPr != null) { |
| | | cell.appendChild(paragraph); |
| | | } else { |
| | | cell.insertBefore(paragraph, cell.getFirstChild()); |
| | | } |
| | | } |
| | | |
| | | private void replaceParagraph(Document document, ParagraphMatcher matcher, String replacement) { |
| | | NodeList paragraphs = document.getElementsByTagNameNS(WORD_NS, "p"); |
| | | for (int i = 0; i < paragraphs.getLength(); i++) { |
| | | Element paragraph = (Element) paragraphs.item(i); |
| | | String text = paragraphText(paragraph); |
| | | if (matcher.matches(text)) { |
| | | setParagraphText(paragraph, replacement); |
| | | return; |
| | | } |
| | | } |
| | | throw new IllegalArgumentException("å¿«æ¥æ¨¡æ¿ç¼ºå°å¯æ¿æ¢æ®µè½: " + replacement); |
| | | } |
| | | |
| | | private void setParagraphText(Element paragraph, String text) { |
| | | Element pPr = firstChildElement(paragraph, "pPr"); |
| | | Element firstRun = firstChildElement(paragraph, "r"); |
| | | Element rPr = firstRun == null ? null : firstChildElement(firstRun, "rPr"); |
| | | List<Node> removals = new ArrayList<>(); |
| | | Node child = paragraph.getFirstChild(); |
| | | while (child != null) { |
| | | Node next = child.getNextSibling(); |
| | | if (child != pPr) removals.add(child); |
| | | child = next; |
| | | } |
| | | for (Node node : removals) paragraph.removeChild(node); |
| | | |
| | | Element run = createElement("r"); |
| | | if (rPr != null) run.appendChild(rPr.cloneNode(true)); |
| | | Element t = createElement("t"); |
| | | t.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve"); |
| | | t.setTextContent(text); |
| | | run.appendChild(t); |
| | | paragraph.appendChild(run); |
| | | } |
| | | |
| | | private String paragraphText(Element paragraph) { |
| | | StringBuilder builder = new StringBuilder(); |
| | | NodeList texts = paragraph.getElementsByTagNameNS(WORD_NS, "t"); |
| | | for (int i = 0; i < texts.getLength(); i++) { |
| | | builder.append(texts.item(i).getTextContent()); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private String ratioPhrase(BigDecimal current, BigDecimal base) { |
| | | if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return "æ 坿¯æ°æ®"; |
| | | BigDecimal ratio = HolidayNumberUtils.divide(current, base); |
| | | BigDecimal percent = HolidayNumberUtils.multiply(ratio.subtract(BigDecimal.ONE), BigDecimal.valueOf(100)); |
| | | if (percent == null || percent.abs().compareTo(EPSILON) < 0) return "æå¹³"; |
| | | return (percent.signum() > 0 ? "å¢é¿" : "ä¸é") + HolidayNumberUtils.format1(percent.abs()) + "%"; |
| | | } |
| | | |
| | | private String ratio(BigDecimal current, BigDecimal base) { |
| | | if (current == null || base == null || base.abs().compareTo(EPSILON) < 0) return "â"; |
| | | return ratioPhrase(current, base); |
| | | } |
| | | |
| | | private String number(BigDecimal value) { |
| | | return value == null ? "â" : formatNumber(value); |
| | | } |
| | | |
| | | private String formatNumber(BigDecimal value) { |
| | | if (value == null) return ""; |
| | | return HolidayNumberUtils.format2(value); |
| | | } |
| | | |
| | | private File resolveTemplateFile() { |
| | | List<File> candidates = new ArrayList<>(); |
| | | File configured = new File(bulletinTemplatePath); |
| | | candidates.add(configured); |
| | | if (!configured.isAbsolute()) { |
| | | candidates.add(new File(System.getProperty("user.dir"), bulletinTemplatePath)); |
| | | candidates.add(new File(System.getProperty("user.dir"), "../" + bulletinTemplatePath)); |
| | | } |
| | | for (File candidate : candidates) { |
| | | try { |
| | | if (candidate.exists() && candidate.isFile()) { |
| | | return candidate.getCanonicalFile(); |
| | | } |
| | | } catch (Exception ignored) { |
| | | // Keep checking. |
| | | } |
| | | } |
| | | return configured; |
| | | } |
| | | |
| | | private byte[] readZipEntry(byte[] zipBytes, String entryName) throws Exception { |
| | | try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { |
| | | ZipEntry entry; |
| | | while ((entry = zin.getNextEntry()) != null) { |
| | | if (entryName.equals(entry.getName())) { |
| | | return readAll(zin); |
| | | } |
| | | } |
| | | } |
| | | throw new IllegalArgumentException("模æ¿ä¸ç¼ºå° " + entryName); |
| | | } |
| | | |
| | | private byte[] replaceZipEntry(byte[] zipBytes, String entryName, byte[] replacement) throws Exception { |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zipBytes)); |
| | | ZipOutputStream zout = new ZipOutputStream(out)) { |
| | | ZipEntry entry; |
| | | byte[] buffer = new byte[8192]; |
| | | while ((entry = zin.getNextEntry()) != null) { |
| | | ZipEntry target = new ZipEntry(entry.getName()); |
| | | zout.putNextEntry(target); |
| | | if (entryName.equals(entry.getName())) { |
| | | zout.write(replacement); |
| | | } else { |
| | | int length; |
| | | while ((length = zin.read(buffer)) > 0) { |
| | | zout.write(buffer, 0, length); |
| | | } |
| | | } |
| | | zout.closeEntry(); |
| | | } |
| | | } |
| | | return out.toByteArray(); |
| | | } |
| | | |
| | | private Document parseDocument(byte[] xml) throws Exception { |
| | | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); |
| | | factory.setNamespaceAware(true); |
| | | return factory.newDocumentBuilder().parse(new ByteArrayInputStream(xml)); |
| | | } |
| | | |
| | | private byte[] serializeDocument(Document document) throws Exception { |
| | | Transformer transformer = TransformerFactory.newInstance().newTransformer(); |
| | | transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); |
| | | transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | transformer.transform(new DOMSource(document), new StreamResult(out)); |
| | | return out.toByteArray(); |
| | | } |
| | | |
| | | private Element firstElement(Document document, String localName) { |
| | | NodeList list = document.getElementsByTagNameNS(WORD_NS, localName); |
| | | return list.getLength() == 0 ? null : (Element) list.item(0); |
| | | } |
| | | |
| | | private Element createElement(String localName) { |
| | | Document document = CURRENT_DOCUMENT.get(); |
| | | if (document == null) { |
| | | throw new IllegalStateException("Word document is not initialized"); |
| | | } |
| | | return document.createElementNS(WORD_NS, "w:" + localName); |
| | | } |
| | | |
| | | private List<Element> childElements(Element parent, String localName) { |
| | | List<Element> result = new ArrayList<>(); |
| | | Node child = parent.getFirstChild(); |
| | | while (child != null) { |
| | | if (child instanceof Element && localName.equals(localName(child))) { |
| | | result.add((Element) child); |
| | | } |
| | | child = child.getNextSibling(); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Element firstChildElement(Element parent, String localName) { |
| | | Node child = parent.getFirstChild(); |
| | | while (child != null) { |
| | | if (child instanceof Element && localName.equals(localName(child))) { |
| | | return (Element) child; |
| | | } |
| | | child = child.getNextSibling(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private String localName(Node node) { |
| | | return node.getLocalName() == null ? node.getNodeName().replaceFirst("^.*:", "") : node.getLocalName(); |
| | | } |
| | | |
| | | private byte[] readAll(InputStream in) throws Exception { |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | byte[] buffer = new byte[8192]; |
| | | int length; |
| | | while ((length = in.read(buffer)) > 0) { |
| | | out.write(buffer, 0, length); |
| | | } |
| | | return out.toByteArray(); |
| | | } |
| | | |
| | | private interface ParagraphMatcher { |
| | | boolean matches(String text); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.trafficaudit.holiday.HolidayConstants; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerCoefficient; |
| | | import com.trafficaudit.holiday.mapper.HolidayCalendarMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayPassengerCoefficientMapper; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Service |
| | | public class HolidayConfigService { |
| | | |
| | | @Resource |
| | | private HolidayCalendarMapper holidayCalendarMapper; |
| | | |
| | | @Resource |
| | | private HolidayPassengerCoefficientMapper coefficientMapper; |
| | | |
| | | public List<HolidayCalendar> listHolidays(Integer year, String holidayType) { |
| | | LambdaQueryWrapper<HolidayCalendar> wrapper = new LambdaQueryWrapper<HolidayCalendar>() |
| | | .eq(year != null, HolidayCalendar::getYear, year) |
| | | .eq(holidayType != null && !holidayType.trim().isEmpty(), HolidayCalendar::getHolidayType, holidayType) |
| | | .orderByAsc(HolidayCalendar::getYear) |
| | | .orderByAsc(HolidayCalendar::getHolidayType) |
| | | .orderByAsc(HolidayCalendar::getDayIndex); |
| | | return holidayCalendarMapper.selectList(wrapper); |
| | | } |
| | | |
| | | public List<HolidayCalendar> findHolidayDays(int year, String holidayType) { |
| | | return holidayCalendarMapper.selectList(new LambdaQueryWrapper<HolidayCalendar>() |
| | | .eq(HolidayCalendar::getYear, year) |
| | | .eq(HolidayCalendar::getHolidayType, holidayType) |
| | | .orderByAsc(HolidayCalendar::getDayIndex)); |
| | | } |
| | | |
| | | public HolidayCalendar findHolidayByDate(LocalDate date) { |
| | | return holidayCalendarMapper.selectOne(new LambdaQueryWrapper<HolidayCalendar>() |
| | | .eq(HolidayCalendar::getHolidayDate, date) |
| | | .orderByAsc(HolidayCalendar::getDayIndex) |
| | | .last("limit 1")); |
| | | } |
| | | |
| | | @Transactional |
| | | public HolidayCalendar saveHoliday(Map<String, Object> params) { |
| | | Long id = longValue(params.get("id")); |
| | | Integer year = intValue(params.get("year")); |
| | | String holidayType = stringValue(params.get("holidayType")); |
| | | LocalDate holidayDate = dateValue(params.get("holidayDate")); |
| | | Integer dayIndex = intValue(params.get("dayIndex")); |
| | | if (year == null || year < 2000 || year > 2100) { |
| | | throw new IllegalArgumentException("年份䏿£ç¡®"); |
| | | } |
| | | if (!HolidayConstants.isValidHolidayType(holidayType)) { |
| | | throw new IllegalArgumentException("è忥åç±»ä¸æ£ç¡®"); |
| | | } |
| | | if (holidayDate == null || holidayDate.getYear() != year) { |
| | | throw new IllegalArgumentException("æ¥æå¿
é¡»å±äºæé年份"); |
| | | } |
| | | if (dayIndex == null || dayIndex < 1) { |
| | | throw new IllegalArgumentException("ç¬¬å æ¥å¿
é¡»ä» 1 å¼å§"); |
| | | } |
| | | LambdaQueryWrapper<HolidayCalendar> duplicate = new LambdaQueryWrapper<HolidayCalendar>() |
| | | .eq(HolidayCalendar::getYear, year) |
| | | .eq(HolidayCalendar::getHolidayType, holidayType) |
| | | .and(w -> w.eq(HolidayCalendar::getHolidayDate, holidayDate) |
| | | .or().eq(HolidayCalendar::getDayIndex, dayIndex)); |
| | | if (id != null) { |
| | | duplicate.ne(HolidayCalendar::getId, id); |
| | | } |
| | | if (holidayCalendarMapper.selectCount(duplicate) > 0) { |
| | | throw new IllegalArgumentException("åä¸è忥ä¸çæ¥ææç¬¬å æ¥å·²åå¨"); |
| | | } |
| | | |
| | | HolidayCalendar entity = id == null ? new HolidayCalendar() : holidayCalendarMapper.selectById(id); |
| | | if (entity == null) { |
| | | throw new IllegalArgumentException("èåæ¥è®°å½ä¸åå¨"); |
| | | } |
| | | if (id == null) { |
| | | entity.setCreatedAt(LocalDateTime.now()); |
| | | } |
| | | entity.setYear(year); |
| | | entity.setHolidayType(holidayType); |
| | | entity.setHolidayDate(holidayDate); |
| | | entity.setDayIndex(dayIndex); |
| | | entity.setUpdatedAt(LocalDateTime.now()); |
| | | if (id == null) { |
| | | holidayCalendarMapper.insert(entity); |
| | | } else { |
| | | holidayCalendarMapper.updateById(entity); |
| | | } |
| | | return entity; |
| | | } |
| | | |
| | | @Transactional |
| | | public void deleteHoliday(Long id) { |
| | | if (id == null) { |
| | | throw new IllegalArgumentException("缺å°è®°å½ id"); |
| | | } |
| | | holidayCalendarMapper.deleteById(id); |
| | | } |
| | | |
| | | public List<HolidayPassengerCoefficient> listCoefficients(Integer year) { |
| | | return coefficientMapper.selectList(new LambdaQueryWrapper<HolidayPassengerCoefficient>() |
| | | .eq(year != null, HolidayPassengerCoefficient::getYear, year) |
| | | .orderByAsc(HolidayPassengerCoefficient::getYear) |
| | | .orderByAsc(HolidayPassengerCoefficient::getDateCategory)); |
| | | } |
| | | |
| | | public BigDecimal coefficientOf(int year, String dateCategory) { |
| | | HolidayPassengerCoefficient row = coefficientMapper.selectOne( |
| | | new LambdaQueryWrapper<HolidayPassengerCoefficient>() |
| | | .eq(HolidayPassengerCoefficient::getYear, year) |
| | | .eq(HolidayPassengerCoefficient::getDateCategory, dateCategory) |
| | | .last("limit 1")); |
| | | return row == null ? null : row.getCoefficient(); |
| | | } |
| | | |
| | | @Transactional |
| | | public HolidayPassengerCoefficient saveCoefficient(Map<String, Object> params) { |
| | | Long id = longValue(params.get("id")); |
| | | Integer year = intValue(params.get("year")); |
| | | String dateCategory = stringValue(params.get("dateCategory")); |
| | | BigDecimal coefficient = decimalValue(params.get("coefficient")); |
| | | if (year == null || year < 2000 || year > 2100) { |
| | | throw new IllegalArgumentException("年份䏿£ç¡®"); |
| | | } |
| | | if (!HolidayConstants.isValidDateCategory(dateCategory)) { |
| | | throw new IllegalArgumentException("æ¥æåç±»ä¸æ£ç¡®"); |
| | | } |
| | | if (coefficient == null || coefficient.compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new IllegalArgumentException("客è¿ç³»æ°å¿
é¡»å¤§äº 0"); |
| | | } |
| | | LambdaQueryWrapper<HolidayPassengerCoefficient> duplicate = new LambdaQueryWrapper<HolidayPassengerCoefficient>() |
| | | .eq(HolidayPassengerCoefficient::getYear, year) |
| | | .eq(HolidayPassengerCoefficient::getDateCategory, dateCategory); |
| | | if (id != null) { |
| | | duplicate.ne(HolidayPassengerCoefficient::getId, id); |
| | | } |
| | | if (coefficientMapper.selectCount(duplicate) > 0) { |
| | | throw new IllegalArgumentException("è¯¥å¹´ä»½åæ¥æåç±»ç客è¿ç³»æ°å·²åå¨"); |
| | | } |
| | | |
| | | HolidayPassengerCoefficient entity = id == null ? new HolidayPassengerCoefficient() : coefficientMapper.selectById(id); |
| | | if (entity == null) { |
| | | throw new IllegalArgumentException("客è¿ç³»æ°è®°å½ä¸åå¨"); |
| | | } |
| | | if (id == null) { |
| | | entity.setCreatedAt(LocalDateTime.now()); |
| | | } |
| | | entity.setYear(year); |
| | | entity.setDateCategory(dateCategory); |
| | | entity.setCoefficient(coefficient); |
| | | entity.setUpdatedAt(LocalDateTime.now()); |
| | | if (id == null) { |
| | | coefficientMapper.insert(entity); |
| | | } else { |
| | | coefficientMapper.updateById(entity); |
| | | } |
| | | return entity; |
| | | } |
| | | |
| | | @Transactional |
| | | public void deleteCoefficient(Long id) { |
| | | if (id == null) { |
| | | throw new IllegalArgumentException("缺å°è®°å½ id"); |
| | | } |
| | | coefficientMapper.deleteById(id); |
| | | } |
| | | |
| | | private Long longValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return Long.valueOf(value.toString()); |
| | | } |
| | | |
| | | private Integer intValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return Integer.valueOf(value.toString()); |
| | | } |
| | | |
| | | private BigDecimal decimalValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return com.trafficaudit.holiday.HolidayNumberUtils.decimalValue(value); |
| | | } |
| | | |
| | | private String stringValue(Object value) { |
| | | return value == null ? null : value.toString().trim(); |
| | | } |
| | | |
| | | private LocalDate dateValue(Object value) { |
| | | if (value == null || value.toString().trim().isEmpty()) return null; |
| | | return LocalDate.parse(value.toString()); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import org.springframework.boot.ApplicationArguments; |
| | | import org.springframework.boot.ApplicationRunner; |
| | | import org.springframework.core.annotation.Order; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import javax.annotation.Resource; |
| | | |
| | | @Component |
| | | @Order(100) |
| | | public class HolidayReferenceDataInitializer implements ApplicationRunner { |
| | | |
| | | @Resource |
| | | private HolidayWorkbookService workbookService; |
| | | |
| | | @Override |
| | | public void run(ApplicationArguments args) { |
| | | workbookService.seedReferenceDataIfNeeded(); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.trafficaudit.dataimport.entity.ImportBatch; |
| | | import com.trafficaudit.dataimport.mapper.ImportBatchMapper; |
| | | import com.trafficaudit.holiday.HolidayConstants; |
| | | import com.trafficaudit.holiday.HolidayNumberUtils; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerFlow; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleFlow; |
| | | import com.trafficaudit.holiday.mapper.HolidayPassengerFlowMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper; |
| | | import lombok.Data; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Service |
| | | public class HolidayStatService { |
| | | |
| | | @Resource |
| | | private HolidayConfigService configService; |
| | | @Resource |
| | | private HolidayWorkbookService workbookService; |
| | | @Resource |
| | | private HolidayPassengerFlowMapper passengerFlowMapper; |
| | | @Resource |
| | | private HolidayVehicleFlowMapper vehicleFlowMapper; |
| | | @Resource |
| | | private ImportBatchMapper importBatchMapper; |
| | | |
| | | public Map<String, Object> overview(int year, String holidayType) { |
| | | List<HolidayCalendar> calendar = configService.findHolidayDays(year, holidayType); |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("year", year); |
| | | result.put("holidayType", holidayType); |
| | | result.put("holidayTypeLabel", HolidayConstants.HOLIDAY_TYPE_LABELS.get(holidayType)); |
| | | result.put("days", calendar); |
| | | result.put("rows", buildOverviewRows(year, holidayType, calendar)); |
| | | result.put("templateFileName", workbookService.referenceFileName()); |
| | | return result; |
| | | } |
| | | |
| | | @Transactional |
| | | public Map<String, Object> importWorkbook(MultipartFile file, int year, String holidayType, boolean allowHistoricalOverwrite) { |
| | | HolidayWorkbookService.ImportSummary summary = workbookService.importWorkbook(file, year, holidayType, allowHistoricalOverwrite); |
| | | if (summary.isConfirmationRequired()) { |
| | | return importResult(summary); |
| | | } |
| | | ImportBatch batch = new ImportBatch(); |
| | | batch.setFileName(file == null ? null : file.getOriginalFilename()); |
| | | batch.setImportType("HOLIDAY_FLOW"); |
| | | batch.setReportPeriod(year + "-" + holidayType); |
| | | batch.setTotalRows(summary.getSuccessRows() + summary.getFailRows()); |
| | | batch.setSuccessRows(summary.getSuccessRows()); |
| | | batch.setFailRows(summary.getFailRows()); |
| | | batch.setErrorDetail(joinErrors(summary.getErrors())); |
| | | batch.setCreatedAt(LocalDateTime.now()); |
| | | importBatchMapper.insert(batch); |
| | | |
| | | return importResult(summary); |
| | | } |
| | | |
| | | private Map<String, Object> importResult(HolidayWorkbookService.ImportSummary summary) { |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("passengerRows", summary.getPassengerRows()); |
| | | result.put("vehicleRows", summary.getVehicleRows()); |
| | | result.put("skippedRows", summary.getSkippedRows()); |
| | | result.put("successRows", summary.getSuccessRows()); |
| | | result.put("failRows", summary.getFailRows()); |
| | | result.put("errors", summary.getErrors()); |
| | | result.put("confirmationRequired", summary.isConfirmationRequired()); |
| | | result.put("historicalChanges", summary.getHistoricalChanges()); |
| | | return result; |
| | | } |
| | | |
| | | public byte[] exportWorkbook(int year, String holidayType) { |
| | | return workbookService.exportWorkbook(year, holidayType); |
| | | } |
| | | |
| | | public List<Map<String, Object>> buildOverviewRows(int year, String holidayType, List<HolidayCalendar> calendar) { |
| | | Map<LocalDate, Map<String, BigDecimal>> passengerByDate = loadPassengerByDate(year); |
| | | Map<LocalDate, Map<String, BigDecimal>> vehicleByDate = loadVehicleByDate(year); |
| | | Map<LocalDate, BigDecimal> comparisonBaseByDate = loadComparisonBaseByDate(year); |
| | | BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY); |
| | | List<Map<String, Object>> rows = new ArrayList<>(); |
| | | BigDecimal previousNationalProvincial = null; |
| | | for (HolidayCalendar day : calendar) { |
| | | Map<String, BigDecimal> passenger = passengerByDate.getOrDefault(day.getHolidayDate(), Collections.emptyMap()); |
| | | Map<String, BigDecimal> vehicle = vehicleByDate.getOrDefault(day.getHolidayDate(), Collections.emptyMap()); |
| | | BigDecimal expressway = vehicle.get(HolidayConstants.ROAD_EXPRESSWAY); |
| | | BigDecimal nationalProvincial = vehicle.get(HolidayConstants.ROAD_NATIONAL_PROVINCIAL); |
| | | BigDecimal comparisonBase = comparisonBaseByDate.get(day.getHolidayDate()); |
| | | BigDecimal nationalProvincialRingRatio = nationalProvincialRingRatio( |
| | | year, nationalProvincial, previousNationalProvincial, comparisonBase); |
| | | BigDecimal nonBusinessPersonFlow = null; |
| | | if (coefficient != null && (expressway != null || nationalProvincial != null)) { |
| | | nonBusinessPersonFlow = HolidayNumberUtils.multiply( |
| | | HolidayNumberUtils.sumNullable(expressway, nationalProvincial), coefficient); |
| | | } |
| | | Map<String, Object> row = new LinkedHashMap<>(); |
| | | row.put("dayIndex", day.getDayIndex()); |
| | | row.put("date", day.getHolidayDate()); |
| | | row.put("rail", passenger.get(HolidayConstants.MODE_RAIL)); |
| | | row.put("civilAviation", passenger.get(HolidayConstants.MODE_CIVIL_AVIATION)); |
| | | row.put("road", passenger.get(HolidayConstants.MODE_ROAD)); |
| | | row.put("water", passenger.get(HolidayConstants.MODE_WATER)); |
| | | row.put("expressway", expressway); |
| | | row.put("nationalProvincial", nationalProvincial); |
| | | row.put("comparisonBase", comparisonBase); |
| | | row.put("nationalProvincialRingRatio", nationalProvincialRingRatio); |
| | | row.put("coefficient", coefficient); |
| | | row.put("nonBusinessPersonFlow", nonBusinessPersonFlow); |
| | | row.put("totalPersonFlow", totalPersonFlow(passenger, nonBusinessPersonFlow)); |
| | | rows.add(row); |
| | | previousNationalProvincial = nationalProvincial; |
| | | } |
| | | return rows; |
| | | } |
| | | |
| | | public DailyMetrics dailyMetrics(int year, String holidayType, int dayIndex) { |
| | | List<HolidayCalendar> calendar = configService.findHolidayDays(year, holidayType); |
| | | HolidayCalendar target = null; |
| | | for (HolidayCalendar day : calendar) { |
| | | if (day.getDayIndex() != null && day.getDayIndex() == dayIndex) { |
| | | target = day; |
| | | break; |
| | | } |
| | | } |
| | | if (target == null) return null; |
| | | |
| | | Map<LocalDate, Map<String, BigDecimal>> passengerByDate = loadPassengerByDate(year); |
| | | Map<LocalDate, Map<String, BigDecimal>> vehicleByDate = loadVehicleByDate(year); |
| | | Map<LocalDate, BigDecimal> comparisonBaseByDate = loadComparisonBaseByDate(year); |
| | | Map<String, BigDecimal> passenger = passengerByDate.getOrDefault(target.getHolidayDate(), Collections.emptyMap()); |
| | | Map<String, BigDecimal> vehicle = vehicleByDate.getOrDefault(target.getHolidayDate(), Collections.emptyMap()); |
| | | BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY); |
| | | BigDecimal expressway = vehicle.get(HolidayConstants.ROAD_EXPRESSWAY); |
| | | BigDecimal nationalProvincial = vehicle.get(HolidayConstants.ROAD_NATIONAL_PROVINCIAL); |
| | | BigDecimal comparisonBase = comparisonBaseByDate.get(target.getHolidayDate()); |
| | | BigDecimal nonBusiness = null; |
| | | if (coefficient != null && (expressway != null || nationalProvincial != null)) { |
| | | nonBusiness = HolidayNumberUtils.multiply( |
| | | HolidayNumberUtils.sumNullable(expressway, nationalProvincial), coefficient); |
| | | } |
| | | |
| | | DailyMetrics metrics = new DailyMetrics(); |
| | | metrics.setYear(year); |
| | | metrics.setHolidayType(holidayType); |
| | | metrics.setDayIndex(dayIndex); |
| | | metrics.setDate(target.getHolidayDate()); |
| | | metrics.setRail(passenger.get(HolidayConstants.MODE_RAIL)); |
| | | metrics.setCivilAviation(passenger.get(HolidayConstants.MODE_CIVIL_AVIATION)); |
| | | metrics.setRoad(passenger.get(HolidayConstants.MODE_ROAD)); |
| | | metrics.setWater(passenger.get(HolidayConstants.MODE_WATER)); |
| | | metrics.setExpressway(expressway); |
| | | metrics.setNationalProvincial(nationalProvincial); |
| | | metrics.setComparisonBase(comparisonBase); |
| | | metrics.setCoefficient(coefficient); |
| | | metrics.setNonBusinessPersonFlow(nonBusiness); |
| | | metrics.setRoadPersonFlow(sumNullable(metrics.getRoad(), nonBusiness)); |
| | | metrics.setTotalPersonFlow(totalPersonFlow(passenger, nonBusiness)); |
| | | return metrics; |
| | | } |
| | | |
| | | public DailyMetrics cumulativeMetrics(int year, String holidayType, int dayIndex) { |
| | | DailyMetrics cumulative = new DailyMetrics(); |
| | | cumulative.setYear(year); |
| | | cumulative.setHolidayType(holidayType); |
| | | cumulative.setDayIndex(dayIndex); |
| | | for (int day = 1; day <= dayIndex; day++) { |
| | | DailyMetrics item = dailyMetrics(year, holidayType, day); |
| | | if (item == null) continue; |
| | | cumulative.setRail(sumNullable(cumulative.getRail(), item.getRail())); |
| | | cumulative.setCivilAviation(sumNullable(cumulative.getCivilAviation(), item.getCivilAviation())); |
| | | cumulative.setRoad(sumNullable(cumulative.getRoad(), item.getRoad())); |
| | | cumulative.setWater(sumNullable(cumulative.getWater(), item.getWater())); |
| | | cumulative.setExpressway(sumNullable(cumulative.getExpressway(), item.getExpressway())); |
| | | cumulative.setNationalProvincial(sumNullable(cumulative.getNationalProvincial(), item.getNationalProvincial())); |
| | | cumulative.setNonBusinessPersonFlow(sumNullable(cumulative.getNonBusinessPersonFlow(), item.getNonBusinessPersonFlow())); |
| | | cumulative.setRoadPersonFlow(sumNullable(cumulative.getRoadPersonFlow(), item.getRoadPersonFlow())); |
| | | cumulative.setTotalPersonFlow(sumNullable(cumulative.getTotalPersonFlow(), item.getTotalPersonFlow())); |
| | | } |
| | | return cumulative; |
| | | } |
| | | |
| | | |
| | | public DailyMetrics findMetricsByDate(LocalDate date) { |
| | | HolidayCalendar calendar = configService.findHolidayByDate(date); |
| | | if (calendar == null) return null; |
| | | return dailyMetrics(calendar.getYear(), calendar.getHolidayType(), calendar.getDayIndex()); |
| | | } |
| | | |
| | | private Map<LocalDate, Map<String, BigDecimal>> loadPassengerByDate(int year) { |
| | | Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>(); |
| | | List<HolidayPassengerFlow> rows = passengerFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayPassengerFlow>().eq(HolidayPassengerFlow::getYear, year)); |
| | | for (HolidayPassengerFlow row : rows) { |
| | | result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>()) |
| | | .put(row.getTransportMode(), row.getPassengerCount()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, Map<String, BigDecimal>> loadVehicleByDate(int year) { |
| | | Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>(); |
| | | List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>().eq(HolidayVehicleFlow::getYear, year)); |
| | | for (HolidayVehicleFlow row : rows) { |
| | | result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>()) |
| | | .put(row.getRoadCategory(), row.getVehicleCount()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, BigDecimal> loadComparisonBaseByDate(int year) { |
| | | Map<LocalDate, BigDecimal> result = new LinkedHashMap<>(); |
| | | List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getYear, year) |
| | | .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL)); |
| | | for (HolidayVehicleFlow row : rows) { |
| | | if (row.getComparisonBase() != null) result.put(row.getFlowDate(), row.getComparisonBase()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private BigDecimal totalPersonFlow(Map<String, BigDecimal> passenger, BigDecimal nonBusinessPersonFlow) { |
| | | BigDecimal total = nonBusinessPersonFlow; |
| | | total = sumNullable(total, passenger.get(HolidayConstants.MODE_ROAD)); |
| | | total = sumNullable(total, passenger.get(HolidayConstants.MODE_RAIL)); |
| | | total = sumNullable(total, passenger.get(HolidayConstants.MODE_CIVIL_AVIATION)); |
| | | total = sumNullable(total, passenger.get(HolidayConstants.MODE_WATER)); |
| | | return total; |
| | | } |
| | | |
| | | private BigDecimal nationalProvincialRingRatio(int year, BigDecimal current, |
| | | BigDecimal previous, BigDecimal comparisonBase) { |
| | | if (current == null) return null; |
| | | if (HolidayConstants.supportsComparisonBase(year) && comparisonBase != null) { |
| | | return ratio(current, comparisonBase); |
| | | } |
| | | return previous == null ? null : ratio(current, previous); |
| | | } |
| | | |
| | | private BigDecimal ratio(BigDecimal current, BigDecimal base) { |
| | | BigDecimal quotient = HolidayNumberUtils.divide(current, base); |
| | | return quotient == null ? null : HolidayNumberUtils.normalize(quotient.subtract(BigDecimal.ONE)); |
| | | } |
| | | |
| | | private BigDecimal sumNullable(BigDecimal left, BigDecimal right) { |
| | | return HolidayNumberUtils.sumNullable(left, right); |
| | | } |
| | | |
| | | private String joinErrors(List<String> errors) { |
| | | if (errors == null || errors.isEmpty()) return null; |
| | | StringBuilder builder = new StringBuilder(); |
| | | int limit = Math.min(errors.size(), 200); |
| | | for (int i = 0; i < limit; i++) { |
| | | if (i > 0) builder.append('\n'); |
| | | builder.append(errors.get(i)); |
| | | } |
| | | if (errors.size() > limit) { |
| | | builder.append("\n... ").append(errors.size() - limit).append(" more"); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | @Data |
| | | public static class DailyMetrics { |
| | | private int year; |
| | | private String holidayType; |
| | | private int dayIndex; |
| | | private LocalDate date; |
| | | private BigDecimal rail; |
| | | private BigDecimal civilAviation; |
| | | private BigDecimal road; |
| | | private BigDecimal water; |
| | | private BigDecimal expressway; |
| | | private BigDecimal nationalProvincial; |
| | | private BigDecimal comparisonBase; |
| | | private BigDecimal coefficient; |
| | | private BigDecimal nonBusinessPersonFlow; |
| | | private BigDecimal roadPersonFlow; |
| | | private BigDecimal totalPersonFlow; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.trafficaudit.holiday.HolidayConstants; |
| | | import com.trafficaudit.holiday.HolidayNumberUtils; |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerCoefficient; |
| | | import com.trafficaudit.holiday.entity.HolidayPassengerFlow; |
| | | import com.trafficaudit.holiday.entity.HolidayVehicleFlow; |
| | | import com.trafficaudit.holiday.mapper.HolidayPassengerCoefficientMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayPassengerFlowMapper; |
| | | import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper; |
| | | import lombok.Data; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.poi.ss.usermodel.Cell; |
| | | import org.apache.poi.ss.usermodel.CellType; |
| | | import org.apache.poi.ss.usermodel.DataFormatter; |
| | | import org.apache.poi.ss.usermodel.FormulaEvaluator; |
| | | import org.apache.poi.ss.usermodel.Row; |
| | | import org.apache.poi.ss.usermodel.Sheet; |
| | | import org.apache.poi.ss.usermodel.Workbook; |
| | | import org.apache.poi.ss.usermodel.WorkbookFactory; |
| | | import org.apache.poi.xssf.usermodel.XSSFWorkbook; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.InputStream; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.format.DateTimeParseException; |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @Slf4j |
| | | @Service |
| | | public class HolidayWorkbookService { |
| | | |
| | | private static final Map<String, String> PASSENGER_SHEET_MODE = new LinkedHashMap<>(); |
| | | private static final int FIRST_DATA_ROW_INDEX = 2; |
| | | private static final int[] REFERENCE_YEARS = {2024, 2025, 2026}; |
| | | |
| | | static { |
| | | PASSENGER_SHEET_MODE.put("éè·¯", HolidayConstants.MODE_RAIL); |
| | | PASSENGER_SHEET_MODE.put("æ°èª", HolidayConstants.MODE_CIVIL_AVIATION); |
| | | PASSENGER_SHEET_MODE.put("éè·¯", HolidayConstants.MODE_ROAD); |
| | | PASSENGER_SHEET_MODE.put("æ°´è·¯", HolidayConstants.MODE_WATER); |
| | | } |
| | | |
| | | @Resource |
| | | private HolidayConfigService configService; |
| | | @Resource |
| | | private HolidayPassengerFlowMapper passengerFlowMapper; |
| | | @Resource |
| | | private HolidayVehicleFlowMapper vehicleFlowMapper; |
| | | @Resource |
| | | private HolidayPassengerCoefficientMapper coefficientMapper; |
| | | |
| | | @Value("${holiday.reference-path:docs/ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´å䏿°æ®.xlsx}") |
| | | private String referencePath; |
| | | |
| | | public File resolveReferenceFile() { |
| | | List<File> candidates = new ArrayList<>(); |
| | | File configured = new File(referencePath); |
| | | candidates.add(configured); |
| | | if (!configured.isAbsolute()) { |
| | | candidates.add(new File(System.getProperty("user.dir"), referencePath)); |
| | | candidates.add(new File(System.getProperty("user.dir"), "../" + referencePath)); |
| | | } |
| | | for (File candidate : candidates) { |
| | | try { |
| | | if (candidate.exists() && candidate.isFile()) { |
| | | return candidate.getCanonicalFile(); |
| | | } |
| | | } catch (Exception ignored) { |
| | | // Keep checking the remaining candidate paths. |
| | | } |
| | | } |
| | | return configured; |
| | | } |
| | | |
| | | public String referenceFileName() { |
| | | return resolveReferenceFile().getName(); |
| | | } |
| | | |
| | | @Transactional |
| | | public ImportSummary importWorkbook(MultipartFile file, int year, String holidayType, boolean allowHistoricalOverwrite) { |
| | | if (file == null || file.isEmpty()) { |
| | | throw new IllegalArgumentException("è¯·éæ©è¦å¯¼å
¥ç Excel æä»¶"); |
| | | } |
| | | List<HolidayCalendar> holidayDays = configService.findHolidayDays(year, holidayType); |
| | | if (holidayDays.isEmpty()) { |
| | | throw new IllegalArgumentException("æé年份åè忥尿ªé
ç½®æ¥æï¼è¯·å
å¨ç³»ç»é
ç½®ä¸ç»´æ¤è忥"); |
| | | } |
| | | Map<Integer, LocalDate> dateByDay = new LinkedHashMap<>(); |
| | | for (HolidayCalendar day : holidayDays) { |
| | | dateByDay.put(day.getDayIndex(), day.getHolidayDate()); |
| | | } |
| | | |
| | | if (!allowHistoricalOverwrite) { |
| | | ImportSummary historicalSummary = detectHistoricalChanges(file, year, dateByDay); |
| | | if (historicalSummary.isConfirmationRequired()) { |
| | | return historicalSummary; |
| | | } |
| | | } |
| | | |
| | | ImportSummary summary = new ImportSummary(); |
| | | try (InputStream in = file.getInputStream(); Workbook workbook = WorkbookFactory.create(in)) { |
| | | FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); |
| | | importPassengerSheets(workbook, evaluator, year, dateByDay, summary); |
| | | importVehicleSheets(workbook, evaluator, year, dateByDay, summary); |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("å·¥ä½ç°¿è§£æå¤±è´¥: " + e.getMessage(), e); |
| | | } |
| | | return summary; |
| | | } |
| | | |
| | | private ImportSummary detectHistoricalChanges(MultipartFile file, int year, Map<Integer, LocalDate> dateByDay) { |
| | | ImportSummary summary = new ImportSummary(); |
| | | try (InputStream in = file.getInputStream(); Workbook workbook = WorkbookFactory.create(in)) { |
| | | FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); |
| | | int latestDay = findLatestIncomingDay(workbook, evaluator, year, dateByDay); |
| | | if (latestDay <= 1) { |
| | | return summary; |
| | | } |
| | | Map<LocalDate, Map<String, BigDecimal>> existingPassenger = loadPassengerByDate(year); |
| | | Map<LocalDate, Map<String, BigDecimal>> existingVehicle = loadVehicleByDate(year); |
| | | detectPassengerHistory(workbook, evaluator, year, dateByDay, latestDay, existingPassenger, summary); |
| | | detectVehicleHistory(workbook, evaluator, year, dateByDay, latestDay, existingVehicle, summary); |
| | | if (!summary.getHistoricalChanges().isEmpty()) { |
| | | summary.setConfirmationRequired(true); |
| | | } |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("å·¥ä½ç°¿è§£æå¤±è´¥: " + e.getMessage(), e); |
| | | } |
| | | return summary; |
| | | } |
| | | |
| | | private int findLatestIncomingDay(Workbook workbook, FormulaEvaluator evaluator, int year, Map<Integer, LocalDate> dateByDay) { |
| | | int latest = 0; |
| | | for (Integer dayIndex : dateByDay.keySet()) { |
| | | if (hasPassengerValue(workbook, evaluator, year, dayIndex) |
| | | || hasVehicleValue(workbook, evaluator, year, dayIndex)) { |
| | | latest = Math.max(latest, dayIndex); |
| | | } |
| | | } |
| | | return latest; |
| | | } |
| | | |
| | | private boolean hasPassengerValue(Workbook workbook, FormulaEvaluator evaluator, int year, int dayIndex) { |
| | | for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) { |
| | | Sheet sheet = workbook.getSheet(entry.getKey()); |
| | | if (sheet == null) continue; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7); |
| | | if (yearColumn == null) continue; |
| | | Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + dayIndex - 1); |
| | | if (row == null) continue; |
| | | BigDecimal value = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (value != null) return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private boolean hasVehicleValue(Workbook workbook, FormulaEvaluator evaluator, int year, int dayIndex) { |
| | | Sheet expressway = workbook.getSheet("é«éå
¬è·¯"); |
| | | if (expressway != null) { |
| | | Integer yearColumn = findYearColumn(expressway, evaluator, year, 2, 7); |
| | | if (yearColumn != null) { |
| | | Row row = expressway.getRow(FIRST_DATA_ROW_INDEX + dayIndex - 1); |
| | | if (row != null && numberValue(row.getCell(yearColumn), evaluator) != null) return true; |
| | | } |
| | | } |
| | | Sheet nationalProvincial = workbook.getSheet("æ®éå½çé"); |
| | | if (nationalProvincial != null) { |
| | | Integer yearColumn = findYearColumn(nationalProvincial, evaluator, year, 3, 8); |
| | | if (yearColumn != null) { |
| | | Row row = nationalProvincial.getRow(FIRST_DATA_ROW_INDEX + dayIndex - 1); |
| | | if (row != null && numberValue(row.getCell(yearColumn), evaluator) != null) return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private void detectPassengerHistory(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, int latestDay, |
| | | Map<LocalDate, Map<String, BigDecimal>> existing, |
| | | ImportSummary summary) { |
| | | for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) { |
| | | Sheet sheet = workbook.getSheet(entry.getKey()); |
| | | if (sheet == null) continue; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7); |
| | | if (yearColumn == null) continue; |
| | | for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) { |
| | | if (day.getKey() >= latestDay) continue; |
| | | Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1); |
| | | if (row == null) continue; |
| | | BigDecimal incoming = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (incoming == null) continue; |
| | | Map<String, BigDecimal> existingDay = existing.get(day.getValue()); |
| | | BigDecimal oldValue = existingDay == null ? null : existingDay.get(entry.getValue()); |
| | | if (oldValue != null && !sameValue(oldValue, incoming)) { |
| | | summary.addHistoricalChange("第" + day.getKey() + "æ¥ " + day.getValue() + " " |
| | | + entry.getKey() + "ï¼åå¼ " + formatChangeValue(oldValue) + "ï¼å¯¼å
¥å¼ " + formatChangeValue(incoming)); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void detectVehicleHistory(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, int latestDay, |
| | | Map<LocalDate, Map<String, BigDecimal>> existing, |
| | | ImportSummary summary) { |
| | | detectVehicleSheetHistory(workbook, evaluator, year, dateByDay, latestDay, existing, summary, |
| | | "é«éå
¬è·¯", HolidayConstants.ROAD_EXPRESSWAY, 2, 7); |
| | | detectVehicleSheetHistory(workbook, evaluator, year, dateByDay, latestDay, existing, summary, |
| | | "æ®éå½çé", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, 3, 8); |
| | | detectComparisonBaseHistory(workbook, evaluator, year, dateByDay, latestDay, existing, summary); |
| | | } |
| | | |
| | | private void detectVehicleSheetHistory(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, int latestDay, |
| | | Map<LocalDate, Map<String, BigDecimal>> existing, |
| | | ImportSummary summary, String sheetName, String roadCategory, |
| | | int startColumn, int endColumn) { |
| | | Sheet sheet = workbook.getSheet(sheetName); |
| | | if (sheet == null) return; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn); |
| | | if (yearColumn == null) return; |
| | | for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) { |
| | | if (day.getKey() >= latestDay) continue; |
| | | Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1); |
| | | if (row == null) continue; |
| | | BigDecimal incoming = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (incoming == null) continue; |
| | | Map<String, BigDecimal> existingDay = existing.get(day.getValue()); |
| | | BigDecimal oldValue = existingDay == null ? null : existingDay.get(roadCategory); |
| | | if (oldValue != null && !sameValue(oldValue, incoming)) { |
| | | summary.addHistoricalChange("第" + day.getKey() + "æ¥ " + day.getValue() + " " |
| | | + sheetName + "ï¼åå¼ " + formatChangeValue(oldValue) + "ï¼å¯¼å
¥å¼ " + formatChangeValue(incoming)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void detectComparisonBaseHistory(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, int latestDay, |
| | | Map<LocalDate, Map<String, BigDecimal>> existingVehicle, |
| | | ImportSummary summary) { |
| | | if (!HolidayConstants.supportsComparisonBase(year)) return; |
| | | Sheet sheet = workbook.getSheet("æ®éå½çé"); |
| | | if (sheet == null) return; |
| | | Map<LocalDate, BigDecimal> existingBase = loadComparisonBaseByDate(year); |
| | | for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) { |
| | | if (day.getKey() >= latestDay) continue; |
| | | Map<String, BigDecimal> existingDay = existingVehicle.get(day.getValue()); |
| | | if (existingDay == null || existingDay.get(HolidayConstants.ROAD_NATIONAL_PROVINCIAL) == null) continue; |
| | | Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1); |
| | | if (row == null) continue; |
| | | BigDecimal incoming; |
| | | try { |
| | | incoming = numberValue(row.getCell(2), evaluator); |
| | | } catch (Exception e) { |
| | | summary.addError("æ®éå½çé 第" + (row.getRowNum() + 1) + "è¡ç¯æ¯åºæï¼" + e.getMessage()); |
| | | continue; |
| | | } |
| | | BigDecimal oldValue = existingBase.get(day.getValue()); |
| | | if (!sameNullableValue(oldValue, incoming)) { |
| | | summary.addHistoricalChange("第" + day.getKey() + "æ¥ " + day.getValue() + " æ®éå½çéç¯æ¯åºæï¼åå¼ " |
| | | + nullableChangeValue(oldValue) + "ï¼å¯¼å
¥å¼ " + nullableChangeValue(incoming)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>(); |
| | | private Map<LocalDate, Map<String, BigDecimal>> loadPassengerByDate(int year) { |
| | | List<HolidayPassengerFlow> rows = passengerFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayPassengerFlow>().eq(HolidayPassengerFlow::getYear, year)); |
| | | for (HolidayPassengerFlow row : rows) { |
| | | result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>()) |
| | | .put(row.getTransportMode(), row.getPassengerCount()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, Map<String, BigDecimal>> loadVehicleByDate(int year) { |
| | | Map<LocalDate, Map<String, BigDecimal>> result = new LinkedHashMap<>(); |
| | | List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>().eq(HolidayVehicleFlow::getYear, year)); |
| | | for (HolidayVehicleFlow row : rows) { |
| | | result.computeIfAbsent(row.getFlowDate(), key -> new LinkedHashMap<>()) |
| | | .put(row.getRoadCategory(), row.getVehicleCount()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, BigDecimal> loadComparisonBaseByDate(int year) { |
| | | Map<LocalDate, BigDecimal> result = new LinkedHashMap<>(); |
| | | List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getYear, year) |
| | | .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL)); |
| | | for (HolidayVehicleFlow row : rows) result.put(row.getFlowDate(), row.getComparisonBase()); |
| | | return result; |
| | | } |
| | | |
| | | private boolean sameValue(BigDecimal left, BigDecimal right) { |
| | | return HolidayNumberUtils.sameValue(left, right); |
| | | } |
| | | |
| | | private boolean sameNullableValue(BigDecimal left, BigDecimal right) { |
| | | if (left == null || right == null) return left == right; |
| | | return sameValue(left, right); |
| | | } |
| | | |
| | | private String nullableChangeValue(BigDecimal value) { |
| | | return value == null ? "空" : formatChangeValue(value); |
| | | } |
| | | |
| | | private String formatChangeValue(BigDecimal value) { |
| | | return HolidayNumberUtils.changeText(value); |
| | | } |
| | | |
| | | @Transactional |
| | | public void seedReferenceDataIfNeeded() { |
| | | File reference = resolveReferenceFile(); |
| | | if (!reference.exists() || !reference.isFile()) { |
| | | log.warn("Holiday reference workbook not found: {}", reference.getAbsolutePath()); |
| | | return; |
| | | } |
| | | try (InputStream in = new FileInputStream(reference); Workbook workbook = WorkbookFactory.create(in)) { |
| | | FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); |
| | | seedNationalDayCalendar(); |
| | | for (int year : REFERENCE_YEARS) { |
| | | if (passengerFlowMapper.selectCount(new LambdaQueryWrapper<HolidayPassengerFlow>() |
| | | .eq(HolidayPassengerFlow::getYear, year)) == 0) { |
| | | seedPassengerYear(workbook, evaluator, year); |
| | | } |
| | | if (vehicleFlowMapper.selectCount(new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getYear, year)) == 0) { |
| | | seedVehicleYear(workbook, evaluator, year); |
| | | } |
| | | if (coefficientMapper.selectCount(new LambdaQueryWrapper<HolidayPassengerCoefficient>() |
| | | .eq(HolidayPassengerCoefficient::getYear, year)) == 0) { |
| | | seedCoefficientYear(workbook, evaluator, year); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("Failed to seed holiday reference data", e); |
| | | } |
| | | } |
| | | |
| | | public byte[] exportWorkbook(int year, String holidayType) { |
| | | File reference = resolveReferenceFile(); |
| | | if (!reference.exists() || !reference.isFile()) { |
| | | throw new IllegalArgumentException("èåæ¥æ¨¡æ¿æä»¶ä¸åå¨: " + reference.getAbsolutePath()); |
| | | } |
| | | try (InputStream in = new FileInputStream(reference); XSSFWorkbook workbook = new XSSFWorkbook(in)) { |
| | | FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); |
| | | fillPassengerSheets(workbook, evaluator, year, holidayType); |
| | | fillVehicleSheets(workbook, evaluator, year, holidayType); |
| | | fillCoefficients(workbook, evaluator, year, holidayType); |
| | | workbook.setForceFormulaRecalculation(true); |
| | | try { |
| | | evaluator.evaluateAll(); |
| | | } catch (Exception e) { |
| | | log.warn("Workbook formula recalculation did not complete; Excel will recalculate on open", e); |
| | | } |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | workbook.write(out); |
| | | return out.toByteArray(); |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("èåæ¥æ°æ®å¯¼åºå¤±è´¥: " + e.getMessage(), e); |
| | | } |
| | | } |
| | | |
| | | private void importPassengerSheets(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, ImportSummary summary) { |
| | | for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) { |
| | | Sheet sheet = workbook.getSheet(entry.getKey()); |
| | | if (sheet == null) { |
| | | summary.addError(entry.getKey() + "ï¼æ¾ä¸å°å·¥ä½è¡¨"); |
| | | continue; |
| | | } |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7); |
| | | if (yearColumn == null) { |
| | | summary.addError(entry.getKey() + "ï¼æ¾ä¸å° " + year + " å¹´æ°æ®å"); |
| | | continue; |
| | | } |
| | | for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) { |
| | | Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1); |
| | | if (row == null) { |
| | | summary.addError(entry.getKey() + " 第" + day.getKey() + "æ¥ï¼å·¥ä½è¡¨ç¼ºå°è¯¥è¡"); |
| | | continue; |
| | | } |
| | | Integer rowDay = parseDayIndex(row.getCell(0), evaluator); |
| | | if (rowDay == null || !rowDay.equals(day.getKey())) { |
| | | summary.addError(entry.getKey() + " 第" + (row.getRowNum() + 1) + "è¡ï¼ç¬¬å æ¥ä¸è忥é
ç½®ä¸ä¸è´"); |
| | | continue; |
| | | } |
| | | BigDecimal value; |
| | | try { |
| | | value = numberValue(row.getCell(yearColumn), evaluator); |
| | | } catch (Exception e) { |
| | | summary.addError(entry.getKey() + " 第" + (row.getRowNum() + 1) + "è¡ï¼" + e.getMessage()); |
| | | continue; |
| | | } |
| | | if (value == null) { |
| | | summary.incrementSkipped(); |
| | | continue; |
| | | } |
| | | upsertPassenger(year, day.getValue(), entry.getValue(), value); |
| | | summary.incrementPassenger(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void importVehicleSheets(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, ImportSummary summary) { |
| | | importVehicleSheet(workbook, evaluator, year, dateByDay, summary, |
| | | "é«éå
¬è·¯", HolidayConstants.ROAD_EXPRESSWAY, HolidayConstants.VEHICLE_SMALL_PASSENGER, 2, 7, null); |
| | | importVehicleSheet(workbook, evaluator, year, dateByDay, summary, |
| | | "æ®éå½çé", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER, 3, 8, |
| | | HolidayConstants.supportsComparisonBase(year) ? 2 : null); |
| | | } |
| | | |
| | | private void importVehicleSheet(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dateByDay, ImportSummary summary, |
| | | String sheetName, String roadCategory, String vehicleType, |
| | | int startColumn, int endColumn, Integer comparisonBaseColumn) { |
| | | Sheet sheet = workbook.getSheet(sheetName); |
| | | if (sheet == null) { |
| | | summary.addError(sheetName + "ï¼æ¾ä¸å°å·¥ä½è¡¨"); |
| | | return; |
| | | } |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn); |
| | | if (yearColumn == null) { |
| | | summary.addError(sheetName + "ï¼æ¾ä¸å° " + year + " å¹´æ°æ®å"); |
| | | return; |
| | | } |
| | | for (Map.Entry<Integer, LocalDate> day : dateByDay.entrySet()) { |
| | | Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1); |
| | | if (row == null) { |
| | | summary.addError(sheetName + " 第" + day.getKey() + "æ¥ï¼å·¥ä½è¡¨ç¼ºå°è¯¥è¡"); |
| | | continue; |
| | | } |
| | | Integer rowDay = parseDayIndex(row.getCell(0), evaluator); |
| | | if (rowDay == null || !rowDay.equals(day.getKey())) { |
| | | summary.addError(sheetName + " 第" + (row.getRowNum() + 1) + "è¡ï¼ç¬¬å æ¥ä¸è忥é
ç½®ä¸ä¸è´"); |
| | | continue; |
| | | } |
| | | BigDecimal value; |
| | | try { |
| | | value = numberValue(row.getCell(yearColumn), evaluator); |
| | | } catch (Exception e) { |
| | | summary.addError(sheetName + " 第" + (row.getRowNum() + 1) + "è¡ï¼" + e.getMessage()); |
| | | continue; |
| | | } |
| | | if (value == null) { |
| | | summary.incrementSkipped(); |
| | | continue; |
| | | } |
| | | BigDecimal comparisonBase = null; |
| | | if (comparisonBaseColumn != null) { |
| | | try { |
| | | comparisonBase = numberValue(row.getCell(comparisonBaseColumn), evaluator); |
| | | } catch (Exception e) { |
| | | summary.addError(sheetName + " 第" + (row.getRowNum() + 1) + "è¡ç¯æ¯åºæï¼" + e.getMessage()); |
| | | continue; |
| | | } |
| | | } |
| | | upsertVehicle(year, day.getValue(), roadCategory, vehicleType, value, comparisonBase); |
| | | summary.incrementVehicle(); |
| | | } |
| | | } |
| | | |
| | | private void fillPassengerSheets(XSSFWorkbook workbook, FormulaEvaluator evaluator, int selectedYear, String holidayType) { |
| | | for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) { |
| | | Sheet sheet = workbook.getSheet(entry.getKey()); |
| | | if (sheet == null) continue; |
| | | for (int year : REFERENCE_YEARS) { |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7); |
| | | if (yearColumn == null) continue; |
| | | Map<Integer, LocalDate> dayDates = dayDates(year, holidayType); |
| | | Map<LocalDate, BigDecimal> values = loadPassengerValues(year, entry.getValue()); |
| | | for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { |
| | | if (!values.containsKey(day.getValue())) continue; |
| | | setNumeric(sheet, FIRST_DATA_ROW_INDEX + day.getKey() - 1, yearColumn, values.get(day.getValue())); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void fillVehicleSheets(XSSFWorkbook workbook, FormulaEvaluator evaluator, int selectedYear, String holidayType) { |
| | | fillVehicleSheet(workbook, evaluator, "é«éå
¬è·¯", HolidayConstants.ROAD_EXPRESSWAY, 2, 7, holidayType, null); |
| | | fillVehicleSheet(workbook, evaluator, "æ®éå½çé", HolidayConstants.ROAD_NATIONAL_PROVINCIAL, 3, 8, holidayType, |
| | | HolidayConstants.supportsComparisonBase(selectedYear) ? 2 : null); |
| | | } |
| | | |
| | | private void fillVehicleSheet(XSSFWorkbook workbook, FormulaEvaluator evaluator, String sheetName, |
| | | String roadCategory, int startColumn, int endColumn, String holidayType, |
| | | Integer comparisonBaseColumn) { |
| | | Sheet sheet = workbook.getSheet(sheetName); |
| | | if (sheet == null) return; |
| | | for (int year : REFERENCE_YEARS) { |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn); |
| | | if (yearColumn == null) continue; |
| | | Map<Integer, LocalDate> dayDates = dayDates(year, holidayType); |
| | | Map<LocalDate, BigDecimal> values = loadVehicleValues(year, roadCategory); |
| | | for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { |
| | | if (!values.containsKey(day.getValue())) continue; |
| | | setNumeric(sheet, FIRST_DATA_ROW_INDEX + day.getKey() - 1, yearColumn, values.get(day.getValue())); |
| | | } |
| | | if (comparisonBaseColumn != null && HolidayConstants.supportsComparisonBase(year)) { |
| | | Map<LocalDate, BigDecimal> bases = loadComparisonBaseValues(year); |
| | | for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { |
| | | if (!bases.containsKey(day.getValue())) continue; |
| | | setNumeric(sheet, FIRST_DATA_ROW_INDEX + day.getKey() - 1, comparisonBaseColumn, bases.get(day.getValue())); |
| | | } |
| | | for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { |
| | | if (!bases.containsKey(day.getValue())) continue; |
| | | int rowIndex = FIRST_DATA_ROW_INDEX + day.getKey() - 1; |
| | | int excelRow = rowIndex + 1; |
| | | setFormula(sheet, rowIndex, 4, "=(D" + excelRow + "-C" + excelRow + ")/C" + excelRow); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void fillCoefficients(XSSFWorkbook workbook, FormulaEvaluator evaluator, int selectedYear, String holidayType) { |
| | | fillCoefficientSheet(workbook, evaluator, "é«éå
¬è·¯", 8, 10, holidayType); |
| | | fillCoefficientSheet(workbook, evaluator, "æ®éå½çé", 9, 11, holidayType); |
| | | } |
| | | |
| | | private void fillCoefficientSheet(XSSFWorkbook workbook, FormulaEvaluator evaluator, String sheetName, |
| | | int startColumn, int endColumn, String holidayType) { |
| | | Sheet sheet = workbook.getSheet(sheetName); |
| | | if (sheet == null) return; |
| | | for (int year : REFERENCE_YEARS) { |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn); |
| | | if (yearColumn == null) continue; |
| | | BigDecimal coefficient = configService.coefficientOf(year, HolidayConstants.CATEGORY_HOLIDAY); |
| | | if (coefficient == null) continue; |
| | | Map<Integer, LocalDate> dayDates = dayDates(year, holidayType); |
| | | for (Integer dayIndex : dayDates.keySet()) { |
| | | setNumeric(sheet, FIRST_DATA_ROW_INDEX + dayIndex - 1, yearColumn, coefficient); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void seedNationalDayCalendar() { |
| | | // Calendar is admin-managed after the first bootstrap. Re-adding missing day indexes on |
| | | // every start would resurrect days the admin deleted, so only seed a fresh table. |
| | | if (!configService.listHolidays(null, null).isEmpty()) { |
| | | return; |
| | | } |
| | | int[] lengths = {7, 8, 8}; |
| | | for (int i = 0; i < REFERENCE_YEARS.length; i++) { |
| | | int year = REFERENCE_YEARS[i]; |
| | | List<HolidayCalendar> existing = configService.findHolidayDays(year, HolidayConstants.TYPE_NATIONAL_DAY); |
| | | Map<Integer, HolidayCalendar> byDay = new LinkedHashMap<>(); |
| | | for (HolidayCalendar row : existing) { |
| | | byDay.put(row.getDayIndex(), row); |
| | | } |
| | | for (int day = 1; day <= lengths[i]; day++) { |
| | | if (byDay.containsKey(day)) continue; |
| | | HolidayCalendar calendar = new HolidayCalendar(); |
| | | calendar.setYear(year); |
| | | calendar.setHolidayType(HolidayConstants.TYPE_NATIONAL_DAY); |
| | | calendar.setHolidayDate(LocalDate.of(year, 10, day)); |
| | | calendar.setDayIndex(day); |
| | | calendar.setCreatedAt(LocalDateTime.now()); |
| | | calendar.setUpdatedAt(LocalDateTime.now()); |
| | | configService.saveHoliday(toMap(calendar)); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void seedPassengerYear(Workbook workbook, FormulaEvaluator evaluator, int year) { |
| | | Map<Integer, LocalDate> dayDates = dayDates(year, HolidayConstants.TYPE_NATIONAL_DAY); |
| | | if (dayDates.isEmpty()) return; |
| | | for (Map.Entry<String, String> entry : PASSENGER_SHEET_MODE.entrySet()) { |
| | | Sheet sheet = workbook.getSheet(entry.getKey()); |
| | | if (sheet == null) continue; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, 2, 7); |
| | | if (yearColumn == null) continue; |
| | | for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { |
| | | Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1); |
| | | if (row == null) continue; |
| | | BigDecimal value = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (value != null) { |
| | | upsertPassenger(year, day.getValue(), entry.getValue(), value); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void seedVehicleYear(Workbook workbook, FormulaEvaluator evaluator, int year) { |
| | | Map<Integer, LocalDate> dayDates = dayDates(year, HolidayConstants.TYPE_NATIONAL_DAY); |
| | | if (dayDates.isEmpty()) return; |
| | | seedVehicleSheet(workbook, evaluator, year, dayDates, "é«éå
¬è·¯", |
| | | HolidayConstants.ROAD_EXPRESSWAY, HolidayConstants.VEHICLE_SMALL_PASSENGER, 2, 7); |
| | | seedVehicleSheet(workbook, evaluator, year, dayDates, "æ®éå½çé", |
| | | HolidayConstants.ROAD_NATIONAL_PROVINCIAL, HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER, 3, 8); |
| | | } |
| | | |
| | | private void seedVehicleSheet(Workbook workbook, FormulaEvaluator evaluator, int year, |
| | | Map<Integer, LocalDate> dayDates, String sheetName, |
| | | String roadCategory, String vehicleType, int startColumn, int endColumn) { |
| | | Sheet sheet = workbook.getSheet(sheetName); |
| | | if (sheet == null) return; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn); |
| | | if (yearColumn == null) return; |
| | | for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { |
| | | Row row = sheet.getRow(FIRST_DATA_ROW_INDEX + day.getKey() - 1); |
| | | if (row == null) continue; |
| | | BigDecimal value = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (value != null) { |
| | | upsertVehicle(year, day.getValue(), roadCategory, vehicleType, value, null); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void seedCoefficientYear(Workbook workbook, FormulaEvaluator evaluator, int year) { |
| | | List<BigDecimal> values = new ArrayList<>(); |
| | | collectCoefficientValues(workbook, evaluator, "é«éå
¬è·¯", year, 8, 10, values); |
| | | collectCoefficientValues(workbook, evaluator, "æ®éå½çé", year, 9, 11, values); |
| | | if (values.isEmpty()) return; |
| | | BigDecimal sum = BigDecimal.ZERO; |
| | | for (BigDecimal value : values) { |
| | | sum = HolidayNumberUtils.normalize(sum.add(value)); |
| | | } |
| | | BigDecimal coefficient = HolidayNumberUtils.divide(sum, BigDecimal.valueOf(values.size())); |
| | | upsertCoefficient(year, HolidayConstants.CATEGORY_HOLIDAY, coefficient); |
| | | upsertCoefficient(year, HolidayConstants.CATEGORY_NON_HOLIDAY, coefficient); |
| | | } |
| | | |
| | | private void collectCoefficientValues(Workbook workbook, FormulaEvaluator evaluator, String sheetName, |
| | | int year, int startColumn, int endColumn, List<BigDecimal> values) { |
| | | Sheet sheet = workbook.getSheet(sheetName); |
| | | if (sheet == null) return; |
| | | Integer yearColumn = findYearColumn(sheet, evaluator, year, startColumn, endColumn); |
| | | if (yearColumn == null) return; |
| | | for (int rowIndex = FIRST_DATA_ROW_INDEX; rowIndex < FIRST_DATA_ROW_INDEX + 10; rowIndex++) { |
| | | Row row = sheet.getRow(rowIndex); |
| | | if (row == null) continue; |
| | | try { |
| | | BigDecimal value = numberValue(row.getCell(yearColumn), evaluator); |
| | | if (value != null && value.compareTo(BigDecimal.ZERO) > 0) values.add(value); |
| | | } catch (Exception ignored) { |
| | | // Skip cells that are not numeric coefficients. |
| | | } |
| | | } |
| | | } |
| | | |
| | | private Map<Integer, LocalDate> dayDates(int year, String holidayType) { |
| | | Map<Integer, LocalDate> result = new LinkedHashMap<>(); |
| | | for (HolidayCalendar row : configService.findHolidayDays(year, holidayType)) { |
| | | result.put(row.getDayIndex(), row.getHolidayDate()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, BigDecimal> loadPassengerValues(int year, String transportMode) { |
| | | Map<LocalDate, BigDecimal> result = new LinkedHashMap<>(); |
| | | List<HolidayPassengerFlow> rows = passengerFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayPassengerFlow>() |
| | | .eq(HolidayPassengerFlow::getYear, year) |
| | | .eq(HolidayPassengerFlow::getTransportMode, transportMode)); |
| | | for (HolidayPassengerFlow row : rows) result.put(row.getFlowDate(), row.getPassengerCount()); |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, BigDecimal> loadVehicleValues(int year, String roadCategory) { |
| | | Map<LocalDate, BigDecimal> result = new LinkedHashMap<>(); |
| | | List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getYear, year) |
| | | .eq(HolidayVehicleFlow::getRoadCategory, roadCategory)); |
| | | for (HolidayVehicleFlow row : rows) result.put(row.getFlowDate(), row.getVehicleCount()); |
| | | return result; |
| | | } |
| | | |
| | | private Map<LocalDate, BigDecimal> loadComparisonBaseValues(int year) { |
| | | Map<LocalDate, BigDecimal> result = new LinkedHashMap<>(); |
| | | List<HolidayVehicleFlow> rows = vehicleFlowMapper.selectList( |
| | | new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getYear, year) |
| | | .eq(HolidayVehicleFlow::getRoadCategory, HolidayConstants.ROAD_NATIONAL_PROVINCIAL)); |
| | | for (HolidayVehicleFlow row : rows) { |
| | | if (row.getComparisonBase() != null) result.put(row.getFlowDate(), row.getComparisonBase()); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private void upsertPassenger(int year, LocalDate date, String mode, BigDecimal value) { |
| | | HolidayPassengerFlow existing = passengerFlowMapper.selectOne(new LambdaQueryWrapper<HolidayPassengerFlow>() |
| | | .eq(HolidayPassengerFlow::getYear, year) |
| | | .eq(HolidayPassengerFlow::getFlowDate, date) |
| | | .eq(HolidayPassengerFlow::getTransportMode, mode) |
| | | .last("limit 1")); |
| | | if (existing == null) { |
| | | existing = new HolidayPassengerFlow(); |
| | | existing.setYear(year); |
| | | existing.setFlowDate(date); |
| | | existing.setTransportMode(mode); |
| | | existing.setPassengerCount(value); |
| | | existing.setCreatedAt(LocalDateTime.now()); |
| | | existing.setUpdatedAt(LocalDateTime.now()); |
| | | passengerFlowMapper.insert(existing); |
| | | } else { |
| | | existing.setPassengerCount(value); |
| | | existing.setUpdatedAt(LocalDateTime.now()); |
| | | passengerFlowMapper.updateById(existing); |
| | | } |
| | | } |
| | | |
| | | private void upsertVehicle(int year, LocalDate date, String roadCategory, String vehicleType, |
| | | BigDecimal value, BigDecimal comparisonBase) { |
| | | HolidayVehicleFlow existing = vehicleFlowMapper.selectOne(new LambdaQueryWrapper<HolidayVehicleFlow>() |
| | | .eq(HolidayVehicleFlow::getYear, year) |
| | | .eq(HolidayVehicleFlow::getFlowDate, date) |
| | | .eq(HolidayVehicleFlow::getRoadCategory, roadCategory) |
| | | .eq(HolidayVehicleFlow::getVehicleType, vehicleType) |
| | | .last("limit 1")); |
| | | if (existing == null) { |
| | | existing = new HolidayVehicleFlow(); |
| | | existing.setYear(year); |
| | | existing.setFlowDate(date); |
| | | existing.setRoadCategory(roadCategory); |
| | | existing.setVehicleType(vehicleType); |
| | | existing.setVehicleCount(value); |
| | | existing.setComparisonBase(comparisonBase); |
| | | existing.setCreatedAt(LocalDateTime.now()); |
| | | existing.setUpdatedAt(LocalDateTime.now()); |
| | | vehicleFlowMapper.insert(existing); |
| | | } else { |
| | | existing.setVehicleCount(value); |
| | | existing.setComparisonBase(comparisonBase); |
| | | existing.setUpdatedAt(LocalDateTime.now()); |
| | | vehicleFlowMapper.updateById(existing); |
| | | } |
| | | } |
| | | |
| | | private void upsertCoefficient(int year, String dateCategory, BigDecimal value) { |
| | | HolidayPassengerCoefficient existing = coefficientMapper.selectOne( |
| | | new LambdaQueryWrapper<HolidayPassengerCoefficient>() |
| | | .eq(HolidayPassengerCoefficient::getYear, year) |
| | | .eq(HolidayPassengerCoefficient::getDateCategory, dateCategory) |
| | | .last("limit 1")); |
| | | if (existing == null) { |
| | | existing = new HolidayPassengerCoefficient(); |
| | | existing.setYear(year); |
| | | existing.setDateCategory(dateCategory); |
| | | existing.setCoefficient(value); |
| | | existing.setCreatedAt(LocalDateTime.now()); |
| | | existing.setUpdatedAt(LocalDateTime.now()); |
| | | coefficientMapper.insert(existing); |
| | | } else { |
| | | existing.setCoefficient(value); |
| | | existing.setUpdatedAt(LocalDateTime.now()); |
| | | coefficientMapper.updateById(existing); |
| | | } |
| | | } |
| | | |
| | | private Integer findYearColumn(Sheet sheet, FormulaEvaluator evaluator, int year, int startColumn, int endColumn) { |
| | | Row header = sheet.getRow(1); |
| | | if (header == null) return null; |
| | | String wanted = year + "å¹´"; |
| | | DataFormatter formatter = new DataFormatter(); |
| | | for (int column = startColumn; column <= endColumn; column++) { |
| | | Cell cell = header.getCell(column); |
| | | if (cell == null) continue; |
| | | String text = formatter.formatCellValue(cell, evaluator).replace("\n", "").trim(); |
| | | if (wanted.equals(text) || text.startsWith(wanted + "累计")) return column; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private Integer parseDayIndex(Cell cell, FormulaEvaluator evaluator) { |
| | | if (cell == null) return null; |
| | | DataFormatter formatter = new DataFormatter(); |
| | | String text = formatter.formatCellValue(cell, evaluator).trim(); |
| | | java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("第\\s*(\\d+)\\s*æ¥").matcher(text); |
| | | return matcher.find() ? Integer.valueOf(matcher.group(1)) : null; |
| | | } |
| | | |
| | | private BigDecimal numberValue(Cell cell, FormulaEvaluator evaluator) { |
| | | if (cell == null) return null; |
| | | CellType type = cell.getCellType(); |
| | | if (type == CellType.FORMULA) { |
| | | type = evaluator.evaluateFormulaCell(cell); |
| | | } |
| | | if (type == CellType.BLANK) return null; |
| | | if (type == CellType.NUMERIC) return HolidayNumberUtils.excelValue(cell.getNumericCellValue()); |
| | | if (type == CellType.STRING) { |
| | | String text = cell.getStringCellValue().trim().replace(",", ""); |
| | | if (text.isEmpty() || "-".equals(text) || "â".equals(text)) return null; |
| | | try { |
| | | return HolidayNumberUtils.decimalValue(text); |
| | | } catch (NumberFormatException e) { |
| | | throw new IllegalArgumentException("åå
æ ¼ä¸æ¯æææ°å: " + text); |
| | | } |
| | | } |
| | | if (type == CellType.BOOLEAN) throw new IllegalArgumentException("åå
æ ¼ä¸è½æ¯å¸å°å¼"); |
| | | throw new IllegalArgumentException("åå
æ ¼ä¸æ¯æææ°å"); |
| | | } |
| | | |
| | | private void setNumeric(Sheet sheet, int rowIndex, int columnIndex, BigDecimal value) { |
| | | if (value == null) return; |
| | | Row row = sheet.getRow(rowIndex); |
| | | if (row == null) row = sheet.createRow(rowIndex); |
| | | Cell cell = row.getCell(columnIndex); |
| | | if (cell == null) cell = row.createCell(columnIndex); |
| | | cell.setCellValue(value.doubleValue()); |
| | | } |
| | | |
| | | private void setFormula(Sheet sheet, int rowIndex, int columnIndex, String formula) { |
| | | Row row = sheet.getRow(rowIndex); |
| | | if (row == null) row = sheet.createRow(rowIndex); |
| | | Cell cell = row.getCell(columnIndex); |
| | | if (cell == null) cell = row.createCell(columnIndex); |
| | | cell.setCellFormula(formula); |
| | | } |
| | | |
| | | private Map<String, Object> toMap(HolidayCalendar calendar) { |
| | | Map<String, Object> params = new LinkedHashMap<>(); |
| | | params.put("year", calendar.getYear()); |
| | | params.put("holidayType", calendar.getHolidayType()); |
| | | params.put("holidayDate", calendar.getHolidayDate().toString()); |
| | | params.put("dayIndex", calendar.getDayIndex()); |
| | | return params; |
| | | } |
| | | |
| | | @Data |
| | | public static class ImportSummary { |
| | | private int passengerRows; |
| | | private int vehicleRows; |
| | | private int skippedRows; |
| | | private boolean confirmationRequired; |
| | | private final List<String> errors = new ArrayList<>(); |
| | | private final List<String> historicalChanges = new ArrayList<>(); |
| | | |
| | | public void incrementPassenger() { |
| | | passengerRows++; |
| | | } |
| | | |
| | | public void incrementVehicle() { |
| | | vehicleRows++; |
| | | } |
| | | |
| | | public void incrementSkipped() { |
| | | skippedRows++; |
| | | } |
| | | |
| | | public void addError(String message) { |
| | | errors.add(message); |
| | | } |
| | | |
| | | public void addHistoricalChange(String message) { |
| | | if (historicalChanges.size() < 200) { |
| | | historicalChanges.add(message); |
| | | } |
| | | } |
| | | |
| | | public int getSuccessRows() { |
| | | return passengerRows + vehicleRows; |
| | | } |
| | | |
| | | public int getFailRows() { |
| | | return errors.size(); |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | // å
¨å±ç»åºï¼æ¸
é¤ç»å½æå¹¶åç»å½é¡µï¼å页é¢ä¾§è¾¹æ ãéåºç»å½ãèåå
±ç¨ï¼ |
| | | Vue.mixin({ |
| | | computed: { |
| | | operatorIsAdmin() { |
| | | return sessionStorage.getItem('username') === 'admin' |
| | | } |
| | | }, |
| | | methods: { |
| | | // el-menu åè·¯ç±å¯¼èªï¼NavigationDuplicatedï¼æ¶ Element UI 伿é«äº®åæ»ä¸ºç¹å»åæ§å¼ï¼ |
| | | // ç¹å»åå»¶è¿ä¸ä¸ªå®ä»»å¡å¼ºå¶çº æ£ï¼å®ä»»å¡æäº push ç微任å¡åè°ï¼ä¿è¯æç»é«äº®æ£ç¡®ï¼ã |
| | |
| | | { path: '/audit', name: 'AuditResult', component: () => import('@/views/AuditResult.vue') }, |
| | | { path: '/report', name: 'ReportExport', component: () => import('@/views/ReportExport.vue') }, |
| | | { path: '/rules', name: 'RuleManage', component: () => import('@/views/RuleManage.vue') }, |
| | | { path: '/users', name: 'UserManage', component: () => import('@/views/UserManage.vue') } |
| | | { path: '/users', name: 'UserManage', component: () => import('@/views/UserManage.vue') }, |
| | | { path: '/holiday-stat', name: 'HolidayStat', component: () => import('@/views/HolidayStat.vue') }, |
| | | { path: '/system-config', name: 'SystemConfig', component: () => import('@/views/SystemConfig.vue'), meta: { requireAdmin: true } } |
| | | ] |
| | | }) |
| | | |
| | |
| | | const token = sessionStorage.getItem('token') |
| | | if (to.path === '/login') { |
| | | next(token ? '/dashboard' : undefined) |
| | | } else if (!token) { |
| | | next('/login') |
| | | } else if (to.meta && to.meta.requireAdmin && sessionStorage.getItem('username') !== 'admin') { |
| | | next('/dashboard') |
| | | } else { |
| | | next(token ? undefined : '/login') |
| | | next() |
| | | } |
| | | }) |
| | | |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | </el-menu> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main :class="{ 'main-focus': focusMode }" style="display:flex;flex-direction:column;min-height:0"> |
| | | <h3 v-show="!focusMode" style="flex:none;margin:0 0 10px">å®¡æ ¸ç»æ</h3> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item></el-menu> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | | <div class="page-scroll"> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | </el-menu> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | | <div class="page-scroll"> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | </el-menu> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | | <h3>导å
¥æ°æ®æ¥ç</h3> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | </el-menu> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | | <h3>ä¼ä¸è§£é</h3> |
| New file |
| | |
| | | <template> |
| | | <el-container direction="vertical"> |
| | | <app-header title="交éç»è®¡æ¥è¡¨å®¡æ ¸ç³»ç» - ä¸é¡¹ç»è®¡"></app-header> |
| | | <el-container> |
| | | <el-aside width="200px"> |
| | | <el-menu ref="sideMenu" router :default-active="$route.path" @select="onMenuSelect"> |
| | | <el-menu-item index="/dashboard">å·¥ä½å°</el-menu-item> |
| | | <el-menu-item index="/import">æ°æ®å¯¼å
¥</el-menu-item> |
| | | <el-menu-item index="/data">æ°æ®æ¥ç</el-menu-item> |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | | <div class="page-scroll"> |
| | | <h3>ä¸é¡¹ç»è®¡ - è忥</h3> |
| | | <div class="toolbar"> |
| | | <el-select v-model="year" style="width:120px" @change="loadOverview"> |
| | | <el-option v-for="item in years" :key="item" :label="item + 'å¹´'" :value="item"></el-option> |
| | | </el-select> |
| | | <el-select v-model="holidayType" style="width:150px" @change="loadOverview"> |
| | | <el-option v-for="item in holidayTypeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option> |
| | | </el-select> |
| | | <el-button icon="el-icon-refresh" @click="loadOverview">å·æ°</el-button> |
| | | <el-button icon="el-icon-download" @click="downloadTemplate">ä¸è½½æ¨¡æ¿</el-button> |
| | | </div> |
| | | |
| | | <el-card shadow="never" class="operation-card"> |
| | | <div class="operation-row"> |
| | | <div class="operation-item"> |
| | | <span class="label">导å
¥æ°æ®</span> |
| | | <el-upload action="#" :auto-upload="false" :show-file-list="true" :limit="1" |
| | | accept=".xlsx,.xls" :on-change="onFileChange" :on-remove="onFileRemove" :file-list="fileList"> |
| | | <el-button type="primary" plain icon="el-icon-folder-opened">éæ©æä»¶</el-button> |
| | | </el-upload> |
| | | <el-button type="primary" icon="el-icon-upload2" :loading="importing" :disabled="!selectedFile" @click="importData">导å
¥</el-button> |
| | | </div> |
| | | <div class="operation-item"> |
| | | <span class="label">å¯¼åºæ°æ®</span> |
| | | <el-button type="success" icon="el-icon-download" :loading="exporting" @click="exportData">å¯¼åº Excel</el-button> |
| | | </div> |
| | | <div class="operation-item"> |
| | | <span class="label">客æµå¿«æ¥</span> |
| | | <el-date-picker v-model="bulletinDate" type="date" value-format="yyyy-MM-dd" style="width:160px" placeholder="éæ©æ¥æ"></el-date-picker> |
| | | <el-button type="warning" icon="el-icon-document" :loading="bulletinLoading" @click="downloadBulletin">ä¸è½½å¿«æ¥</el-button> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <div v-if="importMessage" class="import-message"> |
| | | <pre>{{ importMessage }}</pre> |
| | | </div> |
| | | |
| | | <el-card shadow="never" style="margin-top:14px"> |
| | | <div slot="header">{{ year }}å¹´{{ holidayTypeLabel }}èåæ¥æ°æ®</div> |
| | | <el-table :data="rows" border stripe size="small" v-loading="loading" style="width:100%"> |
| | | <el-table-column prop="dayIndex" label="ç¬¬å æ¥" width="80" align="center"></el-table-column> |
| | | <el-table-column prop="date" label="æ¥æ" width="120" align="center"></el-table-column> |
| | | <el-table-column label="éè·¯ï¼ä¸äººæ¬¡ï¼" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.rail) }}</template></el-table-column> |
| | | <el-table-column label="æ°èªï¼ä¸äººæ¬¡ï¼" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.civilAviation) }}</template></el-table-column> |
| | | <el-table-column label="éè·¯ï¼ä¸äººæ¬¡ï¼" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.road) }}</template></el-table-column> |
| | | <el-table-column label="æ°´è·¯ï¼ä¸äººæ¬¡ï¼" width="120" align="right"><template slot-scope="s">{{ formatNumber(s.row.water) }}</template></el-table-column> |
| | | <el-table-column label="é«é车æµï¼ä¸è¾æ¬¡ï¼" width="150" align="right"><template slot-scope="s">{{ formatNumber(s.row.expressway) }}</template></el-table-column> |
| | | <el-table-column label="å½çé车æµï¼ä¸è¾æ¬¡ï¼" width="160" align="right"><template slot-scope="s">{{ formatNumber(s.row.nationalProvincial) }}</template></el-table-column> |
| | | <el-table-column label="å½çéç¯æ¯åºæï¼ä¸è¾æ¬¡ï¼" width="190" align="right"><template slot-scope="s">{{ formatNumber(s.row.comparisonBase) }}</template></el-table-column> |
| | | <el-table-column label="å½çé车æµç¯æ¯" width="140" align="right"><template slot-scope="s">{{ formatRatio(s.row.nationalProvincialRingRatio) }}</template></el-table-column> |
| | | <el-table-column label="客è¿ç³»æ°" width="100" align="right"><template slot-scope="s">{{ formatCoefficient(s.row.coefficient) }}</template></el-table-column> |
| | | <el-table-column label="éè¥ä¸æ§äººåæµå¨éï¼ä¸äººæ¬¡ï¼" min-width="200" align="right"><template slot-scope="s">{{ formatNumber(s.row.nonBusinessPersonFlow) }}</template></el-table-column> |
| | | <el-table-column label="æ»äººåæµå¨éï¼ä¸äººæ¬¡ï¼" min-width="180" align="right"><template slot-scope="s">{{ formatNumber(s.row.totalPersonFlow) }}</template></el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | </div> |
| | | </el-main> |
| | | </el-container> |
| | | </el-container> |
| | | </template> |
| | | |
| | | <script> |
| | | import api from '@/api' |
| | | import AppHeader from '@/components/AppHeader.vue' |
| | | |
| | | export default { |
| | | components: { AppHeader }, |
| | | name: 'HolidayStat', |
| | | data() { |
| | | return { |
| | | years: [2024, 2025, 2026, 2027, 2028, 2029, 2030], |
| | | year: 2026, |
| | | holidayType: 'NATIONAL_DAY', |
| | | holidayTypeOptions: [], |
| | | rows: [], |
| | | loading: false, |
| | | importing: false, |
| | | exporting: false, |
| | | bulletinLoading: false, |
| | | selectedFile: null, |
| | | fileList: [], |
| | | importMessage: '', |
| | | allowHistoricalOverwrite: false, |
| | | bulletinDate: this.previousDateString() |
| | | } |
| | | }, |
| | | computed: { |
| | | holidayTypeLabel() { |
| | | const item = this.holidayTypeOptions.find(x => x.value === this.holidayType) |
| | | return item ? item.label : this.holidayType |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.loadOptions() |
| | | this.loadOverview() |
| | | }, |
| | | methods: { |
| | | previousDateString() { |
| | | const date = new Date() |
| | | date.setDate(date.getDate() - 1) |
| | | const month = String(date.getMonth() + 1).padStart(2, '0') |
| | | const day = String(date.getDate()).padStart(2, '0') |
| | | return date.getFullYear() + '-' + month + '-' + day |
| | | }, |
| | | loadOptions() { |
| | | api.get('/holiday/config/options').then(res => { |
| | | const map = (res.data && res.data.holidayTypes) || {} |
| | | this.holidayTypeOptions = Object.keys(map).map(key => ({ value: key, label: map[key] })) |
| | | }) |
| | | }, |
| | | loadOverview() { |
| | | this.loading = true |
| | | api.get('/holiday/overview', { params: { year: this.year, holidayType: this.holidayType } }) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'å 载失败') |
| | | this.rows = (res.data && res.data.rows) || [] |
| | | }) |
| | | .catch(err => this.showError(err, 'èåæ¥æ°æ®å 载失败')) |
| | | .finally(() => { this.loading = false }) |
| | | }, |
| | | onFileChange(file) { |
| | | this.selectedFile = file.raw |
| | | this.fileList = [file] |
| | | this.importMessage = '' |
| | | }, |
| | | onFileRemove() { |
| | | this.selectedFile = null |
| | | this.fileList = [] |
| | | }, |
| | | importData(allowHistoricalOverwrite) { |
| | | if (!this.selectedFile) return |
| | | const allowOverwrite = allowHistoricalOverwrite === true || this.allowHistoricalOverwrite === true |
| | | const form = new FormData() |
| | | form.append('file', this.selectedFile) |
| | | form.append('year', this.year) |
| | | form.append('holidayType', this.holidayType) |
| | | form.append('allowHistoricalOverwrite', allowOverwrite ? 'true' : 'false') |
| | | this.importing = true |
| | | this.importMessage = '' |
| | | api.post('/holiday/import', form, { timeout: 120000, headers: { 'Content-Type': 'multipart/form-data' } }) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || '导å
¥å¤±è´¥') |
| | | const data = res.data || {} |
| | | if (data.confirmationRequired) { |
| | | const changes = data.historicalChanges || [] |
| | | this.importMessage = ['æ£æµå°å岿¥æçæ°æ®åçååï¼å°æªå¯¼å
¥ã请确认æ¯å¦å
许è¦çï¼'].concat(changes).join('\n') |
| | | this.$message.warning('æ£æµå°å岿°æ®åçååï¼è¯·ç¡®è®¤æ¯å¦å
许导å
¥') |
| | | this.confirmHistoricalImport(changes) |
| | | return |
| | | } |
| | | const lines = [ |
| | | '导å
¥å®æï¼å®¢æµ ' + (data.passengerRows || 0) + ' æ¡ï¼è½¦æµ ' + (data.vehicleRows || 0) + ' æ¡ï¼è·³è¿ç©ºç½ ' + (data.skippedRows || 0) + ' æ¡ã' |
| | | ] |
| | | if (data.errors && data.errors.length) { |
| | | lines.push('失败æç»ï¼') |
| | | data.errors.forEach(item => lines.push(item)) |
| | | } |
| | | this.importMessage = lines.join('\n') |
| | | this.allowHistoricalOverwrite = false |
| | | this.$message.success('导å
¥å®æ') |
| | | this.loadOverview() |
| | | }) |
| | | .catch(err => { |
| | | this.allowHistoricalOverwrite = false |
| | | this.showError(err, '导å
¥å¤±è´¥') |
| | | }) |
| | | .finally(() => { this.importing = false }) |
| | | }, |
| | | confirmHistoricalImport(changes) { |
| | | const rows = (changes && changes.length ? changes : ['å岿°æ®åçåå']).map(this.escapeHtml) |
| | | const html = '<div>æ£æµå°å·²å叿¥æçæ°æ®åçååãå
许导å
¥å°è¦çè¿äºå岿°æ®ã</div>' |
| | | + '<pre style="max-height:320px;overflow:auto;white-space:pre-wrap;margin-top:10px;background:#f7f7f7;padding:8px;border:1px solid #e4e7ed;">' |
| | | + rows.join('\n') + '</pre>' |
| | | this.$confirm(html, 'å岿°æ®åçåå', { |
| | | dangerouslyUseHTMLString: true, |
| | | confirmButtonText: 'å
许导å
¥', |
| | | cancelButtonText: 'ä¸å
许', |
| | | type: 'warning', |
| | | distinguishCancelAndClose: true |
| | | }).then(() => { |
| | | this.allowHistoricalOverwrite = true |
| | | this.importData(true) |
| | | }).catch(() => { |
| | | this.allowHistoricalOverwrite = false |
| | | }) |
| | | }, |
| | | escapeHtml(value) { |
| | | return String(value === null || value === undefined ? '' : value) |
| | | .replace(/&/g, '&') |
| | | .replace(/</g, '<') |
| | | .replace(/>/g, '>') |
| | | .replace(/"/g, '"') |
| | | .replace(/'/g, ''') |
| | | }, |
| | | exportData() { |
| | | this.exporting = true |
| | | api.get('/holiday/export', { params: { year: this.year, holidayType: this.holidayType }, responseType: 'blob', timeout: 120000 }) |
| | | .then(res => { this.downloadBlob(res, this.year + 'å¹´' + this.holidayTypeLabel + 'æ°æ®.xlsx'); this.$message.success('å¯¼åºæå') }) |
| | | .catch(err => this.handleBlobError(err, '导åºå¤±è´¥')) |
| | | .finally(() => { this.exporting = false }) |
| | | }, |
| | | downloadTemplate() { |
| | | const templatePath = 'ä¸é¡¹ç»è®¡/éè¦è忥/2026å¹´å䏿°æ®.xlsx' |
| | | const encodedPath = templatePath.split('/').map(encodeURIComponent).join('/') |
| | | api.get('/templates/' + encodedPath, { responseType: 'blob' }) |
| | | .then(res => { this.downloadBlob(res, '2026å¹´å䏿°æ®.xlsx'); this.$message.success('模æ¿ä¸è½½æå') }) |
| | | .catch(err => this.handleBlobError(err, '模æ¿ä¸è½½å¤±è´¥')) |
| | | }, |
| | | downloadBulletin() { |
| | | this.bulletinLoading = true |
| | | api.get('/holiday/bulletin', { params: { date: this.bulletinDate }, responseType: 'blob', timeout: 120000 }) |
| | | .then(res => { |
| | | const day = Number((this.bulletinDate || '').slice(-2)) |
| | | this.downloadBlob(res, 'æ¹åçå½åºåæç»¼åè¿è¾å®¢æµå¿«æ¥ï¼10æ' + day + 'æ¥ï¼.docx') |
| | | this.$message.success('å¿«æ¥ä¸è½½æå') |
| | | }) |
| | | .catch(err => this.handleBlobError(err, 'å¿«æ¥çæå¤±è´¥')) |
| | | .finally(() => { this.bulletinLoading = false }) |
| | | }, |
| | | downloadBlob(data, fileName) { |
| | | const blob = data instanceof Blob ? data : new Blob([data]) |
| | | const url = window.URL.createObjectURL(blob) |
| | | const link = document.createElement('a') |
| | | link.href = url |
| | | link.download = fileName |
| | | document.body.appendChild(link) |
| | | link.click() |
| | | document.body.removeChild(link) |
| | | window.URL.revokeObjectURL(url) |
| | | }, |
| | | handleBlobError(err, fallback) { |
| | | if (err && err.response && err.response.data instanceof Blob) { |
| | | err.response.data.text().then(text => { |
| | | try { |
| | | const parsed = JSON.parse(text) |
| | | this.$message.error(parsed.message || fallback) |
| | | } catch (e) { |
| | | this.$message.error(fallback) |
| | | } |
| | | }) |
| | | return |
| | | } |
| | | this.showError(err, fallback) |
| | | }, |
| | | showError(err, fallback) { |
| | | if (err && err._toast) return |
| | | const data = err && err.response && err.response.data |
| | | this.$message.error((data && data.message) || (err && err.message) || fallback) |
| | | }, |
| | | formatNumber(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | return Number(value).toFixed(2) |
| | | }, |
| | | formatCoefficient(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return number.toFixed(15).replace(/0+$/, '').replace(/\.$/, '') |
| | | }, |
| | | formatRatio(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return (number * 100).toFixed(2) + '%' |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; } |
| | | .operation-card { margin-top: 12px; } |
| | | .operation-row { display: flex; gap: 36px; align-items: flex-start; flex-wrap: wrap; } |
| | | .operation-item { display: flex; align-items: center; gap: 8px; } |
| | | .operation-item .label { color: #606266; font-size: 14px; } |
| | | .import-message { margin-top: 12px; padding: 10px 12px; background: #f5f7fa; border: 1px solid #e4e7ed; border-radius: 4px; } |
| | | .import-message pre { margin: 0; white-space: pre-wrap; font-family: inherit; color: #606266; } |
| | | </style> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| New file |
| | |
| | | <template> |
| | | <el-container direction="vertical"> |
| | | <app-header title="交éç»è®¡æ¥è¡¨å®¡æ ¸ç³»ç» - ç³»ç»é
ç½®"></app-header> |
| | | <el-container> |
| | | <el-aside width="200px"> |
| | | <el-menu ref="sideMenu" router :default-active="$route.path" @select="onMenuSelect"> |
| | | <el-menu-item index="/dashboard">å·¥ä½å°</el-menu-item> |
| | | <el-menu-item index="/import">æ°æ®å¯¼å
¥</el-menu-item> |
| | | <el-menu-item index="/data">æ°æ®æ¥ç</el-menu-item> |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |
| | | <div class="page-scroll"> |
| | | <h3>ç³»ç»é
ç½®</h3> |
| | | <el-tabs v-model="activeTab" @tab-click="loadActiveTab"> |
| | | <el-tab-pane label="èåæ¥ç®¡ç" name="holidays"> |
| | | <div class="toolbar"> |
| | | <el-select v-model="holidayQuery.year" clearable placeholder="年份" style="width:130px" @change="loadHolidays"> |
| | | <el-option v-for="year in years" :key="year" :label="year + 'å¹´'" :value="year"></el-option> |
| | | </el-select> |
| | | <el-select v-model="holidayQuery.holidayType" clearable placeholder="è忥åç±»" style="width:150px" @change="loadHolidays"> |
| | | <el-option v-for="item in holidayTypeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option> |
| | | </el-select> |
| | | <el-button type="primary" icon="el-icon-search" @click="loadHolidays">æ¥è¯¢</el-button> |
| | | <el-button type="success" icon="el-icon-plus" @click="openHolidayDialog()">æ°å¢è忥</el-button> |
| | | </div> |
| | | <el-table :data="holidays" border stripe size="small" v-loading="loadingHolidays" style="width:100%"> |
| | | <el-table-column prop="year" label="年份" width="100" align="center"></el-table-column> |
| | | <el-table-column label="è忥åç±»" width="140"> |
| | | <template slot-scope="scope">{{ labelOf(holidayTypeOptions, scope.row.holidayType) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="holidayDate" label="æ¥æ" width="150" align="center"></el-table-column> |
| | | <el-table-column prop="dayIndex" label="ç¬¬å æ¥" width="100" align="center"></el-table-column> |
| | | <el-table-column label="æä½" width="150" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-button type="text" size="mini" @click="openHolidayDialog(scope.row)">ç¼è¾</el-button> |
| | | <el-button type="text" size="mini" class="danger-text" @click="removeHoliday(scope.row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | |
| | | <el-tab-pane label="客è¿ç³»æ°ç®¡ç" name="coefficients"> |
| | | <div class="toolbar"> |
| | | <el-select v-model="coefficientQuery.year" clearable placeholder="年份" style="width:130px" @change="loadCoefficients"> |
| | | <el-option v-for="year in years" :key="year" :label="year + 'å¹´'" :value="year"></el-option> |
| | | </el-select> |
| | | <el-button type="primary" icon="el-icon-search" @click="loadCoefficients">æ¥è¯¢</el-button> |
| | | <el-button type="success" icon="el-icon-plus" @click="openCoefficientDialog()">æ°å¢å®¢è¿ç³»æ°</el-button> |
| | | </div> |
| | | <el-table :data="coefficients" border stripe size="small" v-loading="loadingCoefficients" style="width:100%"> |
| | | <el-table-column prop="year" label="年份" width="120" align="center"></el-table-column> |
| | | <el-table-column label="æ¥æåç±»" width="160"> |
| | | <template slot-scope="scope">{{ labelOf(dateCategoryOptions, scope.row.dateCategory) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="客è¿ç³»æ°" width="160" align="center"> |
| | | <template slot-scope="scope">{{ formatCoefficient(scope.row.coefficient) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="150" align="center"> |
| | | <template slot-scope="scope"> |
| | | <el-button type="text" size="mini" @click="openCoefficientDialog(scope.row)">ç¼è¾</el-button> |
| | | <el-button type="text" size="mini" class="danger-text" @click="removeCoefficient(scope.row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | </div> |
| | | |
| | | <el-dialog :title="holidayForm.id ? 'ç¼è¾è忥' : 'æ°å¢è忥'" :visible.sync="holidayDialog" width="520px" v-dialog-drag> |
| | | <el-form ref="holidayForm" :model="holidayForm" :rules="holidayRules" label-width="110px"> |
| | | <el-form-item label="年份" prop="year"> |
| | | <el-select v-model="holidayForm.year" style="width:100%" @change="onHolidayYearChange"> |
| | | <el-option v-for="year in years" :key="year" :label="year + 'å¹´'" :value="year"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="è忥åç±»" prop="holidayType"> |
| | | <el-select v-model="holidayForm.holidayType" style="width:100%"> |
| | | <el-option v-for="item in holidayTypeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥æ" prop="holidayDate"> |
| | | <el-date-picker v-model="holidayForm.holidayDate" type="date" value-format="yyyy-MM-dd" |
| | | style="width:100%" placeholder="éæ©æ¥æ"></el-date-picker> |
| | | </el-form-item> |
| | | <el-form-item label="ç¬¬å æ¥" prop="dayIndex"> |
| | | <el-input-number v-model="holidayForm.dayIndex" :min="1" :max="31" style="width:100%"></el-input-number> |
| | | </el-form-item> |
| | | </el-form> |
| | | <span slot="footer"> |
| | | <el-button @click="holidayDialog = false">åæ¶</el-button> |
| | | <el-button type="primary" :loading="saving" @click="saveHoliday">ä¿å</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | |
| | | <el-dialog :title="coefficientForm.id ? 'ç¼è¾å®¢è¿ç³»æ°' : 'æ°å¢å®¢è¿ç³»æ°'" :visible.sync="coefficientDialog" width="520px" v-dialog-drag> |
| | | <el-form ref="coefficientForm" :model="coefficientForm" :rules="coefficientRules" label-width="110px"> |
| | | <el-form-item label="年份" prop="year"> |
| | | <el-select v-model="coefficientForm.year" style="width:100%"> |
| | | <el-option v-for="year in years" :key="year" :label="year + 'å¹´'" :value="year"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥æåç±»" prop="dateCategory"> |
| | | <el-select v-model="coefficientForm.dateCategory" style="width:100%"> |
| | | <el-option v-for="item in dateCategoryOptions" :key="item.value" :label="item.label" :value="item.value"></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="客è¿ç³»æ°" prop="coefficient"> |
| | | <el-input-number v-model="coefficientForm.coefficient" :min="0.01" :precision="4" :step="0.1" style="width:100%"></el-input-number> |
| | | </el-form-item> |
| | | </el-form> |
| | | <span slot="footer"> |
| | | <el-button @click="coefficientDialog = false">åæ¶</el-button> |
| | | <el-button type="primary" :loading="saving" @click="saveCoefficient">ä¿å</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | </el-main> |
| | | </el-container> |
| | | </el-container> |
| | | </template> |
| | | |
| | | <script> |
| | | import api from '@/api' |
| | | import AppHeader from '@/components/AppHeader.vue' |
| | | |
| | | export default { |
| | | components: { AppHeader }, |
| | | name: 'SystemConfig', |
| | | data() { |
| | | return { |
| | | activeTab: 'holidays', |
| | | years: [2024, 2025, 2026, 2027, 2028, 2029, 2030], |
| | | holidayTypeOptions: [], |
| | | dateCategoryOptions: [], |
| | | holidayQuery: { year: 2026, holidayType: 'NATIONAL_DAY' }, |
| | | coefficientQuery: { year: 2026 }, |
| | | holidays: [], |
| | | coefficients: [], |
| | | loadingHolidays: false, |
| | | loadingCoefficients: false, |
| | | saving: false, |
| | | holidayDialog: false, |
| | | coefficientDialog: false, |
| | | holidayForm: { id: null, year: 2026, holidayType: 'NATIONAL_DAY', holidayDate: '', dayIndex: 1 }, |
| | | coefficientForm: { id: null, year: 2026, dateCategory: 'HOLIDAY', coefficient: 2.4 }, |
| | | holidayRules: { |
| | | year: [{ required: true, message: 'è¯·éæ©å¹´ä»½', trigger: 'change' }], |
| | | holidayType: [{ required: true, message: 'è¯·éæ©è忥åç±»', trigger: 'change' }], |
| | | holidayDate: [{ required: true, message: 'è¯·éæ©æ¥æ', trigger: 'change' }], |
| | | dayIndex: [{ required: true, message: '请è¾å
¥ç¬¬å æ¥', trigger: 'change' }] |
| | | }, |
| | | coefficientRules: { |
| | | year: [{ required: true, message: 'è¯·éæ©å¹´ä»½', trigger: 'change' }], |
| | | dateCategory: [{ required: true, message: 'è¯·éæ©æ¥æåç±»', trigger: 'change' }], |
| | | coefficient: [{ required: true, message: '请è¾å
¥å®¢è¿ç³»æ°', trigger: 'change' }] |
| | | } |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.loadOptions() |
| | | this.loadHolidays() |
| | | this.loadCoefficients() |
| | | }, |
| | | methods: { |
| | | loadOptions() { |
| | | api.get('/holiday/config/options').then(res => { |
| | | const data = res.data || {} |
| | | this.holidayTypeOptions = this.mapOptions(data.holidayTypes) |
| | | this.dateCategoryOptions = this.mapOptions(data.dateCategories) |
| | | }) |
| | | }, |
| | | mapOptions(map) { |
| | | if (!map) return [] |
| | | return Object.keys(map).map(key => ({ value: key, label: map[key] })) |
| | | }, |
| | | labelOf(options, value) { |
| | | const item = (options || []).find(x => x.value === value) |
| | | return item ? item.label : value |
| | | }, |
| | | loadActiveTab() { |
| | | if (this.activeTab === 'holidays') this.loadHolidays() |
| | | else this.loadCoefficients() |
| | | }, |
| | | loadHolidays() { |
| | | this.loadingHolidays = true |
| | | api.get('/holiday/config/holidays', { params: this.holidayQuery }) |
| | | .then(res => { this.holidays = res.data || [] }) |
| | | .catch(e => { if (!e || !e._toast) this.$message.error('è忥å 载失败') }) |
| | | .finally(() => { this.loadingHolidays = false }) |
| | | }, |
| | | loadCoefficients() { |
| | | this.loadingCoefficients = true |
| | | api.get('/holiday/config/coefficients', { params: this.coefficientQuery }) |
| | | .then(res => { this.coefficients = res.data || [] }) |
| | | .catch(e => { if (!e || !e._toast) this.$message.error('客è¿ç³»æ°å 载失败') }) |
| | | .finally(() => { this.loadingCoefficients = false }) |
| | | }, |
| | | openHolidayDialog(row) { |
| | | this.holidayDialog = true |
| | | this.$nextTick(() => { |
| | | this.holidayForm = row ? Object.assign({}, row) : { id: null, year: 2026, holidayType: 'NATIONAL_DAY', holidayDate: '', dayIndex: 1 } |
| | | if (this.$refs.holidayForm) this.$refs.holidayForm.clearValidate() |
| | | }) |
| | | }, |
| | | onHolidayYearChange(year) { |
| | | if (this.holidayForm.holidayDate && Number(this.holidayForm.holidayDate.slice(0, 4)) !== year) { |
| | | this.holidayForm.holidayDate = '' |
| | | } |
| | | }, |
| | | saveHoliday() { |
| | | this.$refs.holidayForm.validate(valid => { |
| | | if (!valid) return |
| | | this.saving = true |
| | | api.post('/holiday/config/holidays/save', this.holidayForm) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'ä¿å失败') |
| | | this.$message.success('ä¿åæå') |
| | | this.holidayDialog = false |
| | | this.loadHolidays() |
| | | }) |
| | | .catch(err => this.showError(err, 'ä¿å失败')) |
| | | .finally(() => { this.saving = false }) |
| | | }) |
| | | }, |
| | | removeHoliday(row) { |
| | | this.$confirm('ç¡®å®å é¤è¯¥èåæ¥æ¥æåï¼', 'å é¤ç¡®è®¤', { type: 'warning' }) |
| | | .then(() => api.post('/holiday/config/holidays/delete', null, { params: { id: row.id } })) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'å é¤å¤±è´¥') |
| | | this.$message.success('å·²å é¤') |
| | | this.loadHolidays() |
| | | }) |
| | | .catch(err => { if (err !== 'cancel') this.showError(err, 'å é¤å¤±è´¥') }) |
| | | }, |
| | | openCoefficientDialog(row) { |
| | | this.coefficientDialog = true |
| | | this.$nextTick(() => { |
| | | this.coefficientForm = row ? Object.assign({}, row) : { id: null, year: 2026, dateCategory: 'HOLIDAY', coefficient: 2.4 } |
| | | if (this.$refs.coefficientForm) this.$refs.coefficientForm.clearValidate() |
| | | }) |
| | | }, |
| | | saveCoefficient() { |
| | | this.$refs.coefficientForm.validate(valid => { |
| | | if (!valid) return |
| | | this.saving = true |
| | | api.post('/holiday/config/coefficients/save', this.coefficientForm) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'ä¿å失败') |
| | | this.$message.success('ä¿åæå') |
| | | this.coefficientDialog = false |
| | | this.loadCoefficients() |
| | | }) |
| | | .catch(err => this.showError(err, 'ä¿å失败')) |
| | | .finally(() => { this.saving = false }) |
| | | }) |
| | | }, |
| | | removeCoefficient(row) { |
| | | this.$confirm('ç¡®å®å é¤è¯¥å®¢è¿ç³»æ°åï¼', 'å é¤ç¡®è®¤', { type: 'warning' }) |
| | | .then(() => api.post('/holiday/config/coefficients/delete', null, { params: { id: row.id } })) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'å é¤å¤±è´¥') |
| | | this.$message.success('å·²å é¤') |
| | | this.loadCoefficients() |
| | | }) |
| | | .catch(err => { if (err !== 'cancel') this.showError(err, 'å é¤å¤±è´¥') }) |
| | | }, |
| | | formatCoefficient(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return number.toFixed(15).replace(/0+$/, '').replace(/\.$/, '') |
| | | }, |
| | | showError(err, fallback) { |
| | | if (err && err._toast) return |
| | | const data = err && err.response && err.response.data |
| | | this.$message.error((data && data.message) || (err && err.message) || fallback) |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .toolbar { margin-bottom: 12px; display: flex; gap: 8px; align-items: center; } |
| | | .danger-text { color: #f56c6c; } |
| | | </style> |
| | |
| | | <el-menu-item index="/explain">ä¼ä¸è§£é</el-menu-item> |
| | | <el-menu-item index="/audit">å®¡æ ¸ç»æ</el-menu-item> |
| | | <el-menu-item index="/report">æ¥è¡¨çæ</el-menu-item> |
| | | <el-submenu index="/special-stats"> |
| | | <template slot="title">ä¸é¡¹ç»è®¡</template> |
| | | <el-menu-item index="/holiday-stat">è忥</el-menu-item> |
| | | </el-submenu> |
| | | <el-menu-item index="/rules">è§å管ç</el-menu-item> |
| | | <el-menu-item index="/users">ç¨æ·ç®¡ç</el-menu-item> |
| | | <el-menu-item v-if="operatorIsAdmin" index="/system-config">ç³»ç»é
ç½®</el-menu-item> |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main> |