feat(报表): 汇总大表导出异步化 + 结果缓存 + 进度弹窗
用户提问「点导出要等一会才反应,数据多了会不会卡死」:
- 新增 ReportExportTaskService:固定 1 线程池后台生成,提交即返回 taskId,前端轮询进度;
同一报表期并发提交自动去重;结果保留 30 分钟、10 分钟内可复用。
- 缓存带数据版本守卫:版本取 import_batch 最大 id,期间有新导入即失效(取不到版本禁用缓存)。
- ReportExportService 新增带进度回调的 exportSummaryWorkbook 重载(进度点 2/6/12/22/45/70/95%)。
- ReportExportController 新增 /exportTask/summaryWorkbook、/exportTask/{id}、/exportTask/{id}/file 三个接口,原同步接口保留。
- 前端 ReportExport.vue 进度弹窗:步骤/百分比/用时/缓存来源/失败原因 + 保存文件/重新生成/后台运行/关闭。
同时修复一个严重回归:新增 computed 与文件中已有 computed 同名重复(后者覆盖前者),
导致 myModules/visibleModules 丢失、报表生成页 created() 抛错整页白屏;已并入同一 computed 块并浏览器复验。
测试:《功能测试报告_汇总大表异步导出与缓存_2026-09-20.md》17 条用例通过
(接口 8 + 浏览器实测 7 + 复验 2)。
| New file |
| | |
| | | # åè½æµè¯æ¥å_æ±æ»å¤§è¡¨å¼æ¥å¯¼åºä¸ç¼å_2026-09-20 |
| | | |
| | | ## ä¸ãåè½è¯´æ |
| | | |
| | | ç¨æ·ä¸æ¥ï¼é®é¢ 3ï¼ï¼**ãæ±æ»å¤§è¡¨ç¹ä¸è½½åè¦è¿ä¸ä¼æååºä¸è½½ï¼ç°å¨æ°æ®åºçæ°æ®è¿ä¸æ¯å¾å¤ï¼é£æ°æ®åºçæ°æ®å¤äºè¿ä¸ªç¨åºä¼å¡æ»åï¼ã** |
| | | |
| | | èæ¯ï¼æ±æ»å¤§è¡¨å¯¼åºæ¯**åæ¥è¯·æ±**ââç¹å»åæµè§å¨è¦ç 10~20 ç§ææ¿å°æä»¶ï¼æ¯ç读å+åå¡« 6.5sãæ´æ¬çº¦ 4 䏿 ¼å
¬å¼éç® 4.1sãåçä¼ è¾ 3.4sï¼ï¼æé´é¡µé¢æ ä»»ä½åé¦ï¼ |
| | | ä¸çæè¿ç¨ä¸ç´å ç HTTP 请æ±çº¿ç¨ï¼å¤äººåæ¶ç¹ä¼è®©å®¹å¨çº¿ç¨æ± 被é¿ä»»å¡å 满ã |
| | | |
| | | æ¬æ¬¡æ¹é ï¼ç¨æ·ç¡®è®¤ãå¯ä»¥ãï¼ï¼ |
| | | |
| | | 1. **弿¥ä»»å¡å**ï¼æ°å¢ `ReportExportTaskService`ï¼åºå® 1 çº¿ç¨æ± `report-export-task`ï¼daemonï¼ã |
| | | å端æäº¤åç«å³æ¿å°ãä»»å¡å·ãï¼åæ 700ms 轮询è¿åº¦ï¼æ¥éª¤ + ç¾åæ¯ + å·²ç¨æ¶é¿ï¼ï¼å®æååç¬ä¸è½½æä»¶ã |
| | | 2. **ç»æç¼å**ï¼å䏿¥è¡¨æ 10 åéå
çæåç»æç´æ¥å¤ç¨ï¼`fromCache=true`ï¼ï¼è·³è¿æ¯ç读åä¸å
¬å¼éç®ï¼ |
| | | ç¼å带**æ°æ®çæ¬å®å«**ââæ°æ®çæ¬å `SELECT IFNULL(MAX(id),0) FROM import_batch`ï¼æé´åªè¦ææ°å¯¼å
¥å°±èªå¨å¤±æï¼åä¸å°çæ¬æ¶ç¦ç¨ç¼åï¼ã |
| | | 3. **å»é䏿é**ï¼å䏿¥è¡¨æå·²æä»»å¡å¨è·æ¶ç´æ¥å¤ç¨è¯¥ä»»å¡ï¼ä¸ä¼é夿ééç®ã |
| | | 4. **å端è¿åº¦å¼¹çª**ï¼è¿åº¦æ¡ + æ¥éª¤ææ¡ + å·²ç¨æ¶é¿ + ç¼åæ¥æºæ¶é´ + 失败åå ï¼çæä¸å¯ãåå°è¿è¡ãï¼æ¶å¼¹çªï¼å®æåä»èªå¨ä¸è½½ï¼ï¼å®æåå¯ãä¿åæä»¶ / éæ°çæ / å
³éãã |
| | | 5. **è¿åº¦ç¹**ï¼2% æ ¡éªæ¯ç â 6% è¯»åæ¯ç â 12% è¿åæåº¦å â 22% åå¡«å½ææ°æ® â 45% å·æ°æå页ä¸è·¨é¡µåæé¡µ â 70% éç®æ´æ¬å
¬å¼ â 95% ååºæä»¶ â 100% 宿ã |
| | | |
| | | åçç¨æ·çé®ï¼**æ°æ®éå¢å¤§ä¸»è¦æ¯ SQL èååæ
¢ï¼ä¸ä¼ã塿»ã**ï¼æ¹é åçæèµ°åå°çº¿ç¨ãåæ¥è¡¨æç»æå¯å¤ç¨ãåæ¥è¡¨æå¹¶åèªå¨å»éï¼ |
| | | å³ä½¿å¤äººåæ¶ç¹ä¹åªæ¯æéï¼å¼¹çªæ¾ç¤ºãæéä¸ãï¼ï¼ä¸ä¼å 满 Web 线ç¨ã |
| | | |
| | | ## äºãæµè¯ç¯å¢ |
| | | |
| | | - å端 Spring Boot 8090ï¼`Started TrafficAuditApplication` 15:10:53ï¼ãå端 Vue 8080ï¼`Compiled successfully`ï¼ |
| | | - æ°æ®åº MySQL localhost:3308/traffic_audit |
| | | - æ¥è¡¨æï¼2026-08ï¼ææ¥ï¼æ£å¸¸ç¨ä¾ï¼/ 2025-01ï¼æ æ°æ®ï¼è´åç¨ä¾ï¼ |
| | | - æ¥å£èæ¬ï¼`_qa_task.ps1`ã`_qa_task2.ps1`ã`_qa_task3.ps1`ï¼ä¿çæªå ï¼ |
| | | - æµè§å¨å®æµï¼Playwright 1.57 + æ¬æº Chromeï¼æ 头ï¼ï¼çå®ç¹å»ãæ¥è¡¨çæã页æé® |
| | | - è¯æ®æªå¾ï¼`docs/åè½æµè¯æ¥å/æ±æ»å¤§è¡¨å¼æ¥å¯¼åºè¿åº¦å¼¹çª_2026-09-20.png` |
| | | |
| | | ## ä¸ãç¨ä¾è¡¨ |
| | | |
| | | ### 3.1 æ¥å£å± |
| | | |
| | | | ç¼å· | ç¨ä¾ | è¾å
¥ | 颿 | å®é
| ç»è®º | |
| | | | --- | --- | --- | --- | --- | --- | |
| | | | T-A1 | æäº¤å¼æ¥ä»»å¡ | `GET /api/report/exportTask/summaryWorkbook?period=2026-08&mode=month` | ç«å³è¿å taskIdï¼ä¸é»å¡ | æäº¤èæ¶ **0.07s**ï¼è¿åº¦ 6â22â45â70â95â100ï¼ç»æ DONEï¼**14.4s** | éè¿ | |
| | | | T-A2 | ä¸è½½ä»»å¡æä»¶ | `GET /api/report/exportTask/{id}/file` | è¿å xlsx | 1,325,109 åèï¼**1.27s** | éè¿ | |
| | | | T-A3 | 10 åéå
åæ¥è¡¨æå¤ç¨ | 忬¡æäº¤ååæ° | fromCache=trueãå taskIdãè¿ä¹ç¬æ¶ | fromCache=trueï¼**0.01s**ï¼taskId ç¸å | éè¿ | |
| | | | T-A4 | 强å¶éç® | æäº¤ `refresh=true` | æ°ä»»å¡å·ãçå®éç® | æ° taskIdï¼14.2s éç®å®æ | éè¿ | |
| | | | T-A5 | è¿è¡ä¸å¹¶åå»é | è¿å两次 `refresh=true` | è¿ååä¸ taskId | åä¸ taskIdï¼11.1s | éè¿ | |
| | | | T-A6 | æ°å¢å¯¼å
¥ä½¿ç¼å失æ | ä¸´æ¶æå
¥ä¸æ¡ `import_batch`ï¼id 533â534ï¼åæäº¤ | fromCache=falseï¼éæ°çæ | fromCache=falseï¼11.4s éç®ï¼**æµè¯åå·² DELETE æ¸
å id=533** | éè¿ | |
| | | | T-A7 | è´åï¼ä¸åå¨ç taskId | `GET /exportTask/notexist123` / `.../file` | è¿åæç¡®é误 | ç¶ææ¥å£ `code=500`ã导åºä»»å¡ä¸åå¨æå·²è¿æï¼çæç»æä¿ç 30 åéï¼ãï¼æä»¶æ¥å£ HTTP 500 + åæç¤º | éè¿ | |
| | | | T-A8 | è´åï¼æªå®æå°±ä¸è½½ | ä»»å¡ RUNNING æ¶è¯·æ± `/file` | æç»å¹¶è¯´æ | æã导åºä»»å¡å°æªå®æï¼å½åç¶æï¼RUNNINGï¼ã | éè¿ | |
| | | |
| | | ### 3.2 æµè§å¨å®æµï¼çå®ç¹å»ï¼ |
| | | |
| | | | ç¼å· | ç¨ä¾ | è¾å
¥ | 颿 | å®é
| ç»è®º | |
| | | | --- | --- | --- | --- | --- | --- | |
| | | | UI-1 | è¿åº¦å¼¹çª | æ¥è¡¨çæ â æ±æ»å¤§è¡¨ â 2026-08 â ç¹ã导åºã | ç«å³å¼¹çªå¹¶éæ¡£æ¨è¿ | å¼¹çªå³æ¶åºç°ï¼å®æµéæ ·ï¼0.7s è¯»åæ¯çå·¥ä½ç°¿(6%) â 1.5s åå¡«å½ææ°æ®(22%) â 2.2s å·æ°æå页ä¸è·¨é¡µåæé¡µ(45%) â 7.2s éç®æ´æ¬å
¬å¼ï¼çº¦ 4 䏿 ¼ï¼(70%) â 10.1s ååºæä»¶(95%) â 11.6s 宿(100%) | éè¿ | |
| | | | UI-2 | 宿åèªå¨ä¸è½½ | åä¸ | èªå¨è§¦åä¿å | ä¸è½½ `çæ_éè·¯è¿è¾éæ±æ»è¡¨_2026-08.xlsx`ï¼**1,325,109 åè**ï¼ä¸æ¥å£äº§ç©ååå¸ | éè¿ | |
| | | | UI-3 | ç¼åå½ä¸æç¤º | å
³éå忬¡ç¹ã导åºã | æç¤ºæ¥æºæ¶é´ + ãä¸è½½ãæé® | ã䏿¬¡çæç¨æ¶ 11.3 ç§ããæ¬æ¬¡ç´æ¥ä½¿ç¨ 2026-09-20 15:43:50 çæçç»æï¼10 åéå
䏿鴿²¡ææ°å¯¼å
¥æ°æ®ï¼ãï¼æé®åãä¸è½½ãï¼ç§çº§å®æ | éè¿ | |
| | | | UI-4 | ãéæ°çæã | ç¼åæç¹ãéæ°çæã | çå®éç® | è¿åº¦ä» 0 éæ°æ¨è¿ï¼ãå·²ç¨ 2.1 ç§ãï¼ï¼æé®åºååãä¿åæä»¶/éæ°çæ/åå°è¿è¡ãï¼23.4s å
宿并èªå¨ä¸è½½ | éè¿ | |
| | | | UI-5 | ãåå°è¿è¡ã | çæä¸ç¹ãåå°è¿è¡ã | æ¶èµ·å¼¹çªãä»»å¡ç»§ç»ã宿ä»èªå¨ä¸è½½ | å¼¹çªæ¶èµ· + æç¤ºã已转å
¥åå°çæï¼å®æåä¼èªå¨ä¸è½½ãï¼éåèªå¨ä¸è½½ 1,325,109 åè | éè¿ | |
| | | | UI-6 | è´åï¼æ æ°æ®æ¥è¡¨æ | é 2025-01 â å¯¼åº â 确认ãä»è¦å¯¼åºã | æ¾ç¤ºå¤±è´¥åå ãå¯å
³éãä¸å¯ä¿å | å¼¹çªæ¾ç¤ºãçæå¤±è´¥ï½æªæ¾å°æ¨¡æ¿æä»¶: 2025å¹´1æéè·¯è¿è¾éæ±æ»è¡¨.xlsxãï¼X å¯å
³éï¼ãä¿åæä»¶ãç¦ç¨ï¼ãéæ°çæãå¯ç¨ | éè¿ | |
| | | | UI-7 | 产ç©ä¸è´æ§ | å·å¯å¨ä»¶ vs å·æ°éç®ä»¶ | æ°æ®ä¸è´ | 两次产ç©è§£å
å**éæ¡ç®ä¸è´**ï¼æ 差弿¡ç®ï¼ï¼ä¸ä»½ç¼åä¸è½½ä»¶ SHA256 å®å
¨ç¸å | éè¿ | |
| | | |
| | | ### 3.3 顺æå¤éªï¼ç¨æ·é®é¢ 2ï¼ |
| | | |
| | | | ç¼å· | ç¨ä¾ | è¾å
¥ | 颿 | å®é
| ç»è®º | |
| | | | --- | --- | --- | --- | --- | --- | |
| | | | P-1 | é¢è§åæ¯é¡¹æ¯å¦è¡¥å
¨ | æ±æ»å¤§è¡¨é¢è§å¼¹çª | 8 项忝é½å¨ | 12 è¡ï¼è´§è¿/åå¸å®¢è¿/ä¸å£å¾ Ã å½æÂ·ç´¯è®¡ à é·å¨è½¬ï¼ï¼åæ¯åé½å
¨ | éè¿ | |
| | | | P-2 | ã累计货ç©å¨è½¬éÂ·ç´¯è®¡åæ¯ 0%ãæ¯å¦ä¸º bug | æ¥åº `scale_split_transport` | åºä¸ºåæ°æºæ¬èº«çå¼ | åºå
`total_yoy=0.00000346`ï¼çº¦ 0.0003%ï¼â ä¸ä½å°æ°åèäºå
¥å³ **0%**ï¼é计ç®é误 | éè¿ï¼é缺é·ï¼ | |
| | | |
| | | ## åãåç°çé®é¢ä¸å¤ç½® |
| | | |
| | | 1. **`ReportExport.vue` åºç°éå¤ç `computed` é®ï¼æ¬æ¬¡æµè§å¨å®æµåç°ï¼å±ä¸¥éåå½ï¼** |
| | | æ°å¢ç弿¥å¯¼åºè®¡ç®å±æ§ï¼taskRunning/taskDone/taskCacheTimeï¼å¦èµ·äºä¸ä¸ª `computed: {`ï¼ |
| | | JS 对象åé¢é**ååé®åè
è¦çåè
**ï¼å¯¼è´åæç `username / realName / myModules / visibleModules` 计ç®å±æ§å
¨é¨ä¸¢å¤±ï¼ |
| | | ãæ¥è¡¨çæã页 `created()` ç´æ¥æ `Cannot read properties of undefined (reading 'filter')`ï¼**æ´é¡µç½å±ä¸å¯ç¨**ã |
| | | â å·²æ 4 个计ç®å±æ§å¹¶å
¥å `computed` åï¼å¹¶å 注é说æä¸å¯å¦èµ·åå keyï¼ï¼ç¼è¯éè¿å¹¶æµè§å¨å¤éªæ£å¸¸ã |
| | | ï¼æ¥å£å±æµè¯æ æ³åç°æ¤ç±»é®é¢ï¼è¿ä¹æ¯æ¬æ¬¡åæåæµè§å¨ç¹å»å®æµçåå ãï¼ |
| | | 2. **æäº¤/轮询失败æ¶å¼¹çªä¼ç©ºè½¬**ï¼å端ç»ä¸è¿å `Result`ï¼éè¯¯æ¶ HTTP ä» 200ã`data` 为空ï¼ï¼ |
| | | å代ç ç´æ¥ `this.taskInfo = res.data || {}` ä¼å¾å°ä¸ä¸ªç©ºå¯¹è±¡ â è¿åº¦åå¨ 0% ä¸ä¸ç»æç¤ºã |
| | | â æäº¤ä¸è½®è¯¢é½æ¹ä¸ºå
å¤ `res.code !== 200 || !res.data`ï¼æäº¤å¤±è´¥ç«å³æ¾ç¤ºå¤±è´¥åå ï¼è½®è¯¢å¤ä¸ºãä»»å¡å·²å¤±æãå¹¶åæ¢è½®è¯¢ã |
| | | 3. **çæä¸å¼¹çªæ åºå£**ï¼åãå
³éãæé® `:disabled="!taskDone"`ï¼çæä¸ä¸ä¸ªæé®å
¨ç°ãå³ä¸è§ X ä¹éèï¼ç¨æ·è¢«éå¨å¼¹çªéç 10~20 ç§ã |
| | | â æ¹ä¸ºçæä¸æ¾ç¤ºãåå°è¿è¡ãï¼æ¶å¼¹çªãç»§ç»çæã宿èªå¨ä¸è½½ï¼ï¼ç»æåææ¾ç¤ºãå
³éãï¼X 卿åæå¤±è´¥ååå¯è§ã |
| | | 4. **ç¼åæææ¡æ§ä¹**ï¼å½ä¸ç¼åæ¶å
ä¿¡æ¯ä»åãå·²ç¨ 11.3 ç§ãï¼å
¶å®æ¯ä¸æ¬¡çæèæ¶ï¼ã |
| | | â ç¼åææ¹ä¸ºã䏿¬¡çæç¨æ¶ãï¼é¿å
è¯¯ä»¥ä¸ºæ¬æ¬¡ä¹ç®äº 11.3 ç§ã |
| | | |
| | | ## äºãç»è®º |
| | | |
| | | - æ¥å£ç¨ä¾ 8 æ¡ãæµè§å¨å®æµ 7 æ¡ãå¤éª 2 æ¡ï¼**å
¨é¨éè¿**ï¼æ éçæªéªè¯é¡¹ã |
| | | - æ±æ»å¤§è¡¨å¯¼åºç°å¨æ¯ãç¹å»å³åé¦ â è¿åº¦å¯è§ â 宿å³ä¸è½½ãï¼ç¼åå½ä¸æ¶ç§çº§è¿åï¼å䏿¥è¡¨æå¹¶åèªå¨å»éï¼çæä¸å Web 线ç¨ã |
| | | - é带修å¤äºä¼å¯¼è´æ¥è¡¨çæé¡µæ´é¡µç½å±çéå¤ `computed` åå½ï¼è¯¥é®é¢å¨æ¥å£æµè¯ä¸ä¸å¯è§ï¼ã |
| | |
| | | å¹¶æ°å¢ãå¯¼åºæç»ãæé®ï¼CSVï¼å¸¦ BOMï¼å¯ç´æ¥ Excel æå¼ï¼ï¼ç¨äºç¨æ·èªè¡æ ¸å¯¹/ä¿®æ¹ä¸ä¸è´å
容ã |
| | | - è¦ç 4 å¤ï¼æ¹é导å
¥ç»æãå¸å·å表æ¹é导å
¥ç»æãåæä»¶å¯¼å
¥å¤±è´¥ãæèµä¸æèµç³»ç»ä¸ä¸è´æ¸
åã |
| | | |
| | | ### 1.7 æ±æ»å¤§è¡¨å¯¼åºå¼æ¥å + ç»æç¼åï¼ç¨æ·æé®ãç¹ä¸è½½è¦çä¸ä¼ï¼æ°æ®å¤äºä¼ä¸ä¼å¡æ»ãï¼ |
| | | |
| | | - **ç°è±¡**ï¼æ±æ»å¤§è¡¨ç¹ã导åºãåè¦ç 10~20 ç§æè§¦åä¸è½½ï¼æé´é¡µé¢æ ä»»ä½åé¦ï¼åæ¥è¯·æ±ä¸ç´å ç Web 线ç¨ã |
| | | - **å®ä½**ï¼14 ç§éæ¯ç读å+åå¡« 6.5sãæ´æ¬çº¦ 4 䏿 ¼å
¬å¼éç® 4.1sãåçä¼ è¾ 3.4sââèæ¶æ¥èª Excel å¤çï¼**䏿°æ®åºè¡æ°å
³ç³»ä¸å¤§**ï¼æ°æ®å¤äºä¸»è¦æ¯ SQL èååæ
¢ï¼ä¸ä¼ã塿»ãã |
| | | - **æ¹å¨**ï¼ |
| | | 1. æ°å¢ `ReportExportTaskService`ï¼åºå® 1 çº¿ç¨æ± daemonï¼ï¼æäº¤å³è¿å taskIdï¼å端 700ms è½®è¯¢ãæ¥éª¤ + ç¾åæ¯ + å·²ç¨æ¶é¿ãï¼ç»æä¿ç 30 åéï¼å
åæï¼è¿ç¨éå¯å¤±æï¼ã |
| | | 2. **ç»æç¼å 10 åé + æ°æ®çæ¬å®å«**ï¼çæ¬å `SELECT IFNULL(MAX(id),0) FROM import_batch`ï¼æé´åªè¦ææ°å¯¼å
¥å°±èªå¨å¤±æï¼åä¸å°çæ¬æ¶ç¦ç¨ç¼åï¼ï¼å䏿¥è¡¨æå¹¶åæäº¤èªå¨å»éãå¤ç¨åä¸ä»»å¡ã |
| | | 3. `ReportExportService` æ°å¢å¸¦è¿åº¦åè°ç `exportSummaryWorkbook(period, mode, toMonth, progress)` éè½½ï¼å两个éè½½å§æå®ï¼ï¼è¿åº¦ç¹ 2%/6%/12%/22%/45%/70%/95%ã |
| | | 4. `ReportExportController` æ°å¢ 3 个æ¥å£ï¼`/exportTask/summaryWorkbook`ï¼æäº¤ï¼ã`/exportTask/{taskId}`ï¼è¿åº¦ï¼ã`/exportTask/{taskId}/file`ï¼ä¸è½½ï¼ï¼å忥æ¥å£ä¿çå
¼å®¹ã |
| | | 5. å端 `ReportExport.vue` è¿åº¦å¼¹çªï¼è¿åº¦æ¡ + æ¥éª¤ + ç¨æ¶ + ç¼åæ¥æºæ¶é´ + 失败åå ï¼ä»¥åãä¿åæä»¶ / éæ°çæ / åå°è¿è¡ / å
³éãã |
| | | - **宿µ**ï¼æäº¤ 0.07sï¼è¿åº¦ 6â22â45â70â95â100ã14.4s 宿ï¼ç¼åå½ä¸ 0.01sï¼`refresh=true` çå®éç®ï¼ä¸´æ¶æä¸æ¡å¯¼å
¥åç¼åèªå¨å¤±æã |
| | | - **åæ¶ä¿®æä¸ä¸ªä¸¥éåå½**ï¼æ¬æ¬¡æ°å¢ç `computed` 䏿件ä¸å·²æ `computed` **ååéå¤**ï¼JS 对象åè
è¦çåè
â `username/myModules/visibleModules` å
¨é¨ä¸¢å¤±ï¼ |
| | | ãæ¥è¡¨çæã页 `created()` ç´æ¥æéã**æ´é¡µç½å±**ãæ¥å£æµè¯åç°ä¸äºï¼æ¯æµè§å¨å®æµææ´é²çï¼è¯¦è§æµè¯æ¥å第åèï¼ã |
| | | |
| | | ## äºãæµè¯ç»ææè¦ |
| | | |
| | | ãåè½æµè¯æ¥å_æèµæç»è¡¨å°±ç»ªä¸é¢è§ç±»åè¡¥é½_2026-09-20.mdãï¼**7 æ¡ç¨ä¾éè¿ï¼1 æ¡æªå®æµ**ï¼ç¼ºæ¯çæç¤ºï¼æªä¸´æ¶ç§»å¨ç¨æ·æ¯çæä»¶ï¼ã |
| | |
| | | | å
¶å®æèµæ¥è¡¨åå½ | investBillion/County/FiveYearLogistics å 136 è¡ãä¸¤å¼ æç»è¡¨ 39/97 è¡ï¼ready=trueï¼åå¯å¯¼åº | |
| | | | è´å | `types=foo` â ready=falseï¼noteãæªç¥æ¥è¡¨ç±»åï¼fooã | |
| | | |
| | | ãåè½æµè¯æ¥å_æ±æ»å¤§è¡¨å¼æ¥å¯¼åºä¸ç¼å_2026-09-20.mdãï¼**17 æ¡ç¨ä¾éè¿ï¼æ¥å£ 8 + æµè§å¨å®æµ 7 + å¤éª 2ï¼**ã |
| | | |
| | | | æ ¡éªé¡¹ | ç»æ | |
| | | | --- | --- | |
| | | | æäº¤å¼æ¥ä»»å¡ | 0.07s è¿å taskIdï¼è¿åº¦ 6â22â45â70â95â100ï¼14.4s 宿 | |
| | | | 任塿件ä¸è½½ | 1,325,109 åèï¼1.27s | |
| | | | 10 åéå
ç¼åå¤ç¨ | fromCache=trueãå taskIdã0.01s | |
| | | | refresh=true 强å¶éç® | æ°ä»»å¡å·ï¼çå®éç® 14.2s | |
| | | | è¿è¡ä¸å¹¶åå»é | 两次æäº¤è¿ååä¸ taskIdï¼11.1sï¼ | |
| | | | æ°å¢å¯¼å
¥ä½¿ç¼å失æ | import_batch id 533â534 å fromCache=falseï¼æµè¯åå·² DELETE æ¸
å 533ï¼ | |
| | | | è´åï¼ä¸åå¨ / æªå®æ taskId | ã导åºä»»å¡ä¸åå¨æå·²è¿æããå°æªå®æï¼å½åç¶æï¼RUNNINGï¼ã | |
| | | | æµè§å¨å®æµ | è¿åº¦å¼¹çªéæ ·ãèªå¨ä¸è½½ãç¼åæç¤ºãéæ°çæãåå°è¿è¡ã失败æç¤º 6 项å
¨éè¿ | |
| | | | 产ç©ä¸è´æ§ | ç¼åä»¶ä¸éç®ä»¶è§£å
åéæ¡ç®ä¸è´ï¼ä¸ä»½ç¼åä»¶ SHA256 ç¸å | |
| | | | éå¸¦ä¿®å¤ | `computed` éå¤é®å¯¼è´æ¥è¡¨çæé¡µæ´é¡µç½å±ï¼å·²ä¿®å¤å¹¶æµè§å¨å¤éªï¼ | |
| | | | å¤éªï¼ç¨æ·é®é¢ 2ï¼ | é¢è§åæ¯ 12 è¡é½å
¨ï¼ã累计货ç©å¨è½¬é 0%ãç»æ¥åºä¸ºåæ°æºæ¬èº« `total_yoy=0.00000346`ï¼éç¼ºé· | |
| | | |
| | | ## ä¸ãéçé®é¢ä¸å¾
å |
| | | |
| | | 1. æ¦æ±ç©æµé¡¹ç®æ¸
åå£å¾ï¼å·²ç¡®è®¤å¿½ç¥ï¼ã |
| | |
| | | 4. ~~C ç»ï¼åå¸å®¢è¿é¡µç»æå½ä¸ï¼éç¨æ·å
å¨ WPS æ´åå é¤ AJ åï¼~~ **已宿**ï¼æ¹ç¨ Excel COM æ´åå é¤ + å
¬å¼è¿åï¼ï¼ |
| | | D ç»ï¼å¢éæåæªè¾ï¼ï¼B4ï¼å¯¼åºååºæ°ä½æ£ + å¼¹çª + ãæ°æ®ç¼ºå¤±æ¸
åã页ï¼å¾
ææã |
| | | 5. 缺æ¯çæç¤ºï¼T08ï¼æªå®æµï¼å¯å¨ææµè¯å¯æ¬æ¶è¡¥æµã |
| | | 6. æ±æ»å¤§è¡¨å¯¼åºæ¯**åæ¥è¯·æ±**ï¼å½å约 14 ç§ï¼æ¯æ¬¡é½è¦éç®æ´æ¬çº¦ 4 䏿 ¼å
¬å¼ï¼ï¼æ°æ®éåæ¶¨ä¸»è¦æ¯ SQL èååæ
¢ï¼ |
| | | ä¸ä¼ã塿»ãï¼ä½å»ºè®®åç»åæå¼æ¥ä»»å¡ + è¿åº¦æç¤ºã |
| | | 7. é¨ç½²å
`deploy/traffic-audit-deploy-20260913/backend/docs/çææ±æ»å¤§è¡¨/` ä¸ `_deploytest/` ä¸ä»æ¯**带 AJ åçæ§æ¯ç**ï¼ |
| | | 䏿¬¡æå
æ¶éåæ¥ï¼æªç»ç¨æ·ç¡®è®¤ä¸æ
èªæ¹é¨ç½²äº§ç©ï¼ã |
| | | 6. ~~æ±æ»å¤§è¡¨å¯¼åºæ¯åæ¥è¯·æ±ï¼å»ºè®®åæå¼æ¥ä»»å¡ + è¿åº¦æç¤º~~ **已宿**ï¼è§ 1.7ï¼ãç¼åæ¯å
åæï¼è¿ç¨éå¯å³å¤±æï¼å¦éè·¨éå¯/å¤äººå
±ç¨çã导åºè®°å½ã页å¯åè¡¥ã |
| | | 7. é¨ç½²å
`deploy/traffic-audit-deploy-20260913/backend/docs/çææ±æ»å¤§è¡¨/` ä¸ `_deploytest/` ä¸ä»æ¯**带 AJ åçæ§æ¯ç** ââ |
| | | ç¨æ· 2026-09-20 æç¡®ãé¨ç½²å
å
ä¸ç®¡ãï¼**æä¸å¤ç**ï¼å¾
è¦åå
æ¶å忥ï¼ã |
| | | 8. ä¸å£å¾æç» 4 æ ¼ï¼åé
å乡巡游åºç§/ç½çº¦è½¦ï¼å 2025å¹´8æåºæ°ä¸º 0ï¼åæ¯æ¾ç¤º `#DIV/0!`ï¼ä¸å¯æ¯ï¼ï¼å¦éæ¹ç©ºç½éç¨æ·ç¡®è®¤å£å¾ã |
| | | |
| | | ## åãææ¥è®¡å |
| | |
| | | 1. ç天鍿ºæ°æ® / èå·é»ç³å£å¾ç¡®è®¤å补导并å¤éªã |
| | | 2. æ¨è¿ C/D ç»ä¸ B4ã |
| | | 3. ä¸ç¨æ·ç¡®è®¤æ¯å¦æ 09-18 å¤é´èµ·çæ¬å°æäº¤ä¸å¹¶æ¨éå°å
¬å¸ giteaï¼ç¨æ· 09-19 æ¾è¦æ±ãä»å¤©å
䏿¨å
¬å¸ãï¼ã |
| | | 4. è¯ä¼°æ±æ»å¤§è¡¨å¯¼åºå¼æ¥åï¼é¿ä»»å¡è¿åº¦æ¡ / ç»æç¼åï¼ï¼å¹¶ç¡®è®¤é¨ç½²å
æ¯ç忥æ¹å¼ã |
| | | 4. ~~è¯ä¼°æ±æ»å¤§è¡¨å¯¼åºå¼æ¥å~~ **已宿**ï¼è§ 1.7ï¼å¼æ¥ä»»å¡ + è¿åº¦å¼¹çª + 10 åéç»æç¼å + æ°æ®çæ¬å®å«ï¼ï¼é¨ç½²å
æ¯çæç¨æ·è¦æ±æä¸å¤çã |
| | | |
| | | 9. **8 æçåå °æ±æ»è¡¨æªæ¸
ç**ï¼ç¨æ·åªæ¸
äº 7 æçï¼108032â90112 åèï¼ï¼8 æçï¼mtime 2026-08-20ï¼ä»å«æ¦æ± 27 + é»ç³ 4 æ®µï¼ |
| | | è¥é导 8 æç©æµï¼å·²å é¤ç 31 个项ç®ä¼éæ°å»ºæ¡£ â é导åéåæ ·æ¸
çï¼æç±æä»£åï¼å¾
ç¨æ·ç¡®è®¤ï¼ã |
| | |
| | | |
| | | import com.trafficaudit.common.Result; |
| | | import com.trafficaudit.reportexport.service.ReportExportService; |
| | | import com.trafficaudit.reportexport.service.ReportExportTaskService; |
| | | import com.trafficaudit.system.service.OperationLogService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | 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.PathVariable; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | |
| | | |
| | | @Resource |
| | | private ReportExportService reportService; |
| | | @Resource |
| | | private ReportExportTaskService exportTaskService; |
| | | @Resource |
| | | private OperationLogService operationLogService; |
| | | |
| | |
| | | @RequestParam(value = "toMonth", required = false) Integer toMonth) throws Exception { |
| | | logExport("éè·¯è¿è¾éæ±æ»è¡¨.xlsx", period); |
| | | return buildResponse(reportService.exportSummaryWorkbook(period, mode, toMonth), String.format("çæ_éè·¯è¿è¾éæ±æ»è¡¨_%s.xlsx", period)); |
| | | } |
| | | |
| | | /** |
| | | * æ±æ»å¤§è¡¨å¼æ¥å¯¼åºï¼ç«å³è¿åä»»å¡å·ï¼å端轮询è¿åº¦ã宿ååä¸è½½æä»¶ã |
| | | * çæä¸æ¬¡çº¦ 10~20 ç§ï¼èµ°å¼æ¥å¯é¿å
ç¹å»åé¿æ¶é´æ ååºï¼åæ¥è¡¨æ 10 åéå
çç»æç´æ¥å¤ç¨ï¼æé´æå¯¼å
¥åèªå¨å¤±æï¼ï¼ |
| | | * refresh=true å¯å¼ºå¶éæ°çæã |
| | | */ |
| | | @GetMapping("/exportTask/summaryWorkbook") |
| | | public Result<java.util.Map<String, Object>> submitSummaryWorkbookTask(@RequestParam("period") String period, |
| | | @RequestParam(value = "mode", defaultValue = "month") String mode, |
| | | @RequestParam(value = "toMonth", required = false) Integer toMonth, |
| | | @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) { |
| | | ReportExportTaskService.Task task = exportTaskService.submit( |
| | | ReportExportTaskService.TYPE_SUMMARY_WORKBOOK, period, mode, toMonth, refresh); |
| | | if (!task.isFromCache()) { |
| | | logExport("éè·¯è¿è¾éæ±æ»è¡¨.xlsx", period); |
| | | } |
| | | return Result.ok(task.toView()); |
| | | } |
| | | |
| | | /** æ¥è¯¢å¼æ¥å¯¼åºä»»å¡è¿åº¦ï¼ç»æä¿ç 30 åéï¼ */ |
| | | @GetMapping("/exportTask/{taskId}") |
| | | public Result<java.util.Map<String, Object>> exportTaskStatus(@PathVariable("taskId") String taskId) { |
| | | ReportExportTaskService.Task task = exportTaskService.get(taskId); |
| | | if (task == null) { |
| | | return Result.error("导åºä»»å¡ä¸åå¨æå·²è¿æï¼çæç»æä¿ç 30 åéï¼"); |
| | | } |
| | | return Result.ok(task.toView()); |
| | | } |
| | | |
| | | /** ä¸è½½å¼æ¥å¯¼åºä»»å¡å·²çæçæä»¶ */ |
| | | @GetMapping("/exportTask/{taskId}/file") |
| | | public ResponseEntity<byte[]> exportTaskFile(@PathVariable("taskId") String taskId) throws Exception { |
| | | ReportExportTaskService.Task task = exportTaskService.get(taskId); |
| | | if (task == null) { |
| | | throw new RuntimeException("导åºä»»å¡ä¸åå¨æå·²è¿æï¼çæç»æä¿ç 30 åéï¼"); |
| | | } |
| | | if (!task.isDone()) { |
| | | throw new RuntimeException("导åºä»»å¡å°æªå®æï¼å½åç¶æï¼" + task.getStatus() + "ï¼ï¼è¯·ç¨ååè¯"); |
| | | } |
| | | return buildResponse(task.getData(), task.getFileName()); |
| | | } |
| | | |
| | | /** çæ_ç½çº¦è½¦å®¢è¿éæå表.xlsxï¼æåº¦ï¼è®¢åéä»¶1 + å
¨ç pin + 巡游åºç§ææ¥ï¼ */ |
| | |
| | | .body(data); |
| | | } |
| | | } |
| | | |
| | |
| | | * ä¿çå
¨é¨é¡µç¾/çå¼/å
¬å¼/2025 å¹´ç¼åå¼ï¼ä»
æè¡¨å¤´å¹´ä»½å¨æåä¸ºç®æ å¹´ï¼æ°æ®å¡«å
æ P2 èµ·éæ¥æ¥å
¥ã |
| | | */ |
| | | public byte[] exportSummaryWorkbook(String period, String mode) throws Exception { |
| | | return exportSummaryWorkbook(period, mode, null); |
| | | return exportSummaryWorkbook(period, mode, null, null); |
| | | } |
| | | |
| | | /** çæ_éè·¯è¿è¾éæ±æ»è¡¨.xlsxï¼P0-P1 骨æ¶ï¼æ¯æ toMonth è£åªï¼ï¼ |
| | |
| | | * toMonth é空ä¸å°äº M æ¶ï¼æ 8 个å°è´¦/åæé¡µè£å° toMonth æï¼toMonth+1..M ç彿å¼/忝忏
空ãä¿çè¡¨å¤´ï¼ |
| | | * 累计ä¸ç´¯è®¡åæ¯å
¬å¼æ¹ä¸ºæ toMonth æéç®ï¼ï¼ç¨äºâå
çæ 1-6 æã1-7 ææ©åâéªè¯ã */ |
| | | public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth) throws Exception { |
| | | return exportSummaryWorkbook(period, mode, toMonth, null); |
| | | } |
| | | |
| | | /** åä¸ï¼é¢å¤éè¿ progress åè°æ¥åè¿åº¦ï¼å¼æ¥å¯¼åºä»»å¡ç¨ï¼æ¥éª¤ç¾åæ¯ + æ¥éª¤ææ¡ï¼ */ |
| | | public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth, |
| | | java.util.function.BiConsumer<Integer, String> progress) throws Exception { |
| | | reportProgress(progress, 2, "æ ¡éªæ¥è¡¨æä¸æ¯ç"); |
| | | int year = Integer.parseInt(period.contains("-") ? period.split("-")[0] : period); |
| | | int month = period.contains("-") ? monthOf(period, mode) : 12; |
| | | File template = resolveSummaryMother(period, mode); |
| | |
| | | throw new RuntimeException("æªæ¾å°æ±æ»è¡¨æ¯çã" + year + "å¹´" + month + "æéè·¯è¿è¾éæ±æ»è¡¨.xlsxãï¼docs/çææ±æ»å¤§è¡¨ï¼"); |
| | | } |
| | | double savedZipRatio = ZipSecureFile.getMinInflateRatio(); |
| | | reportProgress(progress, 6, "è¯»åæ¯çå·¥ä½ç°¿"); |
| | | ZipSecureFile.setMinInflateRatio(0.0001); // æ°çæ¯ç styles.xml ç± openpyxl é«å缩ï¼0.0099<0.01ï¼è§¦å POI Zip-bomb 误æ¥ï¼è¯»ååè¿å |
| | | try (InputStream in = new FileInputStream(template); |
| | | XSSFWorkbook wb = new XSSFWorkbook(in)) { |
| | | reportProgress(progress, 12, "è¿ååææåº¦å"); |
| | | File donor = resolveSummaryDonor(year, month); |
| | | if (donor != null) { |
| | | try (InputStream din = new FileInputStream(donor); XSSFWorkbook dwb = new XSSFWorkbook(din)) { |
| | |
| | | int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month; |
| | | List<String> dbProblems = new ArrayList<>(); |
| | | String effPeriod = String.format("%04d-%02d", year, effMonth); |
| | | reportProgress(progress, 22, "åå¡«å½ææ°æ®"); |
| | | int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, effPeriod, year, effMonth, dbProblems); |
| | | int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth); |
| | | int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth); |
| | | // ç´¯è®¡åæ¯åæ¯ä¿®æ£ï¼æ¯çæãå»å¹´åæãä¸ãå»å¹´1-Mæç´¯è®¡ãè¾
å©ååæ¶è®¡å
¥ï¼éå¤è®¡/æ¼å½æï¼ |
| | | int cumYoyFixed = summaryWorkbookFiller.repairCumulativeYoyFormulas(wb, year, effMonth); |
| | | reportProgress(progress, 45, "å·æ°æå页ä¸è·¨é¡µåæé¡µ"); |
| | | int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month"); |
| | | int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // åå¼ è·¨é¡µåæé¡µéæ¥è¡¨æå·æ° |
| | | int widthApplied = applySummaryReferenceLayout(wb); |
| | |
| | | applyTwoDecimalFormat(wb); // æ°å¼æ¾ç¤ºä¸¤ä½å°æ°ï¼ä¿çå
¨ç²¾åº¦ï¼ï¼% / æ¥æçæ¢ææ ·å¼ä¸æ¹å |
| | | int inlineFixed = normalizeStaleInlineStrings(wb); // æ¸
æãé inlineStr å´æ®ç <is>ãçéæ³æ ¼ï¼å¦å Excel æç»æå¼ |
| | | if (inlineFixed > 0) log.warn("æ±æ»å·¥ä½ç°¿æ¸
ææ®ç inlineStr éæ³æ ¼ {} ä¸ªï¼æ¯çç± openpyxl ååºæ¶çå
¸åé®é¢ï¼", inlineFixed); |
| | | reportProgress(progress, 70, "éç®æ´æ¬å
¬å¼ï¼çº¦ 4 䏿 ¼ï¼"); |
| | | int[] recalc = recalcWorkbookFormulas(wb); // å
¬å¼éç®å¹¶ååç¼åå¼ï¼å
¨çè¡(Σå¸å·)/累计å/æå SUM æè½è¢«é¢è§ä¸äºæ¬¡å¤çè¯»å° |
| | | log.info("æ±æ»å·¥ä½ç°¿å
¬å¼éç®å®æï¼æå {} æ ¼ï¼å¤±è´¥ {} æ ¼ï¼å¤±è´¥æ ¼ä¿çæ¯çæ§ç¼åå¼ï¼æå¼æ¶æ fullCalcOnLoad éç®ï¼", recalc[0], recalc[1]); |
| | | if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr(); |
| | | wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // è¿å/æ¹åå
¬å¼åæå¼å³éç® |
| | | reportProgress(progress, 95, "ååºæä»¶"); |
| | | return toBytes(wb); |
| | | } finally { |
| | | ZipSecureFile.setMinInflateRatio(savedZipRatio); |
| | | } |
| | | } |
| | | |
| | | /** 导åºè¿åº¦åè°ï¼å¼æ¥å¯¼åºä»»å¡ç¨ï¼ï¼åè°èªèº«å¼å¸¸ä¸å½±åå¯¼åº */ |
| | | private static void reportProgress(java.util.function.BiConsumer<Integer, String> progress, int percent, String step) { |
| | | if (progress == null) return; |
| | | try { |
| | | progress.accept(percent, step); |
| | | } catch (Exception ignore) { |
| | | // è¿åº¦åè°å¤±è´¥ä¸å½±åå¯¼åº |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æ±æ»å·¥ä½ç°¿å
¬å¼éç®ï¼POI åæä»¶ä¸å¸¦ç¼åå¼ï¼å¯¼è´ãå
¨çè¡ï¼Î£17å¸å·ï¼ãã累计åããæå SUMã |
| | | * å¨é¢è§/äºæ¬¡å¤çéåªçå°æ¯çä¸ä¸æçæ§å¼ï¼2026-09-18 ç¨æ·æ¥â8ææ²¡æ°æ® / å
¨çè¡åå¨ä¸ä¸æâçæ ¹å ä¹ä¸ï¼ã |
| New file |
| | |
| | | package com.trafficaudit.reportexport.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.trafficaudit.dataimport.entity.ImportBatch; |
| | | import com.trafficaudit.dataimport.mapper.ImportBatchMapper; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.annotation.PreDestroy; |
| | | import javax.annotation.Resource; |
| | | import java.util.Date; |
| | | import java.util.Iterator; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.UUID; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.ExecutorService; |
| | | import java.util.concurrent.Executors; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | /** |
| | | * æ¥è¡¨å¯¼åºå¼æ¥ä»»å¡ï¼æèæ¶åå ç§çãçæ_éè·¯è¿è¾éæ±æ»è¡¨ãçææªå°åå°çº¿ç¨ï¼ |
| | | * åç«¯åªæ¿ãä»»å¡å· + è¿åº¦ï¼æ¥éª¤/ç¾åæ¯ï¼ãï¼å®æååç¬ä¸è½½æä»¶ã |
| | | * |
| | | * ç®çï¼2026-09-20 ç¨æ·ä¸æ¥ãç¹ä¸è½½è¦çä¸ä¼ï¼æ°æ®å¤äºä¼ä¸ä¼å¡æ»ãï¼ï¼ |
| | | * 1) ç¹å»åç«å»æåé¦ï¼ä¸åé¿æ¶é´æ ååºï¼ |
| | | * 2) çæè¿ç¨ä¸å ç¨ HTTP 请æ±çº¿ç¨ï¼é¿å
å¤äººåæ¶ç¹æå®¹å¨ææ»ï¼å·¥ä½çº¿ç¨åºå® 1 个ï¼å¤ä½è¯·æ±æ¾ç¤ºãæéä¸ãï¼ï¼ |
| | | * 3) å䏿¥è¡¨æ 10 åéå
çç»æå¯å¤ç¨ï¼é¿å
æ¯æ¬¡éç®æ´æ¬çº¦ 4 䏿 ¼å
¬å¼ã |
| | | * |
| | | * ç¼åæ£ç¡®æ§å®å«ï¼åªæå½ãè·ä¸æ¬¡çæä¸è¶
è¿ TTLãä¸ãæé´æ²¡æä»»ä½æ°çæ°æ®å¯¼å
¥ï¼import_batch æå¤§ id æªåï¼ã |
| | | * æ¶æå¤ç¨ï¼å¯¼å
¥å³å¤±æï¼é¿å
æè¿ææ¥è¡¨ç»ç¨æ·ãåç«¯ä»æä¾ãéæ°çæã强å¶å·æ°ã |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class ReportExportTaskService { |
| | | |
| | | /** æ±æ»å·¥ä½ç°¿å¯¼åºä»»å¡ç±»å */ |
| | | public static final String TYPE_SUMMARY_WORKBOOK = "summaryWorkbook"; |
| | | |
| | | /** çæç»æä¿çæ¶é¿ï¼æé´å¯åå¤ä¸è½½ï¼è¿åèªå¨æ¸
ç */ |
| | | private static final long RESULT_TTL_MS = 30 * 60 * 1000L; |
| | | /** å䏿¥è¡¨æç»æå¤ç¨æ¶é¿ï¼ä¸æé´æ æ°å¯¼å
¥æ¶æå¤ç¨ï¼ */ |
| | | private static final long CACHE_TTL_MS = 10 * 60 * 1000L; |
| | | |
| | | @Resource |
| | | private ReportExportService reportService; |
| | | @Resource |
| | | private ImportBatchMapper importBatchMapper; |
| | | |
| | | private final Map<String, Task> tasks = new ConcurrentHashMap<>(); |
| | | /** åæ¥è¡¨ææè¿ä¸æ¬¡æåç»æï¼key -> task */ |
| | | private final Map<String, Task> cache = new ConcurrentHashMap<>(); |
| | | private final ExecutorService pool = Executors.newFixedThreadPool(1, r -> { |
| | | Thread t = new Thread(r, "report-export-task"); |
| | | t.setDaemon(true); |
| | | return t; |
| | | }); |
| | | |
| | | /** 䏿¬¡å¯¼åºä»»å¡ï¼å
åæï¼è¿ç¨éå¯å³å¤±æï¼ */ |
| | | public static class Task { |
| | | private final String id = UUID.randomUUID().toString().replace("-", ""); |
| | | private final String type; |
| | | private final String period; |
| | | private final String mode; |
| | | private final Integer toMonth; |
| | | private final long createdAt = System.currentTimeMillis(); |
| | | /** çææ¶çæ°æ®çæ¬ï¼import_batch æå¤§ idï¼ï¼ç¨äºå¤æç¼åæ¯å¦è¿æ */ |
| | | private final long importVersion; |
| | | private volatile String status = "PENDING"; // PENDING/RUNNING/DONE/FAILED |
| | | private volatile int percent = 0; |
| | | private volatile String step = "æéä¸"; |
| | | private volatile String error; |
| | | private volatile byte[] data; |
| | | private volatile String fileName; |
| | | private volatile boolean fromCache; |
| | | private volatile long finishedAt; |
| | | |
| | | Task(String type, String period, String mode, Integer toMonth, long importVersion) { |
| | | this.type = type; |
| | | this.period = period; |
| | | this.mode = mode; |
| | | this.toMonth = toMonth; |
| | | this.importVersion = importVersion; |
| | | } |
| | | |
| | | void progress(int pct, String stepText) { |
| | | if (pct > this.percent) this.percent = Math.min(pct, 99); |
| | | if (stepText != null) this.step = stepText; |
| | | } |
| | | |
| | | public String getId() { return id; } |
| | | |
| | | public String getStatus() { return status; } |
| | | |
| | | public boolean isDone() { return "DONE".equals(status) && data != null; } |
| | | |
| | | public boolean isFromCache() { return fromCache; } |
| | | |
| | | public byte[] getData() { return data; } |
| | | |
| | | public String getFileName() { return fileName; } |
| | | |
| | | public String getError() { return error; } |
| | | |
| | | public Map<String, Object> toView() { |
| | | Map<String, Object> m = new LinkedHashMap<>(); |
| | | m.put("taskId", id); |
| | | m.put("type", type); |
| | | m.put("period", period); |
| | | m.put("mode", mode); |
| | | m.put("toMonth", toMonth); |
| | | m.put("status", status); |
| | | m.put("percent", "DONE".equals(status) ? 100 : percent); |
| | | m.put("step", step); |
| | | m.put("fromCache", fromCache); |
| | | m.put("fileName", fileName); |
| | | m.put("error", error); |
| | | long end = finishedAt > 0 ? finishedAt : System.currentTimeMillis(); |
| | | m.put("elapsedMs", end - createdAt); |
| | | m.put("finishedAt", finishedAt > 0 ? new Date(finishedAt) : null); |
| | | return m; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * æäº¤å¯¼åºä»»å¡ãforceRefresh=true æ¶å¿½ç¥ç¼åéæ°çæï¼ |
| | | * å¦åå¨ãå䏿¥è¡¨æã10 åéå
ãæé´æ æ°å¯¼å
¥ãçåæä¸ç´æ¥å¤ç¨ä¸æ¬¡ç»æã |
| | | * å䏿¥è¡¨æå·²æä»»å¡å¨è·æ¶ç´æ¥è¿å该任å¡ï¼é¿å
é夿éã |
| | | */ |
| | | public Task submit(String type, String period, String mode, Integer toMonth, boolean forceRefresh) { |
| | | cleanupExpired(); |
| | | String key = cacheKey(type, period, mode, toMonth); |
| | | if (!forceRefresh) { |
| | | Task hit = cache.get(key); |
| | | if (cacheUsable(hit)) { |
| | | hit.fromCache = true; |
| | | log.info("æ¥è¡¨å¯¼åºä»»å¡å½ä¸ç¼åï¼{} {}ï¼çæäº {}ï¼", type, period, new Date(hit.finishedAt)); |
| | | return hit; |
| | | } |
| | | } |
| | | Task running = findRunning(key); |
| | | if (running != null) { |
| | | log.info("æ¥è¡¨å¯¼åºä»»å¡å·²å¨æ§è¡ï¼å¤ç¨è¿è¡ä¸çä»»å¡ï¼{} {}ï¼{}ï¼", type, period, running.id); |
| | | return running; |
| | | } |
| | | Task task = new Task(type, period, mode, toMonth, importVersion()); |
| | | tasks.put(task.id, task); |
| | | pool.submit(() -> run(task)); |
| | | return task; |
| | | } |
| | | |
| | | public Task get(String taskId) { |
| | | if (taskId == null) return null; |
| | | cleanupExpired(); |
| | | return tasks.get(taskId); |
| | | } |
| | | |
| | | private void run(Task t) { |
| | | t.status = "RUNNING"; |
| | | t.step = "å¼å§çæ"; |
| | | long start = System.currentTimeMillis(); |
| | | try { |
| | | byte[] data = reportService.exportSummaryWorkbook(t.period, t.mode, t.toMonth, t::progress); |
| | | t.data = data; |
| | | t.fileName = "çæ_éè·¯è¿è¾éæ±æ»è¡¨_" + t.period + ".xlsx"; |
| | | t.step = "宿"; |
| | | t.status = "DONE"; |
| | | t.finishedAt = System.currentTimeMillis(); |
| | | cache.put(cacheKey(t.type, t.period, t.mode, t.toMonth), t); |
| | | log.info("æ¥è¡¨å¯¼åºä»»å¡å®æï¼{} {} ç¨æ¶ {} msï¼{} åè", t.type, t.period, t.finishedAt - start, data.length); |
| | | } catch (Throwable e) { |
| | | t.status = "FAILED"; |
| | | t.error = e.getMessage() == null ? e.toString() : e.getMessage(); |
| | | t.step = "çæå¤±è´¥"; |
| | | t.finishedAt = System.currentTimeMillis(); |
| | | log.error("æ¥è¡¨å¯¼åºä»»å¡å¤±è´¥ï¼{} {}ï¼ç¨æ¶ {} msï¼", t.type, t.period, t.finishedAt - start, e); |
| | | } |
| | | } |
| | | |
| | | private Task findRunning(String key) { |
| | | for (Task t : tasks.values()) { |
| | | if (!key.equals(cacheKey(t.type, t.period, t.mode, t.toMonth))) continue; |
| | | if ("PENDING".equals(t.status) || "RUNNING".equals(t.status)) return t; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private boolean cacheUsable(Task t) { |
| | | if (t == null || !"DONE".equals(t.status) || t.data == null) return false; |
| | | if (System.currentTimeMillis() - t.finishedAt > CACHE_TTL_MS) return false; |
| | | long now = importVersion(); |
| | | if (now < 0 || t.importVersion < 0) return false; // åä¸å°æ°æ®çæ¬æ¶ä¸åé£é©å¤ç¨ |
| | | return now == t.importVersion; |
| | | } |
| | | |
| | | /** æ°æ®çæ¬ï¼import_batch æå¤§ idï¼ä»»ä½ä¸æ¬¡å¯¼å
¥é½ä¼è®©å®ååï¼åä¸å°è¿å -1ï¼æ¤æ¶ç¦ç¨ç¼åï¼ */ |
| | | private long importVersion() { |
| | | try { |
| | | List<Object> v = importBatchMapper.selectObjs(new QueryWrapper<ImportBatch>().select("IFNULL(MAX(id),0)")); |
| | | if (v == null || v.isEmpty() || v.get(0) == null) return 0L; |
| | | return ((Number) v.get(0)).longValue(); |
| | | } catch (Exception e) { |
| | | log.warn("读å导å
¥æ°æ®çæ¬å¤±è´¥ï¼æ¬æ¬¡ç¦ç¨å¯¼åºç¼åï¼{}", e.getMessage()); |
| | | return -1L; |
| | | } |
| | | } |
| | | |
| | | private String cacheKey(String type, String period, String mode, Integer toMonth) { |
| | | return type + "|" + period + "|" + mode + "|" + (toMonth == null ? "" : toMonth); |
| | | } |
| | | |
| | | private void cleanupExpired() { |
| | | long now = System.currentTimeMillis(); |
| | | for (Iterator<Map.Entry<String, Task>> it = tasks.entrySet().iterator(); it.hasNext(); ) { |
| | | Task t = it.next().getValue(); |
| | | if (t.finishedAt > 0 && now - t.finishedAt > RESULT_TTL_MS) it.remove(); |
| | | } |
| | | for (Iterator<Map.Entry<String, Task>> it = cache.entrySet().iterator(); it.hasNext(); ) { |
| | | Task t = it.next().getValue(); |
| | | if (t.finishedAt <= 0 || now - t.finishedAt > CACHE_TTL_MS) it.remove(); |
| | | } |
| | | } |
| | | |
| | | @PreDestroy |
| | | public void shutdown() { |
| | | pool.shutdownNow(); |
| | | try { |
| | | pool.awaitTermination(3, TimeUnit.SECONDS); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | } |
| | | } |
| | | } |
| | |
| | | <el-button size="small" type="primary" @click="saveNote">ä¿å</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | |
| | | <!-- æ±æ»å¤§è¡¨å¼æ¥çæè¿åº¦ï¼çæä¸æ¬¡çº¦ 10~20 ç§ï¼é¿å
ç¹å»åé¿æ¶é´æ ååºï¼ --> |
| | | <el-dialog title="æ£å¨çææ±æ»å¤§è¡¨" :visible.sync="taskVisible" width="460px" |
| | | :close-on-click-modal="false" :show-close="taskFinished"> |
| | | <div class="task-body"> |
| | | <el-progress :percentage="taskInfo.percent || 0" :status="taskInfo.status === 'FAILED' ? 'exception' : (taskDone ? 'success' : null)" |
| | | :stroke-width="14"></el-progress> |
| | | <p class="task-step">{{ taskInfo.step || 'æéä¸' }}</p> |
| | | <p class="task-meta" v-if="taskInfo.period">æ¥è¡¨æ {{ taskInfo.period }}ã{{ taskInfo.fromCache ? '䏿¬¡çæç¨æ¶' : 'å·²ç¨' }} {{ (taskInfo.elapsedMs / 1000).toFixed(1) }} ç§</p> |
| | | <p class="task-meta" v-if="taskInfo.fromCache">æ¬æ¬¡ç´æ¥ä½¿ç¨ {{ taskCacheTime }} çæçç»æï¼10 åéå
䏿鴿²¡ææ°å¯¼å
¥æ°æ®ï¼ãå¦éæææ°æ°æ®éç®ï¼è¯·ç¹ãéæ°çæãã</p> |
| | | <p class="task-error" v-if="taskInfo.status === 'FAILED'">{{ taskInfo.error }}</p> |
| | | </div> |
| | | <span slot="footer"> |
| | | <el-button size="small" type="primary" :disabled="!taskDone" @click="downloadTaskFile">{{ taskInfo.fromCache ? 'ä¸è½½' : 'ä¿åæä»¶' }}</el-button> |
| | | <el-button size="small" :disabled="taskRunning" @click="restartTask">éæ°çæ</el-button> |
| | | <el-button size="small" v-if="taskRunning" @click="minimizeTask">åå°è¿è¡</el-button> |
| | | <el-button size="small" v-else @click="closeTask">å
³é</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | </el-main> |
| | | </el-container> |
| | | </el-container> |
| | |
| | | noteDialogVisible: false, |
| | | noteEditingKey: '', |
| | | noteDraft: '', |
| | | // æ±æ»å¤§è¡¨å¼æ¥çæä»»å¡ |
| | | taskVisible: false, |
| | | taskInfo: {}, |
| | | taskTimer: null, |
| | | taskKey: 'summaryWorkbook', |
| | | modules: [ |
| | | { |
| | | key: 'summary', title: 'æ±æ»å¤§è¡¨', items: [ |
| | |
| | | options: months.map(mo => ({ label: y + 'å¹´' + mo + 'æ', value: y + '-' + String(mo).padStart(2, '0') })) |
| | | })) |
| | | }, |
| | | // ===== 以ä¸ä¸ºæ±æ»å¤§è¡¨å¼æ¥çæå¼¹çªçæ´¾çç¶æï¼å¿
须并å
¥åä¸ä¸ª computedï¼å¦èµ·åå key ä¼è¦çæä¸é¢ç计ç®å±æ§ï¼===== |
| | | taskRunning() { |
| | | const s = this.taskInfo && this.taskInfo.status |
| | | return s === 'PENDING' || s === 'RUNNING' |
| | | }, |
| | | taskDone() { |
| | | return this.taskInfo && this.taskInfo.status === 'DONE' |
| | | }, |
| | | // çæç»æï¼æåæå¤±è´¥ï¼åæå
许æ¶èµ·å¼¹çª |
| | | taskFinished() { |
| | | const s = this.taskInfo && this.taskInfo.status |
| | | return s === 'DONE' || s === 'FAILED' |
| | | }, |
| | | taskCacheTime() { |
| | | const t = this.taskInfo && this.taskInfo.finishedAt |
| | | if (!t) return '䏿¬¡' |
| | | const d = new Date(t) |
| | | const p = (n) => String(n).padStart(2, '0') |
| | | return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds()) |
| | | } |
| | | }, |
| | | watch: { |
| | | activeKey() { |
| | |
| | | this.activeKey = (mine.length ? mine : this.visibleModules.map(m => m.key))[0] |
| | | this.ensureCycle() |
| | | this.refreshAll() |
| | | }, methods: { |
| | | }, |
| | | beforeDestroy() { |
| | | this.stopTaskPolling() |
| | | }, |
| | | methods: { |
| | | moduleTitle(key) { |
| | | const m = this.modules.find(x => x.key === key) |
| | | return m ? m.title : key |
| | |
| | | if (continuous) return '1~' + cov[cov.length - 1] + 'æåææ°æ®' |
| | | return cov.map(x => x + 'æ').join('ã') + ' ææ°æ®' |
| | | }, |
| | | // ==================== æ±æ»å¤§è¡¨å¼æ¥çæï¼è¿åº¦å¼¹çªï¼ ==================== |
| | | // å端 ReportExportTaskServiceï¼æäº¤ä»»å¡ -> 轮询è¿åº¦ -> 宿åä¸è½½æä»¶ã |
| | | // åæ¥è¡¨æ 10 åéå
䏿鴿²¡ææ°å¯¼å
¥æ°æ®æ¶ï¼åç«¯ç´æ¥è¿å䏿¬¡ç»æï¼å¼¹çªå
ä¼è¯´ææ¥æºæ¶é´ï¼ã |
| | | startSummaryTask(refresh) { |
| | | if (!this.period) { this.$message.warning('请å
鿩卿åæä»½'); return } |
| | | this.stopTaskPolling() |
| | | this.taskInfo = { status: 'PENDING', percent: 0, step: 'æäº¤ä»»å¡', period: this.period, elapsedMs: 0 } |
| | | this.taskVisible = true |
| | | this.loading = 'gen-' + this.curModule.key |
| | | const params = Object.assign({}, this.buildParams(), { refresh: refresh ? 'true' : 'false' }) |
| | | api.get('/report/exportTask/summaryWorkbook', { params, timeout: 60000 }) |
| | | .then(res => { |
| | | // å端ç»ä¸è¿å Resultï¼code é 200 æ¶ data 为空ï¼å¿
é¡»æç¤ºè䏿¯ç©ºè½¬ |
| | | if (!res || res.code !== 200 || !res.data || !res.data.taskId) { |
| | | this.taskInfo = { status: 'FAILED', percent: 0, step: 'æäº¤å¤±è´¥', period: this.period, elapsedMs: 0, |
| | | error: (res && res.message) || 'å端æªè¿åä»»å¡å·' } |
| | | this.loading = '' |
| | | return |
| | | } |
| | | this.taskInfo = res.data |
| | | if (this.taskInfo.status === 'DONE') { this.loading = ''; this.downloadTaskFile(); return } |
| | | if (this.taskInfo.status === 'FAILED') { this.loading = ''; return } |
| | | this.pollTask() |
| | | }) |
| | | .catch(err => { |
| | | this.stopTaskPolling() |
| | | this.loading = '' |
| | | this.taskVisible = false |
| | | this.failWith(err, 'çæ') |
| | | }) |
| | | }, |
| | | pollTask() { |
| | | this.stopTaskPolling() |
| | | const id = this.taskInfo && this.taskInfo.taskId |
| | | if (!id) return |
| | | this.taskTimer = setInterval(() => { |
| | | api.get('/report/exportTask/' + id, { timeout: 30000 }) |
| | | .then(res => { |
| | | if (!res || res.code !== 200 || !res.data) { |
| | | // ä»»å¡è¢«æ¸
ç/è¿æï¼åæ¢è½®è¯¢å¹¶ç»åºåå ï¼ä¸è®©å¼¹çªä¸ç´ç©ºè½¬ |
| | | this.stopTaskPolling() |
| | | this.loading = '' |
| | | this.taskInfo = Object.assign({}, this.taskInfo, { |
| | | status: 'FAILED', step: 'ä»»å¡å·²å¤±æ', |
| | | error: (res && res.message) || '导åºä»»å¡å·²å¤±æï¼è¯·éæ°çæ' |
| | | }) |
| | | return |
| | | } |
| | | this.taskInfo = res.data |
| | | if (this.taskInfo.status === 'DONE') { |
| | | this.stopTaskPolling() |
| | | this.loading = '' |
| | | this.downloadTaskFile() |
| | | } else if (this.taskInfo.status === 'FAILED') { |
| | | this.stopTaskPolling() |
| | | this.loading = '' |
| | | if (!this.taskVisible) this.$message.error('æ±æ»å¤§è¡¨çæå¤±è´¥ï¼' + (this.taskInfo.error || 'æªç¥åå ')) |
| | | } |
| | | }) |
| | | .catch(() => { /* 忬¡è½®è¯¢å¤±è´¥å¿½ç¥ï¼ä¸ä¸è½®ç»§ç» */ }) |
| | | }, 700) |
| | | }, |
| | | stopTaskPolling() { |
| | | if (this.taskTimer) { clearInterval(this.taskTimer); this.taskTimer = null } |
| | | }, |
| | | downloadTaskFile() { |
| | | const id = this.taskInfo && this.taskInfo.taskId |
| | | if (!id) return |
| | | const name = this.taskInfo.fileName || ('çæ_éè·¯è¿è¾éæ±æ»è¡¨_' + this.period + '.xlsx') |
| | | api.get('/report/exportTask/' + id + '/file', { responseType: 'blob', timeout: 120000 }) |
| | | .then(res => { this.downloadBlob(res, name); this.$message.success('æ¥è¡¨å¯¼åºæå') }) |
| | | .catch(err => this.failWith(err)) |
| | | }, |
| | | restartTask() { this.startSummaryTask(true) }, |
| | | closeTask() { this.taskVisible = false; this.stopTaskPolling(); this.loading = '' }, |
| | | // çæä¸æ¶èµ·å¼¹çªï¼ä»»å¡ç»§ç»å¨åå°æ§è¡ï¼å®æåä»ä¼èªå¨ä¸è½½ï¼ä¸åæ¥åæ¥å¯¼åºçãç¹å®å³ä¸è½½ãä¸è´ï¼ |
| | | minimizeTask() { |
| | | this.taskVisible = false |
| | | this.$message.info('已转å
¥åå°çæï¼å®æåä¼èªå¨ä¸è½½') |
| | | }, |
| | | exportChecked(itemList) { |
| | | const m = this.curModule |
| | | if (!this.period) { this.$message.warning('请å
鿩卿åæä»½'); return } |
| | |
| | | const params = this.buildParams() |
| | | const doExport = () => { |
| | | if (items.length === 1) { |
| | | // æ±æ»å¤§è¡¨çæä¸æ¬¡çº¦ 10~20 ç§ï¼èµ°å¼æ¥ä»»å¡ï¼è¿åº¦å¼¹çªï¼ï¼é¿å
ç¹å»åé¿æ¶é´æ ååº |
| | | if (items[0].key === 'summaryWorkbook') { this.startSummaryTask(false); return } |
| | | api.get('/report/export/' + items[0].key, { params, responseType: 'blob', timeout: 120000 }) |
| | | .then(res => { this.downloadBlob(res, this.fileNameOf(items[0])); this.$message.success('æ¥è¡¨å¯¼åºæå'); this.loading = '' }) |
| | | .catch(err => this.failWith(err)) |
| | |
| | | .wb-yoy { text-align:right; color:#606266; } |
| | | .wb-yoy.yoy-empty { color:#C0C4CC; } |
| | | .wb-note { color:#E6A23C; font-size:12px; margin:2px 0 8px; } |
| | | /* 弿¥çæè¿åº¦å¼¹çª */ |
| | | .task-body { padding: 4px 2px; } |
| | | .task-step { margin: 12px 0 4px; font-size: 14px; color:#303133; } |
| | | .task-meta { margin: 2px 0; font-size: 12px; color:#909399; line-height: 1.6; } |
| | | .task-error { margin: 8px 0 0; font-size: 13px; color:#F56C6C; word-break: break-all; } |
| | | </style> |
| | | |
| | | <style> |