From 40a8157440e3b906da8f52e07d939d78c3f4c313 Mon Sep 17 00:00:00 2001
From: wlzboy <66905212@qq.com>
Date: 星期日, 12 四月 2026 16:14:06 +0800
Subject: [PATCH] feat: 任务增加统计、同步增加通知

---
 ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/DeptOrderStatVO.java                  |   36 +
 ruoyi-ui/src/views/system/user/index.vue                                                    |   85 ++
 ruoyi-ui/src/api/system/user.js                                                             |    9 
 ruoyi-ui/src/views/system/gps/trackMap.vue                                                  |  570 +++++++++++++++++++
 ruoyi-system/src/main/java/com/ruoyi/system/service/INotifyTaskService.java                 |    9 
 ruoyi-ui/src/api/task.js                                                                    |   21 
 ruoyi-ui/src/views/task/stat/index.vue                                                      |  219 +++++++
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java |  226 ++++++-
 sql/vehicle_gps_track_map_menu.sql                                                          |   53 +
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/NotifyDispatchServiceImpl.java     |   63 +-
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/NotifyTaskServiceImpl.java         |    8 
 test/1.html                                                                                 |   24 
 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java                    |   10 
 ruoyi-ui/src/api/system/dept.js                                                             |    8 
 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java            |   44 +
 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java         |   31 +
 ruoyi-ui/src/router/index.js                                                                |   23 
 ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml                          |    4 
 ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskStatController.java          |   77 ++
 ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java               |   63 +
 ruoyi-ui/vue.config.js                                                                      |    2 
 sql/dept_order_stat_menu.sql                                                                |   20 
 ruoyi-admin/src/main/resources/application.yml                                              |    2 
 ruoyi-ui/src/api/system/gps.js                                                              |   14 
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysTaskServiceImpl.java            |    7 
 ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml                             |   21 
 ruoyi-admin/src/main/resources/application-prod.yml                                         |    6 
 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskMapper.java                       |   18 
 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java            |   12 
 ruoyi-system/src/main/java/com/ruoyi/system/mapper/NotifyTaskMapper.java                    |    9 
 ruoyi-system/src/main/resources/mapper/system/NotifyTaskMapper.xml                          |    5 
 ruoyi-admin/src/main/resources/application-dev.yml                                          |    6 
 32 files changed, 1,594 insertions(+), 111 deletions(-)

diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java
index 2b08014..6e97177 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysDeptController.java
@@ -74,6 +74,18 @@
     }
 
     /**
+     * 鑾峰彇鎵�鏈夊垎鍏徃鍒楄〃锛坧arentId=100鐨勯儴闂級
+     */
+    @GetMapping("/branch/all")
+    public AjaxResult listAllBranches()
+    {
+        SysDept query = new SysDept();
+        query.setParentId(100L);
+        List<SysDept> depts = deptService.selectDeptList(query);
+        return success(depts);
+    }
+
+    /**
      * 鏌ヨ閮ㄩ棬鍒楄〃锛堟帓闄よ妭鐐癸級
      */
     @GetMapping("/list/exclude/{deptId}")
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 84af926..0ab3cdf 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
@@ -350,4 +350,48 @@
             return error("鏈壘鍒板搴旂殑鐢ㄦ埛淇℃伅");
         }
     }
+
+    /**
+     * 鏇存柊鐢ㄦ埛鍙鐞嗗垎鍏徃锛堥�氳繃oaOrderClass瀛楁淇濆瓨缂栫爜鍒楄〃锛�
+     * 鎺ユ敹閫変腑鐨勫垎鍏徃deptId鍒楄〃锛屾煡璇㈠搴旂紪鐮佸悗鍚堝苟鍐欏叆oaOrderClass
+     */
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
+    @Log(title = "鐢ㄦ埛绠$悊-鍒嗗叕鍙搁厤缃�", businessType = BusinessType.UPDATE)
+    @PutMapping("/branch/{userId}")
+    public AjaxResult updateUserBranch(@PathVariable Long userId, @RequestBody java.util.List<Long> deptIds)
+    {
+        userService.checkUserDataScope(userId);
+        SysUser user = userService.selectUserById(userId);
+        if (user == null) {
+            return error("鐢ㄦ埛涓嶅瓨鍦�");
+        }
+        // 鏍规嵁deptIds鏌ヨ鍒嗗叕鍙镐俊鎭紝鏀堕泦缂栫爜
+        java.util.Set<String> codeSet = new java.util.LinkedHashSet<>();
+        if (deptIds != null && !deptIds.isEmpty()) {
+            SysDept queryDept = new SysDept();
+            queryDept.setParentId(100L);
+            List<SysDept> allBranches = deptService.selectDeptList(queryDept);
+            java.util.Map<Long, SysDept> deptMap = new java.util.HashMap<>();
+            for (SysDept d : allBranches) {
+                deptMap.put(d.getDeptId(), d);
+            }
+            for (Long deptId : deptIds) {
+                SysDept dept = deptMap.get(deptId);
+                if (dept != null) {
+                    if (StringUtils.isNotEmpty(dept.getServiceOrderClass())) {
+                        codeSet.add(dept.getServiceOrderClass().trim());
+                    }
+                    if (StringUtils.isNotEmpty(dept.getDispatchOrderClass())) {
+                        codeSet.add(dept.getDispatchOrderClass().trim());
+                    }
+                }
+            }
+        }
+        String newOaOrderClass = String.join(",", codeSet);
+        SysUser updateUser = new SysUser();
+        updateUser.setUserId(userId);
+        updateUser.setOaOrderClass(newOaOrderClass);
+        updateUser.setUpdateBy(getUsername());
+        return toAjax(userService.updateUserProfile(updateUser));
+    }
 }
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java
index a5a7621..65652d2 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/VehicleGpsController.java
@@ -241,7 +241,7 @@
     }
 
     /**
-     * 鏌ヨ杞﹁締鍘嗗彶杞ㄨ抗
+     * 鏌ヨ杞﹁締鍘嗗彶杞ㄨ抗锛堣皟鐢℅PS骞冲彴鎺ュ彛锛�
      */
     @PreAuthorize("@ss.hasPermi('system:gps:query')")
     @GetMapping("/tracks")
@@ -249,6 +249,35 @@
         return getAnonymousTracks(vehicleNo, beginTime, endTime);
     }
 
