From 4f2925f1974844b66225ac70ae35065b8262b315 Mon Sep 17 00:00:00 2001
From: wlzboy <66905212@qq.com>
Date: 星期四, 04 十二月 2025 13:26:11 +0800
Subject: [PATCH] feat:增加微信token缓存
---
app/api/system/user.js | 12
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml | 1
app/pagesTask/create-emergency.vue | 24 +
ruoyi-ui/src/views/system/mileageStats/index.vue | 103 ++++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleMileageStatsServiceImpl.java | 73 ++
prd/StaffSelector组件分公司用户加载说明.md | 485 ++++++++++++++++++++++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatTaskNotifyServiceImpl.java | 80 +++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java | 14
sql/remove_wechat_token_fields.sql | 8
ruoyi-ui/src/api/system/mileageStats.js | 13
app/pagesTask/components/StaffSelector.vue | 113 ++++
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/BranchUserQueryVO.java | 32 +
app/pagesTask/edit-emergency.vue | 7
sql/fix_segment_mileage_task_association.sql | 134 ++++++
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java | 5
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/channel/alipay/AlipayThirdPartyClient.java | 16
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java | 19
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatLoginServiceImpl.java | 48 ++
sql/add_wechat_token_fields.sql | 9
sql/test_mileage_stats_optimization.sql | 110 +++++
20 files changed, 1,234 insertions(+), 72 deletions(-)
diff --git a/app/api/system/user.js b/app/api/system/user.js
index 1ea6103..8ea2b48 100644
--- a/app/api/system/user.js
+++ b/app/api/system/user.js
@@ -56,3 +56,15 @@
method: 'get'
})
}
+
+// 鏍规嵁鍒嗗叕鍙窱D鍒楄〃鏌ヨ鐢ㄦ埛锛堝皬绋嬪簭绔笓鐢級
+// 鏍规嵁鍒嗗叕鍙窱D鏁扮粍鏌ヨ鐢ㄦ埛(POST鏂瑰紡)
+export function listUsersByBranchDepts(branchDeptIds) {
+ return request({
+ url: '/system/user/branch/users',
+ method: 'post',
+ data: {
+ branchDeptIds
+ }
+ })
+}
diff --git a/app/pagesTask/components/StaffSelector.vue b/app/pagesTask/components/StaffSelector.vue
index 55c5391..9f750ed 100644
--- a/app/pagesTask/components/StaffSelector.vue
+++ b/app/pagesTask/components/StaffSelector.vue
@@ -109,7 +109,7 @@
<script>
import { mapState } from 'vuex'
import uniPopup from '@/uni_modules/uni-popup/components/uni-popup/uni-popup.vue'
-import { listBranchUsers } from "@/api/system/user"
+import { listUsersByBranchDepts } from "@/api/system/user"
export default {
name: 'StaffSelector',
@@ -141,6 +141,16 @@
currentUserRemovable: {
type: Boolean,
default: false
+ },
+ // 鍒嗗叕鍙窱D鍒楄〃锛堝閮ㄤ紶鍏ワ紝鐢ㄤ簬鎸囧畾鍔犺浇鍝簺鍒嗗叕鍙哥殑鐢ㄦ埛锛�
+ branchDeptIds: {
+ type: Array,
+ default: null
+ },
+ // 鍗曚釜鍒嗗叕鍙窱D锛堜粎浼犱竴涓椂鏇翠究鎹凤級
+ branchDeptId: {
+ type: [Number, String],
+ default: null
}
},
data() {
@@ -149,7 +159,9 @@
allStaffList: [],
filteredStaffList: [],
staffSearchKeyword: '',
- staffFilterType: 'driver' // 榛樿閫変腑鍙告満
+ staffFilterType: 'driver', // 榛樿閫変腑鍙告満
+ staffListCache: {}, // 缂撳瓨: { key: { data: [], timestamp: 0 } }
+ cacheExpireTime: 5 * 60 * 1000 // 缂撳瓨杩囨湡鏃堕棿锛�5鍒嗛挓
}
},
computed: {
@@ -174,6 +186,23 @@
},
immediate: true,
deep: true
+ },
+ // 鐩戝惉鍒嗗叕鍙窱D鏁扮粍鍙樺寲锛岄噸鏂板姞杞界敤鎴峰垪琛�
+ branchDeptIds: {
+ handler(newVal, oldVal) {
+ if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
+ console.log('鍒嗗叕鍙窱D鍙樺寲锛岄噸鏂板姞杞界敤鎴�:', newVal)
+ this.loadStaffList()
+ }
+ },
+ deep: true
+ },
+ // 鐩戝惉鍗曚釜鍒嗗叕鍙窱D鍙樺寲
+ branchDeptId(newVal, oldVal) {
+ if (newVal !== oldVal) {
+ console.log('鍒嗗叕鍙窱D鍙樺寲锛岄噸鏂板姞杞界敤鎴�:', newVal)
+ this.loadStaffList()
+ }
}
},
mounted() {
@@ -207,30 +236,74 @@
// 鍔犺浇浜哄憳鍒楄〃
loadStaffList() {
- listBranchUsers().then(response => {
+ // 鑾峰彇鎵�鏈夐儴闂↖D
+ let deptIds = []
+ if (this.branchDeptIds && this.branchDeptIds.length > 0) {
+ deptIds = this.branchDeptIds
+ } else if (this.branchDeptId) {
+ deptIds = [this.branchDeptId]
+ }
+
+ if (deptIds.length > 0) {
+ console.log('鏍规嵁鍒嗗叕鍙窱D鍔犺浇鐢ㄦ埛:', deptIds)
+ this.loadStaffByBranchDepts(deptIds)
+ } else {
+ console.log('鏈紶鍏ュ垎鍏徃ID,缁勪欢涓嶅姞杞戒汉鍛樺垪琛�')
+ this.$modal && this.$modal.showToast && this.$modal.showToast('璇蜂紶鍏ュ垎鍏徃ID')
+ }
+ },
+
+ // 鏍规嵁鍒嗗叕鍙窱D鏁扮粍鍔犺浇鐢ㄦ埛锛堟敮鎸佺紦瀛橈級
+ loadStaffByBranchDepts(deptIds) {
+ // 鐢熸垚缂撳瓨key锛堟寜鎺掑簭鍚巌d鎷兼帴锛�
+ const cacheKey = [...deptIds].sort((a, b) => a - b).join(',')
+ const cached = this.staffListCache[cacheKey]
+ const now = Date.now()
+
+ // 妫�鏌ョ紦瀛樻槸鍚︽湁鏁�
+ if (cached && (now - cached.timestamp) < this.cacheExpireTime) {
+ console.log('浣跨敤缂撳瓨鐨勪汉鍛樺垪琛�:', cacheKey)
+ this.processUserList(cached.data)
+ return
+ }
+
+ // 缂撳瓨澶辨晥鎴栦笉瀛樺湪,璋冪敤鎺ュ彛
+ console.log('鍔犺浇浜哄憳鍒楄〃:', deptIds)
+ listUsersByBranchDepts(deptIds).then(response => {
const userList = response.data || []
-
- this.allStaffList = userList.map(user => ({
- userId: user.userId,
- nickName: user.nickName,
- phonenumber: user.phonenumber,
- deptName: user.dept?.deptName || '',
- postName: user.posts && user.posts.length > 0 ? user.posts[0].postName : '',
- roleName: user.roles && user.roles.length > 0 ? user.roles[0].roleName : '',
- posts: user.posts || [],
- roles: user.roles || [],
- dept: user.dept || null,
- // 鏀寔澶氱绫诲瀷
- types: this.getUserTypes(user),
- type: this.getUserTypes(user)[0] || 'driver' // 涓昏绫诲瀷锛堢敤浜庡悜鍚庡吋瀹癸級
- }))
-
- this.filterStaffList()
+ // 鏇存柊缂撳瓨
+ this.staffListCache[cacheKey] = {
+ data: userList,
+ timestamp: now
+ }
+ this.processUserList(userList)
}).catch(error => {
console.error('鍔犺浇浜哄憳鍒楄〃澶辫触:', error)
this.$modal.showToast('鍔犺浇浜哄憳鍒楄〃澶辫触')
})
},
+
+
+
+ // 澶勭悊鐢ㄦ埛鍒楄〃鏁版嵁
+ processUserList(userList) {
+ this.allStaffList = userList.map(user => ({
+ userId: user.userId,
+ nickName: user.nickName,
+ phonenumber: user.phonenumber,
+ deptName: user.dept?.deptName || '',
+ postName: user.posts && user.posts.length > 0 ? user.posts[0].postName : '',
+ roleName: user.roles && user.roles.length > 0 ? user.roles[0].roleName : '',
+ posts: user.posts || [],
+ roles: user.roles || [],
+ dept: user.dept || null,
+ // 鏀寔澶氱绫诲瀷
+ types: this.getUserTypes(user),
+ type: this.getUserTypes(user)[0] || 'driver' // 涓昏绫诲瀷锛堢敤浜庡悜鍚庡吋瀹癸級
+ }))
+
+ this.filterStaffList()
+ },
// 鏍规嵁鐢ㄦ埛鐨勫矖浣嶆垨瑙掕壊鍒ゆ柇鎵�鏈夌被鍨嬶紙鏀寔澶氱韬唤锛�
getUserTypes(user) {
diff --git a/app/pagesTask/create-emergency.vue b/app/pagesTask/create-emergency.vue
index 5f557c7..a752eda 100644
--- a/app/pagesTask/create-emergency.vue
+++ b/app/pagesTask/create-emergency.vue
@@ -23,6 +23,7 @@
</view>
<view class="form-item">
<OrganizationSelector
+ ref="organizationSelector"
v-model="selectedOrganizationId"
:required="true"
:auto-select-user-dept="true"
@@ -65,6 +66,7 @@
:required="false"
:auto-add-current-user="true"
:current-user-removable="false"
+ :branch-dept-ids="allOrganizationIds"
@change="onStaffChange"
/>
@@ -266,7 +268,8 @@
return {
selectedVehicle: '',
selectedVehicleId: null,
- selectedOrganizationId: null, // 褰掑睘鏈烘瀯ID锛堥儴闂↖D锛�
+ selectedOrganizationId: null, // 褰撳墠閫変腑鐨勫綊灞炴満鏋処D
+ allOrganizationIds: [], // 鎵�鏈夊彲閫夋満鏋処D鏁扮粍
selectedOrganizationServiceOrderClass: '', // 褰掑睘鏈烘瀯鐨勬湇鍔″崟缂栫爜
selectedRegion: '', // 浠庡綊灞炴満鏋勪腑鎻愬彇鐨勫湴鍩熶俊鎭紙濡傦細骞垮窞銆佹繁鍦崇瓑锛�
departureAddress: '', // 鍑哄彂鍦板湴鍧�
@@ -360,6 +363,8 @@
this.loadEmergencyTaskTypes()
// 鍔犺浇鍗曟嵁绫诲瀷鏁版嵁
this.loadDocumentTypes()
+ // 鍔犺浇鎵�鏈夋満鏋処D
+ this.loadAllOrganizationIds()
},
methods: {
// 鑾峰彇鐢ㄦ埛缁戝畾鐨勮溅杈嗕俊鎭�
@@ -446,6 +451,23 @@
return region.replace(/(鍒嗗叕鍙竱鎬诲叕鍙竱鎬婚儴)$/g, '').trim();
},
+ // 鍔犺浇鎵�鏈夋満鏋処D
+ loadAllOrganizationIds() {
+ // 閫氳繃 OrganizationSelector 缁勪欢鑾峰彇鎵�鏈夋満鏋�
+ const orgSelector = this.$refs.organizationSelector
+ if (orgSelector) {
+ orgSelector.reload().then(organizations => {
+ this.allOrganizationIds = organizations.map(org => org.deptId)
+ console.log('鎵�鏈夋満鏋処D:', this.allOrganizationIds)
+ })
+ } else {
+ // 濡傛灉缁勪欢杩樻湭鎸傝浇,绋嶅悗閲嶈瘯
+ setTimeout(() => {
+ this.loadAllOrganizationIds()
+ }, 100)
+ }
+ },
+
// 鍔犺浇绉戝鏁版嵁锛堜粠 SQL Server 鍔ㄦ�佸姞杞斤級
loadDepartments() {
getHospitalDepartments().then(response => {
diff --git a/app/pagesTask/edit-emergency.vue b/app/pagesTask/edit-emergency.vue
index 67c10f4..cb0d7dc 100644
--- a/app/pagesTask/edit-emergency.vue
+++ b/app/pagesTask/edit-emergency.vue
@@ -338,8 +338,10 @@
this.taskForm.hospitalOut.id = info.hospitalOutId || null
this.taskForm.hospitalOut.name = info.hospitalOutName || ''
this.taskForm.hospitalOut.department = info.hospitalOutDepartment || ''
+ this.taskForm.hospitalOut.departmentId = info.hospitalOutDepartmentId || null
this.taskForm.hospitalOut.bedNumber = info.hospitalOutBedNumber || ''
this.taskForm.hospitalOut.address = info.hospitalOutAddress || ''
+ console.log('杞嚭鍖婚櫌绉戝ID:', info.hospitalOutDepartmentId)
// 鍔犺浇杞嚭鍖婚櫌GPS鍧愭爣锛堜笉鏄剧ず锛屼絾淇濆瓨鍦ㄦ暟鎹腑锛�
if (info.hospitalOutLongitude && info.hospitalOutLatitude) {
@@ -354,8 +356,10 @@
this.taskForm.hospitalIn.id = info.hospitalInId || null
this.taskForm.hospitalIn.name = info.hospitalInName || ''
this.taskForm.hospitalIn.department = info.hospitalInDepartment || ''
+ this.taskForm.hospitalIn.departmentId = info.hospitalInDepartmentId || null
this.taskForm.hospitalIn.bedNumber = info.hospitalInBedNumber || ''
this.taskForm.hospitalIn.address = info.hospitalInAddress || ''
+ console.log('杞叆鍖婚櫌绉戝ID:', info.hospitalInDepartmentId)
// 鍔犺浇杞叆鍖婚櫌GPS鍧愭爣锛堜笉鏄剧ず锛屼絾淇濆瓨鍦ㄦ暟鎹腑锛�
if (info.hospitalInLongitude && info.hospitalInLatitude) {
@@ -854,6 +858,9 @@
this.loading = true
const submitData = this.buildSubmitData()
+ console.log('鎻愪氦鏁版嵁 - 杞嚭鍖婚櫌绉戝ID:', submitData.hospitalOut.departmentId)
+ console.log('鎻愪氦鏁版嵁 - 杞叆鍖婚櫌绉戝ID:', submitData.hospitalIn.departmentId)
+
updateTask(submitData).then(response => {
this.loading = false
console.log('浠诲姟鏇存柊鍝嶅簲:', response)
diff --git a/dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/channel/alipay/AlipayThirdPartyClient.java b/dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/channel/alipay/AlipayThirdPartyClient.java
index 81c1363..39b5fcb 100644
--- a/dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/channel/alipay/AlipayThirdPartyClient.java
+++ b/dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/channel/alipay/AlipayThirdPartyClient.java
@@ -1,5 +1,6 @@
package com.ruoyi.payment.infrastructure.channel.alipay;
+import com.ruoyi.payment.infrastructure.config.AlipayConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
@@ -8,7 +9,10 @@
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
/**
* 鏀粯瀹濈涓夋柟鎺ュ彛瀹㈡埛绔�
@@ -20,10 +24,12 @@
@Component
public class AlipayThirdPartyClient {
+ @Autowired
+ private AlipayConfig alipayConfig;
/**
* 绗笁鏂规敮浠樺疂褰撻潰浠樻帴鍙e湴鍧�
*/
- private static final String THIRD_PARTY_ALIPAY_URL = "https://sys.966120.com.cn/alipay_pay_QR_NotifyUrl.php";
+// private String THIRD_PARTY_ALIPAY_URL = "https://sys.966120.com.cn/alipay_pay_QR_NotifyUrl.php";
/**
* 绗笁鏂规敮浠樺疂鏌ヨ鎺ュ彛鍦板潃
@@ -46,22 +52,22 @@
outTradeNo, totalFee, serviceOrdId);
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
- HttpPost httpPost = new HttpPost(THIRD_PARTY_ALIPAY_URL);
+ HttpPost httpPost = new HttpPost(this.alipayConfig.getThirdParty().getUrl());
// 璁剧疆Cookie澶�
httpPost.setHeader("Cookie", "CAMEName=");
-
+ BigDecimal totalFeeYuan = BigDecimal.valueOf(totalFee / 100f);
// 鏋勫缓multipart/form-data璇锋眰浣�
HttpEntity entity = MultipartEntityBuilder.create()
.addTextBody("notify_url", notifyUrl)
.addTextBody("out_trade_no", outTradeNo)
- .addTextBody("total_fee", String.valueOf(totalFee))
+ .addTextBody("total_fee", String.valueOf(totalFeeYuan))
.addTextBody("ServiceOrdID", serviceOrdId)
.build();
httpPost.setEntity(entity);
- log.info("鍙戦�佽姹傚埌绗笁鏂规帴鍙�: {}", THIRD_PARTY_ALIPAY_URL);
+ log.info("鍙戦�佽姹傚埌绗笁鏂规帴鍙�: {}", this.alipayConfig.getThirdParty().getUrl());
// 鍙戦�佽姹�
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
diff --git "a/prd/StaffSelector\347\273\204\344\273\266\345\210\206\345\205\254\345\217\270\347\224\250\346\210\267\345\212\240\350\275\275\350\257\264\346\230\216.md" "b/prd/StaffSelector\347\273\204\344\273\266\345\210\206\345\205\254\345\217\270\347\224\250\346\210\267\345\212\240\350\275\275\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..22c26a6
--- /dev/null
+++ "b/prd/StaffSelector\347\273\204\344\273\266\345\210\206\345\205\254\345\217\270\347\224\250\346\210\267\345\212\240\350\275\275\350\257\264\346\230\216.md"
@@ -0,0 +1,485 @@
+# StaffSelector 缁勪欢鍒嗗叕鍙哥敤鎴峰姞杞藉姛鑳借鏄�
+
+## 馃搵 鍔熻兘姒傝堪
+
+浼樺寲 `StaffSelector` 缁勪欢,鏀寔閫氳繃澶栭儴浼犲叆鍒嗗叕鍙窱D鍒楄〃鏉ュ姞杞芥寚瀹氬垎鍏徃鐨勭敤鎴�,鎻愪緵鏇寸伒娲荤殑浜哄憳閫夋嫨鍔熻兘銆�
+
+---
+
+## 馃攧 淇敼鍐呭
+
+### 1. 鍚庣鎺ュ彛鏂板
+
+#### **鏂囦欢**: `SysUserController.java`
+
+**鏂板鎺ュ彛**: `GET /system/user/branch/users/by-dept-ids`
+
+```java
+/**
+ * 鏍规嵁鍒嗗叕鍙窱D鍒楄〃鑾峰彇鐢ㄦ埛(灏忕▼搴忕涓撶敤)
+ * 鏀寔澶栭儴浼犲叆鍒嗗叕鍙窱D鏁扮粍,鏌ヨ杩欎簺鍒嗗叕鍙稿強鍏舵墍鏈夊瓙閮ㄩ棬鐨勭敤鎴�
+ */
+@GetMapping("/branch/users/by-dept-ids")
+public AjaxResult listUsersByBranchDeptIds(Long[] branchDeptIds)
+```
+
+**鍙傛暟**:
+- `branchDeptIds`: 鍒嗗叕鍙窱D鏁扮粍,渚嬪 `[101, 102]`
+
+**杩斿洖鏁版嵁**:
+```json
+{
+ "code": 200,
+ "msg": "鎿嶄綔鎴愬姛",
+ "data": [
+ {
+ "userId": 1,
+ "nickName": "寮犱笁",
+ "phonenumber": "13800138000",
+ "dept": {
+ "deptId": 201,
+ "deptName": "骞垮窞鍒嗗叕鍙�-杞﹂槦A"
+ },
+ "posts": [...],
+ "roles": [...]
+ }
+ ]
+}
+```
+
+---
+
+### 2. 鍓嶇API鎵╁睍
+
+#### **鏂囦欢**: `app/api/system/user.js`
+
+**鏂板鏂规硶**:
+```javascript
+// 鏍规嵁鍒嗗叕鍙窱D鍒楄〃鏌ヨ鐢ㄦ埛(灏忕▼搴忕涓撶敤)
+export function listUsersByBranchDeptIds(branchDeptIds) {
+ return request({
+ url: '/system/user/branch/users/by-dept-ids',
+ method: 'get',
+ params: {
+ branchDeptIds: branchDeptIds
+ }
+ })
+}
+```
+
+---
+
+### 3. StaffSelector 缁勪欢鍗囩骇
+
+#### **鏂囦欢**: `app/pagesTask/components/StaffSelector.vue`
+
+**鏂板 Props**:
+```javascript
+props: {
+ // ... 鍏朵粬 props
+
+ // 鍒嗗叕鍙窱D鍒楄〃(澶栭儴浼犲叆,鐢ㄤ簬鎸囧畾鍔犺浇鍝簺鍒嗗叕鍙哥殑鐢ㄦ埛)
+ branchDeptIds: {
+ type: Array,
+ default: null
+ }
+}
+```
+
+**鏍稿績閫昏緫**:
+```javascript
+loadStaffList() {
+ // 濡傛灉浼犲叆浜嗗垎鍏徃ID,浣跨敤鎸囧畾鐨勫垎鍏徃
+ if (this.branchDeptIds && this.branchDeptIds.length > 0) {
+ this.loadStaffByBranchDeptIds(this.branchDeptIds)
+ } else {
+ // 鍚﹀垯浣跨敤褰撳墠鐢ㄦ埛鐨勫垎鍏徃
+ this.loadStaffByCurrentUser()
+ }
+}
+```
+
+**鐩戝惉鍙樺寲**:
+```javascript
+watch: {
+ branchDeptIds: {
+ handler(newVal, oldVal) {
+ if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
+ this.loadStaffList()
+ }
+ },
+ deep: true
+ }
+}
+```
+
+---
+
+## 馃摉 浣跨敤鏂瑰紡
+
+### 鏂瑰紡涓�: 榛樿鍔犺浇(浣跨敤褰撳墠鐢ㄦ埛鐨勫垎鍏徃)
+
+```vue
+<template>
+ <staff-selector
+ v-model="selectedStaff"
+ label="鎵ц浠诲姟浜哄憳"
+ :required="true"
+ />
+</template>
+
+<script>
+import StaffSelector from '@/components/StaffSelector.vue'
+
+export default {
+ components: { StaffSelector },
+ data() {
+ return {
+ selectedStaff: []
+ }
+ }
+}
+</script>
+```
+
+**琛屼负**:
+- 鑷姩璋冪敤 `/system/user/branch/users` 鎺ュ彛
+- 鍔犺浇褰撳墠鐧诲綍鐢ㄦ埛鎵�灞炲垎鍏徃鍙婂叾瀛愰儴闂ㄧ殑鎵�鏈夌敤鎴�
+
+---
+
+### 鏂瑰紡浜�: 鎸囧畾鍒嗗叕鍙窱D(澶栭儴浼犲叆)
+
+```vue
+<template>
+ <view>
+ <!-- 鍒嗗叕鍙搁�夋嫨鍣� -->
+ <view class="form-item">
+ <view class="form-label">閫夋嫨鍒嗗叕鍙�</view>
+ <picker
+ mode="multiSelector"
+ @change="onBranchChange"
+ :value="selectedBranchIndex"
+ :range="branchList"
+ range-key="deptName"
+ >
+ <view class="picker-value">
+ {{ selectedBranchNames || '璇烽�夋嫨鍒嗗叕鍙�' }}
+ </view>
+ </picker>
+ </view>
+
+ <!-- 浜哄憳閫夋嫨鍣� -->
+ <staff-selector
+ v-model="selectedStaff"
+ label="鎵ц浠诲姟浜哄憳"
+ :required="true"
+ :branch-dept-ids="selectedBranchIds"
+ />
+ </view>
+</template>
+
+<script>
+import StaffSelector from '@/components/StaffSelector.vue'
+import { listBranchCompany } from '@/api/system/dept'
+
+export default {
+ components: { StaffSelector },
+ data() {
+ return {
+ branchList: [], // 鎵�鏈夊垎鍏徃鍒楄〃
+ selectedBranchIds: [], // 閫変腑鐨勫垎鍏徃ID鏁扮粍
+ selectedBranchNames: '', // 閫変腑鐨勫垎鍏徃鍚嶇О
+ selectedStaff: [] // 閫変腑鐨勪汉鍛�
+ }
+ },
+ mounted() {
+ this.loadBranchList()
+ },
+ methods: {
+ // 鍔犺浇鍒嗗叕鍙稿垪琛�
+ loadBranchList() {
+ listBranchCompany().then(response => {
+ this.branchList = response.data || []
+ })
+ },
+
+ // 鍒嗗叕鍙搁�夋嫨鍙樺寲
+ onBranchChange(e) {
+ const indices = e.detail.value
+ const selected = indices.map(index => this.branchList[index])
+
+ this.selectedBranchIds = selected.map(dept => dept.deptId)
+ this.selectedBranchNames = selected.map(dept => dept.deptName).join('銆�')
+
+ console.log('閫変腑鐨勫垎鍏徃ID:', this.selectedBranchIds)
+ }
+ }
+}
+</script>
+```
+
+**琛屼负**:
+- 鐢ㄦ埛閫夋嫨鍒嗗叕鍙稿悗,`selectedBranchIds` 鏇存柊
+- `StaffSelector` 鐩戝惉鍒� `branchDeptIds` 鍙樺寲
+- 鑷姩璋冪敤 `/system/user/branch/users/by-dept-ids?branchDeptIds=101,102`
+- 鍔犺浇鎸囧畾鍒嗗叕鍙稿強鍏跺瓙閮ㄩ棬鐨勬墍鏈夌敤鎴�
+
+---
+
+### 鏂瑰紡涓�: 鍔ㄦ�佸垏鎹㈠垎鍏徃
+
+```vue
+<template>
+ <view>
+ <!-- Tab 鍒囨崲鍒嗗叕鍙� -->
+ <view class="tabs">
+ <view
+ class="tab-item"
+ :class="{ active: currentBranchId === branch.deptId }"
+ v-for="branch in branchList"
+ :key="branch.deptId"
+ @click="switchBranch(branch)"
+ >
+ {{ branch.deptName }}
+ </view>
+ </view>
+
+ <!-- 浜哄憳閫夋嫨鍣� -->
+ <staff-selector
+ v-model="selectedStaff"
+ label="鎵ц浠诲姟浜哄憳"
+ :branch-dept-ids="[currentBranchId]"
+ />
+ </view>
+</template>
+
+<script>
+import StaffSelector from '@/components/StaffSelector.vue'
+import { listBranchCompany } from '@/api/system/dept'
+
+export default {
+ components: { StaffSelector },
+ data() {
+ return {
+ branchList: [],
+ currentBranchId: null,
+ selectedStaff: []
+ }
+ },
+ mounted() {
+ this.loadBranchList()
+ },
+ methods: {
+ loadBranchList() {
+ listBranchCompany().then(response => {
+ this.branchList = response.data || []
+ if (this.branchList.length > 0) {
+ // 榛樿閫変腑绗竴涓垎鍏徃
+ this.currentBranchId = this.branchList[0].deptId
+ }
+ })
+ },
+
+ switchBranch(branch) {
+ this.currentBranchId = branch.deptId
+ // branchDeptIds 鍙樺寲浼氳嚜鍔ㄨЕ鍙� StaffSelector 閲嶆柊鍔犺浇
+ }
+ }
+}
+</script>
+```
+
+---
+
+## 馃攽 Props 璇存槑
+
+| Props | 绫诲瀷 | 榛樿鍊� | 璇存槑 |
+|-------|------|--------|------|
+| `value` | Array | `[]` | 宸查�夋嫨鐨勪汉鍛樺垪琛�(v-model) |
+| `label` | String | `'鎵ц浠诲姟浜哄憳'` | 鏍囩鏂囨湰 |
+| `required` | Boolean | `false` | 鏄惁蹇呭~ |
+| `autoAddCurrentUser` | Boolean | `true` | 鏄惁鑷姩娣诲姞褰撳墠鐢ㄦ埛 |
+| `currentUserRemovable` | Boolean | `false` | 褰撳墠鐢ㄦ埛鏄惁鍙Щ闄� |
+| **`branchDeptIds`** | Array | `null` | **鍒嗗叕鍙窱D鍒楄〃(鏂板)** |
+
+---
+
+## 馃搳 鏁版嵁娴佺▼
+
+### 榛樿妯″紡(涓嶄紶 branchDeptIds)
+
+```
+1. 缁勪欢 mounted
+ 鈫�
+2. loadStaffList()
+ 鈫�
+3. 鍒ゆ柇 branchDeptIds 涓虹┖
+ 鈫�
+4. 璋冪敤 listBranchUsers()
+ 鈫�
+5. 鍚庣鏍规嵁褰撳墠鐢ㄦ埛鐨� oaOrderClass 鎴� deptId 鏌ヨ鍒嗗叕鍙�
+ 鈫�
+6. 杩斿洖璇ュ垎鍏徃鍙婂叾瀛愰儴闂ㄧ殑鎵�鏈夌敤鎴�
+ 鈫�
+7. processUserList() 澶勭悊鏁版嵁
+ 鈫�
+8. 灞曠ず鐢ㄦ埛鍒楄〃
+```
+
+### 鎸囧畾妯″紡(浼犲叆 branchDeptIds)
+
+```
+1. 缁勪欢 mounted
+ 鈫�
+2. loadStaffList()
+ 鈫�
+3. 鍒ゆ柇 branchDeptIds 鏈夊��
+ 鈫�
+4. 璋冪敤 listUsersByBranchDeptIds(branchDeptIds)
+ 鈫�
+5. 鍚庣鏍规嵁浼犲叆鐨勫垎鍏徃ID鍒楄〃鏌ヨ
+ 鈫�
+6. 杩斿洖鎸囧畾鍒嗗叕鍙稿強鍏跺瓙閮ㄩ棬鐨勬墍鏈夌敤鎴�
+ 鈫�
+7. processUserList() 澶勭悊鏁版嵁
+ 鈫�
+8. 灞曠ず鐢ㄦ埛鍒楄〃
+```
+
+### 鍔ㄦ�佸垏鎹�
+
+```
+1. 鐖剁粍浠朵慨鏀� branchDeptIds
+ 鈫�
+2. StaffSelector 鐩戝惉鍒板彉鍖�(watch)
+ 鈫�
+3. 閲嶆柊鎵ц loadStaffList()
+ 鈫�
+4. 鏍规嵁鏂扮殑 branchDeptIds 鍔犺浇鐢ㄦ埛
+ 鈫�
+5. 鏇存柊鐢ㄦ埛鍒楄〃
+```
+
+---
+
+## 鈿� 鎬ц兘浼樺寲
+
+### 1. 閬垮厤閲嶅鍔犺浇
+```javascript
+watch: {
+ branchDeptIds: {
+ handler(newVal, oldVal) {
+ // 鍙湁鐪熸鍙樺寲鏃舵墠閲嶆柊鍔犺浇
+ if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
+ this.loadStaffList()
+ }
+ },
+ deep: true
+ }
+}
+```
+
+### 2. 鍓嶇缂撳瓨
+鍙互鍦ㄧ埗缁勪欢灞傞潰缂撳瓨鐢ㄦ埛鍒楄〃:
+```javascript
+data() {
+ return {
+ userCache: {} // { '101': [...users], '102': [...users] }
+ }
+}
+```
+
+---
+
+## 鉁� 娴嬭瘯鐢ㄤ緥
+
+### 娴嬭瘯1: 榛樿鍔犺浇
+**杈撳叆**: 涓嶄紶 `branchDeptIds`
+**棰勬湡**: 鍔犺浇褰撳墠鐢ㄦ埛鐨勫垎鍏徃鐢ㄦ埛
+**楠岃瘉**: 妫�鏌ョ敤鎴峰垪琛ㄦ槸鍚︾鍚堥鏈�
+
+### 娴嬭瘯2: 鍗曚釜鍒嗗叕鍙�
+**杈撳叆**: `branchDeptIds: [101]`
+**棰勬湡**: 鍙姞杞� ID=101 鐨勫垎鍏徃鍙婂叾瀛愰儴闂ㄧ敤鎴�
+**楠岃瘉**: 妫�鏌ョ敤鎴烽儴闂ㄦ槸鍚﹂兘灞炰簬璇ュ垎鍏徃
+
+### 娴嬭瘯3: 澶氫釜鍒嗗叕鍙�
+**杈撳叆**: `branchDeptIds: [101, 102]`
+**棰勬湡**: 鍔犺浇涓や釜鍒嗗叕鍙稿強鍏跺瓙閮ㄩ棬鐨勭敤鎴�
+**楠岃瘉**: 妫�鏌ョ敤鎴烽儴闂ㄦ槸鍚﹂兘灞炰簬杩欎袱涓垎鍏徃
+
+### 娴嬭瘯4: 鍔ㄦ�佸垏鎹�
+**鎿嶄綔**: 鍏堜紶 `[101]`,鍐嶆敼涓� `[102]`
+**棰勬湡**: 鐢ㄦ埛鍒楄〃瀹炴椂鏇存柊
+**楠岃瘉**: 妫�鏌ヤ袱娆″姞杞界殑鐢ㄦ埛鏄惁涓嶅悓
+
+### 娴嬭瘯5: 绌烘暟缁�
+**杈撳叆**: `branchDeptIds: []`
+**棰勬湡**: 杩斿洖绌哄垪琛�
+**楠岃瘉**: 鐢ㄦ埛鍒楄〃涓虹┖
+
+---
+
+## 馃挕 浣跨敤寤鸿
+
+### 鍦烘櫙1: 璺ㄥ垎鍏徃鍗忎綔浠诲姟
+褰撲换鍔¢渶瑕佸涓垎鍏徃鐨勪汉鍛樺崗浣滄椂:
+```vue
+<staff-selector
+ :branch-dept-ids="[101, 102, 103]"
+ label="璺ㄥ尯鍩熷崗浣滀汉鍛�"
+/>
+```
+
+### 鍦烘櫙2: 鍖哄煙绛涢��
+鎻愪緵鍖哄煙閫夋嫨鍣�,鐢ㄦ埛鑷�夊垎鍏徃:
+```vue
+<region-selector v-model="selectedRegions" />
+<staff-selector
+ :branch-dept-ids="selectedRegions.map(r => r.deptId)"
+/>
+```
+
+### 鍦烘櫙3: 鏉冮檺鎺у埗
+鏍规嵁鐢ㄦ埛鏉冮檺鍔ㄦ�侀檺鍒跺彲閫夊垎鍏徃:
+```javascript
+computed: {
+ allowedBranchIds() {
+ // 鏍规嵁鐢ㄦ埛瑙掕壊杩斿洖鍏佽鐨勫垎鍏徃ID
+ return this.currentUser.allowedBranches.map(b => b.deptId)
+ }
+}
+```
+
+---
+
+## 馃搶 娉ㄦ剰浜嬮」
+
+1. **鏁扮粍浼犻��**: `branchDeptIds` 蹇呴』鏄暟缁勭被鍨�,鍗充娇鍙湁涓�涓狪D涔熻鐢ㄦ暟缁� `[101]`
+2. **鍔ㄦ�佹洿鏂�**: 淇敼 `branchDeptIds` 浼氳嚜鍔ㄨЕ鍙戦噸鏂板姞杞�,鏃犻渶鎵嬪姩璋冪敤
+3. **绌哄�煎鐞�**: 浼犲叆 `null` 鎴栦笉浼犳椂,浣跨敤榛樿鐨勫綋鍓嶇敤鎴峰垎鍏徃
+4. **鏉冮檺鏍¢獙**: 鍚庣浼氭牴鎹敤鎴锋潈闄愯繃婊ゆ暟鎹�,鍓嶇鏃犻渶棰濆澶勭悊
+
+---
+
+## 馃敆 鐩稿叧鏂囦欢
+
+### 鍚庣
+- `SysUserController.java` - 鐢ㄦ埛鎺у埗鍣�
+- `SysUserService.java` - 鐢ㄦ埛鏈嶅姟鎺ュ彛
+- `SysUserServiceImpl.java` - 鐢ㄦ埛鏈嶅姟瀹炵幇
+- `SysUserMapper.xml` - SQL鏄犲皠鏂囦欢
+
+### 鍓嶇
+- `app/api/system/user.js` - 鐢ㄦ埛API
+- `app/pagesTask/components/StaffSelector.vue` - 浜哄憳閫夋嫨鍣ㄧ粍浠�
+- `app/api/system/dept.js` - 閮ㄩ棬API
+
+---
+
+## 馃摑 鐗堟湰鍘嗗彶
+
+| 鐗堟湰 | 鏃ユ湡 | 璇存槑 |
+|------|------|------|
+| 1.0 | 2025-12-04 | 鍒濆鐗堟湰,鏂板鎸夊垎鍏徃ID鍔犺浇鐢ㄦ埛鍔熻兘 |
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java
index 3142bb6..84af926 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java
@@ -31,6 +31,7 @@
import com.ruoyi.system.service.ISysPostService;
import com.ruoyi.system.service.ISysRoleService;
import com.ruoyi.system.service.ISysUserService;
+import com.ruoyi.system.domain.vo.BranchUserQueryVO;
/**
* 鐢ㄦ埛淇℃伅
@@ -319,6 +320,24 @@
}
/**
+ * 鏍规嵁鍒嗗叕鍙窱D鍒楄〃鑾峰彇鐢ㄦ埛(POST鏂瑰紡)
+ */
+ @PostMapping("/branch/users")
+ public AjaxResult listUsersByBranchDepts(@RequestBody BranchUserQueryVO queryVO)
+ {
+ List<Long> branchDeptIds = queryVO.getBranchDeptIds();
+
+ if (branchDeptIds == null || branchDeptIds.isEmpty()) {
+ return success(new java.util.ArrayList<>());
+ }
+
+ // 鏌ヨ杩欎簺鍒嗗叕鍙稿強鍏舵墍鏈夊瓙閮ㄩ棬鐨勭敤鎴�
+ List<SysUser> users = userService.selectUsersByBranchDeptIds(branchDeptIds);
+
+ return success(users);
+ }
+
+ /**
* 鏍规嵁oaUserId鏌ヨ鐢ㄦ埛淇℃伅
*/
@GetMapping("/oa-user/{oaUserId}")
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
index f568f97..c2c48c6 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
@@ -104,6 +104,8 @@
/** 寰俊鏄电О */
private String wechatNickname;
+
+
public SysUser()
{
@@ -361,6 +363,8 @@
{
this.wechatNickname = wechatNickname;
}
+
+
@Override
public String toString() {
@@ -389,6 +393,7 @@
.append("openId", getOpenId())
.append("unionId", getUnionId())
.append("wechatNickname", getWechatNickname())
+
.toString();
}
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/BranchUserQueryVO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/BranchUserQueryVO.java
new file mode 100644
index 0000000..87489a3
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/BranchUserQueryVO.java
@@ -0,0 +1,32 @@
+package com.ruoyi.system.domain.vo;
+
+import java.util.List;
+
+/**
+ * 鍒嗗叕鍙哥敤鎴锋煡璇O
+ *
+ * @author ruoyi
+ */
+public class BranchUserQueryVO
+{
+ /** 鍒嗗叕鍙窱D鍒楄〃 */
+ private List<Long> branchDeptIds;
+
+ public List<Long> getBranchDeptIds()
+ {
+ return branchDeptIds;
+ }
+
+ public void setBranchDeptIds(List<Long> branchDeptIds)
+ {
+ this.branchDeptIds = branchDeptIds;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "BranchUserQueryVO{" +
+ "branchDeptIds=" + branchDeptIds +
+ '}';
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
index 466e111..a04871a 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java
@@ -2037,12 +2037,7 @@
}
}
}
- if (createVO.getHospitalOut().getLongitude() != null) {
- existingInfo.setHospitalOutLongitude(createVO.getHospitalOut().getLongitude());
- }
- if (createVO.getHospitalOut().getLatitude() != null) {
- existingInfo.setHospitalOutLatitude(createVO.getHospitalOut().getLatitude());
- }
+
}
// 鏇存柊杞叆鍖婚櫌淇℃伅
@@ -2082,12 +2077,7 @@
}
}
}
- if (createVO.getHospitalIn().getLongitude() != null) {
- existingInfo.setHospitalInLongitude(createVO.getHospitalIn().getLongitude());
- }
- if (createVO.getHospitalIn().getLatitude() != null) {
- existingInfo.setHospitalInLatitude(createVO.getHospitalIn().getLatitude());
- }
+
}
// 鏇存柊璐圭敤淇℃伅
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
index e61b9f6..f795378 100644
--- 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
@@ -6,7 +6,9 @@
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -395,26 +397,69 @@
}
}
- // 4. 鏌ヨ璇ユ棩鏈熺殑浠诲姟鏃堕棿鍖洪棿锛岃绠椾换鍔¢噷绋嬪拰闈炰换鍔¢噷绋�
- List<TaskTimeInterval> taskIntervals = vehicleMileageStatsMapper.selectTaskTimeIntervals(vehicleId, dayStart, dayEnd);
-
+ // 4. 璁$畻浠诲姟閲岀▼鍜岄潪浠诲姟閲岀▼锛堜紭鍖栵細浼樺厛浣跨敤task_id鐩存帴鑱氬悎锛�
BigDecimal taskMileage = BigDecimal.ZERO;
BigDecimal nonTaskMileage = BigDecimal.ZERO;
+ int taskCount = 0; // 浠诲姟鏁伴噺
+ // 4.1 缁熻鏈塼ask_id鐨勫垎娈垫暟閲�
+ int segmentsWithTask = 0;
for (VehicleGpsSegmentMileage segment : segments) {
- Date segStart = segment.getSegmentStartTime();
- Date segEnd = segment.getSegmentEndTime();
- BigDecimal segDistance = segment.getSegmentDistance() != null ? segment.getSegmentDistance() : BigDecimal.ZERO;
+ if (segment.getTaskId() != null) {
+ segmentsWithTask++;
+ }
+ }
+
+ // 4.2 濡傛灉澶ч儴鍒嗗垎娈甸兘鏈塼ask_id锛屼娇鐢ㄤ紭鍖栨柟妗堬紙鐩存帴鎸塼ask_id鑱氬悎锛�
+ if (segmentsWithTask > segments.size() * 0.8) {
+ logger.debug("杞﹁締ID: {} 鏃ユ湡: {} 浣跨敤浼樺寲鏂规锛氱洿鎺ユ寜task_id鑱氬悎锛坽}涓垎娈垫湁task_id锛屽崰姣攞}%锛�",
+ vehicleId, statDate, segmentsWithTask, (segmentsWithTask * 100.0 / segments.size()));
- // 璁$畻璇ュ垎娈典笌浠诲姟鏃舵鐨勯噸鍙犳瘮渚�
- double taskRatio = calculateTaskOverlapRatio(segStart, segEnd, taskIntervals);
+ // 浣跨敤Set缁熻鍘婚噸鐨勪换鍔D鏁伴噺
+ Set<Long> uniqueTaskIds = new HashSet<>();
- // 鍒嗘憞閲岀▼
- BigDecimal taskDist = segDistance.multiply(BigDecimal.valueOf(taskRatio));
- BigDecimal nonTaskDist = segDistance.multiply(BigDecimal.valueOf(1 - taskRatio));
+ // 鐩存帴鎸塼ask_id鍒嗙粍鑱氬悎
+ for (VehicleGpsSegmentMileage segment : segments) {
+ BigDecimal segDistance = segment.getSegmentDistance() != null ? segment.getSegmentDistance() : BigDecimal.ZERO;
+
+ if (segment.getTaskId() != null) {
+ // 鏈変换鍔D锛岃鍏ヤ换鍔¢噷绋�
+ taskMileage = taskMileage.add(segDistance);
+ uniqueTaskIds.add(segment.getTaskId());
+ } else {
+ // 娌℃湁浠诲姟ID锛岃鍏ラ潪浠诲姟閲岀▼
+ nonTaskMileage = nonTaskMileage.add(segDistance);
+ }
+ }
- taskMileage = taskMileage.add(taskDist);
- nonTaskMileage = nonTaskMileage.add(nonTaskDist);
+ // 璁剧疆鍘婚噸鍚庣殑浠诲姟鏁伴噺
+ taskCount = uniqueTaskIds.size();
+
+ } else {
+ // 4.3 闄嶇骇鏂规锛氫娇鐢ㄥ師鏈夌殑鏃堕棿閲嶅彔璁$畻鏂瑰紡
+ logger.debug("杞﹁締ID: {} 鏃ユ湡: {} 浣跨敤闄嶇骇鏂规锛氭椂闂撮噸鍙犺绠楋紙鍙湁{}涓垎娈垫湁task_id锛屽崰姣攞}%锛�",
+ vehicleId, statDate, segmentsWithTask, (segmentsWithTask * 100.0 / segments.size()));
+
+ List<TaskTimeInterval> taskIntervals = vehicleMileageStatsMapper.selectTaskTimeIntervals(vehicleId, dayStart, dayEnd);
+
+ for (VehicleGpsSegmentMileage segment : segments) {
+ Date segStart = segment.getSegmentStartTime();
+ Date segEnd = segment.getSegmentEndTime();
+ BigDecimal segDistance = segment.getSegmentDistance() != null ? segment.getSegmentDistance() : BigDecimal.ZERO;
+
+ // 璁$畻璇ュ垎娈典笌浠诲姟鏃舵鐨勯噸鍙犳瘮渚�
+ double taskRatio = calculateTaskOverlapRatio(segStart, segEnd, taskIntervals);
+
+ // 鍒嗘憡閲岀▼
+ BigDecimal taskDist = segDistance.multiply(BigDecimal.valueOf(taskRatio));
+ BigDecimal nonTaskDist = segDistance.multiply(BigDecimal.valueOf(1 - taskRatio));
+
+ taskMileage = taskMileage.add(taskDist);
+ nonTaskMileage = nonTaskMileage.add(nonTaskDist);
+ }
+
+ // 璁剧疆浠诲姟鏁伴噺
+ taskCount = taskIntervals == null ? 0 : taskIntervals.size();
}
// 璁$畻浠诲姟閲岀▼鍗犳瘮
@@ -463,7 +508,7 @@
stats.setNonTaskMileage(nonTaskMileage.setScale(2, RoundingMode.HALF_UP));
stats.setTaskRatio(taskRatio);
stats.setGpsPointCount(totalGpsPoints);
- stats.setTaskCount(taskIntervals == null ? 0 : taskIntervals.size());
+ stats.setTaskCount(taskCount);
stats.setSegmentCount(segments.size());
stats.setDataSource("segment"); // 鏍囪鏁版嵁鏉ユ簮涓哄垎娈垫眹鎬�
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatLoginServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatLoginServiceImpl.java
index cfbed27..50457cc 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatLoginServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatLoginServiceImpl.java
@@ -11,6 +11,9 @@
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpUtils;
+import com.ruoyi.system.domain.SysConfig;
+import com.ruoyi.system.mapper.SysConfigMapper;
+import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.IWechatLoginService;
import com.ruoyi.system.service.ISysUserService;
@@ -30,6 +33,12 @@
@Autowired
private ISysUserService userService;
+ @Autowired
+ private ISysConfigService configService;
+
+ @Autowired
+ private SysConfigMapper configMapper;
+
/**
* 寰俊API - code2Session
*/
@@ -40,10 +49,28 @@
*/
private static final String GET_PHONE_NUMBER_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber";
+ private static final String GET_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
+
/**
* 寰俊API - 鑾峰彇access_token
*/
- private static final String GET_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
+ /**
+ * 鏍规嵁configKey鍐欏叆鎴栨洿鏂皊ys_config
+ */
+ private void upsertConfig(String key, String value) {
+ SysConfig exist = configMapper.checkConfigKeyUnique(key);
+ if (exist != null && exist.getConfigId() != null) {
+ exist.setConfigValue(value);
+ configMapper.updateConfig(exist);
+ } else {
+ SysConfig cfg = new SysConfig();
+ cfg.setConfigKey(key);
+ cfg.setConfigName(key);
+ cfg.setConfigValue(value);
+ cfg.setConfigType("Y");
+ configMapper.insertConfig(cfg);
+ }
+ }
/**
* 閫氳繃寰俊code鑾峰彇openid鍜宻ession_key
@@ -154,6 +181,9 @@
return result;
}
+ // 浼扮畻杩囨湡鏃堕棿(寰俊access_token榛樿7200绉掓湁鏁�)
+ java.util.Date accessTokenExpiresAt = new java.util.Date(System.currentTimeMillis() + 7200L * 1000L);
+
// 鏋勫缓璇锋眰URL
String url = GET_PHONE_NUMBER_URL + "?access_token=" + accessToken;
@@ -186,6 +216,9 @@
result.put("phoneNumber", phoneInfo.getString("phoneNumber"));
result.put("purePhoneNumber", phoneInfo.getString("purePhoneNumber"));
result.put("countryCode", phoneInfo.getString("countryCode"));
+ // 杩斿洖褰撳墠浣跨敤鐨刟ccess_token鍙婃湁鏁堟湡锛屼究浜庤皟鐢ㄦ柟瀛樺偍
+ result.put("accessToken", accessToken);
+ result.put("accessTokenExpiresAt", accessTokenExpiresAt);
return result;
}
@@ -260,7 +293,7 @@
return result;
}
- // 5. 鏇存柊鐢ㄦ埛鐨勫井淇′俊鎭�
+ // 5. 鏇存柊鐢ㄦ埛鐨勫井淇′俊鎭�(鍖呮嫭access_token涓庤繃鏈熸椂闂�)
SysUser updateUser = new SysUser();
updateUser.setUserId(user.getUserId());
updateUser.setOpenId(openId);
@@ -268,6 +301,17 @@
{
updateUser.setUnionId(unionId);
}
+ // 淇濆瓨鏈璋冪敤浣跨敤鐨刟ccess_token鍙婅繃鏈熸椂闂村埌搴旂敤绾ч厤缃紙渚夸簬鍚庣画澶嶇敤锛�
+ Object at = phoneResult.get("accessToken");
+ Object atExp = phoneResult.get("accessTokenExpiresAt");
+ if (at != null) {
+ String appId = wechatConfig.getAppId();
+ String tokenKey = "weixin.access_token." + appId;
+ String expireKey = "weixin.access_token_expires." + appId;
+ upsertConfig(tokenKey, at.toString());
+ long expireTs = (atExp instanceof java.util.Date) ? ((java.util.Date) atExp).getTime() : (System.currentTimeMillis() + 7200L * 1000L);
+ upsertConfig(expireKey, String.valueOf(expireTs));
+ }
userService.updateUser(updateUser);
log.info("鐢ㄦ埛{}寰俊淇℃伅鏇存柊鎴愬姛", user.getUserName());
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatTaskNotifyServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatTaskNotifyServiceImpl.java
index 48833d3..6b91981 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatTaskNotifyServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatTaskNotifyServiceImpl.java
@@ -5,12 +5,17 @@
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.WechatUtils;
-import com.ruoyi.system.domain.SysTask;
-import com.ruoyi.system.domain.SysTaskEmergency;
+import com.ruoyi.system.domain.SysConfig;
+import com.ruoyi.system.mapper.SysConfigMapper;
import com.ruoyi.system.mapper.SysTaskEmergencyMapper;
import com.ruoyi.system.mapper.SysTaskMapper;
+import com.ruoyi.system.domain.SysTask;
+import com.ruoyi.system.domain.SysTaskEmergency;
import com.ruoyi.system.mapper.SysUserMapper;
import com.ruoyi.system.service.IWechatTaskNotifyService;
+import com.ruoyi.system.service.ISysConfigService;
+import com.ruoyi.system.mapper.SysConfigMapper;
+import com.ruoyi.system.domain.SysConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -44,6 +49,73 @@
@Autowired
private WechatConfig wechatConfig;
+ @Autowired
+ private ISysConfigService configService;
+
+ @Autowired
+ private SysConfigMapper configMapper;
+
+
+ /**
+ * 鑾峰彇搴旂敤绾у井淇ccessToken锛堝甫缂撳瓨锛�
+ * 浼樺厛浠巗ys_config璇诲彇骞跺垽鏂湁鏁堟湡锛涜繃鏈熷垯閲嶆柊鑾峰彇骞跺啓鍥瀞ys_config
+ */
+ private String getAppAccessToken() {
+ try {
+ String appId = wechatConfig.getAppId();
+ String tokenKey = "weixin.access_token." + appId;
+ String expireKey = "weixin.access_token_expires." + appId;
+
+ String cachedToken = configService.selectConfigByKey(tokenKey);
+ String cachedExpireStr = configService.selectConfigByKey(expireKey);
+ long now = System.currentTimeMillis();
+ long expireTs = 0L;
+ if (StringUtils.isNotEmpty(cachedExpireStr)) {
+ try {
+ expireTs = Long.parseLong(cachedExpireStr);
+ } catch (NumberFormatException e) {
+ expireTs = 0L;
+ }
+ }
+
+ // 缂撳瓨鏈夋晥涓旀湭杩囨湡锛堥鐣�60绉掑畨鍏ㄨ竟鐣岋級
+ if (StringUtils.isNotEmpty(cachedToken) && expireTs > now + 60000L) {
+ return cachedToken;
+ }
+
+ // 閲嶆柊鑾峰彇锛屽苟鍐欏叆sys_config
+ String newToken = WechatUtils.getAccessToken(wechatConfig.getAppId(), wechatConfig.getAppSecret());
+ if (StringUtils.isEmpty(newToken)) {
+ return null;
+ }
+ long newExpireTs = now + 7200L * 1000L; // 7200绉�
+ upsertConfig(tokenKey, newToken);
+ upsertConfig(expireKey, String.valueOf(newExpireTs));
+ return newToken;
+ } catch (Exception e) {
+ log.error("鑾峰彇搴旂敤绾у井淇ccessToken澶辫触", e);
+ return null;
+ }
+ }
+
+ /**
+ * 鏍规嵁configKey鍐欏叆鎴栨洿鏂皊ys_config
+ */
+ private void upsertConfig(String key, String value) {
+ SysConfig exist = configMapper.checkConfigKeyUnique(key);
+ if (exist != null && exist.getConfigId() != null) {
+ exist.setConfigValue(value);
+ configMapper.updateConfig(exist);
+ } else {
+ SysConfig cfg = new SysConfig();
+ cfg.setConfigKey(key);
+ cfg.setConfigName(key);
+ cfg.setConfigValue(value);
+ cfg.setConfigType("Y"); // 鍐呯疆鍙傛暟
+ configMapper.insertConfig(cfg);
+ }
+ }
+
/**
* 鍙戦�佷换鍔¢�氱煡娑堟伅缁欐寚瀹氱敤鎴峰垪琛�
*
@@ -69,8 +141,8 @@
// 鏌ヨ鎬ユ晳淇℃伅
SysTaskEmergency emergency = sysTaskEmergencyMapper.selectSysTaskEmergencyByTaskId(taskId);
- // 鑾峰彇寰俊AccessToken
- String accessToken = WechatUtils.getAccessToken(wechatConfig.getAppId(), wechatConfig.getAppSecret());
+ // 鑾峰彇寰俊AccessToken锛堣蛋搴旂敤绾х紦瀛橈級
+ String accessToken = getAppAccessToken();
if (StringUtils.isEmpty(accessToken)) {
log.error("鑾峰彇寰俊AccessToken澶辫触锛屾棤娉曞彂閫佷换鍔¢�氱煡");
return 0;
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
index ffc09a9..a1f910a 100644
--- a/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml
@@ -206,7 +206,6 @@
<if test="oaOrderClass != null">oa_order_class = #{oaOrderClass},</if>
<if test="openId != null and openId != ''">open_id = #{openId},</if>
<if test="unionId != null and unionId != ''">union_id = #{unionId},</if>
- <if test="wechatNickname != null and wechatNickname != ''">wechat_nickname = #{wechatNickname},</if>
<if test="loginIp != null and loginIp != ''">login_ip = #{loginIp},</if>
<if test="loginDate != null">login_date = #{loginDate},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
diff --git a/ruoyi-ui/src/api/system/mileageStats.js b/ruoyi-ui/src/api/system/mileageStats.js
index 62f0cb3..0ceb07e 100644
--- a/ruoyi-ui/src/api/system/mileageStats.js
+++ b/ruoyi-ui/src/api/system/mileageStats.js
@@ -65,3 +65,16 @@
}
})
}
+
+// 鏌ヨ杞﹁締鎸囧畾鏃ユ湡鐨凣PS鍒嗘閲岀▼鏄庣粏
+export function getSegmentsByDateRange(vehicleId, startDate, endDate) {
+ return request({
+ url: '/system/gpsSegment/range',
+ method: 'get',
+ params: {
+ vehicleId: vehicleId,
+ startDate: startDate,
+ endDate: endDate
+ }
+ })
+}
diff --git a/ruoyi-ui/src/views/system/mileageStats/index.vue b/ruoyi-ui/src/views/system/mileageStats/index.vue
index 8a6738c..4468509 100644
--- a/ruoyi-ui/src/views/system/mileageStats/index.vue
+++ b/ruoyi-ui/src/views/system/mileageStats/index.vue
@@ -131,9 +131,7 @@
</el-tag>
</template>
</el-table-column>
- <el-table-column label="GPS鐐规暟" align="center" prop="gpsPointCount" width="90" />
- <el-table-column label="浠诲姟鏁�" align="center" prop="taskCount" width="80" />
- <el-table-column label="鍒嗘鏁�" align="center" prop="segmentCount" width="80" />
+
<el-table-column label="缁熻鏃堕棿" align="center" prop="createTime" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
@@ -216,7 +214,7 @@
</el-dialog>
<!-- 璇︽儏瀵硅瘽妗� -->
- <el-dialog title="閲岀▼缁熻璇︽儏" :visible.sync="detailOpen" width="600px" append-to-body>
+ <el-dialog title="閲岀▼缁熻璇︽儏" :visible.sync="detailOpen" width="900px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="杞︾墝鍙�">{{ detailData.vehicleNo }}</el-descriptions-item>
<el-descriptions-item label="褰掑睘鍒嗗叕鍙�">{{ detailData.deptName || '-' }}</el-descriptions-item>
@@ -237,18 +235,58 @@
{{ formatRatio(detailData.taskRatio) }}
</el-tag>
</el-descriptions-item>
- <el-descriptions-item label="GPS鐐规暟">{{ detailData.gpsPointCount }}</el-descriptions-item>
- <el-descriptions-item label="浠诲姟鏁�">{{ detailData.taskCount }}</el-descriptions-item>
- <el-descriptions-item label="鍒嗘鏁�">{{ detailData.segmentCount }}</el-descriptions-item>
- <el-descriptions-item label="鏁版嵁鏉ユ簮">
- <el-tag :type="detailData.dataSource === 'segment' ? 'success' : 'info'" size="small">
- {{ detailData.dataSource === 'segment' ? '浠庡垎娈垫眹鎬�' : '鐩存帴璁$畻' }}
- </el-tag>
- </el-descriptions-item>
<el-descriptions-item label="缁熻鏃堕棿" :span="2">
{{ parseTime(detailData.createTime) }}
</el-descriptions-item>
</el-descriptions>
+
+ <!-- 鍒嗘鏄庣粏琛ㄦ牸 -->
+ <div v-if="segmentList.length > 0" style="margin-top: 20px;">
+ <el-divider content-position="left">
+ <i class="el-icon-tickets"></i> 閲岀▼鍒嗘鏄庣粏
+ </el-divider>
+
+ <el-table
+ :data="segmentList"
+ size="small"
+ :max-height="400"
+ stripe
+ border
+ v-loading="segmentLoading"
+ >
+ <el-table-column type="index" label="搴忓彿" width="50" align="center" />
+ <el-table-column label="寮�濮嬫椂闂�" prop="segmentStartTime" width="160" align="center">
+ <template slot-scope="scope">
+ {{ parseTime(scope.row.segmentStartTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
+ </template>
+ </el-table-column>
+ <el-table-column label="缁撴潫鏃堕棿" prop="segmentEndTime" width="160" align="center">
+ <template slot-scope="scope">
+ {{ parseTime(scope.row.segmentEndTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
+ </template>
+ </el-table-column>
+ <el-table-column label="閲岀▼(km)" prop="segmentDistance" width="100" align="center">
+ <template slot-scope="scope">
+ <span class="mileage-value">{{ (scope.row.segmentDistance || 0).toFixed(2) }}</span>
+ </template>
+ </el-table-column>
+
+ <el-table-column label="鍏宠仈浠诲姟" prop="taskCode" align="center" min-width="120">
+ <template slot-scope="scope">
+ <el-tag v-if="scope.row.taskCode" size="small" type="success">{{ scope.row.taskCode }}</el-tag>
+ <span v-else style="color: #909399;">鏃犱换鍔�</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="璁$畻鏂瑰紡" prop="calculateMethod" align="center" width="100">
+ <template slot-scope="scope">
+ <el-tag size="small" :type="scope.row.calculateMethod === 'haversine' ? 'primary' : 'info'">
+ {{ scope.row.calculateMethod === 'haversine' ? '鐞冮潰璺濈' : '鐩寸嚎璺濈' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
<div slot="footer" class="dialog-footer">
<el-button @click="detailOpen = false">鍏� 闂�</el-button>
</div>
@@ -257,7 +295,7 @@
</template>
<script>
-import { listMileageStats, getMileageStats, delMileageStats, calculateMileageStats, batchCalculateMileageStats } from "@/api/system/mileageStats";
+import { listMileageStats, getMileageStats, delMileageStats, calculateMileageStats, batchCalculateMileageStats, getSegmentsByDateRange } from "@/api/system/mileageStats";
import { listDept } from "@/api/system/dept";
export default {
@@ -292,6 +330,10 @@
detailOpen: false,
// 璇︽儏鏁版嵁
detailData: {},
+ // 鍒嗘鏄庣粏鍒楄〃
+ segmentList: [],
+ // 鍒嗘鏄庣粏鍔犺浇鐘舵��
+ segmentLoading: false,
// 鎵嬪姩缁熻鍔犺浇鐘舵��
calculateLoading: false,
// 鎵归噺缁熻鍔犺浇鐘舵��
@@ -417,9 +459,44 @@
/** 鏌ョ湅璇︽儏鎸夐挳鎿嶄綔 */
handleView(row) {
const statsId = row.statsId;
+
+ // 閲嶇疆鍒嗘鏄庣粏
+ this.segmentList = [];
+
+ // 鍔犺浇缁熻璇︽儏
getMileageStats(statsId).then(response => {
this.detailData = response.data;
this.detailOpen = true;
+
+ // 濡傛灉鏈夎溅杈咺D鍜岀粺璁℃棩鏈燂紝鍔犺浇鍒嗘鏄庣粏
+ if (this.detailData.vehicleId && this.detailData.statDate) {
+ this.loadSegmentDetails(this.detailData.vehicleId, this.detailData.statDate);
+ }
+ });
+ },
+
+ /** 鍔犺浇鍒嗘鏄庣粏 */
+ loadSegmentDetails(vehicleId, statDate) {
+ this.segmentLoading = true;
+
+ // 鏍煎紡鍖栨棩鏈燂細缁熻鏃ユ湡鐨勫紑濮嬪拰缁撴潫鏃堕棿
+ const startDate = this.parseTime(statDate, '{y}-{m}-{d}');
+ const endDate = this.parseTime(statDate, '{y}-{m}-{d}');
+
+ // 鏌ヨ璇ユ棩鏈熺殑鍒嗘閲岀▼鏄庣粏
+ getSegmentsByDateRange(vehicleId, startDate, endDate).then(response => {
+ if (response.code === 200 && response.data) {
+ this.segmentList = response.data;
+ console.log('鍒嗘鏄庣粏鏁版嵁锛�', this.segmentList);
+ } else {
+ this.segmentList = [];
+ }
+ }).catch(error => {
+ console.error('鍔犺浇鍒嗘鏄庣粏澶辫触', error);
+ this.segmentList = [];
+ this.$modal.msgWarning('鍔犺浇鍒嗘鏄庣粏澶辫触锛屼絾涓嶅奖鍝嶇粺璁℃暟鎹煡鐪�');
+ }).finally(() => {
+ this.segmentLoading = false;
});
},
/** 鍒犻櫎鎸夐挳鎿嶄綔 */
diff --git a/sql/add_wechat_token_fields.sql b/sql/add_wechat_token_fields.sql
new file mode 100644
index 0000000..5c3c7dc
--- /dev/null
+++ b/sql/add_wechat_token_fields.sql
@@ -0,0 +1,9 @@
+-- 寰俊鐧诲綍token瀛樺偍瀛楁
+-- 鍦╯ys_user琛ㄦ柊澧炲瓨鍌ㄥ井淇ccess_token涓庤繃鏈熸椂闂寸殑瀛楁
+
+ALTER TABLE `sys_user`
+ ADD COLUMN `wechat_access_token` VARCHAR(1024) NULL COMMENT '寰俊AccessToken锛堢綉椤垫巿鏉冩垨鏈�杩戣幏鍙栵級' AFTER `wechat_nickname`,
+ ADD COLUMN `wechat_token_expires_at` DATETIME NULL COMMENT '寰俊AccessToken杩囨湡鏃堕棿' AFTER `wechat_access_token`;
+
+-- 绱㈠紩鍙�夛紙濡傞渶鎸塼oken鏌ヨ鎴栨竻鐞嗭級
+-- CREATE INDEX `idx_wechat_token_expire` ON `sys_user` (`wechat_token_expires_at`);
diff --git a/sql/fix_segment_mileage_task_association.sql b/sql/fix_segment_mileage_task_association.sql
new file mode 100644
index 0000000..d3b5a21
--- /dev/null
+++ b/sql/fix_segment_mileage_task_association.sql
@@ -0,0 +1,134 @@
+-- ========================================
+-- 淇GPS鍒嗘閲岀▼鐨勪换鍔″叧鑱�
+-- 鐢ㄩ�旓細涓哄凡鏈夌殑鍒嗘閲岀▼鏁版嵁琛ュ厖 task_id 鍜� task_code
+-- 浼樺寲璇存槑锛氳ˉ鍏卼ask_id鍚庯紝缁熻璁$畻灏嗕娇鐢ㄤ紭鍖栨柟妗堬紙鐩存帴SQL鑱氬悎锛屾洿蹇級
+-- ========================================
+
+-- 1. 鏌ョ湅褰撳墠鏈叧鑱斾换鍔$殑鍒嗘鏁伴噺
+SELECT COUNT(*) as '鏈叧鑱斾换鍔$殑鍒嗘鏁�'
+FROM tb_vehicle_gps_segment_mileage
+WHERE task_id IS NULL;
+
+-- 2. 鏌ョ湅鏈夊灏戜换鍔″彲浠ヨ鍏宠仈
+SELECT COUNT(DISTINCT t.task_id) as '鍙叧鑱旂殑浠诲姟鏁�'
+FROM sys_task t
+INNER JOIN sys_task_vehicle tv ON t.task_id = tv.task_id
+WHERE t.del_flag = '0'
+ AND t.task_status NOT IN ('PENDING', 'CANCELLED');
+
+-- 3. 鏇存柊閫昏緫锛氭牴鎹溅杈咺D鍜屾椂闂撮噸鍙犲叧鑱斾换鍔�
+-- 娉ㄦ剰锛氳繖涓煡璇細姣旇緝鎱紝寤鸿鍒嗘壒鎵ц鎴栧湪闈為珮宄版湡鎵ц
+
+UPDATE tb_vehicle_gps_segment_mileage seg
+INNER JOIN (
+ SELECT
+ seg2.segment_id,
+ t.task_id,
+ t.task_code
+ FROM tb_vehicle_gps_segment_mileage seg2
+ INNER JOIN sys_task_vehicle tv ON seg2.vehicle_id = tv.vehicle_id
+ INNER JOIN sys_task t ON tv.task_id = t.task_id
+ WHERE seg2.task_id IS NULL
+ AND t.del_flag = '0'
+ -- AND t.task_status NOT IN ('PENDING', 'CANCELLED')
+ -- 鏃堕棿閲嶅彔鏉′欢锛氬垎娈靛紑濮嬫椂闂� < 浠诲姟缁撴潫鏃堕棿 AND 鍒嗘缁撴潫鏃堕棿 > 浠诲姟寮�濮嬫椂闂�
+ AND seg2.segment_start_time < COALESCE(t.actual_end_time, t.planned_end_time, DATE_ADD(t.create_time, INTERVAL 24 HOUR))
+ AND seg2.segment_end_time > COALESCE(t.actual_start_time, t.planned_start_time, t.create_time)
+ GROUP BY seg2.segment_id, t.task_id, t.task_code
+) task_match ON seg.segment_id = task_match.segment_id
+SET
+ seg.task_id = task_match.task_id,
+ seg.task_code = task_match.task_code;
+
+-- 4. 鏌ョ湅淇缁撴灉
+SELECT
+ '宸插叧鑱斾换鍔�' as '绫诲瀷',
+ COUNT(*) as '鏁伴噺',
+ CONCAT(ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM tb_vehicle_gps_segment_mileage), 2), '%') as '鍗犳瘮'
+FROM tb_vehicle_gps_segment_mileage
+WHERE task_id IS NOT NULL
+UNION ALL
+SELECT
+ '鏈叧鑱斾换鍔�' as '绫诲瀷',
+ COUNT(*) as '鏁伴噺',
+ CONCAT(ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM tb_vehicle_gps_segment_mileage), 2), '%') as '鍗犳瘮'
+FROM tb_vehicle_gps_segment_mileage
+WHERE task_id IS NULL;
+
+-- 5. 鏌ョ湅姣忎釜杞﹁締鐨勫叧鑱旀儏鍐�
+SELECT
+ seg.vehicle_id,
+ seg.vehicle_no,
+ COUNT(*) as '鎬诲垎娈垫暟',
+ SUM(CASE WHEN seg.task_id IS NOT NULL THEN 1 ELSE 0 END) as '宸插叧鑱斿垎娈垫暟',
+ CONCAT(ROUND(SUM(CASE WHEN seg.task_id IS NOT NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2), '%') as '鍏宠仈鐜�'
+FROM tb_vehicle_gps_segment_mileage seg
+GROUP BY seg.vehicle_id, seg.vehicle_no
+ORDER BY COUNT(*) DESC
+LIMIT 20;
+
+-- 6. 鍒嗘瀽鏈兘鍏宠仈鐨勫師鍥�
+-- 鏌ョ湅鏌愪釜鏈叧鑱斿垎娈电殑璇︾粏淇℃伅
+SELECT
+ seg.segment_id,
+ seg.vehicle_id,
+ seg.vehicle_no,
+ seg.segment_start_time,
+ seg.segment_end_time,
+ '璇ユ椂娈佃杞﹁締鐨勪换鍔�' as '璇存槑',
+ t.task_id,
+ t.task_code,
+ t.task_status,
+ t.create_time,
+ COALESCE(t.actual_start_time, t.planned_start_time) as '浠诲姟寮�濮嬫椂闂�',
+ COALESCE(t.actual_end_time, t.planned_end_time) as '浠诲姟缁撴潫鏃堕棿'
+FROM tb_vehicle_gps_segment_mileage seg
+LEFT JOIN sys_task_vehicle tv ON seg.vehicle_id = tv.vehicle_id
+LEFT JOIN sys_task t ON tv.task_id = t.task_id
+ AND t.del_flag = '0'
+ AND seg.segment_start_time < COALESCE(t.actual_end_time, t.planned_end_time, DATE_ADD(t.create_time, INTERVAL 24 HOUR))
+ AND seg.segment_end_time > COALESCE(t.actual_start_time, t.planned_start_time, t.create_time)
+WHERE seg.task_id IS NULL
+ORDER BY seg.segment_start_time DESC
+LIMIT 10;
+
+-- ========================================
+-- 浣跨敤璇存槑锛�
+-- 1. 鍏堟墽琛屾煡璇㈣鍙ワ紙1銆�2锛夋煡鐪嬫暟鎹儏鍐�
+-- 2. 鍦ㄩ潪楂樺嘲鏈熸墽琛屾洿鏂拌鍙ワ紙3锛�
+-- 3. 鎵ц缁撴灉鏌ヨ锛�4銆�5銆�6锛夐獙璇佷慨澶嶆晥鏋�
+-- 4. 濡傛灉鏁版嵁閲忓ぇ锛屽缓璁坊鍔� LIMIT 鍒嗘壒鎵ц
+-- ========================================
+
+-- ========================================
+-- 棰勯槻鎺柦锛氱‘淇濆畾鏃朵换鍔℃甯歌繍琛�
+-- ========================================
+
+-- 7. 妫�鏌PS鍒嗘閲岀▼璁$畻瀹氭椂浠诲姟鐘舵��
+SELECT
+ job_id,
+ job_name,
+ job_group,
+ invoke_target,
+ cron_expression,
+ status as '鐘舵��(0=姝e父,1=鏆傚仠)',
+ create_time,
+ update_time
+FROM sys_job
+WHERE job_name LIKE '%GPS%' OR job_name LIKE '%閲岀▼%'
+ORDER BY create_time DESC;
+
+-- 8. 濡傛灉瀹氭椂浠诲姟鏄殏鍋滅姸鎬侊紝鍚姩瀹�
+-- UPDATE sys_job SET status = '0' WHERE job_name = 'GPS鍒嗘閲岀▼瀹炴椂璁$畻';
+
+-- 9. 妫�鏌ユ渶杩戠殑GPS璁$畻璁板綍
+SELECT
+ vehicle_id,
+ vehicle_no,
+ MAX(segment_end_time) as '鏈�鍚庤绠楁椂闂�',
+ COUNT(*) as '鍒嗘鏁�',
+ SUM(CASE WHEN task_id IS NOT NULL THEN 1 ELSE 0 END) as '鏈変换鍔″叧鑱旂殑鍒嗘鏁�'
+FROM tb_vehicle_gps_segment_mileage
+WHERE segment_end_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
+GROUP BY vehicle_id, vehicle_no
+ORDER BY MAX(segment_end_time) DESC;
diff --git a/sql/remove_wechat_token_fields.sql b/sql/remove_wechat_token_fields.sql
new file mode 100644
index 0000000..83349d2
--- /dev/null
+++ b/sql/remove_wechat_token_fields.sql
@@ -0,0 +1,8 @@
+-- 绉婚櫎鐢ㄦ埛琛ㄤ腑鐨勫井淇oken涓庢湁鏁堟湡瀛楁锛堟敼涓哄簲鐢ㄧ骇缁熶竴瀛樺偍锛�
+-- 鍥炴粴鎴栨竻鐞嗚剼鏈細鍒犻櫎 sys_user.wechat_access_token / sys_user.wechat_token_expires_at
+
+ALTER TABLE `sys_user`
+ DROP COLUMN `wechat_access_token`;
+
+ALTER TABLE `sys_user`
+ DROP COLUMN `wechat_token_expires_at`;
diff --git a/sql/test_mileage_stats_optimization.sql b/sql/test_mileage_stats_optimization.sql
new file mode 100644
index 0000000..d169520
--- /dev/null
+++ b/sql/test_mileage_stats_optimization.sql
@@ -0,0 +1,110 @@
+-- ========================================
+-- 娴嬭瘯閲岀▼缁熻浼樺寲鏁堟灉
+-- 鐢ㄩ�旓細瀵规瘮浼樺寲鍓嶅悗鐨勬�ц兘鍜屽噯纭��
+-- ========================================
+
+-- 1. 鏌ョ湅鏌愪釜杞﹁締鏌愬ぉ鐨勫垎娈垫暟鎹畉ask_id瑕嗙洊鐜�
+-- 鏇挎崲鍙傛暟锛欯vehicleId, @statDate
+SET @vehicleId = 1; -- 鏇挎崲涓哄疄闄呰溅杈咺D
+SET @statDate = '2025-12-04'; -- 鏇挎崲涓哄疄闄呮棩鏈�
+
+SELECT
+ '鍒嗘缁熻' as '绫诲瀷',
+ COUNT(*) as '鎬诲垎娈垫暟',
+ SUM(CASE WHEN task_id IS NOT NULL THEN 1 ELSE 0 END) as '鏈変换鍔D鐨勫垎娈垫暟',
+ CONCAT(ROUND(SUM(CASE WHEN task_id IS NOT NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2), '%') as 'task_id瑕嗙洊鐜�'
+FROM tb_vehicle_gps_segment_mileage
+WHERE vehicle_id = @vehicleId
+ AND DATE(segment_start_time) = @statDate;
+
+-- 2. 瀵规瘮涓ょ璁$畻鏂瑰紡鐨勭粨鏋�
+-- 鏂瑰紡A锛氫紭鍖栨柟妗堬紙鐩存帴鎸塼ask_id鑱氬悎锛�
+SELECT
+ '浼樺寲鏂规' as '璁$畻鏂瑰紡',
+ SUM(segment_distance) as '鎬婚噷绋�',
+ SUM(CASE WHEN task_id IS NOT NULL THEN segment_distance ELSE 0 END) as '浠诲姟閲岀▼',
+ SUM(CASE WHEN task_id IS NULL THEN segment_distance ELSE 0 END) as '闈炰换鍔¢噷绋�',
+ CONCAT(ROUND(SUM(CASE WHEN task_id IS NOT NULL THEN segment_distance ELSE 0 END) * 100.0 /
+ NULLIF(SUM(segment_distance), 0), 2), '%') as '浠诲姟鍗犳瘮'
+FROM tb_vehicle_gps_segment_mileage
+WHERE vehicle_id = @vehicleId
+ AND DATE(segment_start_time) = @statDate;
+
+-- 鏂瑰紡B锛氬綋鍓嶇粺璁¤〃涓殑鏁版嵁锛堝彲鑳戒娇鐢ㄤ簡鏃堕棿閲嶅彔璁$畻锛�
+SELECT
+ '缁熻琛ㄦ暟鎹�' as '璁$畻鏂瑰紡',
+ total_mileage as '鎬婚噷绋�',
+ task_mileage as '浠诲姟閲岀▼',
+ non_task_mileage as '闈炰换鍔¢噷绋�',
+ CONCAT(ROUND(task_ratio * 100, 2), '%') as '浠诲姟鍗犳瘮'
+FROM tb_vehicle_mileage_stats
+WHERE vehicle_id = @vehicleId
+ AND DATE(stat_date) = @statDate;
+
+-- 3. 鏌ョ湅浣跨敤浼樺寲鏂规鐨勮溅杈嗘暟閲忥紙task_id瑕嗙洊鐜� > 80%锛�
+SELECT
+ DATE(segment_start_time) as '鏃ユ湡',
+ vehicle_id,
+ vehicle_no,
+ COUNT(*) as '鎬诲垎娈垫暟',
+ SUM(CASE WHEN task_id IS NOT NULL THEN 1 ELSE 0 END) as '鏈塼ask_id鍒嗘鏁�',
+ CONCAT(ROUND(SUM(CASE WHEN task_id IS NOT NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2), '%') as '瑕嗙洊鐜�',
+ CASE
+ WHEN SUM(CASE WHEN task_id IS NOT NULL THEN 1 ELSE 0 END) > COUNT(*) * 0.8
+ THEN '鉁� 浣跨敤浼樺寲鏂规'
+ ELSE '脳 浣跨敤闄嶇骇鏂规'
+ END as '鏂规閫夋嫨'
+FROM tb_vehicle_gps_segment_mileage
+WHERE segment_start_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
+GROUP BY DATE(segment_start_time), vehicle_id, vehicle_no
+HAVING COUNT(*) > 10 -- 鍙粺璁″垎娈垫暟澶т簬10鐨�
+ORDER BY DATE(segment_start_time) DESC, vehicle_id
+LIMIT 20;
+
+-- 4. 缁熻鏁翠綋浼樺寲鏁堟灉
+SELECT
+ '鏁翠綋缁熻' as '缁熻绫诲瀷',
+ COUNT(DISTINCT CONCAT(vehicle_id, '_', DATE(segment_start_time))) as '杞﹁締-鏃ユ湡缁勫悎鎬绘暟',
+ SUM(CASE WHEN task_coverage > 0.8 THEN 1 ELSE 0 END) as '鍙娇鐢ㄤ紭鍖栨柟妗堢殑鏁伴噺',
+ CONCAT(ROUND(SUM(CASE WHEN task_coverage > 0.8 THEN 1 ELSE 0 END) * 100.0 /
+ COUNT(DISTINCT CONCAT(vehicle_id, '_', DATE(segment_start_time))), 2), '%') as '浼樺寲瑕嗙洊鐜�'
+FROM (
+ SELECT
+ vehicle_id,
+ DATE(segment_start_time) as stat_date,
+ SUM(CASE WHEN task_id IS NOT NULL THEN 1 ELSE 0 END) * 1.0 / COUNT(*) as task_coverage
+ FROM tb_vehicle_gps_segment_mileage
+ WHERE segment_start_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
+ GROUP BY vehicle_id, DATE(segment_start_time)
+ HAVING COUNT(*) > 10
+) coverage_stats;
+
+-- 5. 鎬ц兘娴嬭瘯锛氭墽琛屾椂闂村姣�
+-- 璇存槑锛氫娇鐢‥XPLAIN ANALYZE鏌ョ湅鎵ц璁″垝锛圡ySQL 8.0+锛夋垨浣跨敤BENCHMARK鍑芥暟
+
+-- 浼樺寲鏂规鏌ヨ锛堢洿鎺ヨ仛鍚堬級
+EXPLAIN
+SELECT
+ vehicle_id,
+ SUM(CASE WHEN task_id IS NOT NULL THEN segment_distance ELSE 0 END) as task_mileage,
+ SUM(CASE WHEN task_id IS NULL THEN segment_distance ELSE 0 END) as non_task_mileage
+FROM tb_vehicle_gps_segment_mileage
+WHERE vehicle_id = @vehicleId
+ AND DATE(segment_start_time) = @statDate
+GROUP BY vehicle_id;
+
+-- 6. 寤鸿鐨勪紭鍖栨帾鏂�
+SELECT
+ '浼樺寲寤鸿' as '绫诲瀷',
+ '琛ュ厖鍘嗗彶鏁版嵁鐨則ask_id' as '鎺柦1',
+ '纭繚瀹氭椂浠诲姟姝e父杩愯' as '鎺柦2',
+ '纭繚浠诲姟鐘舵�佹纭洿鏂�' as '鎺柦3',
+ '瀹氭湡妫�鏌ask_id瑕嗙洊鐜�' as '鎺柦4';
+
+-- ========================================
+-- 浣跨敤璇存槑锛�
+-- 1. 璁剧疆鍙傛暟锛氫慨鏀圭7-8琛岀殑@vehicleId鍜孈statDate
+-- 2. 鎵ц鏌ヨ1-4鏌ョ湅浼樺寲鏁堟灉
+-- 3. 濡傛灉瑕嗙洊鐜囦綆浜�80%锛屾墽琛宖ix_segment_mileage_task_association.sql淇
+-- 4. 閲嶆柊缁熻锛岃瀵熸�ц兘鎻愬崌
+-- ========================================
--
Gitblit v1.9.1