656d6f8029f8bf9b2daa9dcc89101a879a70b860..af8cab142a6b15c06e131a8474574dd5b00df982
2025-12-04 wlzboy
feat: 改造微信accesstoken存放在系统配置表中
af8cab 对比 | 目录
2025-12-04 wlzboy
feat:增加微信token缓存
4f2925 对比 | 目录
10个文件已添加
20个文件已修改
1714 ■■■■■ 已修改文件
app/api/system/user.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/components/StaffSelector.vue 130 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/create-emergency.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/detail.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/pagesTask/edit-emergency.vue 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dryad-payment/src/main/java/com/ruoyi/payment/infrastructure/channel/alipay/AlipayThirdPartyClient.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
prd/StaffSelector组件分公司用户加载说明.md 485 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskAttachmentController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/wechat/WechatController.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/BranchUserQueryVO.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/IWechatAccessTokenService.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PaymentSyncServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/VehicleMileageStatsServiceImpl.java 73 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatAccessTokenServiceImpl.java 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatLoginServiceImpl.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatTaskNotifyServiceImpl.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysUserMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/api/system/mileageStats.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/system/mileageStats/index.vue 103 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/add_wechat_token_fields.sql 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/fix_segment_mileage_task_association.sql 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/remove_wechat_token_fields.sql 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/test_mileage_stats_optimization.sql 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/wechat_subscribe_message_config.sql 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/wechat_subscribe_message_config_README.md 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/api/system/user.js
@@ -56,3 +56,15 @@
    method: 'get'
  })
}
// æ ¹æ®åˆ†å…¬å¸ID列表查询用户(小程序端专用)
// æ ¹æ®åˆ†å…¬å¸ID数组查询用户(POST方式)
export function listUsersByBranchDepts(branchDeptIds) {
  return request({
    url: '/system/user/branch/users',
    method: 'post',
    data: {
      branchDeptIds
    }
  })
}
app/pagesTask/components/StaffSelector.vue
@@ -5,7 +5,7 @@
      <view class="staff-list">
        <view class="staff-item" v-for="(staff, index) in selectedStaff" :key="staff.userId">
          <view class="staff-info">
            <text class="staff-name">{{ staff.nickName }}</text>
            <text class="staff-name">{{ getStaffDisplayName(staff) }}</text>
          </view>
          <uni-icons 
            v-if="canRemove(index)"
@@ -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
    },
    // åˆ†å…¬å¸ID列表(外部传入,用于指定加载哪些分公司的用户)
    branchDeptIds: {
      type: Array,
      default: null
    },
    // å•个分公司ID(仅传一个时更便捷)
    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
    },
    // ç›‘听分公司ID数组变化,重新加载用户列表
    branchDeptIds: {
      handler(newVal, oldVal) {
        if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
          console.log('分公司ID变化,重新加载用户:', newVal)
          this.loadStaffList()
        }
      },
      deep: true
    },
    // ç›‘听单个分公司ID变化
    branchDeptId(newVal, oldVal) {
      if (newVal !== oldVal) {
        console.log('分公司ID变化,重新加载用户:', newVal)
        this.loadStaffList()
      }
    }
  },
  mounted() {
@@ -207,29 +236,73 @@
    
    // åŠ è½½äººå‘˜åˆ—è¡¨
    loadStaffList() {
      listBranchUsers().then(response => {
      // èŽ·å–æ‰€æœ‰éƒ¨é—¨ID
      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('根据分公司ID加载用户:', deptIds)
        this.loadStaffByBranchDepts(deptIds)
      } else {
        console.log('未传入分公司ID,组件不加载人员列表')
        this.$modal && this.$modal.showToast && this.$modal.showToast('请传入分公司ID')
      }
    },
    // æ ¹æ®åˆ†å…¬å¸ID数组加载用户(支持缓存)
    loadStaffByBranchDepts(deptIds) {
      // ç”Ÿæˆç¼“å­˜key(按排序后id拼接)
      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()
    },
    
    // æ ¹æ®ç”¨æˆ·çš„岗位或角色判断所有类型(支持多种身份)
@@ -381,6 +454,21 @@
    emitChange() {
      this.$emit('input', this.selectedStaff)
      this.$emit('change', this.selectedStaff)
    },
    // èŽ·å–äººå‘˜æ˜¾ç¤ºåç§°ï¼ˆä¼˜å…ˆæ˜¾ç¤ºå§“åï¼Œå¦‚æžœå§“åä¸ºç©ºåˆ™æ˜¾ç¤ºæ‰‹æœºå·ï¼‰
    getStaffDisplayName(staff) {
      if (!staff) {
        return '未知人员'
      }
      // ä¼˜å…ˆæ˜¾ç¤º nickName,如果为空则显示手机号,都为空则显示 userId
      if (staff.nickName && staff.nickName.trim()) {
        return staff.nickName
      }
      if (staff.phonenumber && staff.phonenumber.trim()) {
        return staff.phonenumber
      }
      return `用户${staff.userId || ''}`
    }
  }
}
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(部门ID)
      selectedOrganizationId: null, // å½“前选中的归属机构ID
      allOrganizationIds: [], // æ‰€æœ‰å¯é€‰æœºæž„ID数组
      selectedOrganizationServiceOrderClass: '', // å½’属机构的服务单编码
      selectedRegion: '', // ä»Žå½’属机构中提取的地域信息(如:广州、深圳等)
      departureAddress: '', // å‡ºå‘地地址