+    /**
+     * 浠庢湰鍦版暟鎹簱鏌ヨ杞﹁締琛岄┒杞ㄨ抗锛堝ぉ鍦板浘杞ㄨ抗椤典娇鐢級
+     * 鏀寔杞︾墝鍙锋ā绯婃煡璇� + 鏃堕棿鑼冨洿绮剧‘鏌ヨ
+     */
+    @PreAuthorize("@ss.hasPermi('system:gps:list')")
+    @GetMapping("/tracksByPlate")
+    public TableDataInfo getTracksByPlate(String vehicleNo, String beginTime, String endTime) {
+        try {
+            if (vehicleNo == null || vehicleNo.trim().isEmpty()) {
+                return getDataTable(new ArrayList<>());
+            }
+            VehicleGps query = new VehicleGps();
+            query.setVehicleNo(vehicleNo.trim());
+            if (beginTime != null && !beginTime.isEmpty()) {
+                query.setBeginTime(beginTime.replace("T", " "));
+            }
+            if (endTime != null && !endTime.isEmpty()) {
+                query.setEndTime(endTime.replace("T", " "));
+            }
+            query.setOrderByColumn("collect_time");
+            query.setIsAsc("asc");
+            List<VehicleGps> list = vehicleGpsService.selectVehicleGpsList(query);
+            return getDataTable(list);
+        } catch (Exception e) {
+            logger.error("浠庢暟鎹簱鏌ヨ杞﹁締杞ㄨ抗澶辫触", e);
+            return getDataTable(new ArrayList<>());
+        }
+    }
+
    
     /**
      * 鍖垮悕鏌ヨ杞﹁締鍘嗗彶杞ㄨ抗
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskStatController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskStatController.java
new file mode 100644
index 0000000..75c46b7
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/task/SysTaskStatController.java
@@ -0,0 +1,77 @@
+package com.ruoyi.web.controller.task;
+
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.system.domain.vo.DeptOrderStatVO;
+import com.ruoyi.system.service.ISysTaskService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 鍒嗗叕鍙稿綍鍗曠粺璁� Controller
+ */
+@RestController
+@RequestMapping("/task/stat")
+public class SysTaskStatController extends BaseController {
+
+    @Autowired
+    private ISysTaskService sysTaskService;
+
+    /**
+     * 鎸夊垎鍏徃銆佹椂闂磋寖鍥存煡璇㈡瘡澶╁綍鍗曠粺璁�
+     *
+     * @param deptIds   鍒嗗叕鍙窱D鍒楄〃锛岄�楀彿鍒嗛殧锛屼负绌哄垯鏌ュ叏閮�
+     * @param startDate 寮�濮嬫棩鏈� yyyy-MM-dd
+     * @param endDate   缁撴潫鏃ユ湡 yyyy-MM-dd
+     */
+    @PreAuthorize("@ss.hasPermi('task:stat:query')")
+    @GetMapping("/deptOrder")
+    public AjaxResult deptOrderStat(
+            @RequestParam(required = false) String deptIds,
+            @RequestParam String startDate,
+            @RequestParam String endDate) {
+        List<Long> deptIdList = parseDeptIds(deptIds);
+        List<DeptOrderStatVO> list = sysTaskService.selectDeptOrderStat(deptIdList, startDate, endDate);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 瀵煎嚭 Excel
+     */
+    @PreAuthorize("@ss.hasPermi('task:stat:export')")
+    @GetMapping("/deptOrder/export")
+    public void exportDeptOrderStat(
+            HttpServletResponse response,
+            @RequestParam(required = false) String deptIds,
+            @RequestParam String startDate,
+            @RequestParam String endDate) {
+        List<Long> deptIdList = parseDeptIds(deptIds);
+        List<DeptOrderStatVO> list = sysTaskService.selectDeptOrderStat(deptIdList, startDate, endDate);
+        ExcelUtil<DeptOrderStatVO> util = new ExcelUtil<>(DeptOrderStatVO.class);
+        util.exportExcel(response, list, "鍒嗗叕鍙稿綍鍗曠粺璁�");
+    }
+
+    /** 瑙f瀽閫楀彿鍒嗛殧鐨� deptIds 瀛楃涓蹭负 Long 鍒楄〃 */
+    private List<Long> parseDeptIds(String deptIds) {
+        List<Long> result = new ArrayList<>();
+        if (deptIds == null || deptIds.trim().isEmpty()) {
+            return result;
+        }
+        for (String id : deptIds.split(",")) {
+            try {
+                result.add(Long.parseLong(id.trim()));
+            } catch (NumberFormatException ignored) {
+            }
+        }
+        return result;
+    }
+}
diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml
index 13f31b7..1495f3f 100644
--- a/ruoyi-admin/src/main/resources/application-dev.yml
+++ b/ruoyi-admin/src/main/resources/application-dev.yml
@@ -12,12 +12,16 @@
             # 浠庡簱鏁版嵁婧�
             # SQL Server鏁版嵁婧�
             sqlserver:
-                url: jdbc:sqlserver://120.25.98.119:1432;databaseName=came
+                url: jdbc:sqlserver://120.25.98.119:1432;databaseName=came;loginTimeout=60;queryTimeout=300
                 username: camesa
                 password: camesa
                 driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
                 enabled: true
                 validationQuery: SELECT 1
+                # 杩炴帴瓒呮椂鏃堕棿锛堟绉掞級锛氳幏鍙栬繛鎺ユ渶闀跨瓑寰呮椂闂达紝60绉�
+                connectTimeout: 60000
+                # Socket璇诲彇瓒呮椂锛堟绉掞級锛歋QL鎵ц鏈�闀跨瓑寰呮椂闂达紝5鍒嗛挓
+                socketTimeout: 300000
             slave:
                 # 浠庢暟鎹簮寮�鍏�/榛樿鍏抽棴
                 enabled: false
diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml
index cf4900c..f9472e0 100644
--- a/ruoyi-admin/src/main/resources/application-prod.yml
+++ b/ruoyi-admin/src/main/resources/application-prod.yml
@@ -12,12 +12,16 @@
       # 浠庡簱鏁版嵁婧�
       # SQL Server鏁版嵁婧�
       sqlserver:
-        url: jdbc:sqlserver://39.108.160.52;databaseName=came
+        url: jdbc:sqlserver://39.108.160.52;databaseName=came;loginTimeout=60;queryTimeout=300
         username: camesa
         password: camesa
         driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
         enabled: true
         validationQuery: SELECT 1
+        # 杩炴帴瓒呮椂鏃堕棿锛堟绉掞級锛氳幏鍙栬繛鎺ユ渶闀跨瓑寰呮椂闂达紝鏀逛负60绉�
+        connectTimeout: 60000
+        # Socket璇诲彇瓒呮椂锛堟绉掞級锛歋QL鎵ц鏈�闀跨瓑寰呮椂闂达紝鏀逛负5鍒嗛挓
+        socketTimeout: 300000
       slave:
         # 浠庢暟鎹簮寮�鍏�/榛樿鍏抽棴
         enabled: false
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index fed985b..ffcd5be 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -58,7 +58,7 @@
     basename: i18n/messages
   profiles:
     # 鐜 dev|test|prod
-    active: dev
+    active: prod
   # 鏂囦欢涓婁紶
   servlet:
     multipart:
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/DeptOrderStatVO.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/DeptOrderStatVO.java
new file mode 100644
index 0000000..86a9f6b
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/DeptOrderStatVO.java
@@ -0,0 +1,36 @@
+package com.ruoyi.system.domain.vo;
+
+import com.ruoyi.common.annotation.Excel;
+
+/**
+ * 鍒嗗叕鍙告瘡鏃ュ綍鍗曠粺璁� VO
+ */
+public class DeptOrderStatVO {
+
+    /** 鍒嗗叕鍙窱D */
+    private Long deptId;
+
+    /** 鍒嗗叕鍙稿悕绉� */
+    @Excel(name = "鍒嗗叕鍙�")
+    private String deptName;
+
+    /** 缁熻鏃ユ湡锛屾牸寮� yyyy-MM-dd */
+    @Excel(name = "鏃ユ湡")
+    private String statDate;
+
+    /** 褰曞崟鏁伴噺 */
+    @Excel(name = "褰曞崟鏁伴噺")
+    private Integer orderCount;
+
+    public Long getDeptId() { return deptId; }
+    public void setDeptId(Long deptId) { this.deptId = deptId; }
+
+    public String getDeptName() { return deptName; }
+    public void setDeptName(String deptName) { this.deptName = deptName; }
+
+    public String getStatDate() { return statDate; }
+    public void setStatDate(String statDate) { this.statDate = statDate; }
+
+    public Integer getOrderCount() { return orderCount; }
+    public void setOrderCount(Integer orderCount) { this.orderCount = orderCount; }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java b/ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java
index 88bfce9..5b20aae 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/listener/TaskMessageListener.java
@@ -1,6 +1,7 @@
 package com.ruoyi.system.listener;
 
 import com.ruoyi.common.core.domain.entity.SysDept;
+import com.ruoyi.common.config.WechatConfig;
 import com.ruoyi.common.utils.DeptUtil;
 import com.ruoyi.common.utils.LongUtil;
 import com.ruoyi.system.domain.*;
@@ -56,6 +57,12 @@
 
     @Autowired
     private INotifyDispatchService notifyDispatchService;
+
+    @Autowired
+    private IQyWechatService qyWechatService;
+
+    @Autowired
+    private WechatConfig wechatConfig;
 
     /** 寰呭噯澶囩姸鎬� - 鍙互鍙戦�佺煭淇¢�氱煡 */
     private static final String TASK_STATUS_PENDING = "PENDING";
@@ -266,6 +273,7 @@
     /**
      * 鐩戝惉浠诲姟鍒嗛厤浜嬩欢
      * 鍒涘缓閫氱煡浠诲姟锛岀敱閫氱煡鍒嗗彂鏈嶅姟鍐冲畾鍙戦�佹笭閬�
+     * 鍚屾椂鐩存帴鍙戦�佷紒涓氬井淇¢�氱煡
      * 
      * @param event 浠诲姟鍒嗛厤浜嬩欢
      */
@@ -295,15 +303,18 @@
             Long creatorId = task.getCreatorId();
             String taskStatus = task.getTaskStatus();
             task.setEmergencyInfo(emergency);
-            // 浠呭湪寰呭噯澶囩姸鎬佷笅鍙戦�侀�氱煡
-            if (!TASK_STATUS_PENDING.equals(taskStatus) && !TASK_STATUS_PREPARING.equals(taskStatus)) {
-                log.info("浠诲姟鐘舵��({})闈炲緟鍑嗗鐘舵�侊紝璺宠繃閫氱煡锛宼askId={}", taskStatus, event.getTaskId());
-                return;
-            }
-
+            
             // 鏋勫缓閫氱煡鍐呭
             String notifyContent = buildNotifyContent(task, emergency);
-            this.sendDispatchNotify(event.getAssigneeIds(), creatorId, event.getTaskId(),task.getShowTaskCode(), notifyContent);
+            
+            // 鐩存帴鍙戦�佷紒涓氬井淇¢�氱煡缁欐墽琛屼汉鍛�
+            sendQyWechatNotifyToAssignees(event.getAssigneeIds(), creatorId, event.getTaskId(), notifyContent);
+            
+            // 鍚屾椂璧板師鏈夐�氱煡鍒嗗彂娴佺▼锛堢珯鍐呮秷鎭瓑锛�
+            // 浠呭湪寰呭噯澶囩姸鎬佷笅鍙戦�佸叾浠栭�氱煡
+            if (TASK_STATUS_PENDING.equals(taskStatus) || TASK_STATUS_PREPARING.equals(taskStatus)) {
+                this.sendDispatchNotify(event.getAssigneeIds(), creatorId, event.getTaskId(), task.getShowTaskCode(), notifyContent);
+            }
 
             
         } catch (Exception e) {
@@ -312,6 +323,44 @@
     }
 
     /**
+     * 鐩存帴鍙戦�佷紒涓氬井淇¢�氱煡缁欐墽琛屼汉鍛�
+     */
+    private void sendQyWechatNotifyToAssignees(List<Long> assigneeIds, Long creatorId, Long taskId, String content) {
+        String appId = wechatConfig.getAppId();
+        String pathPage = "/pagesTask/detail?id=" + taskId;
+        int successCount = 0;
+        
+        for (Long assigneeId : assigneeIds) {
+            // 鎺掗櫎鍒涘缓浜�
+            if (creatorId != null && creatorId.equals(assigneeId)) {
+                log.debug("璺宠繃鍒涘缓浜猴紝涓嶅彂閫佷紒涓氬井淇¢�氱煡锛寀serId={}", assigneeId);
+                continue;
+            }
+            
+            try {
+                boolean success = qyWechatService.sendNotifyMessage(
+                        assigneeId,
+                        "杞繍鍗曚换鍔℃淳鍗曢�氱煡",
+                        content,
+                        appId,
+                        pathPage
+                );
+                
+                if (success) {
+                    successCount++;
+                    log.info("浼佷笟寰俊娲惧崟閫氱煡鍙戦�佹垚鍔燂紝taskId={}, userId={}", taskId, assigneeId);
+                } else {
+                    log.warn("浼佷笟寰俊娲惧崟閫氱煡鍙戦�佸け璐ワ紝taskId={}, userId={}", taskId, assigneeId);
+                }
+            } catch (Exception e) {
+                log.error("浼佷笟寰俊娲惧崟閫氱煡鍙戦�佸紓甯革紝taskId={}, userId={}", taskId, assigneeId, e);
+            }
+        }
+        
+        log.info("浼佷笟寰俊娲惧崟閫氱煡鍙戦�佸畬鎴愶紝taskId={}, 鎴愬姛鏁伴噺={}/{}", taskId, successCount, assigneeIds.size());
+    }
+
+    /**
      * 鍚戞墽琛屼汉鍙戦�佷换鍔″垎閰嶉�氱煡
      * @param assigneeIds
      * @param creatorId
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/NotifyTaskMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/NotifyTaskMapper.java
index 96ecce9..6a067f9 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/NotifyTaskMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/NotifyTaskMapper.java
@@ -47,6 +47,15 @@
     int countByTaskUserType(@Param("taskId") Long taskId, @Param("userId") Long userId, @Param("notifyType") String notifyType);
 
     /**
+     * 鎸塼askId鍜岄�氱煡绫诲瀷鏌ヨ鎵�鏈夐�氱煡浠诲姟
+     *
+     * @param taskId     涓氬姟浠诲姟ID
+     * @param notifyType 閫氱煡绫诲瀷
+     * @return 閫氱煡浠诲姟鍒楄〃
+     */
+    List<NotifyTask> selectByTaskIdAndType(@Param("taskId") Long taskId, @Param("notifyType") String notifyType);
+
+    /**
      * 鏂板閫氱煡浠诲姟
      * 
      * @param notifyTask 閫氱煡浠诲姟
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskMapper.java
index 77d0170..c3e01e0 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysTaskMapper.java
@@ -2,11 +2,13 @@
 
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
 import com.ruoyi.common.annotation.DataSource;
 import com.ruoyi.common.enums.DataSourceType;
 import com.ruoyi.system.domain.SysTask;
 import com.ruoyi.system.domain.vo.TaskQueryVO;
+import com.ruoyi.system.domain.vo.DeptOrderStatVO;
 import com.ruoyi.system.domain.vo.TaskStatisticsVO;
 import org.apache.ibatis.annotations.Param;
 
@@ -153,10 +155,10 @@
     /**
      * 鏌ヨ杞﹁締鍦ㄦ寚瀹氭椂闂磋寖鍥村唴鐨勪换鍔″垪琛�
      * 
-     * @param params鍖呭惈vehicleId銆乻tartTime銆乪ndTime鐨勫弬鏁癕ap
+     * @param params 鍖呭惈vehicleId銆乻tartTime銆乪ndTime鐨勫弬鏁癕ap
      * @return 浠诲姟鍒楄〃
      */
-    public List<SysTask> selectVehicleTasksInTimeRange(java.util.Map<String, Object> params);
+    public List<SysTask> selectVehicleTasksInTimeRange(Map<String, Object> params);
     
     /**
      * 浼樺寲鐨勫鐮佹煡璇㈡柟娉曪紝鍏宠仈sys_task_emergency琛ㄥ苟璁$畻dispatchCode鍜宻erviceCode
@@ -165,4 +167,16 @@
      * @return 浠诲姟绠$悊闆嗗悎
      */
     public List<SysTask> selectSysTaskListByMultiCodeOptimized(TaskQueryVO queryVO);
+    /**
+     * 鎸夊垎鍏徃鎸夊ぉ缁熻褰曞崟鏁伴噺
+     *
+     * @param deptIds   鍒嗗叕鍙窱D鍒楄〃锛堜负null鏃舵煡鍏ㄩ儴锛�
+     * @param startDate 寮�濮嬫棩鏈燂紝yyyy-MM-dd
+     * @param endDate   缁撴潫鏃ユ湡锛寉yyy-MM-dd
+     * @return 缁熻缁撴灉鍒楄〃
+     */
+    List<DeptOrderStatVO> selectDeptOrderStat(
+            @Param("deptIds") List<Long> deptIds,
+            @Param("startDate") String startDate,
+            @Param("endDate") String endDate);
 }
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/INotifyTaskService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/INotifyTaskService.java
index eeaa253..01e0d40 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/INotifyTaskService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/INotifyTaskService.java
@@ -46,6 +46,15 @@
     boolean existsNotifyTask(Long taskId, Long userId, String notifyType);
 
     /**
+     * 鎸塼askId鍜岄�氱煡绫诲瀷鏌ヨ鎵�鏈夐�氱煡浠诲姟
+     *
+     * @param taskId     涓氬姟浠诲姟ID
+     * @param notifyType 閫氱煡绫诲瀷
+     * @return 閫氱煡浠诲姟鍒楄〃
+     */
+    List<NotifyTask> selectByTaskIdAndType(Long taskId, String notifyType);
+
+    /**
      * 鍒涘缓閫氱煡浠诲姟锛堝甫闃查噸锛�
      * 濡傛灉宸插瓨鍦ㄥ垯杩斿洖null锛屽惁鍒欏垱寤哄苟杩斿洖
      * 
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
index 3def5e1..21f81b5 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysTaskService.java
@@ -364,4 +364,14 @@
      */
     public boolean checkTaskDuplicate(String phone, String createDate);
 
+    /**
+     * 鎸夊垎鍏徃鎸夊ぉ缁熻褰曞崟鏁伴噺
+     *
+     * @param deptIds   鍒嗗叕鍙窱D鍒楄〃锛堜负null鎴栫┖鏃舵煡鍏ㄩ儴锛�
+     * @param startDate 寮�濮嬫棩鏈燂紝yyyy-MM-dd
+     * @param endDate   缁撴潫鏃ユ湡锛寉yyy-MM-dd
+     * @return 缁熻缁撴灉鍒楄〃
+     */
+    List<DeptOrderStatVO> selectDeptOrderStat(List<Long> deptIds, String startDate, String endDate);
+
 }
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java
index 997cc28..545b026 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/LegacyTransferSyncServiceImpl.java
@@ -3,6 +3,7 @@
 import com.ruoyi.common.core.domain.entity.SysDept;
 import com.ruoyi.common.core.domain.entity.SysUser;
 import com.ruoyi.common.utils.*;
