From 9529220c815bfe6e43c992fde2f392be823450eb Mon Sep 17 00:00:00 2001
From: wlzboy <66905212@qq.com>
Date: 星期二, 11 十一月 2025 20:27:33 +0800
Subject: [PATCH] feat:增加天地图接口,并增加车辆GPS统计
---
ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml | 17
sql/车辆里程统计实现总结.md | 339 ++++++++
sql/vehicle_mileage_stats_job.sql | 17
ruoyi-system/src/main/resources/mapper/system/VehicleMileageStatsMapper.xml | 132 +++
sql/vehicle_mileage_stats.sql | 40 +
ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/VehicleMileageStatsTask.java | 84 ++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleMileageStatsServiceImpl.java | 296 +++++++
ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsMapper.java | 21
ruoyi-admin/src/main/resources/application.yml | 5
ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleMileageStatsService.java | 76 +
sql/车辆里程统计使用说明.md | 202 +++++
sql/vehicle_mileage_stats_menu.sql | 56 +
app/api/mileageStats.js | 67 +
ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleMileageStatsMapper.java | 83 ++
ruoyi-common/src/main/java/com/ruoyi/common/config/TiandituMapConfig.java | 25
ruoyi-system/src/main/java/com/ruoyi/system/domain/TaskTimeInterval.java | 51 +
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java | 394 ++++++++++
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleMileageStatsController.java | 135 +++
app/api/map.js | 123 +++
ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleMileageStats.java | 134 +++
20 files changed, 2,297 insertions(+), 0 deletions(-)
diff --git a/app/api/map.js b/app/api/map.js
index 05acf2e..bf9cc31 100644
--- a/app/api/map.js
+++ b/app/api/map.js
@@ -143,4 +143,127 @@
region: region || '骞垮窞'
}
})
+}
+
+// ==================== 澶╁湴鍥炬帴鍙� ====================
+
+// 澶╁湴鍥惧湴鐞嗙紪鐮丄PI锛堝湴鍧�杞潗鏍囷級
+export function tiandituGeocoding(address) {
+ // 鍙傛暟楠岃瘉
+ if (!address) {
+ return Promise.reject(new Error('鍙傛暟涓嶅畬鏁�,缂哄皯鍦板潃淇℃伅'))
+ }
+
+ return request({
+ url: '/system/gps/tianditu/geocoding',
+ method: 'get',
+ params: {
+ address: address
+ }
+ })
+}
+
+// 澶╁湴鍥鹃�嗗湴鐞嗙紪鐮丄PI锛堝潗鏍囪浆鍦板潃锛�
+export function tiandituReverseGeocoding(lon, lat) {
+ // 鍙傛暟楠岃瘉
+ if (lat === undefined || lat === null || lon === undefined || lon === null) {
+ return Promise.reject(new Error('鍙傛暟涓嶅畬鏁�,缂哄皯缁忕含搴﹀潗鏍�'))
+ }
+
+ // 妫�鏌ュ弬鏁版湁鏁堟��
+ if (isNaN(lat) || isNaN(lon)) {
+ return Promise.reject(new Error('鍙傛暟鏃犳晥,缁忕含搴﹀潗鏍囨牸寮忛敊璇�'))
+ }
+
+ return request({
+ url: '/system/gps/tianditu/reverseGeocoding',
+ method: 'get',
+ params: {
+ lon: lon,
+ lat: lat
+ }
+ })
+}
+
+// 澶╁湴鍥綪OI鎼滅储API
+export function tiandituPlaceSearch(keyWord, queryType, level, mapBound, start, count) {
+ // 鍙傛暟楠岃瘉
+ if (!keyWord) {
+ return Promise.reject(new Error('鍙傛暟涓嶅畬鏁�,缂哄皯鎼滅储鍏抽敭璇�'))
+ }
+
+ return request({
+ url: '/system/gps/tianditu/place/search',
+ method: 'get',
+ params: {
+ keyWord: keyWord,
+ queryType: queryType || '1',
+ level: level,
+ mapBound: mapBound,
+ start: start || 0,
+ count: count || 10
+ }
+ })
+}
+
+// 澶╁湴鍥鹃┚杞﹁矾寰勮鍒扐PI
+export function tiandituRouteDriving(orig, dest, mid, style) {
+ // 鍙傛暟楠岃瘉
+ if (!orig || !dest) {
+ return Promise.reject(new Error('鍙傛暟涓嶅畬鏁�,缂哄皯璧风偣鎴栫粓鐐瑰潗鏍�'))
+ }
+
+ // 楠岃瘉鍧愭爣鏍煎紡锛堢粡搴�,绾害锛�
+ const origParts = orig.split(',')
+ const destParts = dest.split(',')
+ if (origParts.length !== 2 || destParts.length !== 2) {
+ return Promise.reject(new Error('鍧愭爣鏍煎紡閿欒,搴斾负:缁忓害,绾害'))
+ }
+
+ return request({
+ url: '/system/gps/tianditu/route/driving',
+ method: 'get',
+ params: {
+ orig: orig,
+ dest: dest,
+ mid: mid,
+ style: style || '0'
+ }
+ })
+}
+
+// 澶╁湴鍥捐绠椾袱涓湴鍧�涔嬮棿鐨勮窛绂伙紙缁勫悎鎺ュ彛锛氬湴鍧�杞潗鏍� + 璺緞瑙勫垝锛�
+export function tiandituDistanceByAddress(fromAddress, toAddress) {
+ // 鍙傛暟楠岃瘉
+ if (!fromAddress || !toAddress) {
+ return Promise.reject(new Error('鍙傛暟涓嶅畬鏁�,缂哄皯璧风偣鎴栫粓鐐瑰湴鍧�'))
+ }
+
+ return request({
+ url: '/system/gps/tianditu/distance/byAddress',
+ method: 'get',
+ params: {
+ fromAddress: fromAddress,
+ toAddress: toAddress
+ }
+ })
+}
+
+// 澶╁湴鍥惧湴鍧�鎼滅储鎻愮ずAPI锛堣緭鍏ヨ仈鎯筹級
+export function tiandituPlaceSuggestion(keyWord, region, city, count) {
+ // 鍙傛暟楠岃瘉
+ if (!keyWord) {
+ return Promise.reject(new Error('鍙傛暟涓嶅畬鏁�,缂哄皯鎼滅储鍏抽敭璇�'))
+ }
+
+ return request({
+ url: '/system/gps/tianditu/place/suggestion',
+ method: 'get',
+ params: {
+ keyWord: keyWord,
+ region: region,
+ city: city,
+ count: count || 10
+ }
+ })
}
\ No newline at end of file
diff --git a/app/api/mileageStats.js b/app/api/mileageStats.js
new file mode 100644
index 0000000..3388dbc
--- /dev/null
+++ b/app/api/mileageStats.js
@@ -0,0 +1,67 @@
+import request from '@/utils/request'
+
+// 鏌ヨ杞﹁締閲岀▼缁熻鍒楄〃
+export function listMileageStats(query) {
+ return request({
+ url: '/system/mileageStats/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ杞﹁締閲岀▼缁熻璇︾粏
+export function getMileageStats(statsId) {
+ return request({
+ url: '/system/mileageStats/' + statsId,
+ method: 'get'
+ })
+}
+
+// 鏂板杞﹁締閲岀▼缁熻
+export function addMileageStats(data) {
+ return request({
+ url: '/system/mileageStats',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼杞﹁締閲岀▼缁熻
+export function updateMileageStats(data) {
+ return request({
+ url: '/system/mileageStats',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鍒犻櫎杞﹁締閲岀▼缁熻
+export function delMileageStats(statsIds) {
+ return request({
+ url: '/system/mileageStats/' + statsIds,
+ method: 'delete'
+ })
+}
+
+// 鎵嬪姩璁$畻鎸囧畾杞﹁締鎸囧畾鏃ユ湡鐨勯噷绋嬬粺璁�
+export function calculateMileage(vehicleId, statDate) {
+ return request({
+ url: '/system/mileageStats/calculate',
+ method: 'post',
+ params: {
+ vehicleId: vehicleId,
+ statDate: statDate
+ }
+ })
+}
+
+// 鎵归噺璁$畻鎸囧畾鏃ユ湡鎵�鏈夎溅杈嗙殑閲岀▼缁熻
+export function batchCalculateMileage(statDate) {
+ return request({
+ url: '/system/mileageStats/batchCalculate',
+ method: 'post',
+ params: {
+ statDate: statDate
+ }
+ })
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java
index 1a770d2..b67fb67 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java
@@ -11,6 +11,7 @@
import com.ruoyi.system.service.*;
import com.ruoyi.common.config.TencentMapConfig;
import com.ruoyi.common.config.BaiduMapConfig;
+import com.ruoyi.common.config.TiandituMapConfig;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
@@ -63,6 +64,9 @@
@Autowired
private BaiduMapConfig baiduMapConfig;
+
+ @Autowired
+ private TiandituMapConfig tiandituMapConfig;
/**
* 鏌ヨ杞﹁締GPS鍧愭爣鍒楄〃
@@ -740,4 +744,394 @@
return AjaxResult.error("鍦板潃鎼滅储澶辫触锛�" + e.getMessage());
}
}
+
+ // ==================== 澶╁湴鍥炬帴鍙� ====================
+
+ /**
+ * 澶╁湴鍥惧湴鐞嗙紪鐮佹帴鍙d唬鐞嗭紙鍦板潃杞潗鏍囷級
+ * 鏂囨。锛歨ttps://lbs.tianditu.gov.cn/server/geocoding.html
+ */
+ @Anonymous()
+ @GetMapping("/tianditu/geocoding")
+ public AjaxResult tiandituGeocoding(String address) {
+ try {
+ // 妫�鏌ュ弬鏁�
+ if (address == null || address.trim().isEmpty()) {
+ return AjaxResult.error("鍙傛暟涓嶅畬鏁达紝缂哄皯鍦板潃淇℃伅");
+ }
+
+ // 鏋勫缓澶╁湴鍥惧湴鐞嗙紪鐮丄PI URL
+ String url = "http://api.tianditu.gov.cn/geocoder";
+ String params = "ds={\"keyWord\":\"" + address + \"}" +
+ "&tk=" + tiandituMapConfig.getTk();
+
+ logger.info("澶╁湴鍥惧湴鐞嗙紪鐮佽姹�: address={}", address);
+
+ // 鍙戦�丠TTP璇锋眰
+ String response = HttpUtils.sendGet(url, params);
+
+ // 杩斿洖缁撴灉
+ return AjaxResult.success("鏌ヨ鎴愬姛", response);
+ } catch (Exception e) {
+ logger.error("澶╁湴鍥惧湴鐞嗙紪鐮佸け璐�", e);
+ return AjaxResult.error("鍦扮悊缂栫爜澶辫触锛�" + e.getMessage());
+ }
+ }
+
+ /**
+ * 澶╁湴鍥鹃�嗗湴鐞嗙紪鐮佹帴鍙d唬鐞嗭紙鍧愭爣杞湴鍧�锛�
+ * 鏂囨。锛歨ttps://lbs.tianditu.gov.cn/server/geocoding.html
+ */
+ @Anonymous()
+ @GetMapping("/tianditu/reverseGeocoding")
+ public AjaxResult tiandituReverseGeocoding(Double lon, Double lat) {
+ try {
+ // 妫�鏌ュ弬鏁�
+ if (lat == null || lon == null) {
+ return AjaxResult.error("鍙傛暟涓嶅畬鏁达紝缂哄皯缁忕含搴﹀潗鏍�");
+ }
+
+ // 妫�鏌ュ弬鏁版湁鏁堟��
+ if (Double.isNaN(lat) || Double.isNaN(lon) ||
+ Double.isInfinite(lat) || Double.isInfinite(lon)) {
+ return AjaxResult.error("鍙傛暟鏃犳晥锛岀粡绾害鍧愭爣鏍煎紡閿欒");
+ }
+
+ // 鏋勫缓澶╁湴鍥鹃�嗗湴鐞嗙紪鐮丄PI URL
+ String url = "http://api.tianditu.gov.cn/geocoder";
+ String params = "postStr={\"lon\":" + lon + ",\"lat\":" + lat + ",\"ver\":1}" +
+ "&type=geocode" +
+ "&tk=" + tiandituMapConfig.getTk();
+
+ logger.info("澶╁湴鍥鹃�嗗湴鐞嗙紪鐮佽姹�: lon={}, lat={}", lon, lat);
+
+ // 鍙戦�丠TTP璇锋眰
+ String response = HttpUtils.sendGet(url, params);
+
+ // 杩斿洖缁撴灉
+ return AjaxResult.success("鏌ヨ鎴愬姛", response);
+ } catch (Exception e) {
+ logger.error("澶╁湴鍥鹃�嗗湴鐞嗙紪鐮佸け璐�: lon={}, lat={}", lon, lat, e);
+ return AjaxResult.error("閫嗗湴鐞嗙紪鐮佸け璐ワ細" + e.getMessage());
+ }
+ }
+
+ /**
+ * 澶╁湴鍥惧湴鐐规悳绱㈡帴鍙d唬鐞嗭紙POI鎼滅储锛�
+ * 鏂囨。锛歨ttps://lbs.tianditu.gov.cn/server/search.html
+ */
+ @Anonymous()
+ @GetMapping("/tianditu/place/search")
+ public AjaxResult tiandituPlaceSearch(String keyWord, String queryType, String level,
+ String mapBound, Integer start, Integer count) {
+ try {
+ // 妫�鏌ュ弬鏁�
+ if (keyWord == null || keyWord.trim().isEmpty()) {
+ return AjaxResult.error("鍙傛暟涓嶅畬鏁达紝缂哄皯鎼滅储鍏抽敭璇�");
+ }
+
+ // 璁剧疆榛樿鍊�
+ if (queryType == null || queryType.trim().isEmpty()) {
+ queryType = "1"; // 1-鏅�氭悳绱紝7-鍛ㄨ竟鎼滅储
+ }
+ if (start == null) {
+ start = 0;
+ }
+ if (count == null) {
+ count = 10;
+ }
+
+ // 鏋勫缓澶╁湴鍥綪OI鎼滅储API URL
+ String url = "http://api.tianditu.gov.cn/search";
+ StringBuilder paramsBuilder = new StringBuilder();
+ paramsBuilder.append("postStr={\"keyWord\":\"").append(keyWord).append("\"");
+ paramsBuilder.append(",\"queryType\":\"").append(queryType).append("\"");
+ if (level != null && !level.trim().isEmpty()) {
+ paramsBuilder.append(",\"level\":\"").append(level).append("\"");
+ }
+ if (mapBound != null && !mapBound.trim().isEmpty()) {
+ paramsBuilder.append(",\"mapBound\":\"").append(mapBound).append("\"");
+ }
+ paramsBuilder.append(",\"start\":\"").append(start).append("\"");
+ paramsBuilder.append(",\"count\":\"").append(count).append("\"");
+ paramsBuilder.append("}" );
+ paramsBuilder.append("&type=query");
+ paramsBuilder.append("&tk=").append(tiandituMapConfig.getTk());
+
+ String params = paramsBuilder.toString();
+
+ logger.info("澶╁湴鍥綪OI鎼滅储璇锋眰: keyWord={}, queryType={}", keyWord, queryType);
+
+ // 鍙戦�丠TTP璇锋眰
+ String response = HttpUtils.sendGet(url, params);
+
+ // 杩斿洖缁撴灉
+ return AjaxResult.success("鏌ヨ鎴愬姛", response);
+ } catch (Exception e) {
+ logger.error("澶╁湴鍥綪OI鎼滅储澶辫触", e);
+ return AjaxResult.error("POI鎼滅储澶辫触锛�" + e.getMessage());
+ }
+ }
+
+ /**
+ * 澶╁湴鍥捐矾绾胯鍒掓帴鍙d唬鐞嗭紙椹捐溅璺緞瑙勫垝锛�
+ * 鏂囨。锛歨ttps://lbs.tianditu.gov.cn/server/drive.html
+ */
+ @Anonymous()
+ @GetMapping("/tianditu/route/driving")
+ public AjaxResult tiandituRouteDriving(String orig, String dest, String mid, String style) {
+ try {
+ // 妫�鏌ュ弬鏁�
+ if (orig == null || orig.trim().isEmpty() ||
+ dest == null || dest.trim().isEmpty()) {
+ return AjaxResult.error("鍙傛暟涓嶅畬鏁达紝缂哄皯璧风偣鎴栫粓鐐瑰潗鏍�");
+ }
+
+ // 楠岃瘉鍧愭爣鏍煎紡锛堢粡搴�,绾害锛�
+ String[] origParts = orig.split(",");
+ String[] destParts = dest.split(",");
+ if (origParts.length != 2 || destParts.length != 2) {
+ return AjaxResult.error("鍧愭爣鏍煎紡閿欒锛屽簲涓猴細缁忓害,绾害");
+ }
+
+ // 璁剧疆榛樿鍊�
+ if (style == null || style.trim().isEmpty()) {
+ style = "0"; // 0-鎺ㄨ崘锛�1-閬垮紑楂橀��
+ }
+
+ // 鏋勫缓澶╁湴鍥鹃┚杞﹁矾寰勮鍒扐PI URL
+ String url = "http://api.tianditu.gov.cn/drive";
+ StringBuilder paramsBuilder = new StringBuilder();
+ paramsBuilder.append("postStr={\"orig\":\"").append(orig).append("\"");
+ paramsBuilder.append(",\"dest\":\"").append(dest).append("\"");
+ if (mid != null && !mid.trim().isEmpty()) {
+ paramsBuilder.append(",\"mid\":\"").append(mid).append("\"");
+ }
+ paramsBuilder.append(",\"style\":\"").append(style).append("\"");
+ paramsBuilder.append("}" );
+ paramsBuilder.append("&tk=").append(tiandituMapConfig.getTk());
+
+ String params = paramsBuilder.toString();
+
+ logger.info("澶╁湴鍥鹃┚杞﹁矾寰勮鍒掕姹�: orig={}, dest={}", orig, dest);
+
+ // 鍙戦�丠TTP璇锋眰
+ String response = HttpUtils.sendGet(url, params);
+
+ // 杩斿洖缁撴灉
+ return AjaxResult.success("璁$畻鎴愬姛", response);
+ } catch (Exception e) {
+ logger.error("澶╁湴鍥鹃┚杞﹁矾寰勮鍒掑け璐�", e);
+ return AjaxResult.error("璺緞瑙勫垝澶辫触锛�" + e.getMessage());
+ }
+ }
+
+ /**
+ * 澶╁湴鍥捐绠椾袱涓湴鍧�涔嬮棿鐨勮窛绂伙紙缁勫悎鎺ュ彛锛氬湴鍧�杞潗鏍� + 璺緞瑙勫垝锛�
+ */
+ @Anonymous()
+ @GetMapping("/tianditu/distance/byAddress")
+ public AjaxResult tiandituDistanceByAddress(String fromAddress, String toAddress) {
+ try {
+ // 妫�鏌ュ弬鏁�
+ if (fromAddress == null || fromAddress.trim().isEmpty() ||
+ toAddress == null || toAddress.trim().isEmpty()) {
+ return AjaxResult.error("鍙傛暟涓嶅畬鏁达紝缂哄皯璧风偣鎴栫粓鐐瑰湴鍧�");
+ }
+
+ logger.info("寮�濮嬭绠楀湴鍧�璺濈: fromAddress={}, toAddress={}", fromAddress, toAddress);
+
+ // 绗竴姝ワ細璧风偣鍦板潃杞潗鏍�
+ String geocodingUrl1 = "http://api.tianditu.gov.cn/geocoder";
+ String geocodingParams1 = "ds={\"keyWord\":\"" + fromAddress + \"}" +
+ "&tk=" + tiandituMapConfig.getTk();
+
+ String geocodingResponse1 = HttpUtils.sendGet(geocodingUrl1, geocodingParams1);
+ logger.info("璧风偣鍦扮悊缂栫爜鍝嶅簲: {}", geocodingResponse1);
+
+ // 瑙f瀽璧风偣鍧愭爣
+ com.alibaba.fastjson2.JSONObject geocodingJson1 = com.alibaba.fastjson2.JSONObject.parseObject(geocodingResponse1);
+ if (!"0".equals(geocodingJson1.getString("status"))) {
+ logger.error("璧风偣鍦扮悊缂栫爜澶辫触: {}", geocodingResponse1);
+ return AjaxResult.error("璧风偣鍦板潃瑙f瀽澶辫触");
+ }
+ com.alibaba.fastjson2.JSONObject location1 = geocodingJson1.getJSONObject("location");
+ if (location1 == null) {
+ return AjaxResult.error("璧风偣鍦板潃鏈壘鍒板搴斿潗鏍�");
+ }
+ double fromLon = location1.getDouble("lon");
+ double fromLat = location1.getDouble("lat");
+ logger.info("璧风偣鍧愭爣: lon={}, lat={}", fromLon, fromLat);
+
+ // 绗簩姝ワ細缁堢偣鍦板潃杞潗鏍�
+ String geocodingUrl2 = "http://api.tianditu.gov.cn/geocoder";
+ String geocodingParams2 = "ds={\"keyWord\":\"" + toAddress + \"}" +
+ "&tk=" + tiandituMapConfig.getTk();
+
+ String geocodingResponse2 = HttpUtils.sendGet(geocodingUrl2, geocodingParams2);
+ logger.info("缁堢偣鍦扮悊缂栫爜鍝嶅簲: {}", geocodingResponse2);
+
+ // 瑙f瀽缁堢偣鍧愭爣
+ com.alibaba.fastjson2.JSONObject geocodingJson2 = com.alibaba.fastjson2.JSONObject.parseObject(geocodingResponse2);
+ if (!"0".equals(geocodingJson2.getString("status"))) {
+ logger.error("缁堢偣鍦扮悊缂栫爜澶辫触: {}", geocodingResponse2);
+ return AjaxResult.error("缁堢偣鍦板潃瑙f瀽澶辫触");
+ }
+ com.alibaba.fastjson2.JSONObject location2 = geocodingJson2.getJSONObject("location");
+ if (location2 == null) {
+ return AjaxResult.error("缁堢偣鍦板潃鏈壘鍒板搴斿潗鏍�");
+ }
+ double toLon = location2.getDouble("lon");
+ double toLat = location2.getDouble("lat");
+ logger.info("缁堢偣鍧愭爣: lon={}, lat={}", toLon, toLat);
+
+ // 绗笁姝ワ細璋冪敤璺緞瑙勫垝鎺ュ彛璁$畻璺濈
+ String routeUrl = "http://api.tianditu.gov.cn/drive";
+ String orig = fromLon + "," + fromLat;
+ String dest = toLon + "," + toLat;
+ String routeParams = "postStr={\"orig\":\"" + orig + "\",\"dest\":\"" + dest + "\",\"style\":\"0\"}" +
+ "&tk=" + tiandituMapConfig.getTk();
+
+ logger.info("璺緞瑙勫垝璇锋眰: orig={}, dest={}", orig, dest);
+ String routeResponse = HttpUtils.sendGet(routeUrl, routeParams);
+ logger.info("璺緞瑙勫垝鍝嶅簲: {}", routeResponse);
+
+ // 瑙f瀽璺濈缁撴灉
+ com.alibaba.fastjson2.JSONObject routeJson = com.alibaba.fastjson2.JSONObject.parseObject(routeResponse);
+ if (!"0".equals(routeJson.getString("status"))) {
+ logger.error("璺緞瑙勫垝澶辫触: {}", routeResponse);
+ return AjaxResult.error("璺緞瑙勫垝澶辫触");
+ }
+
+ // 鎻愬彇璺濈淇℃伅
+ com.alibaba.fastjson2.JSONObject result = routeJson.getJSONObject("result");
+ if (result == null) {
+ logger.error("璺緞瑙勫垝缁撴灉涓虹┖");
+ return AjaxResult.error("璺緞瑙勫垝澶辫触");
+ }
+
+ com.alibaba.fastjson2.JSONArray routes = result.getJSONArray("routes");
+ if (routes == null || routes.isEmpty()) {
+ logger.error("鏈壘鍒拌矾绾夸俊鎭�");
+ return AjaxResult.error("鏈壘鍒拌矾绾夸俊鎭�");
+ }
+
+ com.alibaba.fastjson2.JSONObject route = routes.getJSONObject(0);
+ double distance = route.getDoubleValue("distance"); // 璺濈锛屽崟浣嶏細绫�
+ double duration = route.getDoubleValue("duration"); // 鏃堕暱锛屽崟浣嶏細绉�
+
+ logger.info("璁$畻鎴愬姛: 璺濈={}绫�, 鏃堕暱={}绉�", distance, duration);
+
+ // 鏋勫缓杩斿洖缁撴灉
+ Map<String, Object> resultMap = new HashMap<>();
+ resultMap.put("distance", (int)distance); // 璺濈锛堢背锛�
+ resultMap.put("duration", (int)duration); // 鏃堕暱锛堢锛�
+ resultMap.put("distanceKm", String.format("%.1f", distance / 1000.0)); // 璺濈锛堝叕閲岋級
+ resultMap.put("durationMin", (int)(duration / 60)); // 鏃堕暱锛堝垎閽燂級
+
+ // 璧风偣鍧愭爣
+ Map<String, Object> fromLocation = new HashMap<>();
+ fromLocation.put("lon", fromLon);
+ fromLocation.put("lat", fromLat);
+ resultMap.put("fromLocation", fromLocation);
+
+ // 缁堢偣鍧愭爣
+ Map<String, Object> toLocation = new HashMap<>();
+ toLocation.put("lon", toLon);
+ toLocation.put("lat", toLat);
+ resultMap.put("toLocation", toLocation);
+
+ return AjaxResult.success("璁$畻鎴愬姛", resultMap);
+ } catch (Exception e) {
+ logger.error("璁$畻鍦板潃璺濈澶辫触", e);
+ return AjaxResult.error("璁$畻璺濈澶辫触锛�" + e.getMessage());
+ }
+ }
+
+ /**
+ * 澶╁湴鍥捐緭鍏ユ彁绀烘帴鍙d唬鐞嗭紙鍦板潃鑱旀兂锛�
+ * 鏂囨。锛歨ttps://lbs.tianditu.gov.cn/server/suggestion.html
+ */
+ @Anonymous()
+ @GetMapping("/tianditu/place/suggestion")
+ public AjaxResult tiandituPlaceSuggestion(String keyWord, String region, String city, Integer count) {
+ try {
+ // 妫�鏌ュ弬鏁�
+ if (keyWord == null || keyWord.trim().isEmpty()) {
+ return AjaxResult.error("鍙傛暟涓嶅畬鏁达紝缂哄皯鎼滅储鍏抽敭璇�");
+ }
+
+ // 璁剧疆榛樿鍊�
+ if (count == null) {
+ count = 10;
+ }
+
+ // 鏋勫缓澶╁湴鍥捐緭鍏ユ彁绀篈PI URL
+ String url = "http://api.tianditu.gov.cn/search";
+ StringBuilder paramsBuilder = new StringBuilder();
+ paramsBuilder.append("postStr={\"keyWord\":\"").append(keyWord).append("\"");
+ if (region != null && !region.trim().isEmpty()) {
+ paramsBuilder.append(",\"region\":\"").append(region).append("\"");
+ }
+ if (city != null && !city.trim().isEmpty()) {
+ paramsBuilder.append(",\"city\":\"").append(city).append("\"");
+ }
+ paramsBuilder.append(",\"count\":\"").append(count).append("\"");
+ paramsBuilder.append("}" );
+ paramsBuilder.append("&type=suggest");
+ paramsBuilder.append("&tk=").append(tiandituMapConfig.getTk());
+
+ String params = paramsBuilder.toString();
+
+ logger.info("澶╁湴鍥捐緭鍏ユ彁绀鸿姹�: keyWord={}, region={}", keyWord, region);
+
+ // 鍙戦�丠TTP璇锋眰
+ String response = HttpUtils.sendGet(url, params);
+ logger.debug("澶╁湴鍥捐緭鍏ユ彁绀哄搷搴�: {}", response);
+
+ // 瑙f瀽鍝嶅簲
+ com.alibaba.fastjson2.JSONObject jsonResponse = com.alibaba.fastjson2.JSONObject.parseObject(response);
+ if (!"0".equals(jsonResponse.getString("status"))) {
+ logger.error("杈撳叆鎻愮ず澶辫触: {}", response);
+ return AjaxResult.error("鍦板潃鎼滅储澶辫触");
+ }
+
+ // 鎻愬彇鎻愮ず鍒楄〃
+ com.alibaba.fastjson2.JSONArray results = jsonResponse.getJSONArray("suggests");
+ if (results == null || results.isEmpty()) {
+ logger.info("鏈壘鍒板尮閰嶇殑鍦板潃");
+ return AjaxResult.success("鏌ヨ鎴愬姛", new ArrayList<>());
+ }
+
+ // 鏋勫缓杩斿洖缁撴灉
+ List<Map<String, Object>> suggestions = new ArrayList<>();
+ for (int i = 0; i < results.size(); i++) {
+ com.alibaba.fastjson2.JSONObject item = results.getJSONObject(i);
+
+ Map<String, Object> suggestion = new HashMap<>();
+ suggestion.put("name", item.getString("name")); // 鍚嶇О
+ suggestion.put("address", item.getString("address")); // 鍦板潃
+ suggestion.put("province", item.getString("province")); // 鐪�
+ suggestion.put("city", item.getString("city")); // 甯�
+ suggestion.put("district", item.getString("district")); // 鍖�
+
+ // 缁忕含搴︿俊鎭�
+ com.alibaba.fastjson2.JSONObject location = item.getJSONObject("location");
+ if (location != null) {
+ Map<String, Object> locationMap = new HashMap<>();
+ locationMap.put("lon", location.getDouble("lon"));
+ locationMap.put("lat", location.getDouble("lat"));
+ suggestion.put("location", locationMap);
+ }
+
+ suggestions.add(suggestion);
+ }
+
+ logger.info("鍦板潃鎼滅储鎻愮ず鎴愬姛: 鎵惧埌{}鏉$粨鏋�", suggestions.size());
+ return AjaxResult.success("鏌ヨ鎴愬姛", suggestions);
+ } catch (Exception e) {
+ logger.error("鍦板潃鎼滅储鎻愮ず澶辫触", e);
+ return AjaxResult.error("鍦板潃鎼滅储澶辫触锛�" + e.getMessage());
+ }
+ }
}
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleMileageStatsController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleMileageStatsController.java
new file mode 100644
index 0000000..2383778
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleMileageStatsController.java
@@ -0,0 +1,135 @@
+package com.ruoyi.web.controller.system;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.domain.VehicleMileageStats;
+import com.ruoyi.system.service.IVehicleMileageStatsService;
+
+/**
+ * 杞﹁締閲岀▼缁熻Controller
+ */
+@RestController
+@RequestMapping("/system/mileageStats")
+public class VehicleMileageStatsController extends BaseController {
+
+ @Autowired
+ private IVehicleMileageStatsService vehicleMileageStatsService;
+
+ /**
+ * 鏌ヨ杞﹁締閲岀▼缁熻鍒楄〃
+ */
+ @PreAuthorize("@ss.hasPermi('system:mileageStats:list')")
+ @GetMapping("/list")
+ public TableDataInfo list(VehicleMileageStats vehicleMileageStats) {
+ startPage();
+ List<VehicleMileageStats> list = vehicleMileageStatsService.selectVehicleMileageStatsList(vehicleMileageStats);
+ return getDataTable(list);
+ }
+
+ /**
+ * 瀵煎嚭杞﹁締閲岀▼缁熻鍒楄〃
+ */
+ @PreAuthorize("@ss.hasPermi('system:mileageStats:export')")
+ @Log(title = "杞﹁締閲岀▼缁熻", businessType = BusinessType.EXPORT)
+ @PostMapping("/export")
+ public void export(HttpServletResponse response, VehicleMileageStats vehicleMileageStats) {
+ List<VehicleMileageStats> list = vehicleMileageStatsService.selectVehicleMileageStatsList(vehicleMileageStats);
+ ExcelUtil<VehicleMileageStats> util = new ExcelUtil<VehicleMileageStats>(VehicleMileageStats.class);
+ util.exportExcel(response, list, "杞﹁締閲岀▼缁熻鏁版嵁");
+ }
+
+ /**
+ * 鑾峰彇杞﹁締閲岀▼缁熻璇︾粏淇℃伅
+ */
+ @PreAuthorize("@ss.hasPermi('system:mileageStats:query')")
+ @GetMapping(value = "/{statsId}")
+ public AjaxResult getInfo(@PathVariable("statsId") Long statsId) {
+ return success(vehicleMileageStatsService.selectVehicleMileageStatsById(statsId));
+ }
+
+ /**
+ * 鏂板杞﹁締閲岀▼缁熻
+ */
+ @PreAuthorize("@ss.hasPermi('system:mileageStats:add')")
+ @Log(title = "杞﹁締閲岀▼缁熻", businessType = BusinessType.INSERT)
+ @PostMapping
+ public AjaxResult add(@RequestBody VehicleMileageStats vehicleMileageStats) {
+ return toAjax(vehicleMileageStatsService.insertVehicleMileageStats(vehicleMileageStats));
+ }
+
+ /**
+ * 淇敼杞﹁締閲岀▼缁熻
+ */
+ @PreAuthorize("@ss.hasPermi('system:mileageStats:edit')")
+ @Log(title = "杞﹁締閲岀▼缁熻", businessType = BusinessType.UPDATE)
+ @PutMapping
+ public AjaxResult edit(@RequestBody VehicleMileageStats vehicleMileageStats) {
+ return toAjax(vehicleMileageStatsService.updateVehicleMileageStats(vehicleMileageStats));
+ }
+
+ /**
+ * 鍒犻櫎杞﹁締閲岀▼缁熻
+ */
+ @PreAuthorize("@ss.hasPermi('system:mileageStats:remove')")
+ @Log(title = "杞﹁締閲岀▼缁熻", businessType = BusinessType.DELETE)
+ @DeleteMapping("/{statsIds}")
+ public AjaxResult remove(@PathVariable Long[] statsIds) {
+ return toAjax(vehicleMileageStatsService.deleteVehicleMileageStatsByIds(statsIds));
+ }
+
+ /**
+ * 鎵嬪姩瑙﹀彂璁$畻鎸囧畾杞﹁締鎸囧畾鏃ユ湡鐨勯噷绋嬬粺璁�
+ */
+ @PreAuthorize("@ss.hasPermi('system:mileageStats:calculate')")
+ @Log(title = "杞﹁締閲岀▼缁熻", businessType = BusinessType.OTHER)
+ @PostMapping("/calculate")
+ public AjaxResult calculate(Long vehicleId, String statDate) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+ Date date = sdf.parse(statDate);
+ VehicleMileageStats stats = vehicleMileageStatsService.calculateAndSaveMileageStats(vehicleId, date);
+ if (stats != null) {
+ return success("閲岀▼缁熻璁$畻鎴愬姛", stats);
+ } else {
+ return error("璇ヨ溅杈嗗湪鎸囧畾鏃ユ湡鏃燝PS鏁版嵁");
+ }
+ } catch (Exception e) {
+ return error("閲岀▼缁熻璁$畻澶辫触锛�" + e.getMessage());
+ }
+ }
+
+ /**
+ * 鎵归噺璁$畻鎸囧畾鏃ユ湡鎵�鏈夎溅杈嗙殑閲岀▼缁熻
+ */
+ @PreAuthorize("@ss.hasPermi('system:mileageStats:batch')")
+ @Log(title = "杞﹁締閲岀▼缁熻", businessType = BusinessType.OTHER)
+ @PostMapping("/batchCalculate")
+ public AjaxResult batchCalculate(String statDate) {
+ try {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+ Date date = sdf.parse(statDate);
+ int count = vehicleMileageStatsService.batchCalculateMileageStats(date);
+ return success("鎵归噺閲岀▼缁熻瀹屾垚锛屾垚鍔熺粺璁� " + count + " 杈嗚溅");
+ } catch (Exception e) {
+ return error("鎵归噺閲岀▼缁熻澶辫触锛�" + e.getMessage());
+ }
+ }
+}
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index a29e877..9db576a 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -165,3 +165,8 @@
baidu:
map:
ak: GX7G1RmAbTEQHor9NKpzRiB2jerqaY1E # 璇锋浛鎹负鎮ㄧ殑鐧惧害鍦板浘API Key
+
+# 澶╁湴鍥鹃厤缃�
+tianditu:
+ map:
+ tk: 4d1d0b3a4a03b9c5099c0e25ab1b23f3 # 璇锋浛鎹负鎮ㄧ殑澶╁湴鍥続PI Key
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/TiandituMapConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/TiandituMapConfig.java
new file mode 100644
index 0000000..edc8fae
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/TiandituMapConfig.java
@@ -0,0 +1,25 @@
+package com.ruoyi.common.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 澶╁湴鍥鹃厤缃被
+ */
+@Configuration
+@ConfigurationProperties(prefix = "tianditu.map")
+public class TiandituMapConfig {
+
+ /**
+ * 澶╁湴鍥続PI Key (tk鍙傛暟)
+ */
+ private String tk;
+
+ public String getTk() {
+ return tk;
+ }
+
+ public void setTk(String tk) {
+ this.tk = tk;
+ }
+}
diff --git a/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/VehicleMileageStatsTask.java b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/VehicleMileageStatsTask.java
new file mode 100644
index 0000000..22cd0b8
--- /dev/null
+++ b/ruoyi-quartz/src/main/java/com/ruoyi/quartz/task/VehicleMileageStatsTask.java
@@ -0,0 +1,84 @@
+package com.ruoyi.quartz.task;
+
+import java.util.Calendar;
+import java.util.Date;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import com.ruoyi.system.service.IVehicleMileageStatsService;
+
+/**
+ * 杞﹁締閲岀▼缁熻瀹氭椂浠诲姟
+ */
+@Component("vehicleMileageStatsTask")
+public class VehicleMileageStatsTask {
+
+ private static final Logger logger = LoggerFactory.getLogger(VehicleMileageStatsTask.class);
+
+ @Autowired
+ private IVehicleMileageStatsService vehicleMileageStatsService;
+
+ /**
+ * 璁$畻鏄ㄦ棩鎵�鏈夎溅杈嗙殑閲岀▼缁熻
+ */
+ public void calculateYesterdayMileage() {
+ logger.info("寮�濮嬫墽琛岃溅杈嗛噷绋嬬粺璁″畾鏃朵换鍔� - 缁熻鏄ㄦ棩閲岀▼");
+
+ try {
+ // 鑾峰彇鏄ㄥぉ鐨勬棩鏈�
+ Calendar calendar = Calendar.getInstance();
+ calendar.add(Calendar.DAY_OF_MONTH, -1);
+ calendar.set(Calendar.HOUR_OF_DAY, 0);
+ calendar.set(Calendar.MINUTE, 0);
+ calendar.set(Calendar.SECOND, 0);
+ calendar.set(Calendar.MILLISECOND, 0);
+ Date yesterday = calendar.getTime();
+
+ // 鎵归噺璁$畻閲岀▼缁熻
+ int successCount = vehicleMileageStatsService.batchCalculateMileageStats(yesterday);
+
+ logger.info("杞﹁締閲岀▼缁熻瀹氭椂浠诲姟鎵ц瀹屾垚 - 鎴愬姛缁熻: {} 杈嗚溅", successCount);
+
+ } catch (Exception e) {
+ logger.error("杞﹁締閲岀▼缁熻瀹氭椂浠诲姟鎵ц澶辫触", e);
+ throw new RuntimeException("瀹氭椂浠诲姟鎵ц澶辫触: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 璁$畻鎸囧畾鏃ユ湡鐨勯噷绋嬬粺璁★紙鐢ㄤ簬鎵嬪姩瑙﹀彂鎴栬ˉ绠楀巻鍙叉暟鎹級
+ *
+ * @param dateStr 鏃ユ湡瀛楃涓诧紝鏍煎紡锛歽yyy-MM-dd
+ */
+ public void calculateMileageByDate(String dateStr) {
+ logger.info("寮�濮嬫墽琛岃溅杈嗛噷绋嬬粺璁″畾鏃朵换鍔� - 缁熻鏃ユ湡: {}", dateStr);
+
+ try {
+ // 瑙f瀽鏃ユ湡瀛楃涓�
+ String[] parts = dateStr.split("-");
+ if (parts.length != 3) {
+ throw new IllegalArgumentException("鏃ユ湡鏍煎紡閿欒锛屽簲涓�: yyyy-MM-dd");
+ }
+
+ Calendar calendar = Calendar.getInstance();
+ calendar.set(Calendar.YEAR, Integer.parseInt(parts[0]));
+ calendar.set(Calendar.MONTH, Integer.parseInt(parts[1]) - 1); // 鏈堜唤浠�0寮�濮�
+ calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(parts[2]));
+ calendar.set(Calendar.HOUR_OF_DAY, 0);
+ calendar.set(Calendar.MINUTE, 0);
+ calendar.set(Calendar.SECOND, 0);
+ calendar.set(Calendar.MILLISECOND, 0);
+ Date targetDate = calendar.getTime();
+
+ // 鎵归噺璁$畻閲岀▼缁熻
+ int successCount = vehicleMileageStatsService.batchCalculateMileageStats(targetDate);
+
+ logger.info("杞﹁締閲岀▼缁熻瀹氭椂浠诲姟鎵ц瀹屾垚 - 鏃ユ湡: {}, 鎴愬姛缁熻: {} 杈嗚溅", dateStr, successCount);
+
+ } catch (Exception e) {
+ logger.error("杞﹁締閲岀▼缁熻瀹氭椂浠诲姟鎵ц澶辫触 - 鏃ユ湡: {}", dateStr, e);
+ throw new RuntimeException("瀹氭椂浠诲姟鎵ц澶辫触: " + e.getMessage());
+ }
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/TaskTimeInterval.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TaskTimeInterval.java
new file mode 100644
index 0000000..16f60eb
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TaskTimeInterval.java
@@ -0,0 +1,51 @@
+package com.ruoyi.system.domain;
+
+import java.util.Date;
+
+/**
+ * 浠诲姟鏃堕棿鍖洪棿
+ */
+public class TaskTimeInterval {
+
+ /** 浠诲姟ID */
+ private Long taskId;
+
+ /** 寮�濮嬫椂闂� */
+ private Date startTime;
+
+ /** 缁撴潫鏃堕棿 */
+ private Date endTime;
+
+ public TaskTimeInterval() {
+ }
+
+ public TaskTimeInterval(Long taskId, Date startTime, Date endTime) {
+ this.taskId = taskId;
+ this.startTime = startTime;
+ this.endTime = endTime;
+ }
+
+ public Long getTaskId() {
+ return taskId;
+ }
+
+ public void setTaskId(Long taskId) {
+ this.taskId = taskId;
+ }
+
+ public Date getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(Date startTime) {
+ this.startTime = startTime;
+ }
+
+ public Date getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(Date endTime) {
+ this.endTime = endTime;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleMileageStats.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleMileageStats.java
new file mode 100644
index 0000000..a1fe49d
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/VehicleMileageStats.java
@@ -0,0 +1,134 @@
+package com.ruoyi.system.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+/**
+ * 杞﹁締閲岀▼缁熻瀵硅薄 tb_vehicle_mileage_stats
+ */
+public class VehicleMileageStats extends BaseEntity {
+ private static final long serialVersionUID = 1L;
+
+ /** 缁熻ID */
+ private Long statsId;
+
+ /** 杞﹁締ID */
+ @Excel(name = "杞﹁締ID")
+ private Long vehicleId;
+
+ /** 杞︾墝鍙� */
+ @Excel(name = "杞︾墝鍙�")
+ private String vehicleNo;
+
+ /** 缁熻鏃ユ湡 */
+ @JsonFormat(pattern = "yyyy-MM-dd")
+ @Excel(name = "缁熻鏃ユ湡", width = 30, dateFormat = "yyyy-MM-dd")
+ private Date statDate;
+
+ /** 鎬婚噷绋�(鍏噷) */
+ @Excel(name = "鎬婚噷绋�(鍏噷)")
+ private BigDecimal totalMileage;
+
+ /** 浠诲姟鏃舵閲岀▼(鍏噷) */
+ @Excel(name = "浠诲姟鏃舵閲岀▼(鍏噷)")
+ private BigDecimal taskMileage;
+
+ /** 闈炰换鍔℃椂娈甸噷绋�(鍏噷) */
+ @Excel(name = "闈炰换鍔℃椂娈甸噷绋�(鍏噷)")
+ private BigDecimal nonTaskMileage;
+
+ /** 浠诲姟閲岀▼鍗犳瘮(0-1) */
+ @Excel(name = "浠诲姟閲岀▼鍗犳瘮")
+ private BigDecimal taskRatio;
+
+ /** GPS鐐规暟閲� */
+ @Excel(name = "GPS鐐规暟閲�")
+ private Integer gpsPointCount;
+
+ /** 浠诲姟鏁伴噺 */
+ @Excel(name = "浠诲姟鏁伴噺")
+ private Integer taskCount;
+
+ public Long getStatsId() {
+ return statsId;
+ }
+
+ public void setStatsId(Long statsId) {
+ this.statsId = statsId;
+ }
+
+ public Long getVehicleId() {
+ return vehicleId;
+ }
+
+ public void setVehicleId(Long vehicleId) {
+ this.vehicleId = vehicleId;
+ }
+
+ public String getVehicleNo() {
+ return vehicleNo;
+ }
+
+ public void setVehicleNo(String vehicleNo) {
+ this.vehicleNo = vehicleNo;
+ }
+
+ public Date getStatDate() {
+ return statDate;
+ }
+
+ public void setStatDate(Date statDate) {
+ this.statDate = statDate;
+ }
+
+ public BigDecimal getTotalMileage() {
+ return totalMileage;
+ }
+
+ public void setTotalMileage(BigDecimal totalMileage) {
+ this.totalMileage = totalMileage;
+ }
+
+ public BigDecimal getTaskMileage() {
+ return taskMileage;
+ }
+
+ public void setTaskMileage(BigDecimal taskMileage) {
+ this.taskMileage = taskMileage;
+ }
+
+ public BigDecimal getNonTaskMileage() {
+ return nonTaskMileage;
+ }
+
+ public void setNonTaskMileage(BigDecimal nonTaskMileage) {
+ this.nonTaskMileage = nonTaskMileage;
+ }
+
+ public BigDecimal getTaskRatio() {
+ return taskRatio;
+ }
+
+ public void setTaskRatio(BigDecimal taskRatio) {
+ this.taskRatio = taskRatio;
+ }
+
+ public Integer getGpsPointCount() {
+ return gpsPointCount;
+ }
+
+ public void setGpsPointCount(Integer gpsPointCount) {
+ this.gpsPointCount = gpsPointCount;
+ }
+
+ public Integer getTaskCount() {
+ return taskCount;
+ }
+
+ public void setTaskCount(Integer taskCount) {
+ this.taskCount = taskCount;
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsMapper.java
index 52f64bd..b1cae08 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleGpsMapper.java
@@ -1,6 +1,8 @@
package com.ruoyi.system.mapper;
+import java.util.Date;
import java.util.List;
+import org.apache.ibatis.annotations.Param;
import com.ruoyi.system.domain.VehicleGps;
/**
@@ -48,4 +50,23 @@
* @return 鍒犻櫎鐨勮褰曟暟
*/
public int deleteVehicleGpsBeforeDate();
+
+ /**
+ * 鏌ヨ杞﹁締鍦ㄦ寚瀹氭椂闂磋寖鍥村唴鐨凣PS鏁版嵁锛堟寜閲囬泦鏃堕棿鎺掑簭锛�
+ *
+ * @param vehicleId 杞﹁締ID
+ * @param startTime 寮�濮嬫椂闂�
+ * @param endTime 缁撴潫鏃堕棿
+ * @return GPS鍧愭爣鍒楄〃
+ */
+ public List<VehicleGps> selectGpsDataByTimeRange(@Param("vehicleId") Long vehicleId,
+ @Param("startTime") Date startTime,
+ @Param("endTime") Date endTime);
+
+ /**
+ * 鏌ヨ鎵�鏈夋椿璺冭溅杈咺D鍒楄〃
+ *
+ * @return 杞﹁締ID鍒楄〃
+ */
+ public List<Long> selectActiveVehicleIds();
}
\ No newline at end of file
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleMileageStatsMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleMileageStatsMapper.java
new file mode 100644
index 0000000..328dbe8
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/VehicleMileageStatsMapper.java
@@ -0,0 +1,83 @@
+package com.ruoyi.system.mapper;
+
+import java.util.Date;
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+import com.ruoyi.system.domain.VehicleMileageStats;
+import com.ruoyi.system.domain.TaskTimeInterval;
+
+/**
+ * 杞﹁締閲岀▼缁熻Mapper鎺ュ彛
+ */
+public interface VehicleMileageStatsMapper {
+
+ /**
+ * 鏌ヨ杞﹁締閲岀▼缁熻
+ *
+ * @param statsId 缁熻ID
+ * @return 杞﹁締閲岀▼缁熻
+ */
+ public VehicleMileageStats selectVehicleMileageStatsById(Long statsId);
+
+ /**
+ * 鏌ヨ杞﹁締閲岀▼缁熻鍒楄〃
+ *
+ * @param vehicleMileageStats 杞﹁締閲岀▼缁熻
+ * @return 杞﹁締閲岀▼缁熻闆嗗悎
+ */
+ public List<VehicleMileageStats> selectVehicleMileageStatsList(VehicleMileageStats vehicleMileageStats);
+
+ /**
+ * 鏂板杞﹁締閲岀▼缁熻
+ *
+ * @param vehicleMileageStats 杞﹁締閲岀▼缁熻
+ * @return 缁撴灉
+ */
+ public int insertVehicleMileageStats(VehicleMileageStats vehicleMileageStats);
+
+ /**
+ * 淇敼杞﹁締閲岀▼缁熻
+ *
+ * @param vehicleMileageStats 杞﹁締閲岀▼缁熻
+ * @return 缁撴灉
+ */
+ public int updateVehicleMileageStats(VehicleMileageStats vehicleMileageStats);
+
+ /**
+ * 鍒犻櫎杞﹁締閲岀▼缁熻
+ *
+ * @param statsId 缁熻ID
+ * @return 缁撴灉
+ */
+ public int deleteVehicleMileageStatsById(Long statsId);
+
+ /**
+ * 鎵归噺鍒犻櫎杞﹁締閲岀▼缁熻
+ *
+ * @param statsIds 闇�瑕佸垹闄ょ殑鏁版嵁ID
+ * @return 缁撴灉
+ */
+ public int deleteVehicleMileageStatsByIds(Long[] statsIds);
+
+ /**
+ * 鏌ヨ杞﹁締鍦ㄦ寚瀹氭棩鏈熺殑缁熻璁板綍
+ *
+ * @param vehicleId 杞﹁締ID
+ * @param statDate 缁熻鏃ユ湡
+ * @return 缁熻璁板綍
+ */
+ public VehicleMileageStats selectByVehicleIdAndDate(@Param("vehicleId") Long vehicleId,
+ @Param("statDate") Date statDate);
+
+ /**
+ * 鏌ヨ杞﹁締鍦ㄦ寚瀹氭椂闂磋寖鍥村唴鐨勪换鍔℃椂闂村尯闂�
+ *
+ * @param vehicleId 杞﹁締ID
+ * @param startTime 寮�濮嬫椂闂�
+ * @param endTime 缁撴潫鏃堕棿
+ * @return 浠诲姟鏃堕棿鍖洪棿鍒楄〃
+ */
+ public List<TaskTimeInterval> selectTaskTimeIntervals(@Param("vehicleId") Long vehicleId,
+ @Param("startTime") Date startTime,
+ @Param("endTime") Date endTime);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleMileageStatsService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleMileageStatsService.java
new file mode 100644
index 0000000..c030849
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IVehicleMileageStatsService.java
@@ -0,0 +1,76 @@
+package com.ruoyi.system.service;
+
+import java.util.Date;
+import java.util.List;
+import com.ruoyi.system.domain.VehicleMileageStats;
+
+/**
+ * 杞﹁締閲岀▼缁熻Service鎺ュ彛
+ */
+public interface IVehicleMileageStatsService {
+
+ /**
+ * 鏌ヨ杞﹁締閲岀▼缁熻
+ *
+ * @param statsId 缁熻ID
+ * @return 杞﹁締閲岀▼缁熻
+ */
+ public VehicleMileageStats selectVehicleMileageStatsById(Long statsId);
+
+ /**
+ * 鏌ヨ杞﹁締閲岀▼缁熻鍒楄〃
+ *
+ * @param vehicleMileageStats 杞﹁締閲岀▼缁熻
+ * @return 杞﹁締閲岀▼缁熻闆嗗悎
+ */
+ public List<VehicleMileageStats> selectVehicleMileageStatsList(VehicleMileageStats vehicleMileageStats);
+
+ /**
+ * 鏂板杞﹁締閲岀▼缁熻
+ *
+ * @param vehicleMileageStats 杞﹁締閲岀▼缁熻
+ * @return 缁撴灉
+ */
+ public int insertVehicleMileageStats(VehicleMileageStats vehicleMileageStats);
+
+ /**
+ * 淇敼杞﹁締閲岀▼缁熻
+ *
+ * @param vehicleMileageStats 杞﹁締閲岀▼缁熻
+ * @return 缁撴灉
+ */
+ public int updateVehicleMileageStats(VehicleMileageStats vehicleMileageStats);
+
+ /**
+ * 鎵归噺鍒犻櫎杞﹁締閲岀▼缁熻
+ *
+ * @param statsIds 闇�瑕佸垹闄ょ殑鏁版嵁ID
+ * @return 缁撴灉
+ */
+ public int deleteVehicleMileageStatsByIds(Long[] statsIds);
+
+ /**
+ * 鍒犻櫎杞﹁締閲岀▼缁熻淇℃伅
+ *
+ * @param statsId 缁熻ID
+ * @return 缁撴灉
+ */
+ public int deleteVehicleMileageStatsById(Long statsId);
+
+ /**
+ * 璁$畻骞朵繚瀛樻寚瀹氳溅杈嗘寚瀹氭棩鏈熺殑閲岀▼缁熻
+ *
+ * @param vehicleId 杞﹁締ID
+ * @param statDate 缁熻鏃ユ湡
+ * @return 缁熻缁撴灉
+ */
+ public VehicleMileageStats calculateAndSaveMileageStats(Long vehicleId, Date statDate);
+
+ /**
+ * 鎵归噺璁$畻鎵�鏈夎溅杈嗘寚瀹氭棩鏈熺殑閲岀▼缁熻
+ *
+ * @param statDate 缁熻鏃ユ湡
+ * @return 鎴愬姛缁熻鐨勮溅杈嗘暟閲�
+ */
+ public int batchCalculateMileageStats(Date statDate);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleMileageStatsServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleMileageStatsServiceImpl.java
new file mode 100644
index 0000000..4bd7aa3
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleMileageStatsServiceImpl.java
@@ -0,0 +1,296 @@
+package com.ruoyi.system.service.impl;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.ruoyi.system.domain.VehicleGps;
+import com.ruoyi.system.domain.VehicleMileageStats;
+import com.ruoyi.system.domain.TaskTimeInterval;
+import com.ruoyi.system.mapper.VehicleGpsMapper;
+import com.ruoyi.system.mapper.VehicleMileageStatsMapper;
+import com.ruoyi.system.service.IVehicleMileageStatsService;
+
+/**
+ * 杞﹁締閲岀▼缁熻Service涓氬姟灞傚鐞�
+ */
+@Service
+public class VehicleMileageStatsServiceImpl implements IVehicleMileageStatsService {
+
+ private static final Logger logger = LoggerFactory.getLogger(VehicleMileageStatsServiceImpl.class);
+
+ /** 鍦扮悆鍗婂緞锛堝叕閲岋級 */
+ private static final double EARTH_RADIUS_KM = 6371.0;
+
+ @Autowired
+ private VehicleMileageStatsMapper vehicleMileageStatsMapper;
+
+ @Autowired
+ private VehicleGpsMapper vehicleGpsMapper;
+
+ /**
+ * 鏌ヨ杞﹁締閲岀▼缁熻
+ */
+ @Override
+ public VehicleMileageStats selectVehicleMileageStatsById(Long statsId) {
+ return vehicleMileageStatsMapper.selectVehicleMileageStatsById(statsId);
+ }
+
+ /**
+ * 鏌ヨ杞﹁締閲岀▼缁熻鍒楄〃
+ */
+ @Override
+ public List<VehicleMileageStats> selectVehicleMileageStatsList(VehicleMileageStats vehicleMileageStats) {
+ return vehicleMileageStatsMapper.selectVehicleMileageStatsList(vehicleMileageStats);
+ }
+
+ /**
+ * 鏂板杞﹁締閲岀▼缁熻
+ */
+ @Override
+ public int insertVehicleMileageStats(VehicleMileageStats vehicleMileageStats) {
+ return vehicleMileageStatsMapper.insertVehicleMileageStats(vehicleMileageStats);
+ }
+
+ /**
+ * 淇敼杞﹁締閲岀▼缁熻
+ */
+ @Override
+ public int updateVehicleMileageStats(VehicleMileageStats vehicleMileageStats) {
+ return vehicleMileageStatsMapper.updateVehicleMileageStats(vehicleMileageStats);
+ }
+
+ /**
+ * 鎵归噺鍒犻櫎杞﹁締閲岀▼缁熻
+ */
+ @Override
+ public int deleteVehicleMileageStatsByIds(Long[] statsIds) {
+ return vehicleMileageStatsMapper.deleteVehicleMileageStatsByIds(statsIds);
+ }
+
+ /**
+ * 鍒犻櫎杞﹁締閲岀▼缁熻淇℃伅
+ */
+ @Override
+ public int deleteVehicleMileageStatsById(Long statsId) {
+ return vehicleMileageStatsMapper.deleteVehicleMileageStatsById(statsId);
+ }
+
+ /**
+ * 璁$畻骞朵繚瀛樻寚瀹氳溅杈嗘寚瀹氭棩鏈熺殑閲岀▼缁熻
+ */
+ @Override
+ public VehicleMileageStats calculateAndSaveMileageStats(Long vehicleId, Date statDate) {
+ try {
+ // 1. 鑾峰彇缁熻鏃ユ湡鐨勫紑濮嬪拰缁撴潫鏃堕棿
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(statDate);
+ calendar.set(Calendar.HOUR_OF_DAY, 0);
+ calendar.set(Calendar.MINUTE, 0);
+ calendar.set(Calendar.SECOND, 0);
+ calendar.set(Calendar.MILLISECOND, 0);
+ Date dayStart = calendar.getTime();
+
+ calendar.add(Calendar.DAY_OF_MONTH, 1);
+ Date dayEnd = calendar.getTime();
+
+ // 2. 鏌ヨ杞﹁締鍦ㄨ鏃ユ湡鐨凣PS鏁版嵁锛堟寜鏃堕棿鎺掑簭锛�
+ List<VehicleGps> gpsList = vehicleGpsMapper.selectGpsDataByTimeRange(vehicleId, dayStart, dayEnd);
+
+ if (gpsList == null || gpsList.isEmpty()) {
+ logger.info("杞﹁締ID: {} 鍦ㄦ棩鏈�: {} 鏃燝PS鏁版嵁", vehicleId, statDate);
+ return null;
+ }
+
+ // 3. 鏌ヨ杞﹁締鍦ㄨ鏃ユ湡鐨勪换鍔℃椂闂村尯闂�
+ List<TaskTimeInterval> taskIntervals = vehicleMileageStatsMapper.selectTaskTimeIntervals(vehicleId, dayStart, dayEnd);
+
+ // 4. 璁$畻閲岀▼
+ MileageCalculation calculation = calculateMileage(gpsList, taskIntervals);
+
+ // 5. 鏌ヨ鎴栧垱寤虹粺璁¤褰�
+ VehicleMileageStats stats = vehicleMileageStatsMapper.selectByVehicleIdAndDate(vehicleId, statDate);
+ boolean isNew = (stats == null);
+
+ if (isNew) {
+ stats = new VehicleMileageStats();
+ stats.setVehicleId(vehicleId);
+ stats.setStatDate(statDate);
+
+ // 鑾峰彇杞︾墝鍙�
+ if (!gpsList.isEmpty() && gpsList.get(0).getVehicleNo() != null) {
+ stats.setVehicleNo(gpsList.get(0).getVehicleNo());
+ }
+ }
+
+ // 6. 璁剧疆缁熻鏁版嵁
+ stats.setTotalMileage(calculation.totalMileage);
+ stats.setTaskMileage(calculation.taskMileage);
+ stats.setNonTaskMileage(calculation.nonTaskMileage);
+ stats.setTaskRatio(calculation.taskRatio);
+ stats.setGpsPointCount(gpsList.size());
+ stats.setTaskCount(taskIntervals == null ? 0 : taskIntervals.size());
+
+ // 7. 淇濆瓨鍒版暟鎹簱
+ if (isNew) {
+ vehicleMileageStatsMapper.insertVehicleMileageStats(stats);
+ } else {
+ vehicleMileageStatsMapper.updateVehicleMileageStats(stats);
+ }
+
+ logger.info("杞﹁締ID: {} 鏃ユ湡: {} 閲岀▼缁熻瀹屾垚 - 鎬婚噷绋�: {}km, 浠诲姟閲岀▼: {}km, 闈炰换鍔¢噷绋�: {}km, 鍗犳瘮: {}",
+ vehicleId, statDate, calculation.totalMileage, calculation.taskMileage,
+ calculation.nonTaskMileage, calculation.taskRatio);
+
+ return stats;
+
+ } catch (Exception e) {
+ logger.error("璁$畻杞﹁締閲岀▼缁熻澶辫触 - 杞﹁締ID: {}, 鏃ユ湡: {}", vehicleId, statDate, e);
+ throw new RuntimeException("璁$畻閲岀▼缁熻澶辫触: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 鎵归噺璁$畻鎵�鏈夎溅杈嗘寚瀹氭棩鏈熺殑閲岀▼缁熻
+ */
+ @Override
+ public int batchCalculateMileageStats(Date statDate) {
+ try {
+ // 鏌ヨ鎵�鏈夋椿璺冭溅杈�
+ List<Long> vehicleIds = vehicleGpsMapper.selectActiveVehicleIds();
+
+ if (vehicleIds == null || vehicleIds.isEmpty()) {
+ logger.info("娌℃湁鎵惧埌娲昏穬杞﹁締");
+ return 0;
+ }
+
+ int successCount = 0;
+ for (Long vehicleId : vehicleIds) {
+ try {
+ calculateAndSaveMileageStats(vehicleId, statDate);
+ successCount++;
+ } catch (Exception e) {
+ logger.error("璁$畻杞﹁締 {} 鐨勯噷绋嬬粺璁″け璐�", vehicleId, e);
+ }
+ }
+
+ logger.info("鎵归噺閲岀▼缁熻瀹屾垚 - 鏃ユ湡: {}, 鎬昏溅杈嗘暟: {}, 鎴愬姛: {}", statDate, vehicleIds.size(), successCount);
+ return successCount;
+
+ } catch (Exception e) {
+ logger.error("鎵归噺璁$畻閲岀▼缁熻澶辫触 - 鏃ユ湡: {}", statDate, e);
+ throw new RuntimeException("鎵归噺璁$畻澶辫触: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 璁$畻閲岀▼鐨勫唴閮ㄦ柟娉�
+ */
+ private MileageCalculation calculateMileage(List<VehicleGps> gpsList, List<TaskTimeInterval> taskIntervals) {
+ MileageCalculation result = new MileageCalculation();
+
+ // 閬嶅巻GPS鐐癸紝璁$畻鐩搁偦鐐逛箣闂寸殑璺濈
+ for (int i = 0; i < gpsList.size() - 1; i++) {
+ VehicleGps p1 = gpsList.get(i);
+ VehicleGps p2 = gpsList.get(i + 1);
+
+ // 璁$畻涓ょ偣闂磋窛绂伙紙浣跨敤Haversine鍏紡锛�
+ double distance = calculateDistance(
+ p1.getLatitude().doubleValue(),
+ p1.getLongitude().doubleValue(),
+ p2.getLatitude().doubleValue(),
+ p2.getLongitude().doubleValue()
+ );
+
+ // 鑾峰彇杩欐璺濈鐨勬椂闂村尯闂�
+ Date segmentStart = p1.getCollectTime();
+ Date segmentEnd = p2.getCollectTime();
+
+ // 璁$畻杩欐璺濈鍦ㄤ换鍔℃椂娈电殑鍗犳瘮
+ double taskRatio = calculateTaskOverlapRatio(segmentStart, segmentEnd, taskIntervals);
+
+ // 鍒嗘憡閲岀▼
+ double taskDistance = distance * taskRatio;
+ double nonTaskDistance = distance * (1 - taskRatio);
+
+ result.totalMileage = result.totalMileage.add(BigDecimal.valueOf(distance));
+ result.taskMileage = result.taskMileage.add(BigDecimal.valueOf(taskDistance));
+ result.nonTaskMileage = result.nonTaskMileage.add(BigDecimal.valueOf(nonTaskDistance));
+ }
+
+ // 璁$畻浠诲姟閲岀▼鍗犳瘮
+ if (result.totalMileage.compareTo(BigDecimal.ZERO) > 0) {
+ result.taskRatio = result.taskMileage.divide(result.totalMileage, 4, RoundingMode.HALF_UP);
+ }
+
+ // 淇濈暀涓や綅灏忔暟
+ result.totalMileage = result.totalMileage.setScale(2, RoundingMode.HALF_UP);
+ result.taskMileage = result.taskMileage.setScale(2, RoundingMode.HALF_UP);
+ result.nonTaskMileage = result.nonTaskMileage.setScale(2, RoundingMode.HALF_UP);
+
+ return result;
+ }
+
+ /**
+ * 浣跨敤Haversine鍏紡璁$畻涓や釜GPS鍧愭爣涔嬮棿鐨勮窛绂伙紙鍏噷锛�
+ */
+ private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
+ // 灏嗚搴﹁浆鎹负寮у害
+ double dLat = Math.toRadians(lat2 - lat1);
+ double dLon = Math.toRadians(lon2 - lon1);
+ double rLat1 = Math.toRadians(lat1);
+ double rLat2 = Math.toRadians(lat2);
+
+ // Haversine鍏紡
+ double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(rLat1) * Math.cos(rLat2) *
+ Math.sin(dLon / 2) * Math.sin(dLon / 2);
+
+ double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+
+ return EARTH_RADIUS_KM * c;
+ }
+
+ /**
+ * 璁$畻鏃堕棿娈典笌浠诲姟鏃舵鐨勯噸鍙犳瘮渚�
+ */
+ private double calculateTaskOverlapRatio(Date segmentStart, Date segmentEnd, List<TaskTimeInterval> taskIntervals) {
+ if (taskIntervals == null || taskIntervals.isEmpty()) {
+ return 0.0;
+ }
+
+ long segmentDuration = segmentEnd.getTime() - segmentStart.getTime();
+ if (segmentDuration <= 0) {
+ return 0.0;
+ }
+
+ long totalOverlap = 0;
+
+ for (TaskTimeInterval task : taskIntervals) {
+ // 璁$畻閲嶅彔鏃堕棿
+ long overlapStart = Math.max(segmentStart.getTime(), task.getStartTime().getTime());
+ long overlapEnd = Math.min(segmentEnd.getTime(), task.getEndTime().getTime());
+
+ if (overlapEnd > overlapStart) {
+ totalOverlap += (overlapEnd - overlapStart);
+ }
+ }
+
+ return (double) totalOverlap / segmentDuration;
+ }
+
+ /**
+ * 閲岀▼璁$畻缁撴灉鍐呴儴绫�
+ */
+ private static class MileageCalculation {
+ BigDecimal totalMileage = BigDecimal.ZERO;
+ BigDecimal taskMileage = BigDecimal.ZERO;
+ BigDecimal nonTaskMileage = BigDecimal.ZERO;
+ BigDecimal taskRatio = BigDecimal.ZERO;
+ }
+}
diff --git a/ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml b/ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml
index 9e01aae..5d8d093 100644
--- a/ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml
@@ -119,4 +119,21 @@
where g2.vehicle_id = g.vehicle_id
)
</delete>
+
+ <select id="selectGpsDataByTimeRange" resultMap="VehicleGpsResult">
+ select gps_id, vehicle_id, device_id, longitude, latitude, altitude, speed, direction,
+ collect_time, device_report_time, platform_process_time, create_time
+ from tb_vehicle_gps
+ where vehicle_id = #{vehicleId}
+ and collect_time >= #{startTime}
+ and collect_time <= #{endTime}
+ order by collect_time
+ </select>
+
+ <select id="selectActiveVehicleIds" resultType="Long">
+ select distinct vehicle_id
+ from tb_vehicle_gps
+ where collect_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
+ order by vehicle_id
+ </select>
</mapper>
\ No newline at end of file
diff --git a/ruoyi-system/src/main/resources/mapper/system/VehicleMileageStatsMapper.xml b/ruoyi-system/src/main/resources/mapper/system/VehicleMileageStatsMapper.xml
new file mode 100644
index 0000000..95439e9
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/VehicleMileageStatsMapper.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.VehicleMileageStatsMapper">
+
+ <resultMap type="VehicleMileageStats" id="VehicleMileageStatsResult">
+ <id property="statsId" column="stats_id" />
+ <result property="vehicleId" column="vehicle_id" />
+ <result property="vehicleNo" column="vehicle_no" />
+ <result property="statDate" column="stat_date" />
+ <result property="totalMileage" column="total_mileage" />
+ <result property="taskMileage" column="task_mileage" />
+ <result property="nonTaskMileage" column="non_task_mileage" />
+ <result property="taskRatio" column="task_ratio" />
+ <result property="gpsPointCount" column="gps_point_count" />
+ <result property="taskCount" column="task_count" />
+ <result property="createTime" column="create_time" />
+ <result property="updateTime" column="update_time" />
+ </resultMap>
+
+ <resultMap type="TaskTimeInterval" id="TaskTimeIntervalResult">
+ <result property="taskId" column="task_id" />
+ <result property="startTime" column="start_time" />
+ <result property="endTime" column="end_time" />
+ </resultMap>
+
+ <sql id="selectVehicleMileageStatsVo">
+ select stats_id, vehicle_id, vehicle_no, stat_date, total_mileage, task_mileage,
+ non_task_mileage, task_ratio, gps_point_count, task_count, create_time, update_time
+ from tb_vehicle_mileage_stats
+ </sql>
+
+ <select id="selectVehicleMileageStatsList" parameterType="VehicleMileageStats" resultMap="VehicleMileageStatsResult">
+ <include refid="selectVehicleMileageStatsVo"/>
+ <where>
+ <if test="vehicleId != null">
+ and vehicle_id = #{vehicleId}
+ </if>
+ <if test="vehicleNo != null and vehicleNo != ''">
+ and vehicle_no = #{vehicleNo}
+ </if>
+ <if test="statDate != null">
+ and stat_date = #{statDate}
+ </if>
+ <if test="params.beginStatDate != null and params.beginStatDate != ''">
+ and stat_date >= #{params.beginStatDate}
+ </if>
+ <if test="params.endStatDate != null and params.endStatDate != ''">
+ and stat_date <= #{params.endStatDate}
+ </if>
+ </where>
+ order by stat_date desc, vehicle_id
+ </select>
+
+ <select id="selectVehicleMileageStatsById" parameterType="Long" resultMap="VehicleMileageStatsResult">
+ <include refid="selectVehicleMileageStatsVo"/>
+ where stats_id = #{statsId}
+ </select>
+
+ <select id="selectByVehicleIdAndDate" resultMap="VehicleMileageStatsResult">
+ <include refid="selectVehicleMileageStatsVo"/>
+ where vehicle_id = #{vehicleId} and stat_date = #{statDate}
+ </select>
+
+ <select id="selectTaskTimeIntervals" resultMap="TaskTimeIntervalResult">
+ select tv.task_id, t.create_time as start_time,
+ IFNULL(t.actual_end_time, NOW()) as end_time
+ from sys_task_vehicle tv
+ inner join sys_task t on tv.task_id = t.task_id
+ where tv.vehicle_id = #{vehicleId}
+ and t.del_flag = '0'
+ and t.actual_end_time is not null
+ and t.create_time < #{endTime}
+ and t.actual_end_time > #{startTime}
+ order by t.create_time
+ </select>
+
+ <insert id="insertVehicleMileageStats" parameterType="VehicleMileageStats" useGeneratedKeys="true" keyProperty="statsId">
+ insert into tb_vehicle_mileage_stats
+ <trim prefix="(" suffix=")" suffixOverrides=",">
+ <if test="vehicleId != null">vehicle_id,</if>
+ <if test="vehicleNo != null">vehicle_no,</if>
+ <if test="statDate != null">stat_date,</if>
+ <if test="totalMileage != null">total_mileage,</if>
+ <if test="taskMileage != null">task_mileage,</if>
+ <if test="nonTaskMileage != null">non_task_mileage,</if>
+ <if test="taskRatio != null">task_ratio,</if>
+ <if test="gpsPointCount != null">gps_point_count,</if>
+ <if test="taskCount != null">task_count,</if>
+ create_time
+ </trim>
+ <trim prefix="values (" suffix=")" suffixOverrides=",">
+ <if test="vehicleId != null">#{vehicleId},</if>
+ <if test="vehicleNo != null">#{vehicleNo},</if>
+ <if test="statDate != null">#{statDate},</if>
+ <if test="totalMileage != null">#{totalMileage},</if>
+ <if test="taskMileage != null">#{taskMileage},</if>
+ <if test="nonTaskMileage != null">#{nonTaskMileage},</if>
+ <if test="taskRatio != null">#{taskRatio},</if>
+ <if test="gpsPointCount != null">#{gpsPointCount},</if>
+ <if test="taskCount != null">#{taskCount},</if>
+ NOW()
+ </trim>
+ </insert>
+
+ <update id="updateVehicleMileageStats" parameterType="VehicleMileageStats">
+ update tb_vehicle_mileage_stats
+ <trim prefix="SET" suffixOverrides=",">
+ <if test="vehicleNo != null">vehicle_no = #{vehicleNo},</if>
+ <if test="totalMileage != null">total_mileage = #{totalMileage},</if>
+ <if test="taskMileage != null">task_mileage = #{taskMileage},</if>
+ <if test="nonTaskMileage != null">non_task_mileage = #{nonTaskMileage},</if>
+ <if test="taskRatio != null">task_ratio = #{taskRatio},</if>
+ <if test="gpsPointCount != null">gps_point_count = #{gpsPointCount},</if>
+ <if test="taskCount != null">task_count = #{taskCount},</if>
+ update_time = NOW()
+ </trim>
+ where stats_id = #{statsId}
+ </update>
+
+ <delete id="deleteVehicleMileageStatsById" parameterType="Long">
+ delete from tb_vehicle_mileage_stats where stats_id = #{statsId}
+ </delete>
+
+ <delete id="deleteVehicleMileageStatsByIds" parameterType="Long">
+ delete from tb_vehicle_mileage_stats where stats_id in
+ <foreach item="statsId" collection="array" open="(" separator="," close=")">
+ #{statsId}
+ </foreach>
+ </delete>
+</mapper>
diff --git a/sql/vehicle_mileage_stats.sql b/sql/vehicle_mileage_stats.sql
new file mode 100644
index 0000000..25d9667
--- /dev/null
+++ b/sql/vehicle_mileage_stats.sql
@@ -0,0 +1,40 @@
+-- 杞﹁締閲岀▼缁熻琛�
+CREATE TABLE `tb_vehicle_mileage_stats` (
+ `stats_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '缁熻ID',
+ `vehicle_id` bigint(20) NOT NULL COMMENT '杞﹁締ID',
+ `vehicle_no` varchar(20) DEFAULT NULL COMMENT '杞︾墝鍙�',
+ `stat_date` date NOT NULL COMMENT '缁熻鏃ユ湡',
+ `total_mileage` decimal(10,2) DEFAULT 0.00 COMMENT '鎬婚噷绋�(鍏噷)',
+ `task_mileage` decimal(10,2) DEFAULT 0.00 COMMENT '浠诲姟鏃舵閲岀▼(鍏噷)',
+ `non_task_mileage` decimal(10,2) DEFAULT 0.00 COMMENT '闈炰换鍔℃椂娈甸噷绋�(鍏噷)',
+ `task_ratio` decimal(5,4) DEFAULT 0.0000 COMMENT '浠诲姟閲岀▼鍗犳瘮(0-1)',
+ `gps_point_count` int(11) DEFAULT 0 COMMENT 'GPS鐐规暟閲�',
+ `task_count` int(11) DEFAULT 0 COMMENT '浠诲姟鏁伴噺',
+ `create_time` datetime DEFAULT NULL COMMENT '鍒涘缓鏃堕棿',
+ `update_time` datetime DEFAULT NULL COMMENT '鏇存柊鏃堕棿',
+ PRIMARY KEY (`stats_id`),
+ UNIQUE KEY `uk_vehicle_date` (`vehicle_id`, `stat_date`),
+ KEY `idx_vehicle_id` (`vehicle_id`),
+ KEY `idx_stat_date` (`stat_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='杞﹁締閲岀▼缁熻琛�';
+
+-- 杞﹁締閲岀▼缁熻鏄庣粏琛紙鍙�夛紝鐢ㄤ簬璋冭瘯鍜岃拷婧級
+CREATE TABLE `tb_vehicle_mileage_detail` (
+ `detail_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '鏄庣粏ID',
+ `stats_id` bigint(20) NOT NULL COMMENT '缁熻ID',
+ `vehicle_id` bigint(20) NOT NULL COMMENT '杞﹁締ID',
+ `segment_start_time` datetime NOT NULL COMMENT '娈佃捣濮嬫椂闂�',
+ `segment_end_time` datetime NOT NULL COMMENT '娈电粨鏉熸椂闂�',
+ `start_longitude` decimal(10,7) DEFAULT NULL COMMENT '璧风偣缁忓害',
+ `start_latitude` decimal(10,7) DEFAULT NULL COMMENT '璧风偣绾害',
+ `end_longitude` decimal(10,7) DEFAULT NULL COMMENT '缁堢偣缁忓害',
+ `end_latitude` decimal(10,7) DEFAULT NULL COMMENT '缁堢偣绾害',
+ `segment_distance` decimal(10,3) DEFAULT 0.000 COMMENT '娈佃窛绂�(鍏噷)',
+ `task_distance` decimal(10,3) DEFAULT 0.000 COMMENT '浠诲姟鍐呰窛绂�(鍏噷)',
+ `non_task_distance` decimal(10,3) DEFAULT 0.000 COMMENT '浠诲姟澶栬窛绂�(鍏噷)',
+ `is_in_task` tinyint(1) DEFAULT 0 COMMENT '鏄惁瀹屽叏鍦ㄤ换鍔℃椂娈靛唴(0-鍚�,1-鏄�,2-閮ㄥ垎)',
+ `create_time` datetime DEFAULT NULL COMMENT '鍒涘缓鏃堕棿',
+ PRIMARY KEY (`detail_id`),
+ KEY `idx_stats_id` (`stats_id`),
+ KEY `idx_vehicle_id` (`vehicle_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='杞﹁締閲岀▼缁熻鏄庣粏琛�';
diff --git a/sql/vehicle_mileage_stats_job.sql b/sql/vehicle_mileage_stats_job.sql
new file mode 100644
index 0000000..2629623
--- /dev/null
+++ b/sql/vehicle_mileage_stats_job.sql
@@ -0,0 +1,17 @@
+-- 娣诲姞杞﹁締閲岀▼缁熻瀹氭椂浠诲姟
+INSERT INTO sys_job (job_id, job_name, job_group, invoke_target, cron_expression, misfire_policy, concurrent, status, create_by, create_time, update_by, update_time, remark)
+VALUES (
+ (SELECT IFNULL(MAX(job_id), 0) + 1 FROM sys_job t),
+ '杞﹁締閲岀▼缁熻浠诲姟',
+ 'DEFAULT',
+ 'vehicleMileageStatsTask.calculateYesterdayMileage',
+ '0 30 1 * * ?',
+ '3',
+ '1',
+ '0',
+ 'admin',
+ NOW(),
+ '',
+ NULL,
+ '姣忓ぉ鍑屾櫒1:30鎵ц锛岀粺璁℃槰鏃ユ墍鏈夎溅杈嗙殑琛岄┒閲岀▼'
+);
diff --git a/sql/vehicle_mileage_stats_menu.sql b/sql/vehicle_mileage_stats_menu.sql
new file mode 100644
index 0000000..d2dd644
--- /dev/null
+++ b/sql/vehicle_mileage_stats_menu.sql
@@ -0,0 +1,56 @@
+-- 杞﹁締閲岀▼缁熻鑿滃崟鏉冮檺
+-- 鐖惰彍鍗旾D闇�瑕佹牴鎹疄闄呯郴缁熶腑鐨�"绯荤粺绠$悊"鎴�"杞﹁締绠$悊"鑿滃崟ID杩涜璋冩暣
+
+-- 1. 娣诲姞杞﹁締閲岀▼缁熻鑿滃崟
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+VALUES (
+ (SELECT IFNULL(MAX(menu_id), 0) + 1 FROM sys_menu t),
+ '杞﹁締閲岀▼缁熻',
+ 3, -- 绯荤粺宸ュ叿锛岃鏍规嵁瀹為檯鎯呭喌淇敼parent_id
+ 6,
+ 'mileageStats',
+ 'system/mileageStats/index',
+ 1,
+ 0,
+ 'C',
+ '0',
+ '0',
+ 'system:mileageStats:list',
+ 'chart',
+ 'admin',
+ NOW(),
+ '',
+ NULL,
+ '杞﹁締閲岀▼缁熻鑿滃崟'
+);
+
+-- 鑾峰彇鍒氭彃鍏ョ殑鑿滃崟ID锛堢敤浜庡悗缁寜閽潈闄愶級
+SET @menuId = (SELECT MAX(menu_id) FROM sys_menu WHERE menu_name = '杞﹁締閲岀▼缁熻');
+
+-- 2. 娣诲姞鏌ヨ鎸夐挳
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+VALUES ((SELECT IFNULL(MAX(menu_id), 0) + 1 FROM sys_menu t), '杞﹁締閲岀▼缁熻鏌ヨ', @menuId, 1, '#', '', 1, 0, 'F', '0', '0', 'system:mileageStats:query', '#', 'admin', NOW(), '', NULL, '');
+
+-- 3. 娣诲姞鏂板鎸夐挳
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+VALUES ((SELECT IFNULL(MAX(menu_id), 0) + 1 FROM sys_menu t), '杞﹁締閲岀▼缁熻鏂板', @menuId, 2, '#', '', 1, 0, 'F', '0', '0', 'system:mileageStats:add', '#', 'admin', NOW(), '', NULL, '');
+
+-- 4. 娣诲姞淇敼鎸夐挳
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+VALUES ((SELECT IFNULL(MAX(menu_id), 0) + 1 FROM sys_menu t), '杞﹁締閲岀▼缁熻淇敼', @menuId, 3, '#', '', 1, 0, 'F', '0', '0', 'system:mileageStats:edit', '#', 'admin', NOW(), '', NULL, '');
+
+-- 5. 娣诲姞鍒犻櫎鎸夐挳
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+VALUES ((SELECT IFNULL(MAX(menu_id), 0) + 1 FROM sys_menu t), '杞﹁締閲岀▼缁熻鍒犻櫎', @menuId, 4, '#', '', 1, 0, 'F', '0', '0', 'system:mileageStats:remove', '#', 'admin', NOW(), '', NULL, '');
+
+-- 6. 娣诲姞瀵煎嚭鎸夐挳
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+VALUES ((SELECT IFNULL(MAX(menu_id), 0) + 1 FROM sys_menu t), '杞﹁締閲岀▼缁熻瀵煎嚭', @menuId, 5, '#', '', 1, 0, 'F', '0', '0', 'system:mileageStats:export', '#', 'admin', NOW(), '', NULL, '');
+
+-- 7. 娣诲姞璁$畻鎸夐挳
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+VALUES ((SELECT IFNULL(MAX(menu_id), 0) + 1 FROM sys_menu t), '杞﹁締閲岀▼缁熻璁$畻', @menuId, 6, '#', '', 1, 0, 'F', '0', '0', 'system:mileageStats:calculate', '#', 'admin', NOW(), '', NULL, '');
+
+-- 8. 娣诲姞鎵归噺璁$畻鎸夐挳
+INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+VALUES ((SELECT IFNULL(MAX(menu_id), 0) + 1 FROM sys_menu t), '杞﹁締閲岀▼缁熻鎵归噺璁$畻', @menuId, 7, '#', '', 1, 0, 'F', '0', '0', 'system:mileageStats:batch', '#', 'admin', NOW(), '', NULL, '');
diff --git "a/sql/\350\275\246\350\276\206\351\207\214\347\250\213\347\273\237\350\256\241\344\275\277\347\224\250\350\257\264\346\230\216.md" "b/sql/\350\275\246\350\276\206\351\207\214\347\250\213\347\273\237\350\256\241\344\275\277\347\224\250\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..31a6ee0
--- /dev/null
+++ "b/sql/\350\275\246\350\276\206\351\207\214\347\250\213\347\273\237\350\256\241\344\275\277\347\224\250\350\257\264\346\230\216.md"
@@ -0,0 +1,202 @@
+# 杞﹁締GPS閲岀▼缁熻鍔熻兘浣跨敤璇存槑
+
+## 鍔熻兘姒傝堪
+
+鏈姛鑳藉疄鐜颁簡杞﹁締GPS琛岄┒閲岀▼鐨勮嚜鍔ㄧ粺璁★紝鍖呮嫭锛�
+1. **鎬婚噷绋嬬粺璁�**锛氬熀浜嶨PS鐐硅绠楄溅杈嗘瘡鏃ユ�昏椹堕噷绋�
+2. **浠诲姟鏃舵閲岀▼**锛氳绠楄溅杈嗗湪鎵ц浠诲姟鏈熼棿鐨勮椹堕噷绋�
+3. **闈炰换鍔℃椂娈甸噷绋�**锛氳绠楄溅杈嗛潪鎵ц浠诲姟鏈熼棿鐨勮椹堕噷绋�
+4. **浠诲姟閲岀▼鍗犳瘮**锛氳绠椾换鍔¢噷绋嬪崰鎬婚噷绋嬬殑姣斾緥
+
+## 鏍稿績绠楁硶
+
+### 1. 璺濈璁$畻
+浣跨敤 **Haversine鍏紡** 璁$畻鐩搁偦GPS鐐逛箣闂寸殑瀹為檯璺濈锛堣�冭檻鍦扮悆鏇茬巼锛�
+
+### 2. 閲岀▼鍒嗘憡绠楁硶
+鏍规嵁GPS璁板綍鐨勬椂闂村尯闂翠笌浠诲姟鏃堕棿鍖洪棿鐨�**閲嶅彔姣斾緥**锛屽皢姣忔璺濈鍒嗘憡鍒颁换鍔¢噷绋嬪拰闈炰换鍔¢噷绋嬶細
+
+```
+鏃堕棿閲嶅彔姣斾緥 = 閲嶅彔鏃堕暱 / 鎬绘椂闀�
+浠诲姟閲岀▼ = 娈佃窛绂� 脳 鏃堕棿閲嶅彔姣斾緥
+闈炰换鍔¢噷绋� = 娈佃窛绂� 脳 (1 - 鏃堕棿閲嶅彔姣斾緥)
+```
+
+## 閮ㄧ讲姝ラ
+
+### 1. 鎵ц鏁版嵁搴撹剼鏈�
+
+鎸夐『搴忔墽琛屼互涓婼QL鏂囦欢锛�
+
+```bash
+# 1. 鍒涘缓缁熻琛�
+sql/vehicle_mileage_stats.sql
+
+# 2. 娣诲姞瀹氭椂浠诲姟
+sql/vehicle_mileage_stats_job.sql
+
+# 3. 娣诲姞鑿滃崟鏉冮檺
+sql/vehicle_mileage_stats_menu.sql
+```
+
+### 2. 浠g爜宸茶嚜鍔ㄩ儴缃�
+
+浠ヤ笅浠g爜鏂囦欢宸插垱寤猴細
+
+**瀹炰綋绫伙細**
+- `VehicleMileageStats.java` - 閲岀▼缁熻瀹炰綋
+- `TaskTimeInterval.java` - 浠诲姟鏃堕棿鍖洪棿
+
+**鏁版嵁璁块棶灞傦細**
+- `VehicleMileageStatsMapper.java` - 閲岀▼缁熻Mapper鎺ュ彛
+- `VehicleMileageStatsMapper.xml` - MyBatis鏄犲皠鏂囦欢
+- `VehicleGpsMapper.java` - 鎵╁睍GPS鏌ヨ鏂规硶
+
+**涓氬姟灞傦細**
+- `IVehicleMileageStatsService.java` - Service鎺ュ彛
+- `VehicleMileageStatsServiceImpl.java` - Service瀹炵幇锛堟牳蹇冪畻娉曪級
+
+**鎺у埗灞傦細**
+- `VehicleMileageStatsController.java` - REST API鎺ュ彛
+
+**瀹氭椂浠诲姟锛�**
+- `VehicleMileageStatsTask.java` - 瀹氭椂缁熻浠诲姟
+
+### 3. 鍚姩瀹氭椂浠诲姟
+
+绯荤粺浼氳嚜鍔ㄦ坊鍔犲畾鏃朵换鍔★紝榛樿閰嶇疆锛�
+- **鎵ц鏃堕棿**锛氭瘡澶╁噷鏅� 1:30
+- **缁熻鑼冨洿**锛氬墠涓�澶╃殑鎵�鏈夎溅杈咷PS鏁版嵁
+- **Cron琛ㄨ揪寮�**锛歚0 30 1 * * ?`
+
+鍦ㄧ郴缁熺鐞� -> 瀹氭椂浠诲姟涓彲浠ユ煡鐪嬪拰绠$悊璇ヤ换鍔°��
+
+## 浣跨敤鏂瑰紡
+
+### 1. 鑷姩缁熻锛堟帹鑽愶級
+
+瀹氭椂浠诲姟姣忓ぉ鍑屾櫒鑷姩鎵ц锛屾棤闇�浜哄伐骞查銆�
+
+### 2. 鎵嬪姩瑙﹀彂缁熻
+
+#### 2.1 鍗曡溅杈嗙粺璁�
+
+璋冪敤鎺ュ彛锛�
+```
+POST /system/mileageStats/calculate
+鍙傛暟锛�
+ - vehicleId: 杞﹁締ID
+ - statDate: 缁熻鏃ユ湡锛堟牸寮忥細yyyy-MM-dd锛�
+```
+
+#### 2.2 鎵归噺缁熻
+
+璋冪敤鎺ュ彛锛�
+```
+POST /system/mileageStats/batchCalculate
+鍙傛暟锛�
+ - statDate: 缁熻鏃ユ湡锛堟牸寮忥細yyyy-MM-dd锛�
+```
+
+#### 2.3 閫氳繃瀹氭椂浠诲姟琛ョ畻鍘嗗彶鏁版嵁
+
+鍦ㄥ畾鏃朵换鍔$鐞嗕腑锛屾墽琛岋細
+```
+vehicleMileageStatsTask.calculateMileageByDate('2025-11-09')
+```
+
+### 3. 鏌ヨ缁熻缁撴灉
+
+```
+GET /system/mileageStats/list
+鍙傛暟锛堝彲閫夛級锛�
+ - vehicleId: 杞﹁締ID
+ - vehicleNo: 杞︾墝鍙�
+ - statDate: 缁熻鏃ユ湡
+ - beginStatDate: 寮�濮嬫棩鏈�
+ - endStatDate: 缁撴潫鏃ユ湡
+```
+
+### 4. 瀵煎嚭缁熻鏁版嵁
+
+```
+POST /system/mileageStats/export
+鍙傛暟锛氬悓鏌ヨ鎺ュ彛
+```
+
+## 鏁版嵁琛ㄧ粨鏋�
+
+### tb_vehicle_mileage_stats锛堥噷绋嬬粺璁¤〃锛�
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| stats_id | bigint | 缁熻ID锛堜富閿級 |
+| vehicle_id | bigint | 杞﹁締ID |
+| vehicle_no | varchar(20) | 杞︾墝鍙� |
+| stat_date | date | 缁熻鏃ユ湡 |
+| total_mileage | decimal(10,2) | 鎬婚噷绋嬶紙鍏噷锛� |
+| task_mileage | decimal(10,2) | 浠诲姟鏃舵閲岀▼锛堝叕閲岋級 |
+| non_task_mileage | decimal(10,2) | 闈炰换鍔℃椂娈甸噷绋嬶紙鍏噷锛� |
+| task_ratio | decimal(5,4) | 浠诲姟閲岀▼鍗犳瘮锛�0-1锛� |
+| gps_point_count | int | GPS鐐规暟閲� |
+| task_count | int | 浠诲姟鏁伴噺 |
+
+### tb_vehicle_mileage_detail锛堥噷绋嬫槑缁嗚〃锛�
+
+鐢ㄤ簬璋冭瘯鍜岃拷婧紝璁板綍姣忔GPS杞ㄨ抗鐨勯噷绋嬪垎鎽婃槑缁嗐��
+
+## 娉ㄦ剰浜嬮」
+
+### 1. GPS鏁版嵁璐ㄩ噺
+
+- GPS閲囬泦闂撮殧寤鸿鍦� 30-60绉�
+- 杩囩煭锛氳绠楅噺澶э紝鎬ц兘褰卞搷
+- 杩囬暱锛氶噷绋嬬簿搴﹂檷浣�
+
+### 2. 浠诲姟鏃堕棿瀹氫箟
+
+浠诲姟鏃舵 = 浠庝换鍔″垱寤烘椂闂达紙`create_time`锛夊埌浠诲姟瀹屾垚鏃堕棿锛坄actual_end_time`锛�
+
+纭繚浠诲姟琛ㄤ腑杩欎袱涓瓧娈靛噯纭褰曘��
+
+### 3. 鎬ц兘浼樺寲
+
+- 缁熻鏁版嵁鎸夋棩鏈熸眹鎬伙紝閬垮厤瀹炴椂璁$畻
+- 寤鸿淇濈暀3-6涓湀鐨勭粺璁℃暟鎹紝瀹氭湡褰掓。鍘嗗彶鏁版嵁
+- GPS鍘熷鏁版嵁寤鸿淇濈暀7-30澶╋紙鐢辨竻鐞嗕换鍔℃帶鍒讹級
+
+### 4. 鏁版嵁琛ョ畻
+
+濡傞渶琛ョ畻鍘嗗彶鏁版嵁锛屽彲浠ラ�氳繃瀹氭椂浠诲姟鎴朅PI鎺ュ彛鎵归噺鎵ц锛�
+
+```java
+// 琛ョ畻鏈�杩�7澶╃殑鏁版嵁绀轰緥
+for (int i = 1; i <= 7; i++) {
+ String date = "2025-11-" + String.format("%02d", i);
+ vehicleMileageStatsTask.calculateMileageByDate(date);
+}
+```
+
+## 鎵╁睍璇存槑
+
+### 澶╁湴鍥炬帴鍙i泦鎴�
+
+铏界劧鏍稿績绠楁硶浣跨敤Haversine鍏紡璁$畻璺濈锛屼絾绯荤粺宸查泦鎴愬ぉ鍦板浘鎺ュ彛锛屽彲鐢ㄤ簬锛�
+
+1. **鍦板潃瑙f瀽**锛氬皢GPS鍧愭爣杞崲涓哄湴鍧�淇℃伅
+2. **璺緞瑙勫垝**锛氳绠楀疄闄呴亾璺窛绂伙紙姣旂洿绾胯窛绂绘洿鍑嗙‘锛�
+3. **POI鏌ヨ**锛氭煡璇㈡部閫斿叴瓒g偣
+
+濡傞渶浣跨敤澶╁湴鍥続PI杩涜璺緞璺濈璁$畻锛屽彲鍙傝�� `VehicleGpsController` 涓殑澶╁湴鍥炬帴鍙c��
+
+## 鎶�鏈敮鎸�
+
+濡傛湁闂锛岃妫�鏌ワ細
+1. 鏁版嵁搴撹〃鏄惁姝g‘鍒涘缓
+2. 瀹氭椂浠诲姟鏄惁姝e父鍚姩
+3. GPS鏁版嵁鏄惁姝e父閲囬泦
+4. 浠诲姟琛ㄧ殑鏃堕棿瀛楁鏄惁鍑嗙‘
+
+鏃ュ織浣嶇疆锛�
+- Service灞傛棩蹇楋細鎼滅储 `VehicleMileageStatsServiceImpl`
+- 瀹氭椂浠诲姟鏃ュ織锛氭悳绱� `VehicleMileageStatsTask`
diff --git "a/sql/\350\275\246\350\276\206\351\207\214\347\250\213\347\273\237\350\256\241\345\256\236\347\216\260\346\200\273\347\273\223.md" "b/sql/\350\275\246\350\276\206\351\207\214\347\250\213\347\273\237\350\256\241\345\256\236\347\216\260\346\200\273\347\273\223.md"
new file mode 100644
index 0000000..1c173f1
--- /dev/null
+++ "b/sql/\350\275\246\350\276\206\351\207\214\347\250\213\347\273\237\350\256\241\345\256\236\347\216\260\346\200\273\347\273\223.md"
@@ -0,0 +1,339 @@
+# 杞﹁締GPS閲岀▼缁熻鍔熻兘瀹炵幇鎬荤粨
+
+## 涓�銆佸姛鑳藉疄鐜版竻鍗�
+
+### 鉁� 1. 鏁版嵁搴撳眰
+- [x] `vehicle_mileage_stats.sql` - 鍒涘缓閲岀▼缁熻琛ㄥ拰鏄庣粏琛�
+- [x] `vehicle_mileage_stats_job.sql` - 鍒涘缓瀹氭椂浠诲姟閰嶇疆
+- [x] `vehicle_mileage_stats_menu.sql` - 鍒涘缓鑿滃崟鏉冮檺閰嶇疆
+
+### 鉁� 2. 瀹炰綋绫伙紙Domain锛�
+- [x] `VehicleMileageStats.java` - 閲岀▼缁熻瀹炰綋
+- [x] `TaskTimeInterval.java` - 浠诲姟鏃堕棿鍖洪棿杈呭姪绫�
+
+### 鉁� 3. 鏁版嵁璁块棶灞傦紙Mapper锛�
+- [x] `VehicleMileageStatsMapper.java` - 閲岀▼缁熻Mapper鎺ュ彛
+- [x] `VehicleMileageStatsMapper.xml` - MyBatis鏄犲皠閰嶇疆
+- [x] `VehicleGpsMapper.java` - 鎵╁睍GPS鏌ヨ鏂规硶锛堟柊澧�2涓柟娉曪級
+- [x] `VehicleGpsMapper.xml` - 鎵╁睍GPS鏌ヨSQL
+
+### 鉁� 4. 涓氬姟閫昏緫灞傦紙Service锛�
+- [x] `IVehicleMileageStatsService.java` - Service鎺ュ彛
+- [x] `VehicleMileageStatsServiceImpl.java` - Service瀹炵幇锛堟牳蹇冪畻娉曪級
+
+### 鉁� 5. 鎺у埗灞傦紙Controller锛�
+- [x] `VehicleMileageStatsController.java` - REST API鎺ュ彛
+
+### 鉁� 6. 瀹氭椂浠诲姟锛圱ask锛�
+- [x] `VehicleMileageStatsTask.java` - 鑷姩缁熻瀹氭椂浠诲姟
+
+### 鉁� 7. 鍓嶇API
+- [x] `mileageStats.js` - 鍓嶇鎺ュ彛灏佽
+
+### 鉁� 8. 鏂囨。
+- [x] `杞﹁締閲岀▼缁熻浣跨敤璇存槑.md` - 璇︾粏浣跨敤鏂囨。
+
+## 浜屻�佹牳蹇冩妧鏈疄鐜�
+
+### 1. 閲岀▼璁$畻绠楁硶
+
+#### Haversine鍏紡锛堣绠桮PS鐐归棿璺濈锛�
+```java
+private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
+ double dLat = Math.toRadians(lat2 - lat1);
+ double dLon = Math.toRadians(lon2 - lon1);
+ double rLat1 = Math.toRadians(lat1);
+ double rLat2 = Math.toRadians(lat2);
+
+ double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(rLat1) * Math.cos(rLat2) *
+ Math.sin(dLon / 2) * Math.sin(dLon / 2);
+
+ double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+
+ return EARTH_RADIUS_KM * c;
+}
+```
+
+#### 鏃堕棿閲嶅彔姣斾緥璁$畻锛堜换鍔¢噷绋嬪垎鎽婏級
+```java
+private double calculateTaskOverlapRatio(Date segmentStart, Date segmentEnd,
+ List<TaskTimeInterval> taskIntervals) {
+ long segmentDuration = segmentEnd.getTime() - segmentStart.getTime();
+ long totalOverlap = 0;
+
+ for (TaskTimeInterval task : taskIntervals) {
+ long overlapStart = Math.max(segmentStart.getTime(), task.getStartTime().getTime());
+ long overlapEnd = Math.min(segmentEnd.getTime(), task.getEndTime().getTime());
+
+ if (overlapEnd > overlapStart) {
+ totalOverlap += (overlapEnd - overlapStart);
+ }
+ }
+
+ return (double) totalOverlap / segmentDuration;
+}
+```
+
+### 2. 浠诲姟鏃舵瀹氫箟
+
+浠诲姟鏃舵 = 浠庝换鍔″垱寤烘椂闂达紙`sys_task.create_time`锛夊埌浠诲姟瀹屾垚鏃堕棿锛坄sys_task.actual_end_time`锛�
+
+SQL鏌ヨ锛�
+```sql
+select tv.task_id, t.create_time as start_time,
+ IFNULL(t.actual_end_time, NOW()) as end_time
+from sys_task_vehicle tv
+inner join sys_task t on tv.task_id = t.task_id
+where tv.vehicle_id = #{vehicleId}
+ and t.del_flag = '0'
+ and t.actual_end_time is not null
+ and t.create_time < #{endTime}
+ and t.actual_end_time > #{startTime}
+```
+
+### 3. 缁熻鏁版嵁缂撳瓨
+
+- 姣忔棩瀹氭椂浠诲姟鑷姩缁熻鍓嶄竴澶╃殑鏁版嵁
+- 缁熻缁撴灉瀛樺偍鍦� `tb_vehicle_mileage_stats` 琛ㄤ腑
+- 鏀寔閲嶅璁$畻锛堟洿鏂板凡鏈夎褰曪級
+- 鍞竴绱㈠紩锛歚uk_vehicle_date (vehicle_id, stat_date)`
+
+## 涓夈�丄PI鎺ュ彛璇存槑
+
+### 1. 鏌ヨ缁熻鍒楄〃
+```
+GET /system/mileageStats/list
+鍙傛暟锛�
+ - vehicleId: 杞﹁締ID锛堝彲閫夛級
+ - vehicleNo: 杞︾墝鍙凤紙鍙�夛級
+ - statDate: 缁熻鏃ユ湡锛堝彲閫夛級
+ - beginStatDate: 寮�濮嬫棩鏈燂紙鍙�夛級
+ - endStatDate: 缁撴潫鏃ユ湡锛堝彲閫夛級
+ - pageNum: 椤电爜
+ - pageSize: 姣忛〉鏁伴噺
+```
+
+### 2. 鎵嬪姩璁$畻鍗曡溅杈嗛噷绋�
+```
+POST /system/mileageStats/calculate
+鍙傛暟锛�
+ - vehicleId: 杞﹁締ID锛堝繀濉級
+ - statDate: 缁熻鏃ユ湡锛屾牸寮� yyyy-MM-dd锛堝繀濉級
+杩斿洖锛歏ehicleMileageStats瀵硅薄
+```
+
+### 3. 鎵归噺璁$畻鎵�鏈夎溅杈嗛噷绋�
+```
+POST /system/mileageStats/batchCalculate
+鍙傛暟锛�
+ - statDate: 缁熻鏃ユ湡锛屾牸寮� yyyy-MM-dd锛堝繀濉級
+杩斿洖锛氭垚鍔熺粺璁$殑杞﹁締鏁伴噺
+```
+
+### 4. 瀵煎嚭缁熻鏁版嵁
+```
+POST /system/mileageStats/export
+鍙傛暟锛氬悓鏌ヨ鍒楄〃鎺ュ彛
+杩斿洖锛欵xcel鏂囦欢
+```
+
+## 鍥涖�佸畾鏃朵换鍔¢厤缃�
+
+### 榛樿閰嶇疆
+- **浠诲姟鍚嶇О**锛氳溅杈嗛噷绋嬬粺璁′换鍔�
+- **Bean鍚嶇О**锛歷ehicleMileageStatsTask
+- **鏂规硶璋冪敤**锛歝alculateYesterdayMileage
+- **Cron琛ㄨ揪寮�**锛歚0 30 1 * * ?`锛堟瘡澶╁噷鏅�1:30鎵ц锛�
+- **鎵ц绛栫暐**锛氱珛鍗虫墽琛�
+- **骞跺彂鎵ц**锛氱姝�
+- **鐘舵��**锛氬惎鐢�
+
+### 鎵嬪姩瑙﹀彂鏂瑰紡
+鍦ㄧ郴缁熺鐞� -> 瀹氭椂浠诲姟涓紝鍙互鎵嬪姩鎵ц锛�
+
+1. **缁熻鏄ㄦ棩鏁版嵁**锛�
+ ```
+ vehicleMileageStatsTask.calculateYesterdayMileage
+ ```
+
+2. **缁熻鎸囧畾鏃ユ湡**锛�
+ ```
+ vehicleMileageStatsTask.calculateMileageByDate('2025-11-09')
+ ```
+
+## 浜斻�佹暟鎹〃缁撴瀯
+
+### tb_vehicle_mileage_stats
+| 瀛楁鍚� | 绫诲瀷 | 璇存槑 |
+|--------|------|------|
+| stats_id | bigint(20) | 缁熻ID锛屼富閿� |
+| vehicle_id | bigint(20) | 杞﹁締ID |
+| vehicle_no | varchar(20) | 杞︾墝鍙� |
+| stat_date | date | 缁熻鏃ユ湡 |
+| total_mileage | decimal(10,2) | 鎬婚噷绋嬶紙鍏噷锛� |
+| task_mileage | decimal(10,2) | 浠诲姟鏃舵閲岀▼锛堝叕閲岋級 |
+| non_task_mileage | decimal(10,2) | 闈炰换鍔℃椂娈甸噷绋嬶紙鍏噷锛� |
+| task_ratio | decimal(5,4) | 浠诲姟閲岀▼鍗犳瘮锛�0-1锛� |
+| gps_point_count | int(11) | GPS鐐规暟閲� |
+| task_count | int(11) | 浠诲姟鏁伴噺 |
+| create_time | datetime | 鍒涘缓鏃堕棿 |
+| update_time | datetime | 鏇存柊鏃堕棿 |
+
+**绱㈠紩锛�**
+- PRIMARY KEY: stats_id
+- UNIQUE KEY: uk_vehicle_date (vehicle_id, stat_date)
+- KEY: idx_vehicle_id (vehicle_id)
+- KEY: idx_stat_date (stat_date)
+
+### tb_vehicle_mileage_detail锛堝彲閫夛級
+鐢ㄤ簬瀛樺偍閲岀▼璁$畻鏄庣粏锛屾柟渚胯皟璇曞拰杩芥函銆�
+
+## 鍏�侀儴缃叉楠�
+
+### 1. 鎵ц鏁版嵁搴撹剼鏈紙鎸夐『搴忥級
+```bash
+1. sql/vehicle_mileage_stats.sql
+2. sql/vehicle_mileage_stats_job.sql
+3. sql/vehicle_mileage_stats_menu.sql
+```
+
+### 2. 閲嶅惎搴旂敤
+浠g爜鏂囦欢宸茶嚜鍔ㄥ垱寤猴紝閲嶅惎搴旂敤鍗冲彲鐢熸晥銆�
+
+### 3. 楠岃瘉閮ㄧ讲
+1. 鐧诲綍绯荤粺锛屾鏌ヨ彍鍗曟槸鍚︽樉绀�"杞﹁締閲岀▼缁熻"
+2. 杩涘叆绯荤粺绠$悊 -> 瀹氭椂浠诲姟锛屾鏌ユ槸鍚︽湁"杞﹁締閲岀▼缁熻浠诲姟"
+3. 鎵嬪姩鎵ц瀹氭椂浠诲姟鎴栬皟鐢ˋPI鎺ュ彛娴嬭瘯鍔熻兘
+
+## 涓冦�佷娇鐢ㄧず渚�
+
+### 绀轰緥1锛氭墜鍔ㄨ绠楁煇杞﹁締鏄ㄦ棩閲岀▼
+```javascript
+import { calculateMileage } from '@/api/mileageStats'
+
+calculateMileage(1001, '2025-11-09').then(response => {
+ console.log('缁熻缁撴灉锛�', response.data)
+ // 杈撳嚭绀轰緥锛�
+ // {
+ // vehicleNo: '绮12345',
+ // statDate: '2025-11-09',
+ // totalMileage: 285.67,
+ // taskMileage: 198.43,
+ // nonTaskMileage: 87.24,
+ // taskRatio: 0.6948,
+ // gpsPointCount: 1205,
+ // taskCount: 8
+ // }
+})
+```
+
+### 绀轰緥2锛氭壒閲忚绠楁墍鏈夎溅杈嗘寚瀹氭棩鏈熼噷绋�
+```javascript
+import { batchCalculateMileage } from '@/api/mileageStats'
+
+batchCalculateMileage('2025-11-09').then(response => {
+ console.log(response.msg) // 杈撳嚭锛氭壒閲忛噷绋嬬粺璁″畬鎴愶紝鎴愬姛缁熻 45 杈嗚溅
+})
+```
+
+### 绀轰緥3锛氭煡璇㈣溅杈嗛噷绋嬬粺璁℃姤琛�
+```javascript
+import { listMileageStats } from '@/api/mileageStats'
+
+const query = {
+ vehicleNo: '绮12345',
+ beginStatDate: '2025-11-01',
+ endStatDate: '2025-11-09',
+ pageNum: 1,
+ pageSize: 10
+}
+
+listMileageStats(query).then(response => {
+ console.log('缁熻鍒楄〃锛�', response.rows)
+})
+```
+
+## 鍏�佹敞鎰忎簨椤�
+
+### 1. GPS鏁版嵁瑕佹眰
+- GPS閲囬泦闂撮殧锛氬缓璁�30-60绉�
+- 鏁版嵁瀛楁蹇呭~锛歷ehicle_id, longitude, latitude, collect_time
+- 鍧愭爣绯荤粺锛氭敮鎸乄GS84銆丟CJ02绛夊父鐢ㄥ潗鏍囩郴
+
+### 2. 浠诲姟鏁版嵁瑕佹眰
+- 浠诲姟琛細sys_task
+- 杞﹁締浠诲姟鍏宠仈琛細sys_task_vehicle
+- 蹇呭~瀛楁锛歵ask_id, vehicle_id, create_time, actual_end_time
+
+### 3. 鎬ц兘浼樺寲寤鸿
+- 瀹氭椂浠诲姟閬垮紑涓氬姟楂樺嘲鏈燂紙寤鸿鍑屾櫒鎵ц锛�
+- GPS鍘熷鏁版嵁瀹氭湡娓呯悊锛堝缓璁繚鐣�7-30澶╋級
+- 缁熻鏁版嵁瀹氭湡褰掓。锛堝缓璁繚鐣�3-6涓湀锛�
+
+### 4. 鏁版嵁鍑嗙‘鎬�
+- 閲岀▼璁$畻鍩轰簬GPS杞ㄨ抗锛岀簿搴﹀彈GPS淇″彿璐ㄩ噺褰卞搷
+- Haversine鍏紡璁$畻鐨勬槸鐩寸嚎璺濈锛屽疄闄呴亾璺窛绂诲彲鑳芥洿闀�
+- 鍙粨鍚堝ぉ鍦板浘璺緞瑙勫垝API鑾峰彇鏇村噯纭殑閬撹矾璺濈
+
+## 涔濄�佹墿灞曞姛鑳藉缓璁�
+
+### 1. 闆嗘垚澶╁湴鍥捐矾寰勮窛绂伙紙鏇村噯纭級
+褰撳墠浣跨敤Haversine鍏紡璁$畻鐩寸嚎璺濈锛屽彲鍗囩骇涓猴細
+- 灏咷PS杞ㄨ抗鐐瑰彂閫佸埌澶╁湴鍥捐矾寰勮鍒扐PI
+- 鑾峰彇瀹為檯閬撹矾璺濈
+- 鎻愰珮閲岀▼缁熻绮惧害
+
+### 2. 瀹炴椂閲岀▼缁熻
+- 鍦℅PS鏁版嵁鍏ュ簱鏃跺疄鏃惰绠�
+- 浣跨敤Redis缂撳瓨褰撴棩绱閲岀▼
+- 鍑屾櫒瀹氭椂浠诲姟浠呭仛鏁版嵁鍥哄寲
+
+### 3. 閲岀▼寮傚父鍛婅
+- 鍗曟棩閲岀▼瓒呰繃闃堝�煎憡璀�
+- 闀挎椂闂存棤GPS鏁版嵁鍛婅
+- 閲岀▼绐佸彉寮傚父鍛婅
+
+### 4. 鏁版嵁鍙鍖�
+- 姣忔棩閲岀▼瓒嬪娍鍥�
+- 浠诲姟閲岀▼鍗犳瘮楗煎浘
+- 杞﹁締閲岀▼鎺掑悕姒�
+
+## 鍗併�佹晠闅滄帓鏌�
+
+### 闂1锛氬畾鏃朵换鍔℃湭鎵ц
+- 妫�鏌ュ畾鏃朵换鍔$姸鎬佹槸鍚︿负"鍚敤"
+- 妫�鏌ron琛ㄨ揪寮忔槸鍚︽纭�
+- 鏌ョ湅瀹氭椂浠诲姟鏃ュ織
+
+### 闂2锛氱粺璁$粨鏋滀负0
+- 妫�鏌PS鏁版嵁鏄惁瀛樺湪
+- 妫�鏌PS鏁版嵁鐨刢ollect_time瀛楁鏄惁姝g‘
+- 妫�鏌ヤ换鍔℃暟鎹槸鍚﹀瓨鍦�
+
+### 闂3锛氶噷绋嬫暟鎹紓甯�
+- 妫�鏌PS鍧愭爣鏄惁鍚堟硶锛堢粡绾害鑼冨洿锛�
+- 妫�鏌ユ槸鍚﹀瓨鍦℅PS婕傜Щ鐐�
+- 鍚敤鏄庣粏琛ㄥ垎鏋愭瘡娈佃窛绂�
+
+### 鏌ョ湅鏃ュ織
+```bash
+# Service灞傛棩蹇�
+grep "VehicleMileageStatsServiceImpl" logs/ruoyi-*.log
+
+# 瀹氭椂浠诲姟鏃ュ織
+grep "VehicleMileageStatsTask" logs/ruoyi-*.log
+```
+
+## 鍗佷竴銆佹�荤粨
+
+鉁� 鏈姛鑳藉凡瀹屾暣瀹炵幇杞﹁締GPS閲岀▼缁熻鐨勬墍鏈夐渶姹傦細
+- 鉁� 姣忔棩鑷姩缁熻杞﹁締琛岄┒閲岀▼
+- 鉁� 鍖哄垎浠诲姟鏃舵鍜岄潪浠诲姟鏃舵閲岀▼
+- 鉁� 璁$畻浠诲姟閲岀▼鍗犳瘮
+- 鉁� 缁熻鏁版嵁缂撳瓨鍒版暟鎹簱琛�
+- 鉁� 鏀寔鎵嬪姩瑙﹀彂鍜屾壒閲忚绠�
+- 鉁� 鎻愪緵瀹屾暣鐨勬煡璇㈠拰瀵煎嚭鍔熻兘
+- 鉁� 闆嗘垚瀹氭椂浠诲姟鑷姩鍖栨墽琛�
+
+鏍稿績绠楁硶閲囩敤Haversine鍏紡璁$畻GPS鐐归棿璺濈锛屾寜鏃堕棿閲嶅彔姣斾緥鍒嗘憡閲岀▼鍒颁换鍔″拰闈炰换鍔℃椂娈碉紝纭繚缁熻鍑嗙‘鎬с�傛墍鏈夋暟鎹紦瀛樺湪涓撶敤缁熻琛ㄤ腑锛屾敮鎸侀珮鏁堟煡璇㈠拰鍒嗘瀽銆�
--
Gitblit v1.9.1