@@ -360,6 +363,8 @@
    this.loadEmergencyTaskTypes()
    // åŠ è½½å•æ®ç±»åž‹æ•°æ®
    this.loadDocumentTypes()
    // åŠ è½½æ‰€æœ‰æœºæž„ID
    this.loadAllOrganizationIds()
  },
  methods: {
    // èŽ·å–ç”¨æˆ·ç»‘å®šçš„è½¦è¾†ä¿¡æ¯
@@ -446,6 +451,23 @@
        return region.replace(/(分公司|总公司|总部)$/g, '').trim();
    },
    
    // åŠ è½½æ‰€æœ‰æœºæž„ID
    loadAllOrganizationIds() {
      // é€šè¿‡ OrganizationSelector ç»„件获取所有机构
      const orgSelector = this.$refs.organizationSelector
      if (orgSelector) {
        orgSelector.reload().then(organizations => {
          this.allOrganizationIds = organizations.map(org => org.deptId)
          console.log('所有机构ID:', this.allOrganizationIds)
        })
      } else {
        // å¦‚果组件还未挂载,稍后重试
        setTimeout(() => {
          this.loadAllOrganizationIds()
        }, 100)
      }
    },
    // åŠ è½½ç§‘å®¤æ•°æ®ï¼ˆä»Ž SQL Server åŠ¨æ€åŠ è½½ï¼‰
    loadDepartments() {
      getHospitalDepartments().then(response => {
app/pagesTask/detail.vue
@@ -804,6 +804,14 @@
      
      // æ£€æŸ¥è½¦è¾†çŠ¶æ€å¹¶å‡ºå‘
      checkVehicleAndDepart() {
        // æ£€æŸ¥å‡ºå‘时间是否为空或1900年(修复:防止无效时间)
        if (!this.taskDetail.plannedStartTime || this.taskDetail.plannedStartTime.startsWith('1900')) {
          this.$modal.confirm('任务的转运时间未设置或无效,需要先修改任务补充转运时间后才能出发。是否现在去修改?').then(() => {
            this.handleEdit()
          }).catch(() => {})
          return
        }
        // èŽ·å–ä»»åŠ¡è½¦è¾†ID
        const vehicleId = this.getVehicleId();
        if (!vehicleId) {
app/pagesTask/edit-emergency.vue
@@ -19,7 +19,8 @@
      />
      
      <view class="form-item">
        <OrganizationSelector
        <OrganizationSelector
          ref="organizationSelector"
          v-model="selectedOrganizationId"
          :required="true"
          :auto-select-user-dept="false"
@@ -53,6 +54,7 @@
        :required="false"
        :auto-add-current-user="false"
        :current-user-removable="true"
        :branch-dept-ids="allOrganizationIds"
        @change="onStaffChange"
      />
      
@@ -224,6 +226,7 @@
      taskDetail: null,
      selectedVehicleId: null,
      selectedOrganizationId: null,
      allOrganizationIds: [], // æ‰€æœ‰å¯é€‰æœºæž„ID数组
      selectedRegion: '',
      mapSelectorType: '',
      // æ‰©å±• addressCoordinates æ”¯æŒå¤šç§é”®å
@@ -292,6 +295,11 @@
      }, 1500)
    }
  },
  mounted() {
    // é¡µé¢æŒ‚载后加载所有机构ID
    this.loadAllOrganizationIds()
  },
  methods: {
    // åŠ è½½ä»»åŠ¡è¯¦æƒ…
    loadTaskDetail() {
@@ -320,8 +328,9 @@
          const info = this.taskDetail.emergencyInfo
          console.log('转运任务信息:', info)
          
          // è½¬è¿æ—¶é—´
          this.taskForm.transferTime = this.taskDetail.plannedStartTime || ''
          // è½¬è¿æ—¶é—´ï¼ˆä¿®å¤ï¼š1900年的日期显示为空)
          const transferTime = this.taskDetail.plannedStartTime || ''
          this.taskForm.transferTime = transferTime && transferTime.startsWith('1900') ? '' : transferTime
          
          // æ‚£è€…信息
          this.taskForm.patient.contact = info.patientContact || ''
@@ -338,8 +347,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 +365,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) {
@@ -372,7 +385,8 @@
        } else {
          console.warn('任务详情中没有emergencyInfo字段,尝试从主对象获取数据')
          // å…¼å®¹å¤„理:如果emergencyInfo不存在,尝试从主对象获取
          this.taskForm.transferTime = this.taskDetail.plannedStartTime || ''
          const transferTime = this.taskDetail.plannedStartTime || ''
          this.taskForm.transferTime = transferTime && transferTime.startsWith('1900') ? '' : transferTime
          this.taskForm.transferDistance = this.taskDetail.estimatedDistance ? String(this.taskDetail.estimatedDistance) : ''
        }
        
@@ -417,16 +431,24 @@
          console.log('设置目标地坐标:', this.taskDetail.destinationLongitude, this.taskDetail.destinationLatitude)
        }
        
        // è®¾ç½®æ‰§è¡Œäººå‘˜ï¼ˆä¿®å¤ï¼šç¡®ä¿ assignees ä¸ä¸º null)
        // è®¾ç½®æ‰§è¡Œäººå‘˜ï¼ˆä¿®å¤ï¼šç¡®ä¿ assignees ä¸ä¸º null,并正确映射字段)
        if (this.taskDetail.assignees && Array.isArray(this.taskDetail.assignees) && this.taskDetail.assignees.length > 0) {
          console.log('原始执行人员数据:', this.taskDetail.assignees)
          this.selectedStaff = this.taskDetail.assignees.map(assignee => ({
            userId: assignee.userId,
            nickName: assignee.userName,
            type: assignee.userType || 'driver',
            phonenumber: '',
            deptName: ''
          }))
          this.selectedStaff = this.taskDetail.assignees.map(assignee => {
            console.log('处理执行人员:', assignee)
            console.log('  - userName:', assignee.userName)
            console.log('  - nickName:', assignee.nickName)
            console.log('  - phonenumber:', assignee.phonenumber)
            console.log('  - phone:', assignee.phone)
            return {
              userId: assignee.userId,
              nickName: assignee.userName || assignee.nickName || '',
              type: assignee.userType || 'driver',
              phonenumber: assignee.phonenumber || assignee.phone || '',
              deptName: assignee.deptName || ''
            }
          })
          console.log('处理后的执行人员列表:', this.selectedStaff)
        } else {
          console.warn('任务没有分配执行人员或assignees为空')
@@ -449,6 +471,23 @@
    // è½¦è¾†é€‰æ‹©å˜åŒ–
    onVehicleChange(vehicle) {
      console.log('选中车辆:', vehicle)
    },
    // åŠ è½½æ‰€æœ‰æœºæž„ID
    loadAllOrganizationIds() {
      // é€šè¿‡ OrganizationSelector ç»„件获取所有机构
      const orgSelector = this.$refs.organizationSelector
      if (orgSelector) {
        orgSelector.reload().then(organizations => {
          this.allOrganizationIds = organizations.map(org => org.deptId)
          console.log('所有机构ID:', this.allOrganizationIds)
        })
      } else {
        // å¦‚果组件还未挂载,稍后重试
        setTimeout(() => {
          this.loadAllOrganizationIds()
        }, 100)
      }
    },
    
    // å½’属机构选择变化
@@ -854,6 +893,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)
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;
    /**
     * ç¬¬ä¸‰æ–¹æ”¯ä»˜å®å½“面付接口地址
     */
    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)) {
prd/StaffSelector×é¼þ·Ö¹«Ë¾Óû§¼ÓÔØËµÃ÷.md
New file
@@ -0,0 +1,485 @@
# StaffSelector ç»„件分公司用户加载功能说明
## ðŸ“‹ åŠŸèƒ½æ¦‚è¿°
优化 `StaffSelector` ç»„ä»¶,支持通过外部传入分公司ID列表来加载指定分公司的用户,提供更灵活的人员选择功能。
---
## ðŸ”„ ä¿®æ”¹å†…容
### 1. åŽç«¯æŽ¥å£æ–°å¢ž
#### **文件**: `SysUserController.java`
**新增接口**: `GET /system/user/branch/users/by-dept-ids`
```java
/**
 * æ ¹æ®åˆ†å…¬å¸ID列表获取用户(小程序端专用)
 * æ”¯æŒå¤–部传入分公司ID数组,查询这些分公司及其所有子部门的用户
 */
@GetMapping("/branch/users/by-dept-ids")
public AjaxResult listUsersByBranchDeptIds(Long[] branchDeptIds)
```
**参数**:
- `branchDeptIds`: åˆ†å…¬å¸ID数组,例如 `[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
// æ ¹æ®åˆ†å…¬å¸ID列表查询用户(小程序端专用)
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
  // åˆ†å…¬å¸ID列表(外部传入,用于指定加载哪些分公司的用户)
  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` æŽ¥å£
- åŠ è½½å½“å‰ç™»å½•ç”¨æˆ·æ‰€å±žåˆ†å…¬å¸åŠå…¶å­éƒ¨é—¨çš„æ‰€æœ‰ç”¨æˆ·
---
### æ–¹å¼äºŒ: æŒ‡å®šåˆ†å…¬å¸ID(外部传入)
```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` | **分公司ID列表(新增)** |
---
## ðŸ“Š æ•°æ®æµç¨‹
### é»˜è®¤æ¨¡å¼(不传 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` å¿…须是数组类型,即使只有一个ID也要用数组 `[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加载用户功能 |
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 @@
    }
    
    /**
     * æ ¹æ®åˆ†å…¬å¸ID列表获取用户(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}")
ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskAttachmentController.java
@@ -14,6 +14,7 @@
import com.ruoyi.system.service.ISysTaskEmergencyService;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.system.service.ITaskAttachmentSyncService;
import com.ruoyi.system.service.IWechatAccessTokenService;
import com.ruoyi.system.task.ITaskAttachmentService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
@@ -62,6 +63,9 @@
    @Autowired
    private ISysUserService userService;
    @Autowired
    private IWechatAccessTokenService wechatAccessTokenService;
    /**
     * æŸ¥è¯¢ä»»åŠ¡é™„ä»¶åˆ—è¡¨
     */