+import com.ruoyi.system.domain.SysTask;
 import com.ruoyi.system.domain.SysTaskEmergency;
 import com.ruoyi.system.domain.VehicleInfo;
 import com.ruoyi.system.domain.enums.TaskStatus;
@@ -17,6 +18,9 @@
 import com.ruoyi.system.mapper.VehicleInfoMapper;
 import com.ruoyi.system.service.ISysUserService;
 import com.ruoyi.system.service.IWechatTaskNotifyService;
+import com.ruoyi.system.service.INotifyTaskService;
+import com.ruoyi.system.service.INotifyDispatchService;
+import com.ruoyi.system.domain.NotifyTask;
 import com.ruoyi.system.utils.TaskStatusConverter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -28,8 +32,10 @@
 import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * 鏃х郴缁熻浆杩愬崟鍚屾Service涓氬姟灞傚鐞�
@@ -66,35 +72,71 @@
     @Autowired
     private IWechatTaskNotifyService wechatTaskNotifyService;
 
+    @Autowired
+    private INotifyTaskService notifyTaskService;
+
+    @Autowired
+    private INotifyDispatchService notifyDispatchService;
+
 
 
     /**
      * 鍚屾鎸囧畾鏃ユ湡鑼冨洿鐨勬棫绯荤粺杞繍鍗曞埌鏂扮郴缁�
-     * 
-     * @param daysAgo 澶氬皯澶╁墠鐨勬暟鎹紙濡�7琛ㄧず7澶╁墠鐨勬暟鎹級
+     * 浼樺寲锛氬皢澶氬ぉ鑼冨洿鎷嗗垎涓洪�愬ぉ寰幆锛屾瘡娆′粎鏌ヨ1澶╂暟鎹紝閬垮厤澶ф暟鎹噺瀵艰嚧SQL Server瓒呮椂
+     *
+     * @param daysAgo 澶氬皯澶╁墠鐨勬暟鎹紙濡�7琛ㄧず鍚屾鏈�杩�7澶╃殑鏁版嵁锛�
      * @return 鎴愬姛鍚屾鐨勮浆杩愬崟鏁伴噺
      */
     @Override
     public int syncLegacyTransferOrders(int daysAgo) {
-//        log.info("寮�濮嬪悓姝}澶╁墠鐨勬棫绯荤粺杞繍鍗曟暟鎹�", daysAgo);
-        
         try {
             // 鍙傛暟楠岃瘉
             if (daysAgo <= 0) {
                 log.error("澶╂暟鍙傛暟蹇呴』澶т簬0");
                 return 0;
             }
-            
-            // 璁$畻鏃ユ湡鑼冨洿
-            Date startDate = DateUtils.addDays(new Date(), -daysAgo);
-            String startDateStr = DateUtils.parseDateToStr("yyyy-MM-dd", startDate);
-            String endDateStr = DateUtils.parseDateToStr("yyyy-MM-dd", new Date());
-            
-            // Keyset娓告爣鍒嗛〉浠� SQL Server 鎷夊彇杞繍鍗曟暟鎹紝姣忛〉 10 鏉★紝璧颁富閿储寮曞交搴曡閬胯秴鏃�
-            final int PAGE_SIZE = 5;
-            long lastId = 0L;   // 娓告爣锛氳褰曚笂涓�椤垫渶鍚庝竴鏉$殑 ServiceOrdID锛岄娆′紶 0
-            int successCount = 0;
 
+            log.info("[杞繍鍗曞悓姝 寮�濮嬪悓姝ワ紝鑼冨洿: 鏈�杩憑}澶�", daysAgo);
+            int totalSuccessCount = 0;
+            int totalDays = daysAgo + 1;
+
+            // 鎸夊ぉ鎷嗗垎锛屾瘡娆″彧鍚屾1澶╃殑鏁版嵁锛岄伩鍏嶅ぇ鑼冨洿鏌ヨ瓒呮椂
+            for (int i = daysAgo; i >= 0; i--) {
+                Date dayStart = DateUtils.addDays(new Date(), -i);
+                String dayStartStr = DateUtils.parseDateToStr("yyyy-MM-dd", dayStart) + " 00:00:00";
+                String dayEndStr   = DateUtils.parseDateToStr("yyyy-MM-dd", dayStart) + " 23:59:59";
+
+                int dayIndex = totalDays - i;
+                log.info("[杞繍鍗曞悓姝 澶勭悊澶� {}/{}: {}", dayIndex, totalDays, dayStartStr);
+                int daySuccessCount = syncSingleDayOrders(dayStartStr, dayEndStr);
+                totalSuccessCount += daySuccessCount;
+                log.info("[杞繍鍗曞悓姝 {} 瀹屾垚锛屾柊澧炲悓姝�: {}鏉★紝绱: {}鏉�", dayStartStr, daySuccessCount, totalSuccessCount);
+            }
+
+            log.info("[杞繍鍗曞悓姝 鍏ㄩ儴瀹屾垚锛屽叡鏂板鍚屾 {}鏉�", totalSuccessCount);
+            return totalSuccessCount;
+
+        } catch (Exception e) {
+            log.error("鍚屾{}澶╁墠鐨勬棫绯荤粺杞繍鍗曟暟鎹紓甯�", daysAgo, e);
+            return 0;
+        }
+    }
+
+    /**
+     * 鍚屾鍗曞ぉ鐨勮浆杩愬崟鏁版嵁锛圞eyset娓告爣鍒嗛〉锛�
+     *
+     * @param startDateStr 寮�濮嬫棩鏈熷瓧绗︿覆锛坹yyy-MM-dd锛�
+     * @param endDateStr   缁撴潫鏃ユ湡瀛楃涓诧紙yyyy-MM-dd锛�
+     * @return 鎴愬姛鍚屾鐨勮浆杩愬崟鏁伴噺
+     */
+    private int syncSingleDayOrders(String startDateStr, String endDateStr) {
+        final int PAGE_SIZE = 5;
+        long lastId = 0L;
+        int successCount = 0;
+        int pageNum = 0;
+        int totalProcessed = 0;
+
+        try {
             while (true) {
                 List<Map<String, Object>> transferOrders = legacyTransferSyncMapper.selectTransferOrders(startDateStr, endDateStr, lastId, PAGE_SIZE);
 
@@ -102,36 +144,39 @@
                     break;
                 }
 
+                pageNum++;
                 int totalCount = transferOrders.size();
                 int processedCount = 0;
 
+                log.info("[杞繍鍗曞悓姝 {} 绗瑊}椤碉紝鑾峰彇{}.鏉℃暟鎹紝lastId={}", startDateStr, pageNum, totalCount, lastId);
+
                 for (Map<String, Object> order : transferOrders) {
                     processedCount++;
+                    totalProcessed++;
                     try {
                         Long serviceOrdID = MapValueUtils.getLongValue(order, "ServiceOrdID");
                         Long dispatchOrdID = MapValueUtils.getLongValue(order, "DispatchOrdID");
 
-                        // 妫�鏌ュ弬鏁版湁鏁堟��
                         if (serviceOrdID == null || serviceOrdID <= 0) {
                             log.warn("绗瑊}鏉℃暟鎹湇鍔″崟ID涓虹┖锛岃烦杩囧鐞�", processedCount);
                             continue;
                         }
 
-//                        log.debug("姝e湪澶勭悊绗瑊}/{}鏉¤浆杩愬崟: ServiceOrdID={}, DispatchOrdID={}",
-//                                 processedCount, totalCount, serviceOrdID, dispatchOrdID);
-
                         // 妫�鏌ユ槸鍚﹀凡鍚屾
                         if (isTransferOrderSynced(serviceOrdID, dispatchOrdID)) {
-//                            log.debug("杞繍鍗曞凡鍚屾锛岃烦杩�: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
-                            //杩涜鏇存柊鎿嶄綔
+                            log.debug("[杞繍鍗曞悓姝 宸插瓨鍦紝鎵ц鏇存柊: ServiceOrdID={}", serviceOrdID);
                             updateTransferOrder(serviceOrdID, dispatchOrdID, order);
                             continue;
                         }
 
                         // 鍚屾鍗曚釜杞繍鍗�
+                        log.info("[杞繍鍗曞悓姝 鏂板鍚屾: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
                         boolean success = syncSingleTransferOrder(serviceOrdID, dispatchOrdID, order);
                         if (success) {
                             successCount++;
+                            log.info("[杞繍鍗曞悓姝 鍚屾鎴愬姛: ServiceOrdID={}, 褰撳ぉ鏂板绱: {}", serviceOrdID, successCount);
+                        } else {
+                            log.warn("[杞繍鍗曞悓姝 鍚屾澶辫触: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID);
                         }
 
                         // 鎺у埗鍚屾棰戠巼锛岄伩鍏嶈姹傝繃蹇�
@@ -139,7 +184,7 @@
                     } catch (InterruptedException ie) {
                         log.warn("鍚屾浠诲姟琚腑鏂�");
                         Thread.currentThread().interrupt();
-                        break;
+                        return successCount;
                     } catch (Exception e) {
                         log.error("鍚屾鍗曚釜杞繍鍗曞け璐�: ServiceOrdID={}, DispatchOrdID={}",
                                 MapValueUtils.getStringValue(order, "ServiceOrdID"),
@@ -162,13 +207,12 @@
                 }
             }
 
-//            log.info("鍚屾瀹屾垚锛屾垚鍔熷悓姝}鏉¤浆杩愬崟鏁版嵁", successCount);
-            return successCount;
-            
+            log.info("[杞繍鍗曞悓姝 {} 鍒嗛〉瀹屾垚锛屽叡澶勭悊: {}鏉★紝鏂板鍚屾: {}鏉�", startDateStr, totalProcessed, successCount);
         } catch (Exception e) {
-            log.error("鍚屾{}澶╁墠鐨勬棫绯荤粺杞繍鍗曟暟鎹紓甯�", daysAgo, e);
-            return 0;
+            log.error("鍚屾鍗曞ぉ杞繍鍗曟暟鎹紓甯�: date={}", startDateStr, e);
         }
+
+        return successCount;
     }
     
     /**
@@ -288,7 +332,13 @@
 //                log.info("杞繍鍗曞悓姝ユ垚鍔�: ServiceOrdID={}, DispatchOrdID={}, 鍒涘缓鐨勪换鍔D={}", serviceOrdID, dispatchOrdID, result);
 
                 try {
-                    notifyTransferOrderByWechat((long) result, serviceOrdID, dispatchOrdID, serviceOrdNo, ServiceOrd_CC_Time, dept, order);
+                    // 鐩存帴浣跨敤鏂规硶澶撮儴宸叉煡璇㈢殑 emergency 鑾峰彇 taskId
+                    Long taskId = emergency.getTaskId();
+                    if (taskId != null) {
+                        notifyTransferOrderByWechat(taskId, serviceOrdID, dispatchOrdID, serviceOrdNo, ServiceOrd_CC_Time, dept, order);
+                    } else {
+                        log.warn("鏇存柊鍚庢壘涓嶅埌taskId锛岃烦杩囬�氱煡: ServiceOrdID={}", serviceOrdID);
+                    }
                 } catch (Exception e) {
                     log.error("杞繍鍗曞悓姝ユ垚鍔熷悗鍙戦�佸井淇¢�氱煡澶辫触: ServiceOrdID={}, DispatchOrdID={}", serviceOrdID, dispatchOrdID, e);
                 }
@@ -873,27 +923,123 @@
                                              SysDept dept,
                                              Map<String, Object> order) {
         try {
-            // 鑾峰彇閫氱煡鎺ユ敹浜哄垪琛�
-            List<SysUser> receivers = getWechatNotifyUsers(dispatchOrdID, dept);
-            if (receivers == null || receivers.isEmpty()) {
-//                log.info("鏃х郴缁熷悓姝ヨ浆杩愬崟鏃犲彲鐢ㄥ井淇℃帴鏀朵汉锛宼askId={}", taskId);
+            // 1. 鑾峰彇鎵ц浜哄垪琛�
+            List<TaskCreateVO.AssigneeInfo> assignees = queryAssignees(dispatchOrdID);
+            if (assignees.isEmpty()) {
+                log.info("鏃х郴缁熷悓姝ヨ浆杩愬崟鏃犳墽琛屼汉锛宼askId={}", taskId);
                 return;
             }
 
-            // 鎻愬彇鎺ユ敹浜� ID 鍒楄〃
-            List<Long> userIds = new ArrayList<>();
-            for (SysUser user : receivers) {
-                if (user != null && user.getUserId() != null) {
-                    userIds.add(user.getUserId());
+            // 2. 鏌ヨ浠诲姟鑾峰彇showTaskCode
+            SysTask sysTask = sysTaskService.getTaskDetail(taskId);
+            String showTaskCode = sysTask != null ? sysTask.getShowTaskCode() : serviceOrdNo;
+
+            // 3. 鏋勫缓閫氱煡鍐呭
+            String notifyContent = buildLegacyNotifyContent(showTaskCode, serviceOrdCcTime, order);
+
+            // 4. 鏌ヨ璇askId宸叉湁鐨勯�氱煡璁板綍锛屾敹闆嗗凡瀛樺湪鐨剈serId闆嗗悎
+            List<NotifyTask> existingTasks = notifyTaskService.selectByTaskIdAndType(taskId, NotifyTask.NOTIFY_TYPE_TASK_ASSIGN);
+            Set<Long> existingUserIds = new HashSet<>();
+            List<NotifyTask> pendingTasks = new ArrayList<>();
+            if (existingTasks != null && !existingTasks.isEmpty()) {
+                for (NotifyTask t : existingTasks) {
+                    existingUserIds.add(t.getUserId());
+                    // 灏嗘湭瀹屾垚鐨勮褰曟敹闆嗙粰寰呭垎鍙戝垪琛�
+                    if (!NotifyTask.STATUS_COMPLETED.equals(t.getStatus())) {
+                        pendingTasks.add(t);
+                    }
                 }
             }
 
-            // 璋冪敤缁熶竴鐨勫井淇¢�氱煡鏈嶅姟
-            int successCount = wechatTaskNotifyService.sendTaskNotifyMessage(taskId, userIds);
-//            log.info("鏃х郴缁熷悓姝ヨ浆杩愬崟寰俊閫氱煡鍙戦�佸畬鎴愶紝taskId={}, 鎴愬姛={}", taskId, successCount);
+            // 5. 鍙鏂版墽琛屼汉鍒涘缓閫氱煡浠诲姟
+            List<NotifyTask> notifyTasks = new ArrayList<>(pendingTasks);
+            for (TaskCreateVO.AssigneeInfo assignee : assignees) {
+                if (assignee == null || assignee.getUserId() == null) {
+                    continue;
+                }
+                // 璇ョ敤鎴峰凡鏈夐�氱煡璁板綍锛岃烦杩�
+                if (existingUserIds.contains(assignee.getUserId())) {
+                    log.info("鐢ㄦ埛宸叉湁閫氱煡璁板綍锛岃烦杩囧垱寤猴紝taskId={}, userId={}", taskId, assignee.getUserId());
+                    continue;
+                }
+
+                SysUser user = sysUserService.selectUserById(assignee.getUserId());
+                if (user == null) {
+                    log.warn("鎵句笉鍒版墽琛屼汉鐢ㄦ埛淇℃伅锛寀serId={}", assignee.getUserId());
+                    continue;
+                }
+
+                NotifyTask notifyTask = new NotifyTask();
+                notifyTask.setTaskId(taskId);
+                notifyTask.setTaskCode(showTaskCode);
+                notifyTask.setNotifyType(NotifyTask.NOTIFY_TYPE_TASK_ASSIGN);
+                notifyTask.setUserId(user.getUserId());
+                notifyTask.setUserName(user.getNickName());
+                notifyTask.setUserPhone(user.getPhonenumber());
+                notifyTask.setTitle("杞繍鍗曚换鍔℃淳鍗曢�氱煡");
+                notifyTask.setContent(notifyContent);
+                notifyTask.setCreateBy("绯荤粺鍚屾");
+
+                NotifyTask created = notifyTaskService.createNotifyTask(notifyTask);
+                if (created != null) {
+                    notifyTasks.add(created);
+                    log.info("鍒涘缓閫氱煡浠诲姟鎴愬姛锛宨d={}, userId={}", created.getId(), user.getUserId());
+                }
+            }
+
+            // 6. 鍒嗗彂閫氱煡浠诲姟
+            if (!notifyTasks.isEmpty()) {
+                int successCount = notifyDispatchService.dispatchNotifies(notifyTasks);
+                log.info("鏃х郴缁熷悓姝ヨ浆杩愬崟閫氱煡鍒嗗彂瀹屾垚锛宼askId={}, 鍒嗗彂鏁伴噺={}, 鎴愬姛鏁伴噺={}",
+                        taskId, notifyTasks.size(), successCount);
+            } else {
+                log.info("鏃х郴缁熷悓姝ヨ浆杩愬崟鏃犻渶鏂板閫氱煡锛宼askId={}", taskId);
+            }
+
+            // 5. 鍚屾椂淇濈暀鍘熸湁鐨勫井淇¢�氱煡鏈嶅姟锛堝吋瀹癸級
+            // List<Long> userIds = new ArrayList<>();
+            // for (TaskCreateVO.AssigneeInfo assignee : assignees) {
+            //     if (assignee != null && assignee.getUserId() != null) {
+            //         userIds.add(assignee.getUserId());
+            //     }
+            // }
+            // if (!userIds.isEmpty()) {
+            //     int wxCount = wechatTaskNotifyService.sendTaskNotifyMessage(taskId, userIds);
+            //     log.info("鏃х郴缁熷悓姝ヨ浆杩愬崟寰俊閫氱煡鍙戦�佸畬鎴愶紝taskId={}, 鎴愬姛={}", taskId, wxCount);
+            // }
+
         } catch (Exception e) {
             log.error("notifyTransferOrderByWechat鍙戠敓寮傚父, serviceOrdID={}, dispatchOrdID={}", serviceOrdID, dispatchOrdID, e);
         }
+    }
+
+    /**
+     * 鏋勫缓鏃х郴缁熷悓姝ヨ浆杩愬崟鐨勯�氱煡鍐呭
+     */
+    private String buildLegacyNotifyContent(String serviceOrdNo, Date serviceOrdCcTime, Map<String, Object> order) {
+        StringBuilder content = new StringBuilder();
+        content.append("鎮ㄦ湁鏂扮殑杞繍浠诲姟锛屼换鍔″崟鍙�:").append(serviceOrdNo);
+
+        // 鍑哄彂鏃堕棿
+        if (serviceOrdCcTime != null) {
+            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+            content.append("锛屽嚭鍙戞椂闂�:").append(df.format(serviceOrdCcTime));
+        }
+
+        // 鍑哄彂鍦�
+        String departure = MapValueUtils.getStringValue(order, "ServiceOrdTraVia");
+        if (StringUtils.isNotEmpty(departure)) {
+            content.append("锛屽嚭鍙戝湴:").append(departure);
+        }
+
+        // 鐩殑鍦�
+        String destination = MapValueUtils.getStringValue(order, "ServiceOrdTraEnd");
+        if (StringUtils.isNotEmpty(destination)) {
+            content.append("锛岀洰鐨勫湴:").append(destination);
+        }
+
+        content.append("锛岃鍙婃椂澶勭悊銆�");
+        return content.toString();
     }
 
     private List<SysUser> getWechatNotifyUsers(Long dispatchOrdID, SysDept dept) {
@@ -901,7 +1047,7 @@
             List<SysUser> result = new ArrayList<>();
 
             List<TaskCreateVO.AssigneeInfo> assignees = queryAssignees(dispatchOrdID);
-            if (assignees != null && !assignees.isEmpty()) {
+            if (!assignees.isEmpty()) {
                 for (TaskCreateVO.AssigneeInfo assigneeInfo : assignees) {
                     if (assigneeInfo == null || assigneeInfo.getUserId() == null) {
                         continue;
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/NotifyDispatchServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/NotifyDispatchServiceImpl.java
index 61e9051..7eb559c 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/NotifyDispatchServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/NotifyDispatchServiceImpl.java
@@ -191,6 +191,7 @@
             sendLog.setSendTime(DateUtils.getNowDate());
             sendLog.setSendContent(notifyTask.getContent());
             sendLog.setResponseMsg(errorMsg);
+            sendLog.setSendResult(success ? "鍙戦�佹垚鍔�" : ("鍙戦�佸け璐�: " + (errorMsg != null ? errorMsg : "鏈煡鍘熷洜")));
             
             notifySendLogService.insertNotifySendLog(sendLog);
         } catch (Exception e) {
@@ -341,40 +342,34 @@
      */
     @Override
     public boolean sendQyWechatMessage(NotifyTask notifyTask) {
-        try {
-            // 妫�鏌ヤ紒涓氬井淇℃湇鍔℃槸鍚﹀惎鐢�
-            if (!qyWechatService.isEnabled()) {
-                log.info("浼佷笟寰俊鏈嶅姟宸插叧闂紝璺宠繃鍙戦��");
-                return false;
-            }
-            Long taskId= notifyTask.getTaskId();
-           SysTaskEmergency emergency = this.sysEmergencyTaskService.selectSysTaskEmergencyByTaskId(taskId);
-           if(emergency==null){
-               return false;
-           }
-//           Long dispatchOrderId = emergency.getLegacyDispatchOrdId();
-//          String oldsiteUrl= sysConfigService.selectConfigByKey("oldsite.url");
-//          if(oldsiteUrl==null){
-//              oldsiteUrl="https://sys.966120.com.cn/m_DispatchOrder.gds?DispatchOrdID=";
-//          }
-            String appId=wechatConfig.getAppId();
-           String pathPage="/pagesTask/detail?id="+taskId;
-            // 鍙戦�佷紒涓氬井淇℃秷鎭�
-            boolean success = qyWechatService.sendNotifyMessage(
-                    notifyTask.getUserId(), 
-                    notifyTask.getTitle(), 
-                    notifyTask.getContent(),appId,pathPage
-            );
-            
-            if (success) {
-                log.info("浼佷笟寰俊娑堟伅鍙戦�佹垚鍔燂紝userId={}", notifyTask.getUserId());
-            } else {
-                log.warn("浼佷笟寰俊娑堟伅鍙戦�佸け璐ワ紝userId={}", notifyTask.getUserId());
-            }
-            return success;
-        } catch (Exception e) {
-            log.error("浼佷笟寰俊娑堟伅鍙戦�佸紓甯革紝taskId={}, userId={}", notifyTask.getTaskId(), notifyTask.getUserId(), e);
-            return false;
+        // 妫�鏌ヤ紒涓氬井淇℃湇鍔℃槸鍚﹀惎鐢�
+        if (!qyWechatService.isEnabled()) {
+            throw new RuntimeException("浼佷笟寰俊鏈嶅姟鏈惎鐢�");
         }
+        Long taskId = notifyTask.getTaskId();
+        SysTaskEmergency emergency = this.sysEmergencyTaskService.selectSysTaskEmergencyByTaskId(taskId);
+        if (emergency == null) {
+            throw new RuntimeException("鎵句笉鍒板搴旂殑鎬ユ晳浠诲姟淇℃伅锛宼askId=" + taskId);
+        }
+        // 妫�鏌ョ敤鎴锋槸鍚︾粦瀹氫紒涓氬井淇�
+        Long userId = notifyTask.getUserId();
+        String qyUserId = qyWechatService.getQyUserIdByUserId(userId);
+        if (qyUserId == null || qyUserId.isEmpty()) {
+            throw new RuntimeException("鐢ㄦ埛鏈粦瀹氫紒涓氬井淇D锛寀serId=" + userId);
+        }
+        String appId = wechatConfig.getAppId();
+        String pathPage = "/pagesTask/detail?id=" + taskId;
+        // 鍙戦�佷紒涓氬井淇℃秷鎭�
+        boolean success = qyWechatService.sendNotifyMessage(
+                userId,
+                notifyTask.getTitle(),
+                notifyTask.getContent(), appId, pathPage
+        );
+        if (success) {
+            log.info("浼佷笟寰俊娑堟伅鍙戦�佹垚鍔燂紝userId={}", userId);
+        } else {
+            throw new RuntimeException("浼佷笟寰俊API杩斿洖澶辫触锛寀serId=" + userId);
+        }
+        return true;
     }
 }
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/NotifyTaskServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/NotifyTaskServiceImpl.java
index 98e0ad3..e35bc93 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/NotifyTaskServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/NotifyTaskServiceImpl.java
@@ -58,6 +58,14 @@
     }
 
     /**
+     * 鎸塼askId鍜岄�氱煡绫诲瀷鏌ヨ鎵�鏈夐�氱煡浠诲姟
+     */
+    @Override
+    public List<NotifyTask> selectByTaskIdAndType(Long taskId, String notifyType) {
+        return notifyTaskMapper.selectByTaskIdAndType(taskId, notifyType);
+    }
+
+    /**
      * 鍒涘缓閫氱煡浠诲姟锛堝甫闃查噸锛�
      */
     @Override
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 0681c0a..f5708fe 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
@@ -1851,6 +1851,13 @@
         return count > 0;
     }
 
+    @Override
+    public List<DeptOrderStatVO> selectDeptOrderStat(List<Long> deptIds, String startDate, String endDate) {
+        return sysTaskMapper.selectDeptOrderStat(
+                (deptIds != null && !deptIds.isEmpty()) ? deptIds : null,
+                startDate, endDate);
+    }
+
 
    
 }
diff --git a/ruoyi-system/src/main/resources/mapper/system/NotifyTaskMapper.xml b/ruoyi-system/src/main/resources/mapper/system/NotifyTaskMapper.xml
index be01049..3a11590 100644
--- a/ruoyi-system/src/main/resources/mapper/system/NotifyTaskMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/NotifyTaskMapper.xml
@@ -73,6 +73,11 @@
         where task_id = #{taskId} and user_id = #{userId} and notify_type = #{notifyType}
     </select>
 
+    <select id="selectByTaskIdAndType" resultMap="NotifyTaskResult">
+        <include refid="selectNotifyTaskVo"/>
+        where task_id = #{taskId} and notify_type = #{notifyType}
+    </select>
+
     <insert id="insertNotifyTask" parameterType="NotifyTask" useGeneratedKeys="true" keyProperty="id">
         insert into sys_notify_task (
             task_id, task_code, notify_type, user_id, user_name, user_phone,
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml
index fb24bf8..5c76e9d 100644
--- a/ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/SysTaskMapper.xml
@@ -525,4 +525,25 @@
             END,
             t.create_time desc
     </select>
+
+    <!-- 鎸夊垎鍏徃鎸夊ぉ缁熻褰曞崟鏁伴噺 -->
+    <select id="selectDeptOrderStat" resultType="com.ruoyi.system.domain.vo.DeptOrderStatVO">
+        SELECT
+            d.dept_id   AS deptId,
+            d.dept_name AS deptName,
+            DATE_FORMAT(t.create_time, '%Y-%m-%d') AS statDate,
+            COUNT(t.task_id) AS orderCount
+        FROM sys_task t
+        LEFT JOIN sys_dept d ON t.dept_id = d.dept_id
+        WHERE t.del_flag = '0'
+          AND DATE(t.create_time) BETWEEN #{startDate} AND #{endDate}
+          <if test="deptIds != null and deptIds.size() > 0">
+              AND t.dept_id IN
+              <foreach collection="deptIds" item="id" open="(" separator="," close=")">
+                  #{id}
+              </foreach>
+          </if>
+        GROUP BY d.dept_id, d.dept_name, DATE_FORMAT(t.create_time, '%Y-%m-%d')
+        ORDER BY d.dept_name, statDate
+    </select>
 </mapper>
diff --git a/ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml b/ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml
index 984d7f1..df86c7f 100644
--- a/ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/VehicleGpsMapper.xml
@@ -37,10 +37,10 @@
             <if test="direction != null "> and direction = #{direction}</if>
             <if test="collectTime != null "> and collect_time = #{collectTime}</if>
             <if test="beginTime != null and beginTime != ''"><!-- 寮�濮嬫椂闂存绱� -->
-                AND date_format(collect_time,'%y%m%d') &gt;= date_format(#{beginTime},'%y%m%d')
+                AND collect_time &gt;= #{beginTime}
             </if>
             <if test="endTime != null and endTime != ''"><!-- 缁撴潫鏃堕棿妫�绱� -->
-                AND date_format(collect_time,'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d')
+                AND collect_time &lt;= #{endTime}
             </if>
         </where>
         order by collect_time desc
diff --git a/ruoyi-ui/src/api/system/dept.js b/ruoyi-ui/src/api/system/dept.js
index 71aef07..2785154 100644
--- a/ruoyi-ui/src/api/system/dept.js
+++ b/ruoyi-ui/src/api/system/dept.js
@@ -74,3 +74,11 @@
     method: 'get'
   })
 }
+
+// 鑾峰彇鎵�鏈夊垎鍏徃鍒楄〃锛坧arentId=100鐨勯儴闂級
+export function listAllBranches() {
+  return request({
+    url: '/system/dept/branch/all',
+    method: 'get'
+  })
+}
diff --git a/ruoyi-ui/src/api/system/gps.js b/ruoyi-ui/src/api/system/gps.js
index 6b13c2d..96e5717 100644
--- a/ruoyi-ui/src/api/system/gps.js
+++ b/ruoyi-ui/src/api/system/gps.js
@@ -88,4 +88,18 @@
       endTime
     }
   })
+}
+
+// 鎸夎溅鐗屽彿鍜屾椂闂磋寖鍥存煡璇㈣建杩癸紙浠庢湰鍦版暟鎹簱tb_vehicle_gps锛屽ぉ鍦板浘杞ㄨ抗椤典娇鐢級
+export function getTracksByPlate(vehicleNo, beginTime, endTime) {
+  return request({
+    url: '/system/gps/tracksByPlate',
+    method: 'get',
+    params: {
+      vehicleNo,
+      beginTime,
+      endTime
+    },
+    timeout: 180000
+  })
 } 
