feat(observation): 新增观测站分日调查数据导入与明细查看
| | |
| | | - **éªè¯**ï¼`HolidayBulletinServiceTest` 4 个ç¨ä¾éè¿ï¼æ°å¢ `cumulativeParagraphUsesWanUnitAndSingleRounding` åºåãä¸äººæ¬¡ + 䏿¬¡åæ´ãï¼ï¼éå¯å端åéæ°çæ 2026-10-02 å¿«æ¥ï¼æ£æä¸º `累计3088.63ä¸äººæ¬¡ï¼æ¯2025å¹´åæä¸é4.4%ï¼æ¯2024å¹´åæä¸é1.6%`ï¼ä¸æå·¥ç¨¿ä»
å·®æ¢å®çåæ´å£å¾ï¼2026-10-01 å彿£å¸¸ï¼`累计1643.17ä¸äººæ¬¡`ï¼ã |
| | | - **ç¶æ**ï¼å¾
ç¨æ·ãéªæ¶éè¿ãåæäº¤/æ¨éã |
| | | |
| | | ### 10-03 æï¼4ï¼ï¼æ°å¢ãè§æµç«åæ¥è°æ¥æ°æ®ãåå§æ°æ®å¯¼å
¥ï¼å¾
éªæ¶ï¼ |
| | | - **èæ¯ï¼ç¨æ· 2026-10-03 æåºï¼**ï¼æ®éå½çé车æµéçæ¥æºä¹ä¸æ¯è§æµç«åæ¥è°æ¥æ°æ®ï¼æ¨¡æ¿ `docs/ä¸é¡¹ç»è®¡/éè¦è忥/è§æµç«åæ¥è°æ¥æ°æ® (2026å¹´10æ1æ¥).xls`ï¼å
¶ä¸å段ãä¸å°å®¢æµéã峿®éå½çé车æµéæ¥æºï¼ã**ç³»ç»å
ä¸åå
³è**ï¼è忥ç»è®¡ä»èµ°åæå¯¼å
¥ï¼æ¬åè½åªååå§æ°æ®çåï¼ä¸ºåç»åæææç§¯ç´¯æ°æ®åºç¡ã |
| | | - **æ°æ®è¡¨**ï¼æ°å¢ `observation_station_flow`ï¼DDL è§ `docs/sql_observation_station.sql`ï¼é¨ç½²å
åæ¥ä¸º `deploy-templates/db/sql_observation_station.sql`ãä¿ç模æ¿å
¨é¨ 26 åï¼è§æµç«ç¼å·/åç§°/è§æµéç¨/å°å¸/åºå¿/è¡é©¶æ¹å + åè½¦åæµé + 客车/货车/汽车/æºå¨è½¦æµé + åç±»å½é + æºå¨è½¦é度 + æ¥æ¤åº¦ + æ¥æºæä»¶åï¼ãå¯ä¸é® `(obs_date, station_code, direction)`ï¼**æ°å¼åä¸å¾ `DECIMAL(30,15)`**ï¼Java å®ä½å段ä¸å¾ `BigDecimal`ï¼ä¸èåæ¥è¡¨åå£å¾ï¼æ float/doubleã |
| | | - **导å
¥å®ç°**ï¼æ°å¢å
`com.trafficaudit.observation`ï¼entity / mapper / dto / service / controllerï¼ãç¨ POI `WorkbookFactory` åæ¶æ¯æ `.xls` å `.xlsx`ï¼**表头æååå¹é
**ï¼å顺åºååä¸å½±å导å
¥ï¼ï¼æ«æå 10 è¡å®ä½è¡¨å¤´ï¼æ¥æå
¼å®¹ Excel æ¥ææ ¼å¼ä¸ `yyyy-M-d`/`yyyy/M/d`/`yyyyMMdd`/ä¸æå¹´ææ¥ï¼å䏿¥æ**éå¤å¯¼å
¥å³æ´ä½è¦ç**ï¼å
å è¯¥æ¥æåæå
¥ï¼ï¼å¹çã |
| | | - **æ¥å£**ï¼`POST /api/observation/import`ã`GET /api/observation/dates`ï¼ææ¥ææ±æ»ç«æ°ãä¸å°å®¢æµéå计ãæè¿å¯¼å
¥æ¶é´ï¼ã`GET /api/observation/rows?date=&limit=`ã`POST /api/observation/delete?date=`ã |
| | | - **å端**ï¼`traffic-audit-web/src/views/HolidayStat.vue`ï¼ä¸é¡¹ç»è®¡ - èåæ¥ï¼æ°å¢ãè§æµç«åæ¥è°æ¥æ°æ®ãå¡çï¼éæ©æä»¶ â 导å
¥ â ææ¥ææ±æ»è¡¨ â æ¥çæç»ï¼å¼¹çªï¼â å é¤ã |
| | | - **宿µï¼ç宿¨¡æ¿ï¼æ¬æº 3305ï¼**ï¼å¯¼å
¥ 692 è¡ / 692 ç« / 0 è·³è¿ / 0 失败ï¼ä¸é¡¹åè®¡ä¸æºæä»¶é项ä¸è´ï¼ä¸å°å®¢æµé 3,523,873ãæºå¨è½¦æµé 5,487,965ãæºå¨è½¦å½é 7,140,575ï¼ï¼**692 è¡ Ã 25 åæ®µå
¨ééè¡æ¯å¯¹ 0 å¤ä¸ä¸è´**ï¼ä¸æï¼ç«å/å°å¸/åºå¿ï¼è½åºæ ä¹±ç ï¼éå¤å¯¼å
¥ä»ä¸º 692 è¡ï¼è¦çèéç´¯å ï¼ï¼ä¸ä¼ é Excel è¿åæç¡®é误ãæè¯ï¼692 ç«ä¸å°å®¢æµéå计 3,523,873 è¾ = 352.3873 ä¸è¾ï¼ä¸ 2026-10-01 èåæ¥ãæ®éå½çéãå
¥åºå¼å®å
¨ä¸è´ï¼å°è¯äºæ°æ®æ¥æºå
³ç³»ã |
| | | - **顺带修å¤**ï¼`pack-deploy.ps1` æ db èæ¬å¤å¶è¿å
å
docs çæ¸
åéæ¼äº `sql_holiday_previous_day_migration.sql`ï¼æ¬æ¬¡ä¸å¹¶è¡¥ä¸ `sql_observation_station.sql`ï¼`deploy-templates/README-é¨ç½²è¯´æ.txt` ç db æ¸
ååæ¥æ´æ°ã |
| | | - **10-03 æ·±å¤è¿½å ï¼è§æµç«çé¢å¾®è°ï¼**ï¼â ãæ¥çãæç»å¼¹çªææ¨¡æ¿è¡¨å¤´å±ç¤ºåå§æ°æ®å
¨é¨å段ï¼è§æµæ¥æ/ç«å·/ç«å/éç¨/å°å¸/åºå¿/æ¹å + å
¨é¨è½¦åæµéã客货汽车æºå¨è½¦æµéãåç±»å½éãéåº¦ãæ¥æ¤åº¦ï¼å¦éæ¥æºæä»¶åï¼å
± 27 åï¼æ¨ªåæ»å¨ï¼ï¼â¡æ±æ»è¡¨ãä¸å°å®¢æµéå计ãåä½ç±è¾æ¬¡æ¹ä¸º**ä¸è¾æ¬¡**ãä¿ç 4 ä½å°æ°ï¼`formatWanVehicles`ï¼å¦ 3,523,873 â 352.3873ï¼ãå端 dev server å·²çæ´æ°ç¼è¯éè¿ã |
| | | - **10-03 æ·±å¤è¿½å ï¼è§æµç«å表/æç»ä¿®æ£ï¼**ï¼â **æç»è¡åº**忥æ `stationCode` æåºï¼ä¸ Excel è¡åºå¯¹ä¸ä¸ï¼æ¹ä¸ºæå¯¼å
¥é¡ºåºï¼`id ASC`ï¼å³ Excel è¡åºï¼ï¼ä¾¿äºéè¡æ¯å¯¹ï¼â¡æç»**å顺åº**䏿¨¡æ¿ 26 åå®å
¨ä¸è´ï¼æ«å°¾éæ¥æºæä»¶åï¼ï¼â¢**è§æµéç¨/æºå¨è½¦é度/æ¥æ¤åº¦**æ¹ä¸ºæåå§å°æ°æ¾ç¤ºï¼`formatPlain`ï¼å»å°¾é¶ï¼å¦ 50.92 / 43.882 / 0.777 / 58ï¼ï¼ä¸ååºå® 2 ä½ï¼â£**17 个æµéä¸å½éå**æ¹ä¸ºæ´æ°æ¾ç¤ºï¼`formatInteger`ï¼å¦ 6413ï¼ï¼ä¸ Excel ä¸è´ï¼â¤æ±æ»å表ãè§æµæ¥æãæ¹ä¸º**æ£åº**ï¼mapper `ORDER BY obs_date ASC`ï¼ãå·²ç¨ 10-01/10-02 ç宿°æ®éªè¯è¡åºä¸ Excel å 5 è¡ä¸è´ï¼ä¸å¸®/ä¸åº/沼山ç«/é¾åç«/ç½äºï¼ã |
| | | - **ç¶æ**ï¼å¾
ç¨æ·ãéªæ¶éè¿ãåæäº¤/æ¨éã |
| | | |
| | | --- |
| | | |
| | | ### è¡¥å
ï¼10-03 ä¸åï¼ï¼æå¡å¨é¨ç½²æ¥ âAccess denied ... (using password: NO)â ââ æ ¹å ä¸ä¿®å¤ |
| | |
| | | - **2026-08-28 夿 ¸ãç¹æ°æ®å¯¼å
¥æµè§å¨å´©æºãï¼æå
æµè¯ç¯å¢ï¼é¨åç¨æ·ï¼**ï¼éç¹ææ¥ `GET /api/import/batches`ââå端 limit ä¸é 200ï¼å
¨è¡¨ 81 æ¡ãerror_detail æé¿ 2.6KBãå
¨éååºçº¦ 17KBï¼å端åªå 50 æ¡ï¼ï¼åç«¯è¡¨æ ¼ä»
8 åçåæ®µï¼errorDetail 䏿¸²æï¼ãæ 轮询/宿¶å¨ã**ç»è®ºï¼æ¥å£ä¸æ¯å´©æºæ ¹å **ï¼è¥æ¥å£æé®é¢ï¼ææç¨æ·é½ä¼å´©ï¼èé"é¨åç¨æ·"ï¼ãç°è±¡ç¬¦å客æ·ç«¯ç¯å¢å·®å¼ï¼ç¬¬ä¸æ¹æ©å±æ³¨å
¥/æ§æ¾å¡é©±å¨/ä½é
å
åï¼ä¸ 08-27 è¿
é·æ©å±+Intel æ§é©±å¨ç»è®ºä¸è´ï¼ã建议对崩æºç¨æ·ä¾æ¬¡éªè¯ï¼æ çæ¨¡å¼âæ¢ Edgeâç¦æ©å±ï¼å¹¶ç chrome://crashesã |
| | | - ãæ°éæ±Â·ä»
è®°å½æªå®ç°Â·2026-08-28ãå¢å **ç»æµææ è¯ç®åè½**ï¼é¢å¯¼é常å
³æ³¨ç»æµææ ï¼**è´§è¿æ¯å¤§å¤´**ï¼å¸¸é©»ç°åºçææäººé½è¦ä¼è®¡ç®è´§è¿ç¸å
³æ°æ®ï¼è´§è¿é/å¨è½¬éçï¼ãé¢å¯¼ç»å¸¸è¦æ±ææä¸ªæ°æ®ãåä¸åãï¼ä»åèèå¨è½¯ä»¶éå¢å è¯ç®åè½ââ坿éè¦è°æ´/æµç®ææ æ°å¼ï¼å¦æ¨¡æä¸è°æä¼ä¸/æå£å¾çè´§è¿éåçæ±æ»ä¸è¶å¿å½±åï¼ï¼è¾
å©ç°åºäººåå¿«éè¯ç®ä¸åºå¯¹ã |
| | | |
| | | - ãå¾
å·2026-10-03ã**æ°å¼ç²¾åº¦ç»ä¸æ¹é **ï¼ææ¢æä¸å¡è¡¨çæ°å¼åç± `double` æ¹ä¸º `DECIMAL(30,15)`ï¼Java å®ä½åæ®µç± `Double/Float` æ¹ä¸º `BigDecimal`ï¼ä¸èåæ¥è¡¨ãè§æµç«è¡¨ç»ä¸å£å¾ï¼ç¨æ· 2026-10-03 æç¡®è¦æ±ï¼æ©æºå®æ½ï¼ã |
| | | - ç°ç¶çç¹ï¼traffic_auditï¼2026-10-03 æ¥è¯¢ï¼ï¼**16 å¼ è¡¨ãå
± 228 个 double å**ââ`freight_turnover_import` 48ã`city_bus_monthly` 34ã`h2032_enterprise_monthly` 33ã`city_taxi_monthly` 29ã`wyc_order_monthly` 22ã`h2031_enterprise_monthly` 18ã`h204_vehicle_quarterly` 12ã`scale_split_transport` 9ã`investment_monthly` 5ã`investment_system` 5ã`wyc_total_monthly` 4ã`h204_auth_vehicle` 2ã`investment_project` 2ã`passenger_individual_monthly` 2ã`transport_auth_vehicle` 2ã`vehicle_track_mileage` 1ã |
| | | - å·²æ¯ decimal å£å¾ãæ éæ¹å¨ï¼`holiday_calendar` / `holiday_passenger_coefficient` / `holiday_passenger_flow` / `holiday_vehicle_flow` / `observation_station_flow`ã |
| | | - æ¹é èå´ä¸åªæ¯æ¹åç±»åï¼å®ä½ç±»å段类åã导å
¥è§£æï¼ä¸è¦ç» double ä¸è½¬ï¼ã计ç®ä¸åæ´é»è¾ãæ¥è¡¨å¯¼åºæ ¼å¼åï¼ä»¥ååéæ°æ®çè¿ç§»èæ¬ï¼é½è¦ä¸èµ·æ¹ï¼æ¹å®è¦åé表æ°å¼æ½æ¥ã |
| | | |
| | | ## å
«ãæ¯æ¥äº¤æ¥çº¦å® |
| | | 1. æ°å¯¹è¯ç¬¬ä¸æ¥ï¼è¯» `AGENTS.md` + `HANDOFF.md`ï¼æ¶å AI åæ/å®¡æ ¸æ¶åè `docs/` 䏿°æ®åºè¡¨ç»æã |
| | | 2. æ¯å¤©ç»ææ¶ï¼æ´æ° `HANDOFF.md`ï¼è¿å±ãåãæ°éæ±ãå¾
åï¼ï¼åå
³å¯¹è¯ã |
| | |
| | | sql_holiday_decimal_migration.sql è忥æ°å¼å精度è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | sql_holiday_comparison_base_migration.sql èåæ¥ç¯æ¯åºæå段è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | sql_holiday_previous_day_migration.sql è忥å䏿¥é
ç½®è¿ç§»ï¼æ§åºåçº§ï¼ |
| | | sql_observation_station.sql è§æµç«åæ¥è°æ¥æ°æ®è¡¨ï¼æ°åè½ï¼æ§åºç¼ºè¡¨æ¶æ§è¡ï¼ |
| | | README-é¨ç½²è¯´æ.txt |
| | | |
| | | ãé¨ç½²æºç¯å¢è¦æ±ã |
| New file |
| | |
| | | -- Observation station daily survey data (raw retention for future analysis). |
| | | -- Run against the traffic_audit database. |
| | | CREATE TABLE IF NOT EXISTS observation_station_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | obs_date DATE NOT NULL COMMENT 'Observation date', |
| | | station_code VARCHAR(64) NOT NULL COMMENT 'Station code', |
| | | station_name VARCHAR(128) DEFAULT NULL COMMENT 'Station name', |
| | | mileage DECIMAL(30,15) DEFAULT NULL COMMENT 'Observed mileage (km)', |
| | | city_name VARCHAR(64) DEFAULT NULL COMMENT 'City name', |
| | | county_name VARCHAR(64) DEFAULT NULL COMMENT 'County name', |
| | | direction VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'Travel direction', |
| | | small_medium_passenger DECIMAL(30,15) DEFAULT NULL COMMENT 'Small/medium passenger cars', |
| | | large_passenger DECIMAL(30,15) DEFAULT NULL COMMENT 'Large passenger cars', |
| | | small_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Small trucks', |
| | | medium_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Medium trucks', |
| | | large_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Large trucks', |
| | | extra_large_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Extra large trucks', |
| | | container_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Container trucks', |
| | | motorcycle DECIMAL(30,15) DEFAULT NULL COMMENT 'Motorcycles', |
| | | tractor DECIMAL(30,15) DEFAULT NULL COMMENT 'Tractors', |
| | | passenger_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total passenger vehicles', |
| | | truck_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total trucks', |
| | | car_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total automobiles', |
| | | motor_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total motor vehicles', |
| | | passenger_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Passenger car equivalent', |
| | | truck_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Truck equivalent', |
| | | car_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Automobile equivalent', |
| | | motor_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Motor vehicle equivalent', |
| | | vehicle_speed DECIMAL(30,15) DEFAULT NULL COMMENT 'Motor vehicle speed', |
| | | congestion_degree DECIMAL(30,15) DEFAULT NULL COMMENT 'Congestion degree', |
| | | source_file VARCHAR(255) DEFAULT NULL COMMENT 'Source file name', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_observation_station (obs_date, station_code, direction), |
| | | KEY idx_observation_station_date (obs_date), |
| | | KEY idx_observation_station_city (city_name), |
| | | KEY idx_observation_station_code (station_code) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| New file |
| | |
| | | -- Observation station daily survey data (raw retention for future analysis). |
| | | -- Run against the traffic_audit database. |
| | | CREATE TABLE IF NOT EXISTS observation_station_flow ( |
| | | id BIGINT NOT NULL AUTO_INCREMENT, |
| | | obs_date DATE NOT NULL COMMENT 'Observation date', |
| | | station_code VARCHAR(64) NOT NULL COMMENT 'Station code', |
| | | station_name VARCHAR(128) DEFAULT NULL COMMENT 'Station name', |
| | | mileage DECIMAL(30,15) DEFAULT NULL COMMENT 'Observed mileage (km)', |
| | | city_name VARCHAR(64) DEFAULT NULL COMMENT 'City name', |
| | | county_name VARCHAR(64) DEFAULT NULL COMMENT 'County name', |
| | | direction VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'Travel direction', |
| | | small_medium_passenger DECIMAL(30,15) DEFAULT NULL COMMENT 'Small/medium passenger cars', |
| | | large_passenger DECIMAL(30,15) DEFAULT NULL COMMENT 'Large passenger cars', |
| | | small_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Small trucks', |
| | | medium_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Medium trucks', |
| | | large_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Large trucks', |
| | | extra_large_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Extra large trucks', |
| | | container_truck DECIMAL(30,15) DEFAULT NULL COMMENT 'Container trucks', |
| | | motorcycle DECIMAL(30,15) DEFAULT NULL COMMENT 'Motorcycles', |
| | | tractor DECIMAL(30,15) DEFAULT NULL COMMENT 'Tractors', |
| | | passenger_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total passenger vehicles', |
| | | truck_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total trucks', |
| | | car_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total automobiles', |
| | | motor_total DECIMAL(30,15) DEFAULT NULL COMMENT 'Total motor vehicles', |
| | | passenger_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Passenger car equivalent', |
| | | truck_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Truck equivalent', |
| | | car_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Automobile equivalent', |
| | | motor_equivalent DECIMAL(30,15) DEFAULT NULL COMMENT 'Motor vehicle equivalent', |
| | | vehicle_speed DECIMAL(30,15) DEFAULT NULL COMMENT 'Motor vehicle speed', |
| | | congestion_degree DECIMAL(30,15) DEFAULT NULL COMMENT 'Congestion degree', |
| | | source_file VARCHAR(255) DEFAULT NULL COMMENT 'Source file name', |
| | | created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
| | | updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
| | | PRIMARY KEY (id), |
| | | UNIQUE KEY uk_observation_station (obs_date, station_code, direction), |
| | | KEY idx_observation_station_date (obs_date), |
| | | KEY idx_observation_station_city (city_name), |
| | | KEY idx_observation_station_code (station_code) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
| | |
| | | cmd /c "robocopy `"$root\docs`" `"$docsDest`" /E /NFL /NDL /NJH /NJS /NP /XD _cache 工使¥ç» åè½æµè¯æ¥å é®é¢æ±æ» /XF çæ_*.xlsx _bak_* >nul" |
| | | if ($LASTEXITCODE -ge 8) { throw "docs å¤å¶å¤±è´¥ï¼robocopy è¿å $LASTEXITCODEï¼" } |
| | | # db èæ¬ä¹æ¾ä¸ä»½å° docsï¼ä¸æ§å
ä¸è´ï¼docs/init.sql çï¼ |
| | | Copy-Item (Join-Path $tplDir 'db\init.sql'), (Join-Path $tplDir 'db\sql_city_bus.sql'), (Join-Path $tplDir 'db\sql_city_taxi.sql'), (Join-Path $tplDir 'db\sql_wyc.sql'), (Join-Path $tplDir 'db\sql_holiday.sql'), (Join-Path $tplDir 'db\sql_holiday_decimal_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_comparison_base_migration.sql') $docsDest -Force -ErrorAction SilentlyContinue |
| | | Copy-Item (Join-Path $tplDir 'db\init.sql'), (Join-Path $tplDir 'db\sql_city_bus.sql'), (Join-Path $tplDir 'db\sql_city_taxi.sql'), (Join-Path $tplDir 'db\sql_wyc.sql'), (Join-Path $tplDir 'db\sql_holiday.sql'), (Join-Path $tplDir 'db\sql_holiday_decimal_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_comparison_base_migration.sql'), (Join-Path $tplDir 'db\sql_holiday_previous_day_migration.sql'), (Join-Path $tplDir 'db\sql_observation_station.sql') $docsDest -Force -ErrorAction SilentlyContinue |
| | | |
| | | # READMEï¼ç¨æ¨¡æ¿å¹¶æ¿æ¢æ¥æå ä½ |
| | | $readme = Get-Content (Join-Path $tplDir 'README-é¨ç½²è¯´æ.txt') -Raw -Encoding UTF8 |
| New file |
| | |
| | | package com.trafficaudit.observation.controller; |
| | | |
| | | import com.trafficaudit.common.Result; |
| | | import com.trafficaudit.observation.dto.ObservationDateSummary; |
| | | import com.trafficaudit.observation.entity.ObservationStationFlow; |
| | | import com.trafficaudit.observation.service.ObservationStationImportService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.time.LocalDate; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * Observation station raw data. Collected and stored only; not linked to holiday flow data. |
| | | */ |
| | | @Slf4j |
| | | @RestController |
| | | @RequestMapping("/api/observation") |
| | | public class ObservationStationController { |
| | | |
| | | @Resource |
| | | private ObservationStationImportService importService; |
| | | |
| | | @PostMapping("/import") |
| | | public Result<Map<String, Object>> importWorkbook(@RequestParam("file") MultipartFile file) { |
| | | try { |
| | | return Result.ok(importService.importWorkbook(file)); |
| | | } catch (Exception e) { |
| | | log.error("observation station import error", e); |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @GetMapping("/dates") |
| | | public Result<List<ObservationDateSummary>> dates() { |
| | | return Result.ok(importService.listDateSummaries()); |
| | | } |
| | | |
| | | @GetMapping("/rows") |
| | | public Result<List<ObservationStationFlow>> rows(@RequestParam(value = "date", required = false) String date, |
| | | @RequestParam(value = "limit", defaultValue = "2000") int limit) { |
| | | try { |
| | | LocalDate target = (date == null || date.trim().isEmpty()) ? null : LocalDate.parse(date); |
| | | return Result.ok(importService.listRows(target, limit)); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | @PostMapping("/delete") |
| | | public Result<Integer> delete(@RequestParam("date") String date) { |
| | | try { |
| | | return Result.ok(importService.deleteDate(LocalDate.parse(date))); |
| | | } catch (Exception e) { |
| | | return Result.error(400, e.getMessage()); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.observation.dto; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | |
| | | /** Per-date rollup shown on the import page. */ |
| | | @Data |
| | | public class ObservationDateSummary { |
| | | private LocalDate obsDate; |
| | | private Integer stationCount; |
| | | private BigDecimal smallMediumPassengerTotal; |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | private LocalDateTime importedAt; |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.observation.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | |
| | | /** |
| | | * Observation station daily survey data. |
| | | * Stored raw for later analysis; it is deliberately NOT linked to the holiday flow tables, |
| | | * even though ä¸å°å®¢æµé is one of the sources of the ordinary national/provincial road flow. |
| | | */ |
| | | @Data |
| | | @TableName("observation_station_flow") |
| | | public class ObservationStationFlow { |
| | | @TableId(type = IdType.AUTO) |
| | | private Long id; |
| | | private LocalDate obsDate; |
| | | private String stationCode; |
| | | private String stationName; |
| | | private BigDecimal mileage; |
| | | private String cityName; |
| | | private String countyName; |
| | | private String direction; |
| | | private BigDecimal smallMediumPassenger; |
| | | private BigDecimal largePassenger; |
| | | private BigDecimal smallTruck; |
| | | private BigDecimal mediumTruck; |
| | | private BigDecimal largeTruck; |
| | | private BigDecimal extraLargeTruck; |
| | | private BigDecimal containerTruck; |
| | | private BigDecimal motorcycle; |
| | | private BigDecimal tractor; |
| | | private BigDecimal passengerTotal; |
| | | private BigDecimal truckTotal; |
| | | private BigDecimal carTotal; |
| | | private BigDecimal motorTotal; |
| | | private BigDecimal passengerEquivalent; |
| | | private BigDecimal truckEquivalent; |
| | | private BigDecimal carEquivalent; |
| | | private BigDecimal motorEquivalent; |
| | | private BigDecimal vehicleSpeed; |
| | | private BigDecimal congestionDegree; |
| | | private String sourceFile; |
| | | private LocalDateTime createdAt; |
| | | private LocalDateTime updatedAt; |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.observation.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.trafficaudit.observation.dto.ObservationDateSummary; |
| | | import com.trafficaudit.observation.entity.ObservationStationFlow; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.apache.ibatis.annotations.Select; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Mapper |
| | | public interface ObservationStationFlowMapper extends BaseMapper<ObservationStationFlow> { |
| | | |
| | | @Select("SELECT obs_date, COUNT(*) AS station_count, " |
| | | + "SUM(small_medium_passenger) AS small_medium_passenger_total, " |
| | | + "MAX(updated_at) AS imported_at " |
| | | + "FROM observation_station_flow GROUP BY obs_date ORDER BY obs_date ASC") |
| | | List<ObservationDateSummary> selectDateSummary(); |
| | | } |
| New file |
| | |
| | | package com.trafficaudit.observation.service; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.trafficaudit.observation.dto.ObservationDateSummary; |
| | | import com.trafficaudit.observation.entity.ObservationStationFlow; |
| | | import com.trafficaudit.observation.mapper.ObservationStationFlowMapper; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.poi.ss.usermodel.Cell; |
| | | import org.apache.poi.ss.usermodel.CellType; |
| | | import org.apache.poi.ss.usermodel.DataFormatter; |
| | | import org.apache.poi.ss.usermodel.DateUtil; |
| | | import org.apache.poi.ss.usermodel.Row; |
| | | import org.apache.poi.ss.usermodel.Sheet; |
| | | import org.apache.poi.ss.usermodel.Workbook; |
| | | import org.apache.poi.ss.usermodel.WorkbookFactory; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.io.InputStream; |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDate; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | |
| | | /** |
| | | * Imports the "observation station daily survey" workbook (.xls/.xlsx). |
| | | * Header names are matched by text, so column order may change between releases. |
| | | * Re-importing the same date replaces that date's rows. |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class ObservationStationImportService { |
| | | |
| | | private static final String H_DATE = "è§æµæ¥æ"; |
| | | private static final String H_CODE = "è§æµç«ç¼å·"; |
| | | private static final String H_NAME = "è§æµç«åç§°"; |
| | | private static final String H_MILEAGE = "è§æµéç¨"; |
| | | private static final String H_CITY = "å°å¸åç§°"; |
| | | private static final String H_COUNTY = "åºå¿åç§°"; |
| | | private static final String H_DIRECTION = "è¡é©¶æ¹å"; |
| | | private static final String H_SMALL_MEDIUM_PASSENGER = "ä¸å°å®¢æµé"; |
| | | private static final String H_LARGE_PASSENGER = "大客车æµé"; |
| | | private static final String H_SMALL_TRUCK = "å°è´§è½¦æµé"; |
| | | private static final String H_MEDIUM_TRUCK = "ä¸è´§è½¦æµé"; |
| | | private static final String H_LARGE_TRUCK = "大货车æµé"; |
| | | private static final String H_EXTRA_LARGE_TRUCK = "ç¹å¤§è´§æµé"; |
| | | private static final String H_CONTAINER = "éè£
ç®±æµé"; |
| | | private static final String H_MOTORCYCLE = "æ©æè½¦æµé"; |
| | | private static final String H_TRACTOR = "æææºæµé"; |
| | | private static final String H_PASSENGER_TOTAL = "客车æµé"; |
| | | private static final String H_TRUCK_TOTAL = "货车æµé"; |
| | | private static final String H_CAR_TOTAL = "汽车æµé"; |
| | | private static final String H_MOTOR_TOTAL = "æºå¨è½¦æµé"; |
| | | private static final String H_PASSENGER_EQUIVALENT = "客车å½é"; |
| | | private static final String H_TRUCK_EQUIVALENT = "货车å½é"; |
| | | private static final String H_CAR_EQUIVALENT = "汽车å½é"; |
| | | private static final String H_MOTOR_EQUIVALENT = "æºå¨è½¦å½é"; |
| | | private static final String H_SPEED = "æºå¨è½¦é度"; |
| | | private static final String H_CONGESTION = "æ¥æ¤åº¦"; |
| | | |
| | | private static final int HEADER_SCAN_ROWS = 10; |
| | | private static final int MAX_REPORTED_ERRORS = 50; |
| | | |
| | | private static final DateTimeFormatter[] DATE_FORMATS = { |
| | | DateTimeFormatter.ofPattern("yyyy-M-d"), |
| | | DateTimeFormatter.ofPattern("yyyy/M/d"), |
| | | DateTimeFormatter.ofPattern("yyyy.M.d"), |
| | | DateTimeFormatter.ofPattern("yyyyMMdd") |
| | | }; |
| | | |
| | | @Resource |
| | | private ObservationStationFlowMapper stationFlowMapper; |
| | | |
| | | @Transactional |
| | | public Map<String, Object> importWorkbook(MultipartFile file) { |
| | | if (file == null || file.isEmpty()) { |
| | | throw new IllegalArgumentException("è¯·éæ©è¦å¯¼å
¥ç Excel æä»¶"); |
| | | } |
| | | String sourceFile = file.getOriginalFilename(); |
| | | List<ObservationStationFlow> rows = new ArrayList<>(); |
| | | Set<LocalDate> dates = new LinkedHashSet<>(); |
| | | Set<String> stationCodes = new LinkedHashSet<>(); |
| | | List<String> errors = new ArrayList<>(); |
| | | int skipped = 0; |
| | | |
| | | DataFormatter formatter = new DataFormatter(); |
| | | try (InputStream in = file.getInputStream(); Workbook workbook = WorkbookFactory.create(in)) { |
| | | Sheet sheet = workbook.getNumberOfSheets() > 0 ? workbook.getSheetAt(0) : null; |
| | | if (sheet == null) { |
| | | throw new IllegalArgumentException("å·¥ä½ç°¿ä¸æ²¡æå¯è¯»åçå·¥ä½è¡¨"); |
| | | } |
| | | int headerRowIndex = findHeaderRow(sheet, formatter); |
| | | if (headerRowIndex < 0) { |
| | | throw new IllegalArgumentException("æªæ¾å°è¡¨å¤´ï¼è¯·ç¡®è®¤æä»¶å
å«\"è§æµæ¥æ\"å\"è§æµç«ç¼å·\"两å"); |
| | | } |
| | | Map<String, Integer> headerIndex = buildHeaderIndex(sheet.getRow(headerRowIndex), formatter); |
| | | |
| | | for (int r = headerRowIndex + 1; r <= sheet.getLastRowNum(); r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) { |
| | | skipped++; |
| | | continue; |
| | | } |
| | | String code = stringCell(row, headerIndex, H_CODE, formatter); |
| | | LocalDate obsDate = dateCell(row, headerIndex, H_DATE, formatter); |
| | | if (isBlank(code) && obsDate == null) { |
| | | skipped++; |
| | | continue; |
| | | } |
| | | int rowNo = r + 1; |
| | | if (obsDate == null) { |
| | | addError(errors, "第" + rowNo + "è¡ï¼è§æµæ¥ææ æ³è¯å«"); |
| | | continue; |
| | | } |
| | | if (isBlank(code)) { |
| | | addError(errors, "第" + rowNo + "è¡ï¼è§æµç«ç¼å·ä¸ºç©º"); |
| | | continue; |
| | | } |
| | | ObservationStationFlow entity = new ObservationStationFlow(); |
| | | entity.setObsDate(obsDate); |
| | | entity.setStationCode(code); |
| | | entity.setStationName(stringCell(row, headerIndex, H_NAME, formatter)); |
| | | entity.setMileage(decimalCell(row, headerIndex, H_MILEAGE, formatter)); |
| | | entity.setCityName(stringCell(row, headerIndex, H_CITY, formatter)); |
| | | entity.setCountyName(stringCell(row, headerIndex, H_COUNTY, formatter)); |
| | | String direction = stringCell(row, headerIndex, H_DIRECTION, formatter); |
| | | entity.setDirection(direction == null ? "" : direction); |
| | | entity.setSmallMediumPassenger(decimalCell(row, headerIndex, H_SMALL_MEDIUM_PASSENGER, formatter)); |
| | | entity.setLargePassenger(decimalCell(row, headerIndex, H_LARGE_PASSENGER, formatter)); |
| | | entity.setSmallTruck(decimalCell(row, headerIndex, H_SMALL_TRUCK, formatter)); |
| | | entity.setMediumTruck(decimalCell(row, headerIndex, H_MEDIUM_TRUCK, formatter)); |
| | | entity.setLargeTruck(decimalCell(row, headerIndex, H_LARGE_TRUCK, formatter)); |
| | | entity.setExtraLargeTruck(decimalCell(row, headerIndex, H_EXTRA_LARGE_TRUCK, formatter)); |
| | | entity.setContainerTruck(decimalCell(row, headerIndex, H_CONTAINER, formatter)); |
| | | entity.setMotorcycle(decimalCell(row, headerIndex, H_MOTORCYCLE, formatter)); |
| | | entity.setTractor(decimalCell(row, headerIndex, H_TRACTOR, formatter)); |
| | | entity.setPassengerTotal(decimalCell(row, headerIndex, H_PASSENGER_TOTAL, formatter)); |
| | | entity.setTruckTotal(decimalCell(row, headerIndex, H_TRUCK_TOTAL, formatter)); |
| | | entity.setCarTotal(decimalCell(row, headerIndex, H_CAR_TOTAL, formatter)); |
| | | entity.setMotorTotal(decimalCell(row, headerIndex, H_MOTOR_TOTAL, formatter)); |
| | | entity.setPassengerEquivalent(decimalCell(row, headerIndex, H_PASSENGER_EQUIVALENT, formatter)); |
| | | entity.setTruckEquivalent(decimalCell(row, headerIndex, H_TRUCK_EQUIVALENT, formatter)); |
| | | entity.setCarEquivalent(decimalCell(row, headerIndex, H_CAR_EQUIVALENT, formatter)); |
| | | entity.setMotorEquivalent(decimalCell(row, headerIndex, H_MOTOR_EQUIVALENT, formatter)); |
| | | entity.setVehicleSpeed(decimalCell(row, headerIndex, H_SPEED, formatter)); |
| | | entity.setCongestionDegree(decimalCell(row, headerIndex, H_CONGESTION, formatter)); |
| | | entity.setSourceFile(sourceFile); |
| | | rows.add(entity); |
| | | dates.add(obsDate); |
| | | stationCodes.add(code); |
| | | } |
| | | } catch (IllegalArgumentException e) { |
| | | throw e; |
| | | } catch (Exception e) { |
| | | throw new IllegalArgumentException("å·¥ä½ç°¿è§£æå¤±è´¥: " + e.getMessage(), e); |
| | | } |
| | | |
| | | if (!rows.isEmpty()) { |
| | | stationFlowMapper.delete(new LambdaQueryWrapper<ObservationStationFlow>() |
| | | .in(ObservationStationFlow::getObsDate, dates)); |
| | | for (ObservationStationFlow row : rows) { |
| | | stationFlowMapper.insert(row); |
| | | } |
| | | } |
| | | |
| | | List<String> dateTexts = new ArrayList<>(); |
| | | for (LocalDate date : dates) { |
| | | dateTexts.add(date.toString()); |
| | | } |
| | | BigDecimal smallMediumTotal = BigDecimal.ZERO; |
| | | for (ObservationStationFlow row : rows) { |
| | | if (row.getSmallMediumPassenger() != null) { |
| | | smallMediumTotal = smallMediumTotal.add(row.getSmallMediumPassenger()); |
| | | } |
| | | } |
| | | |
| | | Map<String, Object> summary = new LinkedHashMap<>(); |
| | | summary.put("totalRows", rows.size()); |
| | | summary.put("stationCount", stationCodes.size()); |
| | | summary.put("dates", dateTexts); |
| | | summary.put("skippedRows", skipped); |
| | | summary.put("smallMediumPassengerTotal", smallMediumTotal); |
| | | summary.put("errors", errors); |
| | | log.info("Observation station import done: file={} rows={} dates={} skipped={} failed={}", |
| | | sourceFile, rows.size(), dateTexts, skipped, errors.size()); |
| | | return summary; |
| | | } |
| | | |
| | | public List<ObservationDateSummary> listDateSummaries() { |
| | | return stationFlowMapper.selectDateSummary(); |
| | | } |
| | | |
| | | public List<ObservationStationFlow> listRows(LocalDate date, int limit) { |
| | | int effectiveLimit = limit <= 0 ? 2000 : Math.min(limit, 5000); |
| | | return stationFlowMapper.selectList(new LambdaQueryWrapper<ObservationStationFlow>() |
| | | .eq(date != null, ObservationStationFlow::getObsDate, date) |
| | | .orderByAsc(ObservationStationFlow::getId) |
| | | .last("LIMIT " + effectiveLimit)); |
| | | } |
| | | |
| | | public int deleteDate(LocalDate date) { |
| | | if (date == null) { |
| | | throw new IllegalArgumentException("è¯·éæ©è¦å é¤çè§æµæ¥æ"); |
| | | } |
| | | return stationFlowMapper.delete(new LambdaQueryWrapper<ObservationStationFlow>() |
| | | .eq(ObservationStationFlow::getObsDate, date)); |
| | | } |
| | | |
| | | private void addError(List<String> errors, String message) { |
| | | if (errors.size() < MAX_REPORTED_ERRORS) { |
| | | errors.add(message); |
| | | } |
| | | } |
| | | |
| | | private int findHeaderRow(Sheet sheet, DataFormatter formatter) { |
| | | int lastRow = Math.min(sheet.getLastRowNum(), HEADER_SCAN_ROWS); |
| | | for (int r = 0; r <= lastRow; r++) { |
| | | Row row = sheet.getRow(r); |
| | | if (row == null) { |
| | | continue; |
| | | } |
| | | Map<String, Integer> index = buildHeaderIndex(row, formatter); |
| | | if (index.containsKey(normalize(H_DATE)) && index.containsKey(normalize(H_CODE))) { |
| | | return r; |
| | | } |
| | | } |
| | | return -1; |
| | | } |
| | | |
| | | private Map<String, Integer> buildHeaderIndex(Row row, DataFormatter formatter) { |
| | | Map<String, Integer> index = new LinkedHashMap<>(); |
| | | if (row == null) { |
| | | return index; |
| | | } |
| | | for (int c = row.getFirstCellNum(); c < row.getLastCellNum(); c++) { |
| | | Cell cell = row.getCell(c); |
| | | String header = normalize(formatter.formatCellValue(cell)); |
| | | if (!header.isEmpty() && !index.containsKey(header)) { |
| | | index.put(header, c); |
| | | } |
| | | } |
| | | return index; |
| | | } |
| | | |
| | | private String normalize(String value) { |
| | | if (value == null) { |
| | | return ""; |
| | | } |
| | | StringBuilder builder = new StringBuilder(value.length()); |
| | | for (int i = 0; i < value.length(); i++) { |
| | | char ch = value.charAt(i); |
| | | if (ch == 0xFEFF || Character.isWhitespace(ch)) { |
| | | continue; |
| | | } |
| | | builder.append(ch); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private String stringCell(Row row, Map<String, Integer> headerIndex, String header, DataFormatter formatter) { |
| | | Integer column = headerIndex.get(normalize(header)); |
| | | if (column == null) { |
| | | return null; |
| | | } |
| | | Cell cell = row.getCell(column); |
| | | if (cell == null) { |
| | | return null; |
| | | } |
| | | String value = formatter.formatCellValue(cell).trim(); |
| | | return value.isEmpty() ? null : value; |
| | | } |
| | | |
| | | private LocalDate dateCell(Row row, Map<String, Integer> headerIndex, String header, DataFormatter formatter) { |
| | | Integer column = headerIndex.get(normalize(header)); |
| | | if (column == null) { |
| | | return null; |
| | | } |
| | | return readDate(row.getCell(column), formatter); |
| | | } |
| | | |
| | | private BigDecimal decimalCell(Row row, Map<String, Integer> headerIndex, String header, DataFormatter formatter) { |
| | | Integer column = headerIndex.get(normalize(header)); |
| | | if (column == null) { |
| | | return null; |
| | | } |
| | | return readDecimal(row.getCell(column), formatter); |
| | | } |
| | | |
| | | private LocalDate readDate(Cell cell, DataFormatter formatter) { |
| | | if (cell == null) { |
| | | return null; |
| | | } |
| | | CellType type = cell.getCellType(); |
| | | if (type == CellType.FORMULA) { |
| | | type = cell.getCachedFormulaResultType(); |
| | | } |
| | | if (type == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) { |
| | | return cell.getLocalDateTimeCellValue().toLocalDate(); |
| | | } |
| | | String text = formatter.formatCellValue(cell).trim(); |
| | | if (text.isEmpty()) { |
| | | return null; |
| | | } |
| | | text = text.replace("å¹´", "-").replace("æ", "-").replace("æ¥", "").trim(); |
| | | for (DateTimeFormatter format : DATE_FORMATS) { |
| | | try { |
| | | return LocalDate.parse(text, format); |
| | | } catch (Exception ignored) { |
| | | // Try the next supported pattern. |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private BigDecimal readDecimal(Cell cell, DataFormatter formatter) { |
| | | if (cell == null) { |
| | | return null; |
| | | } |
| | | CellType type = cell.getCellType(); |
| | | if (type == CellType.FORMULA) { |
| | | type = cell.getCachedFormulaResultType(); |
| | | } |
| | | if (type == CellType.NUMERIC) { |
| | | return BigDecimal.valueOf(cell.getNumericCellValue()); |
| | | } |
| | | if (type == CellType.BLANK) { |
| | | return null; |
| | | } |
| | | String text = formatter.formatCellValue(cell).trim().replace(",", "").replace(" ", ""); |
| | | if (text.isEmpty() || "-".equals(text) || "â".equals(text) || "/".equals(text)) { |
| | | return null; |
| | | } |
| | | try { |
| | | return new BigDecimal(text); |
| | | } catch (NumberFormatException e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private boolean isBlank(String value) { |
| | | return value == null || value.trim().isEmpty(); |
| | | } |
| | | } |
| | |
| | | <el-table-column label="æ»äººåæµå¨éï¼ä¸äººæ¬¡ï¼" min-width="180" align="right"><template slot-scope="s">{{ formatNumber(s.row.totalPersonFlow) }}</template></el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <el-card shadow="never" style="margin-top:14px"> |
| | | <div slot="header"> |
| | | <span>è§æµç«åæ¥è°æ¥æ°æ®</span> |
| | | <span class="hint">ç¨äºåå§æ°æ®çåï¼ä¸ä¸ä¸æ¹è忥ç»è®¡æ°æ®å
³è</span> |
| | | </div> |
| | | <div class="operation-row"> |
| | | <div class="operation-item"> |
| | | <span class="label">导å
¥æ°æ®</span> |
| | | <el-upload action="#" :auto-upload="false" :show-file-list="true" :limit="1" |
| | | accept=".xls,.xlsx" :on-change="onObservationFileChange" :on-remove="onObservationFileRemove" |
| | | :file-list="observationFileList"> |
| | | <el-button type="primary" plain icon="el-icon-folder-opened">éæ©æä»¶</el-button> |
| | | </el-upload> |
| | | <el-button type="primary" icon="el-icon-upload2" :loading="observationImporting" |
| | | :disabled="!observationFile" @click="importObservation">导å
¥</el-button> |
| | | </div> |
| | | </div> |
| | | <div v-if="observationMessage" class="import-message"> |
| | | <pre>{{ observationMessage }}</pre> |
| | | </div> |
| | | <el-table :data="observationDates" border stripe size="small" style="margin-top:12px" v-loading="observationLoading"> |
| | | <el-table-column prop="obsDate" label="è§æµæ¥æ" width="130" align="center"></el-table-column> |
| | | <el-table-column prop="stationCount" label="è§æµç«æ°" width="110" align="right"></el-table-column> |
| | | <el-table-column label="ä¸å°å®¢æµéå计ï¼ä¸è¾æ¬¡ï¼" width="210" align="right"> |
| | | <template slot-scope="s">{{ formatWanVehicles(s.row.smallMediumPassengerTotal) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="importedAt" label="æè¿å¯¼å
¥æ¶é´" min-width="180"></el-table-column> |
| | | <el-table-column label="æä½" width="150" align="center"> |
| | | <template slot-scope="s"> |
| | | <el-button type="text" @click="viewObservation(s.row.obsDate)">æ¥ç</el-button> |
| | | <el-button type="text" class="danger-text" @click="deleteObservation(s.row.obsDate)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <el-dialog :title="'è§æµç«æç» - ' + observationDialogDate" :visible.sync="observationDialogVisible" width="92%" top="5vh"> |
| | | <el-table :data="observationRows" border stripe size="mini" height="540" v-loading="observationRowsLoading"> |
| | | <el-table-column prop="obsDate" label="è§æµæ¥æ" width="110" fixed="left" align="center"></el-table-column> |
| | | <el-table-column prop="stationCode" label="è§æµç«ç¼å·" width="130" fixed="left"></el-table-column> |
| | | <el-table-column prop="stationName" label="è§æµç«åç§°" width="120"></el-table-column> |
| | | <el-table-column label="è§æµéç¨" width="100" align="right"><template slot-scope="s">{{ formatPlain(s.row.mileage) }}</template></el-table-column> |
| | | <el-table-column prop="cityName" label="å°å¸åç§°" width="140"></el-table-column> |
| | | <el-table-column prop="countyName" label="åºå¿åç§°" width="110"></el-table-column> |
| | | <el-table-column prop="direction" label="è¡é©¶æ¹å" width="90"></el-table-column> |
| | | <el-table-column label="ä¸å°å®¢æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.smallMediumPassenger) }}</template></el-table-column> |
| | | <el-table-column label="大客车æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.largePassenger) }}</template></el-table-column> |
| | | <el-table-column label="å°è´§è½¦æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.smallTruck) }}</template></el-table-column> |
| | | <el-table-column label="ä¸è´§è½¦æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.mediumTruck) }}</template></el-table-column> |
| | | <el-table-column label="大货车æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.largeTruck) }}</template></el-table-column> |
| | | <el-table-column label="ç¹å¤§è´§æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.extraLargeTruck) }}</template></el-table-column> |
| | | <el-table-column label="éè£
ç®±æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.containerTruck) }}</template></el-table-column> |
| | | <el-table-column label="æ©æè½¦æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.motorcycle) }}</template></el-table-column> |
| | | <el-table-column label="æææºæµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.tractor) }}</template></el-table-column> |
| | | <el-table-column label="客车æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.passengerTotal) }}</template></el-table-column> |
| | | <el-table-column label="货车æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.truckTotal) }}</template></el-table-column> |
| | | <el-table-column label="汽车æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.carTotal) }}</template></el-table-column> |
| | | <el-table-column label="æºå¨è½¦æµé" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.motorTotal) }}</template></el-table-column> |
| | | <el-table-column label="客车å½é" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.passengerEquivalent) }}</template></el-table-column> |
| | | <el-table-column label="货车å½é" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.truckEquivalent) }}</template></el-table-column> |
| | | <el-table-column label="汽车å½é" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.carEquivalent) }}</template></el-table-column> |
| | | <el-table-column label="æºå¨è½¦å½é" width="110" align="right"><template slot-scope="s">{{ formatInteger(s.row.motorEquivalent) }}</template></el-table-column> |
| | | <el-table-column label="æºå¨è½¦é度" width="110" align="right"><template slot-scope="s">{{ formatPlain(s.row.vehicleSpeed) }}</template></el-table-column> |
| | | <el-table-column label="æ¥æ¤åº¦" width="100" align="right"><template slot-scope="s">{{ formatPlain(s.row.congestionDegree) }}</template></el-table-column> |
| | | <el-table-column prop="sourceFile" label="æ¥æºæä»¶å" min-width="220" show-overflow-tooltip></el-table-column> |
| | | </el-table> |
| | | </el-dialog> |
| | | </div> |
| | | </el-main> |
| | | </el-container> |
| | |
| | | fileList: [], |
| | | importMessage: '', |
| | | allowHistoricalOverwrite: false, |
| | | bulletinDate: this.previousDateString() |
| | | bulletinDate: this.previousDateString(), |
| | | observationFile: null, |
| | | observationFileList: [], |
| | | observationImporting: false, |
| | | observationLoading: false, |
| | | observationMessage: '', |
| | | observationDates: [], |
| | | observationDialogVisible: false, |
| | | observationDialogDate: '', |
| | | observationRows: [], |
| | | observationRowsLoading: false |
| | | } |
| | | }, |
| | | computed: { |
| | |
| | | mounted() { |
| | | this.loadOptions() |
| | | this.loadOverview() |
| | | this.loadObservationDates() |
| | | }, |
| | | methods: { |
| | | previousDateString() { |
| | |
| | | const data = err && err.response && err.response.data |
| | | this.$message.error((data && data.message) || (err && err.message) || fallback) |
| | | }, |
| | | onObservationFileChange(file) { |
| | | this.observationFile = file.raw |
| | | this.observationFileList = [file] |
| | | this.observationMessage = '' |
| | | }, |
| | | onObservationFileRemove() { |
| | | this.observationFile = null |
| | | this.observationFileList = [] |
| | | }, |
| | | loadObservationDates() { |
| | | this.observationLoading = true |
| | | api.get('/observation/dates') |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'å 载失败') |
| | | this.observationDates = res.data || [] |
| | | }) |
| | | .catch(err => this.showError(err, 'è§æµç«æ°æ®å 载失败')) |
| | | .finally(() => { this.observationLoading = false }) |
| | | }, |
| | | importObservation() { |
| | | if (!this.observationFile) return |
| | | const form = new FormData() |
| | | form.append('file', this.observationFile) |
| | | this.observationImporting = true |
| | | this.observationMessage = '' |
| | | api.post('/observation/import', form, { timeout: 300000, headers: { 'Content-Type': 'multipart/form-data' } }) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || '导å
¥å¤±è´¥') |
| | | const data = res.data || {} |
| | | const dates = (data.dates && data.dates.length) ? data.dates.join('ã') : 'â' |
| | | const lines = ['导å
¥å®æï¼' + (data.totalRows || 0) + ' æ¡ï¼è§æµç« ' + (data.stationCount || 0) |
| | | + ' ä¸ªï¼æ¥æ ' + dates + 'ï¼è·³è¿ç©ºç½ ' + (data.skippedRows || 0) + ' æ¡ã'] |
| | | if (data.errors && data.errors.length) { |
| | | lines.push('失败æç»ï¼') |
| | | data.errors.forEach(item => lines.push(item)) |
| | | } |
| | | this.observationMessage = lines.join('\n') |
| | | this.$message.success('è§æµç«æ°æ®å¯¼å
¥å®æ') |
| | | this.observationFile = null |
| | | this.observationFileList = [] |
| | | this.loadObservationDates() |
| | | }) |
| | | .catch(err => this.showError(err, 'è§æµç«æ°æ®å¯¼å
¥å¤±è´¥')) |
| | | .finally(() => { this.observationImporting = false }) |
| | | }, |
| | | viewObservation(date) { |
| | | this.observationDialogDate = date |
| | | this.observationDialogVisible = true |
| | | this.observationRowsLoading = true |
| | | this.observationRows = [] |
| | | api.get('/observation/rows', { params: { date: date, limit: 2000 } }) |
| | | .then(res => { |
| | | if (res.code !== 200) throw new Error(res.message || 'å 载失败') |
| | | this.observationRows = res.data || [] |
| | | }) |
| | | .catch(err => this.showError(err, 'è§æµç«æç»å 载失败')) |
| | | .finally(() => { this.observationRowsLoading = false }) |
| | | }, |
| | | deleteObservation(date) { |
| | | this.$confirm('ç¡®å®å é¤ ' + date + ' çè§æµç«æ°æ®åï¼', 'å é¤ç¡®è®¤', { type: 'warning' }) |
| | | .then(() => api.post('/observation/delete', null, { params: { date: date } })) |
| | | .then(res => { |
| | | if (res && res.code !== 200) throw new Error(res.message || 'å é¤å¤±è´¥') |
| | | this.$message.success('å·²å é¤') |
| | | this.loadObservationDates() |
| | | }) |
| | | .catch(err => { if (err !== 'cancel' && err !== 'close') this.showError(err, 'å é¤å¤±è´¥') }) |
| | | }, |
| | | formatNumber(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | return Number(value).toFixed(2) |
| | | }, |
| | | formatWanVehicles(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return (number / 10000).toFixed(4) |
| | | }, |
| | | formatInteger(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return String(Math.round(number)) |
| | | }, |
| | | formatPlain(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | | const number = Number(value) |
| | | if (!Number.isFinite(number)) return value |
| | | return String(number) |
| | | }, |
| | | formatCoefficient(value) { |
| | | if (value === null || value === undefined || value === '') return 'â' |
| | |
| | | .operation-item .label { color: #606266; font-size: 14px; } |
| | | .import-message { margin-top: 12px; padding: 10px 12px; background: #f5f7fa; border: 1px solid #e4e7ed; border-radius: 4px; } |
| | | .import-message pre { margin: 0; white-space: pre-wrap; font-family: inherit; color: #606266; } |
| | | .hint { margin-left: 10px; color: #909399; font-size: 12px; } |
| | | .danger-text { color: #f56c6c; } |
| | | </style> |