@@ -135,6 +139,7 @@
    
    /**
     * ä»Žå¾®ä¿¡mediaId上传附件(微信小程序专用)
     * ä½¿ç”¨åº”用级缓存的AccessToken
     */
    @Log(title = "任务附件", businessType = BusinessType.INSERT)
    @PostMapping("/uploadFromWechat/{taskId}")
@@ -142,11 +147,8 @@
                                       @RequestParam("mediaId") String mediaId,
                                       @RequestParam(value = "category", required = false) String category) {
        try {
            // èŽ·å–å¾®ä¿¡AccessToken
            String accessToken = WechatUtils.getAccessToken(
                wechatConfig.getAppId(),
                wechatConfig.getAppSecret()
            );
            // èŽ·å–å¾®ä¿¡AccessToken(使用应用级缓存)
            String accessToken = wechatAccessTokenService.getAppAccessToken();
            if (accessToken == null || accessToken.isEmpty()) {
                return error("获取微信AccessToken失败");
            }
ruoyi-admin/src/main/java/com/ruoyi/web/controller/wechat/WechatController.java
@@ -20,6 +20,7 @@
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.system.service.IWechatTaskNotifyService;
import com.ruoyi.system.service.IWechatAccessTokenService;
import java.util.ArrayList;
import java.util.HashMap;
@@ -49,17 +50,17 @@
    @Autowired
    private IWechatTaskNotifyService wechatTaskNotifyService;
    @Autowired
    private IWechatAccessTokenService wechatAccessTokenService;
    
    /**
     * èŽ·å–å¾®ä¿¡AccessToken
     * èŽ·å–å¾®ä¿¡AccessToken(使用应用级缓存)
     */
    @GetMapping("/accessToken")
    public AjaxResult getAccessToken() {
        try {
            String accessToken = WechatUtils.getAccessToken(
                wechatConfig.getAppId(),
                wechatConfig.getAppSecret()
            );
            String accessToken = wechatAccessTokenService.getAppAccessToken();
            if (accessToken == null || accessToken.isEmpty()) {
                return error("获取微信AccessToken失败");
            }
ruoyi-admin/src/main/resources/application.yml
@@ -58,7 +58,7 @@
    basename: i18n/messages
  profiles:
    # çŽ¯å¢ƒ dev|test|prod
    active: dev
    active: prod
  # æ–‡ä»¶ä¸Šä¼ 
  servlet:
    multipart:
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();
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/BranchUserQueryVO.java
New file
@@ -0,0 +1,32 @@
package com.ruoyi.system.domain.vo;
import java.util.List;
/**
 * åˆ†å…¬å¸ç”¨æˆ·æŸ¥è¯¢VO
 *
 * @author ruoyi
 */
public class BranchUserQueryVO
{
    /** åˆ†å…¬å¸ID列表 */
    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 +
                '}';
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/IWechatAccessTokenService.java
New file
@@ -0,0 +1,27 @@
package com.ruoyi.system.service;
/**
 * å¾®ä¿¡AccessToken服务接口
 * æä¾›åº”用级AccessToken的统一管理,使用sys_config表缓存
 *
 * @author ruoyi
 * @date 2025-12-04
 */
public interface IWechatAccessTokenService {
    /**
     * èŽ·å–åº”ç”¨çº§å¾®ä¿¡AccessToken(带缓存)
     * ä¼˜å…ˆä»Žsys_config读取并判断有效期;过期则重新获取并写回sys_config
     *
     * @return Access Token,失败返回null
     */
    String getAppAccessToken();
    /**
     * å¼ºåˆ¶åˆ·æ–°AccessToken
     * å¿½ç•¥ç¼“存,直接从微信获取新Token并更新缓存
     *
     * @return æ–°çš„Access Token,失败返回null
     */
    String refreshAppAccessToken();
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java
@@ -672,7 +672,7 @@
                            if (sysUser != null) {
                                TaskCreateVO.AssigneeInfo assigneeInfo = new TaskCreateVO.AssigneeInfo();
                                assigneeInfo.setUserId(sysUser.getUserId()); // ä½¿ç”¨ç³»ç»Ÿç”¨æˆ·ID
                                assigneeInfo.setUserName(sysUser.getUserName());
                                assigneeInfo.setUserName(sysUser.getNickName());
                                // æ ¹æ®EntourageState确定角色类型
                                // 1,2 å¸æœºï¼Œ3,5 åŒ»ç”Ÿï¼Œ4,6 æŠ¤å£«
                                if ("1".equals(entourageState) || "2".equals(entourageState)) {
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PaymentSyncServiceImpl.java
@@ -73,7 +73,6 @@
     * å°†æ–°ç³»ç»Ÿæ”¯ä»˜è®°å½•同步到旧系统PaidMoney表
     */
    @Override
    @Transactional
    public boolean syncPaymentToLegacy(SysTaskPayment payment) {
        Long paymentId = payment.getId();
        try {
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());
            }
        }
        
        // æ›´æ–°è´¹ç”¨ä¿¡æ¯
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 ç»Ÿè®¡æœ‰task_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 å¦‚果大部分分段都有task_id,使用优化方案(直接按task_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统计去重的任务ID数量
                Set<Long> uniqueTaskIds = new HashSet<>();
                
                // åˆ†æ‘‡é‡Œç¨‹
                BigDecimal taskDist = segDistance.multiply(BigDecimal.valueOf(taskRatio));
                BigDecimal nonTaskDist = segDistance.multiply(BigDecimal.valueOf(1 - taskRatio));
                // ç›´æŽ¥æŒ‰task_id分组聚合
                for (VehicleGpsSegmentMileage segment : segments) {
                    BigDecimal segDistance = segment.getSegmentDistance() != null ? segment.getSegmentDistance() : BigDecimal.ZERO;
                    if (segment.getTaskId() != null) {
                        // æœ‰ä»»åŠ¡ID,计入任务里程
                        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"); // æ ‡è®°æ•°æ®æ¥æºä¸ºåˆ†æ®µæ±‡æ€»
            
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatAccessTokenServiceImpl.java
New file
@@ -0,0 +1,123 @@
package com.ruoyi.system.service.impl;
import com.ruoyi.common.config.WechatConfig;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.WechatUtils;
import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.mapper.SysConfigMapper;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.IWechatAccessTokenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
 * å¾®ä¿¡AccessToken服务实现
 * æä¾›åº”用级AccessToken的统一管理,使用sys_config表缓存
 *
 * @author ruoyi
 * @date 2025-12-04
 */
@Service
public class WechatAccessTokenServiceImpl implements IWechatAccessTokenService {
    private static final Logger log = LoggerFactory.getLogger(WechatAccessTokenServiceImpl.class);
    @Autowired
    private WechatConfig wechatConfig;
    @Autowired
    private ISysConfigService configService;
    @Autowired
    private SysConfigMapper configMapper;
    /**
     * èŽ·å–åº”ç”¨çº§å¾®ä¿¡AccessToken(带缓存)
     * ä¼˜å…ˆä»Žsys_config读取并判断有效期;过期则重新获取并写回sys_config
     */
    @Override
    public 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) {
                log.debug("使用缓存的AccessToken,剩余有效期:{}秒", (expireTs - now) / 1000);
                return cachedToken;
            }
            // é‡æ–°èŽ·å–ï¼Œå¹¶å†™å…¥sys_config
            log.info("AccessToken已过期或不存在,重新获取");
            return refreshAppAccessToken();
        } catch (Exception e) {
            log.error("获取应用级微信AccessToken失败", e);
            return null;
        }
    }
    /**
     * å¼ºåˆ¶åˆ·æ–°AccessToken
     * å¿½ç•¥ç¼“存,直接从微信获取新Token并更新缓存
     */
    @Override
    public String refreshAppAccessToken() {
        try {
            String appId = wechatConfig.getAppId();
            String appSecret = wechatConfig.getAppSecret();
            String tokenKey = "weixin.access_token." + appId;
            String expireKey = "weixin.access_token_expires." + appId;
            String newToken = WechatUtils.getAccessToken(appId, appSecret);
            if (StringUtils.isEmpty(newToken)) {
                log.error("从微信获取AccessToken失败");
                return null;
            }
            long now = System.currentTimeMillis();
            long newExpireTs = now + 7200L * 1000L; // 7200秒
            upsertConfig(tokenKey, newToken);
            upsertConfig(expireKey, String.valueOf(newExpireTs));
            log.info("AccessToken刷新成功,有效期:7200秒");
            return newToken;
        } catch (Exception e) {
            log.error("刷新应用级微信AccessToken失败", e);
            return null;
        }
    }
    /**
     * æ ¹æ®configKey写入或更新sys_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);
        }
    }
}
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写入或更新sys_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和session_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"));
            // è¿”回当前使用的access_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);
            }
            // ä¿å­˜æœ¬æ¬¡è°ƒç”¨ä½¿ç”¨çš„access_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());
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatTaskNotifyServiceImpl.java
@@ -5,12 +5,16 @@
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.IWechatAccessTokenService;
import com.ruoyi.system.service.ISysConfigService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -44,6 +48,30 @@
    @Autowired
    private WechatConfig wechatConfig;
    
    @Autowired
    private IWechatAccessTokenService wechatAccessTokenService;
    @Autowired
    private ISysConfigService configService;
    /**
     * æ£€æŸ¥æ˜¯å¦å¯ç”¨è®¢é˜…消息发送
     *
     * @return true=启用,false=禁用
     */
    private boolean isSubscribeMessageEnabled() {
        try {
            String enabled = configService.selectConfigByKey("wechat.subscribe.message.enabled");
            return "true".equalsIgnoreCase(enabled);
        } catch (Exception e) {
            log.warn("获取订阅消息开关配置失败,默认启用", e);
            return true; // é»˜è®¤å¯ç”¨
        }
    }
    /**
     * å‘送任务通知消息给指定用户列表
     * 
@@ -54,6 +82,11 @@
     */
    @Override
    public int sendTaskNotifyMessage(Long taskId, List<Long> userIds, Long excludeUserId) {
        // æ£€æŸ¥è®¢é˜…消息开关
        if (!isSubscribeMessageEnabled()) {
            log.info("订阅消息发送已关闭,跳过发送,taskId={}", taskId);
            return 0;
        }
        if (taskId == null || userIds == null || userIds.isEmpty()) {
            log.warn("发送微信任务通知参数不完整,taskId={}, userIds={}", taskId, userIds);
            return 0;
@@ -69,8 +102,8 @@
        // æŸ¥è¯¢æ€¥æ•‘信息
        SysTaskEmergency emergency = sysTaskEmergencyMapper.selectSysTaskEmergencyByTaskId(taskId);
        
        // èŽ·å–å¾®ä¿¡AccessToken
        String accessToken = WechatUtils.getAccessToken(wechatConfig.getAppId(), wechatConfig.getAppSecret());
        // èŽ·å–å¾®ä¿¡AccessToken(走应用级缓存)
        String accessToken = wechatAccessTokenService.getAppAccessToken();
        if (StringUtils.isEmpty(accessToken)) {
            log.error("获取微信AccessToken失败,无法发送任务通知");
            return 0;
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>
ruoyi-ui/src/api/system/mileageStats.js
@@ -65,3 +65,16 @@
    }
  })
}
// æŸ¥è¯¢è½¦è¾†æŒ‡å®šæ—¥æœŸçš„GPS分段里程明细
export function getSegmentsByDateRange(vehicleId, startDate, endDate) {
  return request({
    url: '/system/gpsSegment/range',
    method: 'get',
    params: {
      vehicleId: vehicleId,
      startDate: startDate,
      endDate: endDate
    }
  })
}
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;
        // å¦‚果有车辆ID和统计日期,加载分段明细
        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;
      });
    },
    /** åˆ é™¤æŒ‰é’®æ“ä½œ */
sql/add_wechat_token_fields.sql
New file
@@ -0,0 +1,9 @@
-- å¾®ä¿¡ç™»å½•token存储字段
-- åœ¨sys_user表新增存储微信access_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`;
-- ç´¢å¼•可选(如需按token查询或清理)
-- CREATE INDEX `idx_wechat_token_expire` ON `sys_user` (`wechat_token_expires_at`);
sql/fix_segment_mileage_task_association.sql
New file
@@ -0,0 +1,134 @@
-- ========================================
-- ä¿®å¤GPS分段里程的任务关联
-- ç”¨é€”:为已有的分段里程数据补充 task_id å’Œ task_code
-- ä¼˜åŒ–说明:补充task_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. æ›´æ–°é€»è¾‘:根据车辆ID和时间重叠关联任务
-- æ³¨æ„ï¼šè¿™ä¸ªæŸ¥è¯¢ä¼šæ¯”较慢,建议分批执行或在非高峰期执行
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. æ£€æŸ¥GPS分段里程计算定时任务状态
SELECT
    job_id,
    job_name,
    job_group,
    invoke_target,
    cron_expression,
    status as '状态(0=正常,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;
sql/remove_wechat_token_fields.sql
New file
@@ -0,0 +1,8 @@
-- ç§»é™¤ç”¨æˆ·è¡¨ä¸­çš„微信token与有效期字段(改为应用级统一存储)
-- å›žæ»šæˆ–清理脚本:删除 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`;
sql/test_mileage_stats_optimization.sql
New file
@@ -0,0 +1,110 @@
-- ========================================
-- æµ‹è¯•里程统计优化效果
-- ç”¨é€”:对比优化前后的性能和准确性
-- ========================================
-- 1. æŸ¥çœ‹æŸä¸ªè½¦è¾†æŸå¤©çš„分段数据task_id覆盖率
-- æ›¿æ¢å‚数:@vehicleId, @statDate
SET @vehicleId = 1;  -- æ›¿æ¢ä¸ºå®žé™…车辆ID
SET @statDate = '2025-12-04';  -- æ›¿æ¢ä¸ºå®žé™…日期
SELECT
    '分段统计' as '类型',
    COUNT(*) as '总分段数',
    SUM(CASE WHEN task_id IS NOT NULL THEN 1 ELSE 0 END) as '有任务ID的分段数',
    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:优化方案(直接按task_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 '有task_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. æ€§èƒ½æµ‹è¯•:执行时间对比
-- è¯´æ˜Žï¼šä½¿ç”¨EXPLAIN ANALYZE查看执行计划(MySQL 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 '类型',
    '补充历史数据的task_id' as '措施1',
    '确保定时任务正常运行' as '措施2',
    '确保任务状态正确更新' as '措施3',
    '定期检查task_id覆盖率' as '措施4';
-- ========================================
-- ä½¿ç”¨è¯´æ˜Žï¼š
-- 1. è®¾ç½®å‚数:修改第7-8行的@vehicleId和@statDate
-- 2. æ‰§è¡ŒæŸ¥è¯¢1-4查看优化效果
-- 3. å¦‚果覆盖率低于80%,执行fix_segment_mileage_task_association.sql修复
-- 4. é‡æ–°ç»Ÿè®¡ï¼Œè§‚察性能提升
-- ========================================
sql/wechat_subscribe_message_config.sql
New file
@@ -0,0 +1,18 @@
-- å¾®ä¿¡è®¢é˜…消息发送开关配置
-- ç”¨äºŽæŽ§åˆ¶ç³»ç»Ÿæ˜¯å¦å¯ç”¨å¾®ä¿¡è®¢é˜…消息推送功能
-- æ’入订阅消息开关配置(默认开启)
INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark)
VALUES
('微信订阅消息开关', 'wechat.subscribe.message.enabled', 'true', 'N', 'admin', NOW(), 'admin', NOW(),
 '控制是否启用微信订阅消息推送功能。true=启用,false=禁用。关闭后系统将不再发送任何订阅消息。');
-- è¯´æ˜Žï¼š
-- 1. wechat.subscribe.message.enabled æŽ§åˆ¶å…¨å±€è®¢é˜…消息发送开关
-- 2. è®¾ç½®ä¸º true æ—¶ï¼Œç³»ç»Ÿæ­£å¸¸å‘送订阅消息
-- 3. è®¾ç½®ä¸º false æ—¶ï¼Œæ‰€æœ‰è®¢é˜…消息发送将被跳过
-- 4. é€‚用场景:
--    - ç”Ÿäº§çŽ¯å¢ƒä¸´æ—¶å…³é—­æ¶ˆæ¯æŽ¨é€
--    - æµ‹è¯•环境避免误发消息
--    - ç³»ç»Ÿç»´æŠ¤æœŸé—´æš‚停通知
-- 5. å¯åœ¨ç³»ç»Ÿç®¡ç†->参数设置中动态修改,无需重启服务
sql/wechat_subscribe_message_config_README.md
New file
@@ -0,0 +1,167 @@
# å¾®ä¿¡è®¢é˜…消息开关使用说明
## ä¸€ã€é…ç½®é¡¹è¯´æ˜Ž
### é…ç½®é”®ï¼š`wechat.subscribe.message.enabled`
| å±žæ€§ | å€¼ |
|------|-----|
| é…ç½®åç§° | å¾®ä¿¡è®¢é˜…消息开关 |
| é…ç½®é”® | wechat.subscribe.message.enabled |
| é…ç½®ç±»åž‹ | N (普通参数) |
| é»˜è®¤å€¼ | true |
| å¯é€‰å€¼ | true / false |
### é…ç½®è¯´æ˜Ž
- **true**: å¯ç”¨å¾®ä¿¡è®¢é˜…消息推送(默认)
- **false**: ç¦ç”¨å¾®ä¿¡è®¢é˜…消息推送
## äºŒã€SQL脚本
执行以下SQL脚本添加配置:
```sql
INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark)
VALUES
('微信订阅消息开关', 'wechat.subscribe.message.enabled', 'true', 'N', 'admin', NOW(), 'admin', NOW(),
 '控制是否启用微信订阅消息推送功能。true=启用,false=禁用。关闭后系统将不再发送任何订阅消息。');
```
## ä¸‰ã€åŠŸèƒ½è¯´æ˜Ž
### 1. å¼€å…³æŽ§åˆ¶èŒƒå›´
该开关控制以下场景的订阅消息发送:
- âœ… æ–°ä»»åŠ¡åˆ›å»ºæ—¶é€šçŸ¥æ‰§è¡Œäºº
- âœ… ä»»åŠ¡æ›´æ–°æ—¶é€šçŸ¥ç›¸å…³äººå‘˜
- âœ… æ—§ç³»ç»ŸåŒæ­¥ä»»åŠ¡æ—¶çš„é€šçŸ¥
- âœ… æ‰‹åŠ¨è§¦å‘çš„ä»»åŠ¡é€šçŸ¥
- âœ… æ‰€æœ‰é€šè¿‡ `IWechatTaskNotifyService` å‘送的消息
### 2. å¼€å…³å…³é—­åŽçš„表现
当开关设置为 `false` æ—¶ï¼š
- ç³»ç»Ÿå°†è·³è¿‡æ‰€æœ‰è®¢é˜…消息发送
- æ—¥å¿—记录:`订阅消息发送已关闭,跳过发送,taskId=xxx`
- ä¸å½±å“å…¶ä»–业务逻辑
- è¿”回值为 0(发送成功数量为0)
### 3. å¼‚常处理
- å¦‚果配置读取失败,默认为 **启用** çŠ¶æ€
- ç¡®ä¿ç³»ç»Ÿæ­£å¸¸è¿è¡Œï¼Œé¿å…å› é…ç½®å¼‚常导致功能中断
## å››ã€ä½¿ç”¨åœºæ™¯
### 1. æµ‹è¯•环境
在测试环境中关闭消息推送,避免测试数据触发真实消息:
```sql
UPDATE sys_config
SET config_value = 'false'
WHERE config_key = 'wechat.subscribe.message.enabled';
```
### 2. ç”Ÿäº§ç»´æŠ¤
在系统维护期间临时关闭消息推送:
```sql
-- å…³é—­æ¶ˆæ¯æŽ¨é€
UPDATE sys_config
SET config_value = 'false'
WHERE config_key = 'wechat.subscribe.message.enabled';
-- ç»´æŠ¤å®ŒæˆåŽæ¢å¤
UPDATE sys_config
SET config_value = 'true'
WHERE config_key = 'wechat.subscribe.message.enabled';
```
### 3. æ•…障排查
当微信接口异常时,临时关闭消息推送,避免大量错误日志:
```sql
UPDATE sys_config
SET config_value = 'false'
WHERE config_key = 'wechat.subscribe.message.enabled';
```
## äº”、管理界面操作
### é€šè¿‡åŽå°ç®¡ç†ç•Œé¢ä¿®æ”¹
1. ç™»å½•系统管理后台
2. è¿›å…¥ **系统管理 > å‚数设置**
3. æœç´¢ **微信订阅消息开关**
4. ä¿®æ”¹å‚数值为 `true` æˆ– `false`
5. ä¿å­˜åŽ **立即生效**,无需重启服务
## å…­ã€æŠ€æœ¯å®žçް
### ä»£ç ä½ç½®
- **配置SQL**: `/sql/wechat_subscribe_message_config.sql`
- **服务实现**: `/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/WechatTaskNotifyServiceImpl.java`
### æ ¸å¿ƒä»£ç 
```java
/**
 * æ£€æŸ¥æ˜¯å¦å¯ç”¨è®¢é˜…消息发送
 */
private boolean isSubscribeMessageEnabled() {
    try {
        String enabled = configService.selectConfigByKey("wechat.subscribe.message.enabled");
        return "true".equalsIgnoreCase(enabled);
    } catch (Exception e) {
        log.warn("获取订阅消息开关配置失败,默认启用", e);
        return true; // é»˜è®¤å¯ç”¨
    }
}
```
## ä¸ƒã€æ³¨æ„äº‹é¡¹
1. âš ï¸ ä¿®æ”¹é…ç½®åŽç«‹å³ç”Ÿæ•ˆï¼Œæ— éœ€é‡å¯æœåŠ¡
2. âš ï¸ å…³é—­å¼€å…³ä¼šå½±å“æ‰€æœ‰è®¢é˜…消息发送
3. âš ï¸ é»˜è®¤å€¼ä¸º `true`(启用),确保正常业务不受影响
4. âš ï¸ å»ºè®®åœ¨ç”Ÿäº§çŽ¯å¢ƒä¿æŒå¼€å¯çŠ¶æ€
5. âš ï¸ æµ‹è¯•环境建议关闭,避免误发消息给真实用户
## å…«ã€éªŒè¯æ–¹æ³•
### 1. éªŒè¯é…ç½®å·²æ·»åŠ 
```sql
SELECT * FROM sys_config WHERE config_key = 'wechat.subscribe.message.enabled';
```
### 2. éªŒè¯å¼€å…³ç”Ÿæ•ˆ
**关闭开关:**
```sql
UPDATE sys_config SET config_value = 'false' WHERE config_key = 'wechat.subscribe.message.enabled';
```
**创建测试任务:**
- è§‚察日志是否输出:`订阅消息发送已关闭,跳过发送`
- ç¡®è®¤æ²¡æœ‰å®žé™…发送微信消息
**恢复开关:**
```sql
UPDATE sys_config SET config_value = 'true' WHERE config_key = 'wechat.subscribe.message.enabled';
```
## ä¹ã€ç›¸å…³é…ç½®
该功能配合以下配置使用:
- `weixin.access_token.{appId}` - å¾®ä¿¡AccessToken缓存
- `weixin.access_token_expires.{appId}` - AccessToken过期时间
- `wechat.task.notify.template.id` - ä»»åŠ¡é€šçŸ¥æ¨¡æ¿ID (application.yml)
- `wechat.task.detail.page` - ä»»åŠ¡è¯¦æƒ…é¡µè·¯å¾„ (application.yml)