\ No newline at end of file
diff --git a/ruoyi-ui/src/api/system/user.js b/ruoyi-ui/src/api/system/user.js
index b5e3edd..9d727db 100644
--- a/ruoyi-ui/src/api/system/user.js
+++ b/ruoyi-ui/src/api/system/user.js
@@ -134,3 +134,12 @@
     method: 'get'
   })
 }
+
+// 鏇存柊鐢ㄦ埛鍙鐞嗗垎鍏徃锛堜紶鍏eptId鏁扮粍锛�
+export function updateUserBranch(userId, deptIds) {
+  return request({
+    url: '/system/user/branch/' + userId,
+    method: 'put',
+    data: deptIds
+  })
+}
diff --git a/ruoyi-ui/src/api/task.js b/ruoyi-ui/src/api/task.js
index 77b459e..b9d6e10 100644
--- a/ruoyi-ui/src/api/task.js
+++ b/ruoyi-ui/src/api/task.js
@@ -339,3 +339,24 @@
     method: 'get'
   })
 }
+
+// ========== 鍒嗗叕鍙稿綍鍗曠粺璁$浉鍏矨PI ==========
+
+// 鎸夊垎鍏徃鎸夊ぉ缁熻褰曞崟鏁伴噺
+export function getDeptOrderStat(params) {
+  return request({
+    url: '/task/stat/deptOrder',
+    method: 'get',
+    params
+  })
+}
+
+// 瀵煎嚭鍒嗗叕鍙稿綍鍗曠粺璁� Excel
+export function exportDeptOrderStat(params) {
+  return request({
+    url: '/task/stat/deptOrder/export',
+    method: 'get',
+    params,
+    responseType: 'blob'
+  })
+}
diff --git a/ruoyi-ui/src/router/index.js b/ruoyi-ui/src/router/index.js
index 341c2b2..f89f19b 100644
--- a/ruoyi-ui/src/router/index.js
+++ b/ruoyi-ui/src/router/index.js
@@ -131,12 +131,18 @@
     component: (resolve) => require(['@/views/system/payInfoTest/index'], resolve),
     hidden: true,
     meta: { title: '鏀粯淇℃伅娴嬭瘯', anonymous: true }
-  }
-  ,{
+  },
+  {
     path: '/system/gps/mapNeed',
     component: () => import('@/views/system/gps/mapNeed'),
     name: 'GpsMapNeed',
     meta: { title: '杞﹁締杞ㄨ抗', icon: 'map' }
+  },
+  {
+    path: '/system/gps/trackMap',
+    component: () => import('@/views/system/gps/trackMap'),
+    name: 'GpsTrackMap',
+    meta: { title: '杞﹁締琛岄┒杞ㄨ抗', icon: 'map' }
   },
   
   {
@@ -329,6 +335,19 @@
         meta: { title: '浠诲姟璇︽儏', activeMenu: '/task/general' }
       }
     ]
+  },
+  {
+    path: '/task/stat',
+    component: Layout,
+    hidden: false,
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/task/stat/index'),
+        name: 'DeptOrderStat',
+        meta: { title: '鍒嗗叕鍙稿綍鍗曠粺璁�', icon: 'chart', noCache: false }
+      }
+    ]
   }
 ]
 
diff --git a/ruoyi-ui/src/views/system/gps/trackMap.vue b/ruoyi-ui/src/views/system/gps/trackMap.vue
new file mode 100644
index 0000000..80a591c
--- /dev/null
+++ b/ruoyi-ui/src/views/system/gps/trackMap.vue
@@ -0,0 +1,570 @@
+<template>
+  <div class="app-container">
+    <!-- 鏌ヨ鏉′欢 -->
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="杞︾墝鍙�" prop="vehicleNo">
+        <el-autocomplete
+          v-model="queryParams.vehicleNo"
+          :fetch-suggestions="queryVehicleSearch"
+          placeholder="杈撳叆杞︾墝鍙锋悳绱�"
+          size="small"
+          style="width: 200px"
+          clearable
+          :trigger-on-focus="true"
+          value-key="vehicleNo"
+          @select="handleVehicleSelect"
+          @keyup.enter.native="handleQuery"
+        >
+          <template slot-scope="{ item }">
+            <span>{{ item.vehicleNo }}</span>
+            <span style="float:right;color:#909399;font-size:12px;margin-left:12px">{{ item.vehicleType || '' }}</span>
+          </template>
+        </el-autocomplete>
+      </el-form-item>
+      <el-form-item label="鏃堕棿鑼冨洿">
+        <el-date-picker
+          v-model="dateRange"
+          size="small"
+          style="width: 340px"
+          value-format="yyyy-MM-dd HH:mm:ss"
+          type="datetimerange"
+          range-separator="-"
+          start-placeholder="寮�濮嬫椂闂�"
+          end-placeholder="缁撴潫鏃堕棿"
+          :default-time="['00:00:00', '23:59:59']"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" :loading="loading" @click="handleQuery">鏌ヨ</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 缁熻淇℃伅鏉� -->
+    <div class="stats-bar" v-if="gpsList.length > 0">
+      <el-tag type="info" size="small">鍏� {{ gpsList.length }} 涓建杩圭偣</el-tag>
+      <el-tag type="success" size="small" style="margin-left:8px">璧风偣鏃堕棿锛歿{ gpsList[0] && gpsList[0].collectTime }}</el-tag>
+      <el-tag type="warning" size="small" style="margin-left:8px">缁堢偣鏃堕棿锛歿{ gpsList[gpsList.length-1] && gpsList[gpsList.length-1].collectTime }}</el-tag>
+      <el-button-group style="margin-left:16px">
+        <el-button size="mini" @click="showPreviousSegment" :disabled="segmentIndex === 0">
+          <i class="el-icon-arrow-left"></i> 涓婁竴娈�
+        </el-button>
+        <el-button size="mini" @click="showNextSegment" :disabled="(segmentIndex + 1) * segmentSize >= gpsList.length">
+          涓嬩竴娈� <i class="el-icon-arrow-right"></i>
+        </el-button>
+        <el-button size="mini" style="margin-left:8px">
+          绗� {{ segmentIndex + 1 }} / {{ Math.ceil(gpsList.length / segmentSize) }} 娈�
+        </el-button>
+      </el-button-group>
+      <el-button-group style="margin-left:8px">
+        <el-button size="mini" type="primary" icon="el-icon-video-play" :disabled="isPlaying" @click="startPlayback">鍥炴斁</el-button>
+        <el-button size="mini" type="danger" icon="el-icon-video-pause" :disabled="!isPlaying" @click="stopPlayback">鍋滄</el-button>
+        <el-select v-model="playSpeed" size="mini" style="width:90px;margin-left:4px">
+          <el-option label="鎱㈤��" :value="1500" />
+          <el-option label="姝e父" :value="800" />
+          <el-option label="蹇��" :value="300" />
+          <el-option label="鏋侀��" :value="80" />
+        </el-select>
+      </el-button-group>
+    </div>
+
+    <el-row :gutter="10" style="margin-top:10px">
+      <!-- 宸︿晶杞ㄨ抗鍒楄〃 -->
+      <el-col :span="6">
+        <el-card class="track-list-card">
+          <div slot="header">
+            <span>杞ㄨ抗鐐瑰垪琛�</span>
+            <el-tag size="mini" style="float:right" v-if="gpsList.length">{{ currentSegmentStart }}-{{ currentSegmentEnd }} / {{ gpsList.length }}</el-tag>
+          </div>
+          <el-table
+            v-loading="loading"
+            :data="currentSegmentList"
+            height="560"
+            size="mini"
+            highlight-current-row
+            @row-click="handleRowClick"
+          >
+            <el-table-column label="鏃堕棿" prop="collectTime" min-width="100">
+              <template slot-scope="scope">
+                <span style="font-size:11px">{{ scope.row.collectTime }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="閫熷害" prop="speed" width="65" align="center">
+              <template slot-scope="scope">
+                <el-tag size="mini" :type="getSpeedTagType(scope.row.speed)">{{ formatSpeed(scope.row.speed) }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="缁忓害" prop="longitude" width="80" align="center">
+              <template slot-scope="scope">
+                <span style="font-size:11px">{{ scope.row.longitude }}</span>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </el-col>
+
+      <!-- 鍙充晶澶╁湴鍥� -->
+      <el-col :span="18">
+        <div style="position:relative">
+          <div id="tianMapContainer" style="height:620px;border-radius:4px;overflow:hidden"></div>
+          <!-- 鍦板浘鍔犺浇涓彁绀� -->
+          <div v-if="mapLoading" class="map-loading-mask">
+            <i class="el-icon-loading" style="font-size:32px;color:#409EFF"></i>
+            <p style="margin-top:8px;color:#409EFF">鍦板浘鍔犺浇涓�...</p>
+          </div>
+          <!-- 鏃犳暟鎹彁绀� -->
+          <div v-if="!mapLoading && gpsList.length === 0 && !loading" class="map-empty-tip">
+            <i class="el-icon-map-location" style="font-size:48px;color:#909399"></i>
+            <p style="margin-top:8px;color:#909399">璇疯緭鍏ヨ溅鐗屽彿鍜屾椂闂磋寖鍥存煡璇㈣建杩�</p>
+          </div>
+        </div>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { getTracksByPlate } from '@/api/system/gps'
+import { listVehicle } from '@/api/system/vehicle'
+
+// 澶╁湴鍥� API Key锛堜笌鍚庣閰嶇疆淇濇寔涓�鑷达級
+const TIAN_DI_TU_TK = '079300b89bce333ead2df1476c43ecb0'
+
+export default {
+  name: 'GpsTrackMap',
+  data() {
+    return {
+      loading: false,
+      mapLoading: true,
+      showSearch: true,
+      gpsList: [],
+      dateRange: [],
+      // 杞︾墝鍙蜂笅鎷夋悳绱�
+      vehicleOptions: [],
+      vehicleSearchLoading: false,
+      queryParams: {
+        vehicleNo: '',
+        beginTime: null,
+        endTime: null
+      },
+      // 鍦板浘瀵硅薄
+      map: null,
+      // 杞ㄨ抗瑕嗙洊鐗�
+      trackPolyline: null,
+      markerStart: null,
+      markerEnd: null,
+      markerCurrent: null,
+      infoWindow: null,
+      // 鍒嗘
+      segmentIndex: 0,
+      segmentSize: 200,
+      // 鍥炴斁
+      isPlaying: false,
+      playTimer: null,
+      playIndex: 0,
+      playSpeed: 800
+    }
+  },
+  computed: {
+    currentSegmentStart() {
+      return this.segmentIndex * this.segmentSize + 1
+    },
+    currentSegmentEnd() {
+      return Math.min((this.segmentIndex + 1) * this.segmentSize, this.gpsList.length)
+    },
+    currentSegmentList() {
+      return this.gpsList.slice(this.currentSegmentStart - 1, this.currentSegmentEnd)
+    }
+  },
+  mounted() {
+    // 鍒濆鍖栭粯璁ゆ椂闂翠负浠婂ぉ
+    this.initDefaultDateRange()
+    // 鍔犺浇澶╁湴鍥�
+    this.loadTianDiTuScript().then(() => {
+      this.initMap()
+    })
+  },
+  methods: {
+    /** 杞︾墝鍙疯嚜鍔ㄥ畬鎴愭悳绱� */
+    queryVehicleSearch(queryStr, callback) {
+      const keyword = queryStr || ''
+      listVehicle({ vehicleNo: keyword, pageSize: 30, pageNum: 1 }).then(res => {
+        const list = (res.rows || []).map(item => ({
+          vehicleId: item.vehicleId,
+          vehicleNo: item.vehicleNo,
+          vehicleType: item.vehicleType,
+          value: item.vehicleNo
+        }))
+        callback(list)
+      }).catch(() => {
+        callback([])
+      })
+    },
+    /** 閫変腑杞︾墝鍙� */
+    handleVehicleSelect(item) {
+      this.queryParams.vehicleNo = item.vehicleNo
+    },
+    /** 杩滅▼鎼滅储杞︾墝鍙� */
+    remoteSearchVehicle(query) {
+      if (query === '' || query === null) {
+        this.vehicleOptions = []
+        return
+      }
+      this.vehicleSearchLoading = true
+      listVehicle({ vehicleNo: query, pageSize: 20, pageNum: 1 }).then(res => {
+        this.vehicleOptions = res.rows || []
+        this.vehicleSearchLoading = false
+      }).catch(() => {
+        this.vehicleSearchLoading = false
+      })
+    },
+    /** 鍒濆鍖栭粯璁ゆ椂闂磋寖鍥达紙浠婂ぉ锛� */
+    initDefaultDateRange() {
+      const now = new Date()
+      const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
+      const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
+      this.dateRange = [this.formatDatetime(start), this.formatDatetime(end)]
+    },
+    /** 鏍煎紡鍖栨棩鏈熶负瀛楃涓� */
+    formatDatetime(date) {
+      const y = date.getFullYear()
+      const m = String(date.getMonth() + 1).padStart(2, '0')
+      const d = String(date.getDate()).padStart(2, '0')
+      const h = String(date.getHours()).padStart(2, '0')
+      const min = String(date.getMinutes()).padStart(2, '0')
+      const s = String(date.getSeconds()).padStart(2, '0')
+      return `${y}-${m}-${d} ${h}:${min}:${s}`
+    },
+    /** 鏍煎紡鍖栭�熷害鏄剧ず */
+    formatSpeed(speed) {
+      if (speed == null) return '0'
+      return parseFloat(speed).toFixed(1)
+    },
+    /** 閫熷害鏍囩绫诲瀷 */
+    getSpeedTagType(speed) {
+      const v = parseFloat(speed) || 0
+      if (v === 0) return 'info'
+      if (v < 60) return 'success'
+      if (v < 100) return 'warning'
+      return 'danger'
+    },
+    /** 鍔犺浇澶╁湴鍥� JS API */
+    loadTianDiTuScript() {
+      return new Promise((resolve, reject) => {
+        if (window.T) {
+          resolve()
+          return
+        }
+        const script = document.createElement('script')
+        script.type = 'text/javascript'
+        script.src = `http://api.tianditu.gov.cn/api?v=4.0&tk=${TIAN_DI_TU_TK}`
+        script.onload = () => {
+          // 绛夊緟 T 瀵硅薄鍙敤
+          const checkT = setInterval(() => {
+            if (window.T) {
+              clearInterval(checkT)
+              resolve()
+            }
+          }, 100)
+        }
+        script.onerror = (err) => {
+          console.error('澶╁湴鍥捐剼鏈姞杞藉け璐�', err)
+          reject(err)
+        }
+        document.head.appendChild(script)
+      })
+    },
+    /** 鍒濆鍖栧ぉ鍦板浘 */
+    initMap() {
+      try {
+        this.map = new T.Map('tianMapContainer')
+        // 榛樿灞呬腑骞垮窞
+        this.map.centerAndZoom(new T.LngLat(113.33, 23.12), 11)
+        // 娣诲姞鎺т欢
+        this.map.addControl(new T.Control.Zoom())
+        this.map.addControl(new T.Control.Scale())
+        this.mapLoading = false
+      } catch (e) {
+        console.error('澶╁湴鍥惧垵濮嬪寲澶辫触', e)
+        this.$message.error('鍦板浘鍒濆鍖栧け璐ワ紝璇峰埛鏂伴噸璇�')
+        this.mapLoading = false
+      }
+    },
+    /** 鏌ヨ鎸夐挳 */
+    handleQuery() {
+      if (!this.queryParams.vehicleNo || !this.queryParams.vehicleNo.trim()) {
+        this.$message.warning('璇疯緭鍏ヨ溅鐗屽彿')
+        return
+      }
+      if (!this.dateRange || this.dateRange.length !== 2) {
+        this.$message.warning('璇烽�夋嫨鏃堕棿鑼冨洿')
+        return
+      }
+      this.queryParams.beginTime = this.dateRange[0]
+      this.queryParams.endTime = this.dateRange[1]
+      this.segmentIndex = 0
+      this.stopPlayback()
+      this.getTrackData()
+    },
+    /** 閲嶇疆 */
+    resetQuery() {
+      this.$refs.queryForm.resetFields()
+      this.initDefaultDateRange()
+      this.gpsList = []
+      this.segmentIndex = 0
+      this.stopPlayback()
+      this.clearMap()
+    },
+    /** 鑾峰彇杞ㄨ抗鏁版嵁 */
+    getTrackData() {
+      this.loading = true
+      getTracksByPlate(
+        this.queryParams.vehicleNo.trim(),
+        this.queryParams.beginTime,
+        this.queryParams.endTime
+      ).then(res => {
+        this.gpsList = (res.rows || []).sort((a, b) => {
+          return new Date(a.collectTime) - new Date(b.collectTime)
+        })
+        this.loading = false
+        if (this.gpsList.length === 0) {
+          this.$message.info('璇ユ椂闂存鍐呮湭鏌ヨ鍒拌建杩规暟鎹�')
+          this.clearMap()
+        } else {
+          this.$message.success(`鍏辨煡璇㈠埌 ${this.gpsList.length} 涓建杩圭偣`)
+          this.drawTrack()
+        }
+      }).catch(err => {
+        this.loading = false
+        this.$message.error('鏌ヨ杞ㄨ抗澶辫触锛�' + (err.message || '鏈煡閿欒'))
+      })
+    },
+    /** 娓呯┖鍦板浘瑕嗙洊鐗� */
+    clearMap() {
+      if (!this.map) return
+      this.map.clearOverLays()
+      this.trackPolyline = null
+      this.markerStart = null
+      this.markerEnd = null
+      this.markerCurrent = null
+    },
+    /** 缁樺埗杞ㄨ抗 */
+    drawTrack() {
+      if (!this.map) return
+      this.clearMap()
+
+      const segment = this.currentSegmentList
+      if (!segment || segment.length === 0) return
+
+      // 鏋勫缓杞ㄨ抗鐐规暟缁�
+      const points = segment
+        .filter(p => p.longitude != null && p.latitude != null)
+        .map(p => new T.LngLat(p.longitude, p.latitude))
+
+      if (points.length < 2) {
+        this.$message.warning('鏈夋晥杞ㄨ抗鐐逛笉瓒筹紝鏃犳硶缁樺埗')
+        return
+      }
+
+      // 缁樺埗杞ㄨ抗绾�
+      this.trackPolyline = new T.Polyline(points, {
+        color: '#3388ff',
+        weight: 4,
+        opacity: 0.85
+      })
+      this.map.addOverLay(this.trackPolyline)
+
+      // 璧风偣鏍囪
+      const startPoint = points[0]
+      const startMarker = this.createStartMarker(startPoint, segment[0])
+      this.map.addOverLay(startMarker)
+      this.markerStart = startMarker
+
+      // 缁堢偣鏍囪锛堣溅杈嗗浘鏍囷級
+      const endIdx = segment.length - 1
+      const endPoint = points[points.length - 1]
+      const endMarker = this.createVehicleMarker(endPoint, segment[endIdx])
+      this.map.addOverLay(endMarker)
+      this.markerEnd = endMarker
+
+      // 鑷�傚簲瑙嗛噹
+      this.map.setViewport(points)
+    },
+    /** 鍒涘缓璧风偣鏍囪 */
+    createStartMarker(lngLat, data) {
+      const icon = new T.Icon({
+        iconUrl: this.createSvgIconUrl('#2ecc71', '璧�'),
+        iconSize: new T.Point(28, 28),
+        iconAnchor: new T.Point(14, 14)
+      })
+      const marker = new T.Marker(lngLat, { icon })
+      marker.addEventListener('click', () => {
+        this.showInfoWindow(lngLat, data, '璧风偣')
+      })
+      return marker
+    },
+    /** 鍒涘缓杞﹁締鏍囪 */
+    createVehicleMarker(lngLat, data) {
+      const direction = data.direction || 0
+      const icon = new T.Icon({
+        iconUrl: this.createCarIconUrl(direction),
+        iconSize: new T.Point(32, 32),
+        iconAnchor: new T.Point(16, 16)
+      })
+      const marker = new T.Marker(lngLat, { icon })
+      marker.addEventListener('click', () => {
+        this.showInfoWindow(lngLat, data, '褰撳墠浣嶇疆')
+      })
+      return marker
+    },
+    /** 鐢熸垚璧风偣/缁堢偣 SVG 鍥炬爣 URL */
+    createSvgIconUrl(color, text) {
+      const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 28 28">
+        <circle cx="14" cy="14" r="13" fill="${color}" stroke="white" stroke-width="2"/>
+        <text x="14" y="18" text-anchor="middle" fill="white" font-size="12" font-weight="bold">${text}</text>
+      </svg>`
+      return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg)
+    },
+    /** 鐢熸垚姹借溅鍥炬爣 SVG URL锛堝甫鏂瑰悜鏃嬭浆锛� */
+    createCarIconUrl(direction) {
+      const deg = direction || 0
+      const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
+        <g transform="rotate(${deg}, 16, 16)">
+          <ellipse cx="16" cy="16" rx="10" ry="13" fill="#3388ff" stroke="white" stroke-width="2"/>
+          <polygon points="16,3 21,12 11,12" fill="white"/>
+          <rect x="11" y="22" width="4" height="3" rx="1" fill="white" opacity="0.8"/>
+          <rect x="17" y="22" width="4" height="3" rx="1" fill="white" opacity="0.8"/>
+        </g>
+      </svg>`
+      return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg)
+    },
+    /** 鏄剧ず淇℃伅绐楀彛 */
+    showInfoWindow(lngLat, data, label) {
+      if (!this.map) return
+      if (this.infoWindow) {
+        this.map.closeInfoWindow()
+      }
+      const content = `
+        <div style="padding:8px;min-width:180px;line-height:1.8">
+          <b style="color:#3388ff">${label || ''}</b><br/>
+          <span>杞︾墝锛�${data.vehicleNo || '-'}</span><br/>
+          <span>鏃堕棿锛�${data.collectTime || '-'}</span><br/>
+          <span>閫熷害锛�${this.formatSpeed(data.speed)} km/h</span><br/>
+          <span>鏂瑰悜锛�${data.direction || 0}掳</span><br/>
+          <span>缁忓害锛�${data.longitude}</span><br/>
+          <span>绾害锛�${data.latitude}</span>
+        </div>
+      `
+      this.infoWindow = new T.InfoWindow(content, {
+        offset: new T.Point(0, -15)
+      })
+      this.map.openInfoWindow(this.infoWindow, lngLat)
+    },
+    /** 鐐瑰嚮鍒楄〃琛� */
+    handleRowClick(row) {
+      if (!row || row.longitude == null) return
+      const lngLat = new T.LngLat(row.longitude, row.latitude)
+      this.map.panTo(lngLat)
+      this.map.setZoom(15)
+      this.showInfoWindow(lngLat, row, '閫変腑鐐�')
+    },
+    /** 涓婁竴娈� */
+    showPreviousSegment() {
+      if (this.segmentIndex > 0) {
+        this.segmentIndex--
+        this.stopPlayback()
+        this.drawTrack()
+      }
+    },
+    /** 涓嬩竴娈� */
+    showNextSegment() {
+      if ((this.segmentIndex + 1) * this.segmentSize < this.gpsList.length) {
+        this.segmentIndex++
+        this.stopPlayback()
+        this.drawTrack()
+      }
+    },
+    /** 寮�濮嬪洖鏀� */
+    startPlayback() {
+      if (this.isPlaying || !this.gpsList.length) return
+      this.isPlaying = true
+      this.playIndex = this.currentSegmentStart - 1
+      this.playNextPoint()
+    },
+    /** 鍥炴斁涓嬩竴涓偣 */
+    playNextPoint() {
+      if (!this.isPlaying) return
+      const allList = this.gpsList
+      if (this.playIndex >= this.currentSegmentEnd) {
+        this.stopPlayback()
+        this.$message.success('杞ㄨ抗鍥炴斁瀹屾垚')
+        return
+      }
+      const item = allList[this.playIndex]
+      if (item && item.longitude != null) {
+        const lngLat = new T.LngLat(item.longitude, item.latitude)
+        // 绉婚櫎鏃х殑鍥炴斁鏍囪
+        if (this.markerCurrent) {
+          this.map.removeOverLay(this.markerCurrent)
+        }
+        const marker = this.createVehicleMarker(lngLat, item)
+        this.map.addOverLay(marker)
+        this.markerCurrent = marker
+        this.map.panTo(lngLat)
+      }
+      this.playIndex++
+      this.playTimer = setTimeout(() => this.playNextPoint(), this.playSpeed)
+    },
+    /** 鍋滄鍥炴斁 */
+    stopPlayback() {
+      if (this.playTimer) {
+        clearTimeout(this.playTimer)
+        this.playTimer = null
+      }
+      this.isPlaying = false
+    }
+  },
+  beforeDestroy() {
+    this.stopPlayback()
+  }
+}
+</script>
+
+<style scoped>
+.stats-bar {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 4px;
+  padding: 8px 12px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  margin-bottom: 4px;
+}
+.track-list-card {
+  height: 660px;
+}
+.track-list-card >>> .el-card__body {
+  padding: 0;
+  overflow: hidden;
+}
+.map-loading-mask {
+  position: absolute;
+  top: 0; left: 0; right: 0; bottom: 0;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: rgba(255,255,255,0.85);
+  z-index: 999;
+}
+.map-empty-tip {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  text-align: center;
+  pointer-events: none;
+  z-index: 10;
+}
+</style>
diff --git a/ruoyi-ui/src/views/system/user/index.vue b/ruoyi-ui/src/views/system/user/index.vue
index d10e40c..2f33959 100644
--- a/ruoyi-ui/src/views/system/user/index.vue
+++ b/ruoyi-ui/src/views/system/user/index.vue
@@ -248,16 +248,30 @@
     </el-dialog>
 
     <!-- 鏂板锛氱敤鎴风鐞嗗垎鍏徃閰嶇疆瀵硅瘽妗� -->
-    <el-dialog title="閰嶇疆绠$悊鍒嗗叕鍙�" :visible.sync="branchDialog.open" width="500px" append-to-body>
-      <el-form label-width="100px">
-        <el-form-item label="鍙鐞嗗垎鍏徃">
+    <el-dialog title="閰嶇疆绠$悊鍒嗗叕鍙�" :visible.sync="branchDialog.open" width="520px" append-to-body>
+      <el-form label-width="100px" v-loading="branchDialog.loading">
+        <el-form-item label="宸插叧鑱斿垎鍏徃">
+          <div style="margin-bottom:8px;color:#666;font-size:12px;">OA鑷姩鍏宠仈锛堝彧璇伙級锛�</div>
           <div>
-            <el-tag v-for="bc in branchDialog.companies" :key="bc.deptId" type="info" size="small" style="margin-right:6px;margin-bottom:6px">{{ bc.deptName }}</el-tag>
-            <span v-if="branchDialog.companies.length === 0" style="color:#999">鏆傛棤鍒嗗叕鍙�</span>
+            <el-tag v-for="bc in branchDialog.companies" :key="'oa-'+bc.deptId" type="success" size="small" style="margin-right:6px;margin-bottom:6px">{{ bc.deptName }}</el-tag>
+            <span v-if="branchDialog.companies.length === 0" style="color:#999;font-size:12px">鏃燨A鑷姩鍏宠仈</span>
           </div>
+        </el-form-item>
+        <el-form-item label="鎵嬪姩娣诲姞">
+          <div style="margin-bottom:6px;color:#666;font-size:12px;">閫夋嫨闇�瑕侀澶栨坊鍔犵殑鍒嗗叕鍙革細</div>
+          <el-checkbox-group v-model="branchDialog.selectedDeptIds">
+            <el-checkbox
+              v-for="branch in branchDialog.allBranches"
+              :key="branch.deptId"
+              :label="branch.deptId"
+              style="display:block;margin-bottom:4px;"
+            >{{ branch.deptName }}</el-checkbox>
+          </el-checkbox-group>
+          <div v-if="branchDialog.allBranches.length === 0" style="color:#999;font-size:12px">鏆傛棤鍒嗗叕鍙告暟鎹�</div>
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitBranchCompanies" :loading="branchDialog.loading">淇� 瀛�</el-button>
         <el-button @click="branchDialog.open = false">鍏� 闂�</el-button>
       </div>
     </el-dialog>
@@ -266,13 +280,14 @@
 
 <script>
 import { listUser, getUser, delUser, addUser, updateUser, resetUserPwd, changeUserStatus, deptTreeSelect } from "@/api/system/user";
+import { updateUserBranch } from "@/api/system/user";
 import { getToken } from "@/utils/auth";
 import Treeselect from "@riophae/vue-treeselect";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
 import { Splitpanes, Pane } from "splitpanes";
 import "splitpanes/dist/splitpanes.css";
 import request from "@/utils/request";
-import { listDept, listBranchByUser } from "@/api/system/dept";
+import { listDept, listBranchByUser, listAllBranches } from "@/api/system/dept";
 
 export default {
   name: "User",
@@ -388,9 +403,10 @@
       branchDialog: {
         open: false,
         userId: null,
-        deptOptions: [],
+        allBranches: [],
         selectedDeptIds: [],
-        companies: []
+        companies: [],
+        loading: false
       },
       // 鍩轰簬 oa_order_class 璁$畻鐨勫垎鍏徃鍒楄〃锛堝彧璇诲睍绀猴級
       userBranchCompanies: []
@@ -606,22 +622,53 @@
     handleManageBranch(row) {
       const userId = row.userId;
       this.branchDialog.userId = userId;
-      // 鍔犺浇鍒嗗叕鍙稿垪琛紙OA鑷姩鎺у埗锛屽彧璇诲睍绀猴級
-      listBranchByUser(userId).then(res => {
-        const list = res.data || [];
-        this.branchDialog.companies = (list || []).map(d => ({ deptId: d.deptId, deptName: d.deptName }));
-        this.branchDialog.open = true;
+      this.branchDialog.companies = [];
+      this.branchDialog.allBranches = [];
+      this.branchDialog.selectedDeptIds = [];
+      this.branchDialog.loading = true;
+      this.branchDialog.open = true;
+      // 骞惰鍔犺浇锛氬綋鍓嶇敤鎴稯A鍏宠仈鍒嗗叕鍙� + 鎵�鏈夊垎鍏徃鍒楄〃
+      const p1 = listBranchByUser(userId).then(res => {
+        return (res.data || []).map(d => ({ deptId: d.deptId, deptName: d.deptName }));
+      });
+      const p2 = listAllBranches().then(res => {
+        return res.data || [];
+      });
+      Promise.all([p1, p2]).then(([oaCompanies, allBranches]) => {
+        this.branchDialog.companies = oaCompanies;
+        this.branchDialog.allBranches = allBranches;
+        // 鍒濆鍖栧凡閫夛細浠� userList 涓壘鍒拌鐢ㄦ埛鐨� oaOrderClass锛屽尮閰嶅垎鍏徃
+        const userRow = (this.userList || []).find(u => u.userId === userId);
+        if (userRow && userRow.oaOrderClass) {
+          const codes = userRow.oaOrderClass.split(',').map(s => s.trim()).filter(s => s);
+          const codeSet = new Set(codes);
+          const selected = allBranches
+            .filter(d => {
+              return (d.serviceOrderClass && codeSet.has(d.serviceOrderClass.trim()))
+                || (d.dispatchOrderClass && codeSet.has(d.dispatchOrderClass.trim()));
+            })
+            .map(d => d.deptId);
+          this.branchDialog.selectedDeptIds = selected;
+        }
+        this.branchDialog.loading = false;
       }).catch(() => {
         this.$modal.msgError('鍔犺浇鍒嗗叕鍙搁厤缃け璐�');
+        this.branchDialog.loading = false;
       });
     },
     /** 淇濆瓨鍒嗗叕鍙搁厤缃� */
-    // 宸插彇娑堜繚瀛橈紝OA鑷姩鎺у埗锛堜粎鍙灞曠ず锛�
-    // submitBranchCompanies() {
-    //   const userId = this.branchDialog.userId;
-    //   const deptIds = this.branchDialog.selectedDeptIds || [];
-    //   // 淇濈暀鍗犱綅锛岄伩鍏嶈璋冪敤
-    // },
+    submitBranchCompanies() {
+      this.branchDialog.loading = true;
+      updateUserBranch(this.branchDialog.userId, this.branchDialog.selectedDeptIds).then(() => {
+        this.$modal.msgSuccess('鍒嗗叕鍙搁厤缃繚瀛樻垚鍔�');
+        this.branchDialog.loading = false;
+        this.branchDialog.open = false;
+        this.getList();
+      }).catch(() => {
+        this.$modal.msgError('淇濆瓨鍒嗗叕鍙搁厤缃け璐�');
+        this.branchDialog.loading = false;
+      });
+    },
     /** 鎻愪氦鎸夐挳 */
     submitForm: function() {
       this.$refs["form"].validate(valid => {
diff --git a/ruoyi-ui/src/views/task/stat/index.vue b/ruoyi-ui/src/views/task/stat/index.vue
new file mode 100644
index 0000000..c9fc5d8
--- /dev/null
+++ b/ruoyi-ui/src/views/task/stat/index.vue
@@ -0,0 +1,219 @@
+<template>
+  <div class="app-container">
+    <!-- 鏌ヨ鏉′欢 -->
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px">
+      <el-form-item label="鍒嗗叕鍙�" prop="deptIds">
+        <el-select
+          v-model="queryParams.deptIds"
+          placeholder="鍏ㄩ儴鍒嗗叕鍙�"
+          clearable
+          filterable
+          multiple
+          collapse-tags
+          style="width: 240px"
+        >
+          <el-option
+            v-for="dept in branchList"
+            :key="dept.deptId"
+            :label="dept.deptName"
+            :value="dept.deptId"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="鏃堕棿鑼冨洿" prop="dateRange">
+        <el-date-picker
+          v-model="dateRange"
+          type="daterange"
+          value-format="yyyy-MM-dd"
+          range-separator="-"
+          start-placeholder="寮�濮嬫棩鏈�"
+          end-placeholder="缁撴潫鏃ユ湡"
+          style="width: 240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">鏌ヨ</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 鎿嶄綔鏍� -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['task:stat:export']"
+        >瀵煎嚭Excel</el-button>
+      </el-col>
+    </el-row>
+
+    <!-- 鏁版嵁琛ㄦ牸 -->
+    <el-table
+      v-loading="loading"
+      :data="tableData"
+      border
+      style="width: 100%"
+      :span-method="mergeDeptRows"
+    >
+      <el-table-column label="鍒嗗叕鍙�" prop="deptName" align="center" min-width="140" />
+      <el-table-column label="鏃ユ湡" prop="statDate" align="center" width="120" />
+      <el-table-column label="褰曞崟鏁伴噺" prop="orderCount" align="center" width="120">
+        <template slot-scope="scope">
+          <el-tag type="primary">{{ scope.row.orderCount }}</el-tag>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 姹囨�讳俊鎭� -->
+    <div v-if="tableData.length > 0" style="margin-top: 12px; color: #606266; font-size: 13px;">
+      鍏� <strong>{{ tableData.length }}</strong> 鏉¤褰曪紝鍚堣褰曞崟
+      <strong style="color: #E6A23C; font-size: 16px;">{{ totalCount }}</strong> 鍗�
+    </div>
+  </div>
+</template>
+
+<script>
+import { getDeptOrderStat, exportDeptOrderStat } from "@/api/task";
+import { listAllBranches } from "@/api/system/dept";
+
+export default {
+  name: "DeptOrderStat",
+  data() {
+    const now = new Date();
+    const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
+    const fmt = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
+    return {
+      loading: false,
+      exportLoading: false,
+      tableData: [],
+      branchList: [],
+      // 榛樿閫夋嫨褰撴湀
+      dateRange: [fmt(firstDay), fmt(now)],
+      queryParams: {
+        deptIds: []
+      },
+      // 鍚堝苟琛岀浉鍏�
+      mergeMap: {}
+    };
+  },
+  computed: {
+    totalCount() {
+      return this.tableData.reduce((sum, row) => sum + (row.orderCount || 0), 0);
+    }
+  },
+  created() {
+    this.loadBranchList();
+    this.getList();
+  },
+  methods: {
+    /** 鍔犺浇鍒嗗叕鍙稿垪琛� */
+    loadBranchList() {
+      listAllBranches().then(res => {
+        this.branchList = res.data || [];
+      }).catch(() => {
+        this.branchList = [];
+      });
+    },
+
+    /** 鏌ヨ缁熻鏁版嵁 */
+    getList() {
+      if (!this.dateRange || this.dateRange.length !== 2) {
+        this.$message.warning("璇烽�夋嫨鏃堕棿鑼冨洿");
+        return;
+      }
+      this.loading = true;
+      const params = {
+        startDate: this.dateRange[0],
+        endDate: this.dateRange[1],
+        deptIds: this.queryParams.deptIds && this.queryParams.deptIds.length > 0
+          ? this.queryParams.deptIds.join(",")
+          : undefined
+      };
+      getDeptOrderStat(params).then(res => {
+        this.tableData = res.data || [];
+        this.buildMergeMap();
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+
+    /** 鏋勫缓鍚堝苟琛屾槧灏� */
+    buildMergeMap() {
+      const map = {};
+      const data = this.tableData;
+      let i = 0;
+      while (i < data.length) {
+        let j = i;
+        while (j < data.length && data[j].deptName === data[i].deptName) {
+          j++;
+        }
+        map[i] = j - i;
+        for (let k = i + 1; k < j; k++) {
+          map[k] = 0;
+        }
+        i = j;
+      }
+      this.mergeMap = map;
+    },
+
+    /** 鍚堝苟鍒嗗叕鍙稿垪锛堢浉鍚屽垎鍏徃琛屽悎骞讹級 */
+    mergeDeptRows({ row, column, rowIndex, columnIndex }) {
+      if (columnIndex === 0) {
+        const span = this.mergeMap[rowIndex];
+        if (span !== undefined) {
+          return { rowspan: span, colspan: span === 0 ? 0 : 1 };
+        }
+      }
+    },
+
+    /** 鎼滅储 */
+    handleQuery() {
+      this.getList();
+    },
+
+    /** 閲嶇疆 */
+    resetQuery() {
+      const now = new Date();
+      const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
+      const fmt = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
+      this.dateRange = [fmt(firstDay), fmt(now)];
+      this.queryParams.deptIds = [];
+      this.getList();
+    },
+
+    /** 瀵煎嚭 Excel */
+    handleExport() {
+      if (!this.dateRange || this.dateRange.length !== 2) {
+        this.$message.warning("璇烽�夋嫨鏃堕棿鑼冨洿");
+        return;
+      }
+      this.exportLoading = true;
+      const params = {
+        startDate: this.dateRange[0],
+        endDate: this.dateRange[1],
+        deptIds: this.queryParams.deptIds && this.queryParams.deptIds.length > 0
+          ? this.queryParams.deptIds.join(",")
+          : undefined
+      };
+      exportDeptOrderStat(params).then(res => {
+        const blob = new Blob([res], { type: 'application/vnd.ms-excel' });
+        const url = window.URL.createObjectURL(blob);
+        const a = document.createElement('a');
+        a.href = url;
+        a.download = `鍒嗗叕鍙稿綍鍗曠粺璁${params.startDate}_${params.endDate}.xlsx`;
+        a.click();
+        window.URL.revokeObjectURL(url);
+        this.exportLoading = false;
+      }).catch(() => {
+        this.exportLoading = false;
+      });
+    }
+  }
+};
+</script>
diff --git a/ruoyi-ui/vue.config.js b/ruoyi-ui/vue.config.js
index ccb6122..00a4808 100644
--- a/ruoyi-ui/vue.config.js
+++ b/ruoyi-ui/vue.config.js
@@ -11,7 +11,7 @@
 
 const baseUrl = 'http://localhost:8080' // 鍚庣鎺ュ彛
 
-const port = process.env.port || process.env.npm_config_port || 80 // 绔彛
+const port = process.env.port || process.env.npm_config_port || 8086 // 绔彛
 
 // vue.config.js 閰嶇疆璇存槑
 //瀹樻柟vue.config.js 鍙傝�冩枃妗� https://cli.vuejs.org/zh/config/#css-loaderoptions
diff --git a/sql/dept_order_stat_menu.sql b/sql/dept_order_stat_menu.sql
new file mode 100644
index 0000000..58b0aae
--- /dev/null
+++ b/sql/dept_order_stat_menu.sql
@@ -0,0 +1,20 @@
+-- ----------------------------
+-- 鍒嗗叕鍙稿綍鍗曠粺璁� 鑿滃崟鏉冮檺閰嶇疆
+-- 鎸傝浇鍦ㄥ凡鏈夌殑"浠诲姟绠$悊"鐩綍涓�
+-- ----------------------------
+
+-- 鑾峰彇"浠诲姟绠$悊"鐩綍鐨勮彍鍗旾D
+SET @taskParentId = (SELECT menu_id FROM sys_menu WHERE menu_name = '浠诲姟绠$悊' AND menu_type = 'M' LIMIT 1);
+
+-- 鍒嗗叕鍙稿綍鍗曠粺璁� 鑿滃崟锛堝瓙鑿滃崟锛�
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+VALUES ('鍒嗗叕鍙稿綍鍗曠粺璁�', @taskParentId, 9, 'stat/index', 'task/stat/index', 1, 0, 'C', '0', '0', 'task:stat:query', 'chart', 'admin', SYSDATE(), '', NULL, '鍒嗗叕鍙稿綍鍗曠粺璁¤彍鍗�');
+
+-- 鑾峰彇鍒氭彃鍏ョ殑鑿滃崟ID
+SET @statMenuId = (SELECT menu_id FROM sys_menu WHERE menu_name = '鍒嗗叕鍙稿綍鍗曠粺璁�' AND menu_type = 'C' LIMIT 1);
+
+-- 鎸夐挳鏉冮檺
+INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+VALUES
+('褰曞崟缁熻鏌ヨ', @statMenuId, 1, '#', '', 1, 0, 'F', '0', '0', 'task:stat:query',  '#', 'admin', SYSDATE(), '', NULL, ''),
+('褰曞崟缁熻瀵煎嚭', @statMenuId, 2, '#', '', 1, 0, 'F', '0', '0', 'task:stat:export', '#', 'admin', SYSDATE(), '', NULL, '');
diff --git a/sql/vehicle_gps_track_map_menu.sql b/sql/vehicle_gps_track_map_menu.sql
new file mode 100644
index 0000000..38a7c47
--- /dev/null
+++ b/sql/vehicle_gps_track_map_menu.sql
@@ -0,0 +1,53 @@
+-- =====================================================
+-- 杞﹁締琛岄┒杞ㄨ抗锛堝ぉ鍦板浘锛夎彍鍗曢厤缃甋QL
+-- 璇存槑锛氬湪绯荤粺绠$悊鑿滃崟涓坊鍔�"杞﹁締琛岄┒杞ㄨ抗"鑿滃崟椤�
+--       鍓嶇璺敱锛�/system/gps/trackMap
+--       缁勪欢璺緞锛歴ystem/gps/trackMap
+--       鎵�闇�鏉冮檺锛歴ystem:gps:list
+-- 鎵ц鏂瑰紡锛歮ysql -u root -p 鏁版嵁搴撳悕 < vehicle_gps_track_map_menu.sql
+-- =====================================================
+
+-- 娣诲姞"杞﹁締琛岄┒杞ㄨ抗"鑿滃崟锛堢埗鑿滃崟ID=3锛屽嵆杞﹁締绠$悊鐩綍锛�
+-- parent_id 鏍规嵁瀹為檯鏁版嵁搴撲腑鐨勮溅杈嗙鐞嗙洰褰旾D璋冩暣
+INSERT INTO sys_menu (
+    menu_name,
+    parent_id,
+    order_num,
+    path,
+    component,
+    is_frame,
+    is_cache,
+    menu_type,
+    visible,
+    status,
+    perms,
+    icon,
+    create_by,
+    create_time,
+    update_by,
+    update_time,
+    remark
+) VALUES (
+    '杞﹁締琛岄┒杞ㄨ抗',
+    3,            -- 杞﹁締绠$悊鐩綍ID锛堝涓嶅璇蜂慨鏀逛负瀹為檯ID锛�
+    10,
+    'trackMap',
+    'system/gps/trackMap',
+    1,
+    0,
+    'C',
+    '0',
+    '0',
+    'system:gps:list',
+    'guide',
+    'admin',
+    sysdate(),
+    '',
+    null,
+    '杞﹁締琛岄┒杞ㄨ抗锛堝ぉ鍦板浘锛夋煡璇㈤〉闈�'
+);
+
+-- 鏌ヨ缁撴灉楠岃瘉
+SELECT menu_id, menu_name, parent_id, path, component, perms, status
+FROM sys_menu
+WHERE menu_name = '杞﹁締琛岄┒杞ㄨ抗';
diff --git a/test/1.html b/test/1.html
new file mode 100644
index 0000000..6ddfa32
--- /dev/null
+++ b/test/1.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
+    <meta name="keywords" content="澶╁湴鍥�"/>
+    <title>澶╁湴鍥撅紞鍦板浘API锛嶈寖渚嬶紞缁忕含搴︾洿鎶曞湴鍥�</title>
+    <script type="text/javascript" src="http://api.tianditu.gov.cn/api?v=4.0&tk=079300b89bce333ead2df1476c43ecb0"></script>
+    <style type="text/css">body,html{width:100%;height:100%;margin:0;font-family:"Microsoft YaHei"}#mapDiv{width:100%;height:400px}input,b,p{margin-left:5px;font-size:14px}</style>
+    <script>
+        var map;
+        var zoom = 12;
+        function onLoad() {
+            map = new T.Map('mapDiv', {
+                projection: 'EPSG:4326'
+            });
+            map.centerAndZoom(new T.LngLat(116.40769, 39.89945), zoom);
+        }
+    </script>
+</head>
+<body onLoad="onLoad()">
+<div id="mapDiv"></div>
+<p>鏈ず渚嬫紨绀哄浣曟樉绀虹粡绾害鐩存姇鍦板浘銆�</p>
+</body>
+</html>
\ No newline at end of file

--
Gitblit v1.9.1