Selaa lähdekoodia

Merge branch 'master' into feature/master-homepage-20260115

chenshudong 2 kuukautta sitten
vanhempi
commit
4e54338758

+ 6 - 6
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/WeakModuleRuleController.java

@@ -41,7 +41,7 @@ public class WeakModuleRuleController extends BaseController {
41 41
      * 查询规则列表
42 42
      */
43 43
     @ApiOperation("查询规则列表")
44
-    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:list')")
44
+    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:list') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
45 45
     @GetMapping("/list")
46 46
     public TableDataInfo list() {
47 47
         startPage();
@@ -53,7 +53,7 @@ public class WeakModuleRuleController extends BaseController {
53 53
      * 获取规则详情
54 54
      */
55 55
     @ApiOperation("获取规则详情")
56
-    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:query')")
56
+    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:query') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
57 57
     @GetMapping("/{id}")
58 58
     public AjaxResult getInfo(@PathVariable Long id) {
59 59
         return success(weakModuleRuleService.getById(id));
@@ -63,7 +63,7 @@ public class WeakModuleRuleController extends BaseController {
63 63
      * 新增规则
64 64
      */
65 65
     @ApiOperation("新增规则")
66
-    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:add')")
66
+    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:add') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
67 67
     @PostMapping
68 68
     public AjaxResult add(@Validated @RequestBody WeakModuleRuleDTO dto) {
69 69
         return toAjax(weakModuleRuleService.insert(dto));
@@ -73,7 +73,7 @@ public class WeakModuleRuleController extends BaseController {
73 73
      * 修改规则
74 74
      */
75 75
     @ApiOperation("修改规则")
76
-    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:edit')")
76
+    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:edit') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
77 77
     @PutMapping
78 78
     public AjaxResult edit(@Validated @RequestBody WeakModuleRuleDTO dto) {
79 79
         return toAjax(weakModuleRuleService.update(dto));
@@ -83,7 +83,7 @@ public class WeakModuleRuleController extends BaseController {
83 83
      * 删除规则
84 84
      */
85 85
     @ApiOperation("删除规则")
86
-    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:remove')")
86
+    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:remove') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
87 87
     @DeleteMapping("/{id}")
88 88
     public AjaxResult remove(@PathVariable Long id) {
89 89
         return toAjax(weakModuleRuleService.deleteById(id));
@@ -93,7 +93,7 @@ public class WeakModuleRuleController extends BaseController {
93 93
      * 启用规则
94 94
      */
95 95
     @ApiOperation("启用规则(同时只能有一个启用)")
96
-    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:edit')")
96
+    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:edit') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
97 97
     @PutMapping("/enable/{id}")
98 98
     public AjaxResult enable(@PathVariable Long id) {
99 99
         return toAjax(weakModuleRuleService.enable(id));

+ 1 - 1
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysDeptController.java

@@ -115,7 +115,7 @@ public class SysDeptController extends BaseController {
115 115
 
116 116
     @PostMapping("/teamList")
117 117
     public AjaxResult teamList(@RequestBody SysDept dept) {
118
-        return success(buildTree(deptService.selectDeptList(dept)));
118
+        return success(buildTree(deptService.selectDeptInfoAll(dept)));
119 119
     }
120 120
 
121 121
     private Map<String, String> buildTree(List<SysDept> list) {

+ 18 - 2
airport-common/src/main/java/com/sundot/airport/common/cache/StatisticsCacheConfig.java

@@ -11,6 +11,8 @@ import org.springframework.data.redis.serializer.RedisSerializationContext;
11 11
 import org.springframework.data.redis.serializer.StringRedisSerializer;
12 12
 
13 13
 import java.time.Duration;
14
+import java.util.HashMap;
15
+import java.util.Map;
14 16
 
15 17
 /**
16 18
  * 统计缓存Redis缓存配置
@@ -19,17 +21,31 @@ import java.time.Duration;
19 21
 @EnableCaching
20 22
 public class StatisticsCacheConfig {
21 23
 
24
+    public static final String DASHBOARD_CACHE = "dashboardCache";
25
+
22 26
     @Bean
23 27
     public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
24
-        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
28
+        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
25 29
                 .entryTtl(Duration.ofHours(6)) // 默认6小时
26 30
                 .disableCachingNullValues() // 不缓存null值
27 31
                 .computePrefixWith(cacheName -> "airport:cache:" + cacheName + ":")
28 32
                 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
29 33
                 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
30 34
 
35
+        // dashboard缓存: 5分钟TTL
36
+        RedisCacheConfiguration dashboardConfig = RedisCacheConfiguration.defaultCacheConfig()
37
+                .entryTtl(Duration.ofMinutes(5))
38
+                .disableCachingNullValues()
39
+                .computePrefixWith(cacheName -> "airport:cache:" + cacheName + ":")
40
+                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
41
+                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
42
+
43
+        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
44
+        cacheConfigurations.put(DASHBOARD_CACHE, dashboardConfig);
45
+
31 46
         return RedisCacheManager.builder(factory)
32
-                .cacheDefaults(config)
47
+                .cacheDefaults(defaultConfig)
48
+                .withInitialCacheConfigurations(cacheConfigurations)
33 49
                 .build();
34 50
     }
35 51
 

+ 2 - 2
airport-exam/src/main/java/com/sundot/airport/exam/controller/DailyStatisticsController.java

@@ -83,7 +83,7 @@ public class DailyStatisticsController extends BaseController {
83 83
     /**
84 84
      * 手动触发薄弱模块分析更新(管理员功能)
85 85
      */
86
-    @PreAuthorize("@ss.hasPermi('exam:daily:statistics:update')")
86
+    @PreAuthorize("@ss.hasPermi('exam:daily:statistics:update') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
87 87
     @PostMapping("/update-weak-analysis/{userId}")
88 88
     public AjaxResult updateWeakAnalysis(@PathVariable Long userId) {
89 89
         try {
@@ -99,7 +99,7 @@ public class DailyStatisticsController extends BaseController {
99 99
      * 获取全局统计数据(管理员视角)
100 100
      * 统计所有用户的任务完成情况、答题正确率、部门排名等
101 101
      */
102
-    @PreAuthorize("@ss.hasPermi('exam:daily:statistics:admin')")
102
+    @PreAuthorize("@ss.hasPermi('exam:daily:statistics:admin') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
103 103
     @GetMapping("/global-statistics")
104 104
     public AjaxResult getGlobalStatistics() {
105 105
         try {

+ 7 - 7
airport-exam/src/main/java/com/sundot/airport/exam/controller/DailyTaskAdminController.java

@@ -42,7 +42,7 @@ public class DailyTaskAdminController extends BaseController {
42 42
      * 查询任务列表(管理员)
43 43
      * 支持多条件筛选
44 44
      */
45
-    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:list')")
45
+    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:list') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
46 46
     @GetMapping("/list")
47 47
     public TableDataInfo list(DailyTaskAdminQueryDTO queryDTO) {
48 48
         LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
@@ -124,7 +124,7 @@ public class DailyTaskAdminController extends BaseController {
124 124
      * 查询任务详情(管理员)
125 125
      * 管理员可以查看任何用户的任务详情
126 126
      */
127
-    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:query')")
127
+    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:query') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
128 128
     @GetMapping("/{taskId}")
129 129
     public AjaxResult getTaskDetail(@PathVariable String taskId) {
130 130
         // 查询任务主记录
@@ -155,7 +155,7 @@ public class DailyTaskAdminController extends BaseController {
155 155
      * 任务统计(管理员)
156 156
      * 按状态、部门、角色等维度统计
157 157
      */
158
-    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:statistics')")
158
+    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:statistics') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
159 159
     @GetMapping("/statistics")
160 160
     public AjaxResult statistics(DailyTaskAdminQueryDTO queryDTO) {
161 161
         LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
@@ -187,7 +187,7 @@ public class DailyTaskAdminController extends BaseController {
187 187
     /**
188 188
      * 按部门统计任务完成情况
189 189
      */
190
-    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:statistics')")
190
+    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:statistics') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
191 191
     @GetMapping("/statistics/by-dept")
192 192
     public AjaxResult statisticsByDept(DailyTaskAdminQueryDTO queryDTO) {
193 193
         try {
@@ -203,7 +203,7 @@ public class DailyTaskAdminController extends BaseController {
203 203
     /**
204 204
      * 按用户统计任务完成情况
205 205
      */
206
-    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:statistics')")
206
+    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:statistics') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
207 207
     @GetMapping("/statistics/by-user")
208 208
     public AjaxResult statisticsByUser(DailyTaskAdminQueryDTO queryDTO) {
209 209
         try {
@@ -219,7 +219,7 @@ public class DailyTaskAdminController extends BaseController {
219 219
     /**
220 220
      * 删除任务(逻辑删除)
221 221
      */
222
-    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:remove')")
222
+    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:remove') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
223 223
     @Log(title = "删除每日任务", businessType = BusinessType.DELETE)
224 224
     @DeleteMapping("/{taskId}")
225 225
     public AjaxResult remove(@PathVariable String taskId) {
@@ -240,7 +240,7 @@ public class DailyTaskAdminController extends BaseController {
240 240
     /**
241 241
      * 批量删除任务(逻辑删除)
242 242
      */
243
-    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:remove')")
243
+    @PreAuthorize("@ss.hasPermi('exam:daily:task:admin:remove') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
244 244
     @Log(title = "批量删除每日任务", businessType = BusinessType.DELETE)
245 245
     @DeleteMapping("/batch/{taskIds}")
246 246
     public AjaxResult removeBatch(@PathVariable String[] taskIds) {

+ 3 - 3
airport-exam/src/main/java/com/sundot/airport/exam/controller/DailyTaskExpireController.java

@@ -30,7 +30,7 @@ public class DailyTaskExpireController extends BaseController {
30 30
     /**
31 31
      * 手动触发逾期任务处理(管理员功能)
32 32
      */
33
-    @PreAuthorize("@ss.hasPermi('exam:daily:expire:process')")
33
+    @PreAuthorize("@ss.hasPermi('exam:daily:expire:process') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
34 34
     @Log(title = "手动处理逾期任务", businessType = BusinessType.UPDATE)
35 35
     @PostMapping("/process")
36 36
     public AjaxResult processExpiredTasks() {
@@ -49,7 +49,7 @@ public class DailyTaskExpireController extends BaseController {
49 49
     /**
50 50
      * 提醒用户完成逾期任务(单个)
51 51
      */
52
-    @PreAuthorize("@ss.hasPermi('exam:daily:expire:notify')")
52
+    @PreAuthorize("@ss.hasPermi('exam:daily:expire:notify') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
53 53
     @Log(title = "提醒用户完成逾期任务", businessType = BusinessType.OTHER)
54 54
     @PostMapping("/notify")
55 55
     public AjaxResult notifyExpiredTask(@Valid @RequestBody NotifyTaskDTO dto) {
@@ -76,7 +76,7 @@ public class DailyTaskExpireController extends BaseController {
76 76
     /**
77 77
      * 批量提醒用户完成逾期任务
78 78
      */
79
-    @PreAuthorize("@ss.hasPermi('exam:daily:expire:notify')")
79
+    @PreAuthorize("@ss.hasPermi('exam:daily:expire:notify') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
80 80
     @Log(title = "批量提醒用户完成逾期任务", businessType = BusinessType.OTHER)
81 81
     @PostMapping("/batch-notify")
82 82
     public AjaxResult batchNotifyExpiredTasks(@Valid @RequestBody NotifyTaskDTO dto) {

+ 55 - 100
airport-exam/src/main/java/com/sundot/airport/exam/dto/DashboardOverviewDTO.java

@@ -15,128 +15,81 @@ public class DashboardOverviewDTO implements Serializable {
15 15
     private static final long serialVersionUID = 1L;
16 16
 
17 17
     /**
18
-     * 今日数据
18
+     * 总任务数
19 19
      */
20
-    private PeriodStatistics today;
20
+    private Long totalTasks;
21 21
 
22 22
     /**
23
-     * 本周数据
23
+     * 已完成任务数
24 24
      */
25
-    private PeriodStatistics week;
25
+    private Long completedTasks;
26 26
 
27 27
     /**
28
-     * 本月数据
28
+     * 完成率
29 29
      */
30
-    private PeriodStatistics month;
30
+    private Double completionRate;
31 31
 
32
-    public PeriodStatistics getToday() {
33
-        return today;
34
-    }
32
+    /**
33
+     * 平均得分
34
+     */
35
+    private Double avgScore;
35 36
 
36
-    public void setToday(PeriodStatistics today) {
37
-        this.today = today;
38
-    }
37
+    /**
38
+     * 逾期任务数
39
+     */
40
+    private Long expiredTasks;
39 41
 
40
-    public PeriodStatistics getWeek() {
41
-        return week;
42
-    }
42
+    /**
43
+     * 环比增长数据
44
+     */
45
+    private GrowthData growth;
43 46
 
44
-    public void setWeek(PeriodStatistics week) {
45
-        this.week = week;
47
+    public Long getTotalTasks() {
48
+        return totalTasks;
46 49
     }
47 50
 
48
-    public PeriodStatistics getMonth() {
49
-        return month;
51
+    public void setTotalTasks(Long totalTasks) {
52
+        this.totalTasks = totalTasks;
50 53
     }
51 54
 
52
-    public void setMonth(PeriodStatistics month) {
53
-        this.month = month;
55
+    public Long getCompletedTasks() {
56
+        return completedTasks;
54 57
     }
55 58
 
56
-    /**
57
-     * 时间段统计数据
58
-     */
59
-    public static class PeriodStatistics implements Serializable {
60
-
61
-        private static final long serialVersionUID = 1L;
62
-
63
-        /**
64
-         * 总任务数
65
-         */
66
-        private Long totalTasks;
67
-
68
-        /**
69
-         * 已完成任务数
70
-         */
71
-        private Long completedTasks;
72
-
73
-        /**
74
-         * 完成率
75
-         */
76
-        private Double completionRate;
77
-
78
-        /**
79
-         * 平均得分
80
-         */
81
-        private Double avgScore;
82
-
83
-        /**
84
-         * 逾期任务数
85
-         */
86
-        private Long expiredTasks;
87
-
88
-        /**
89
-         * 环比增长数据
90
-         */
91
-        private GrowthData growth;
92
-
93
-        public Long getTotalTasks() {
94
-            return totalTasks;
95
-        }
96
-
97
-        public void setTotalTasks(Long totalTasks) {
98
-            this.totalTasks = totalTasks;
99
-        }
100
-
101
-        public Long getCompletedTasks() {
102
-            return completedTasks;
103
-        }
104
-
105
-        public void setCompletedTasks(Long completedTasks) {
106
-            this.completedTasks = completedTasks;
107
-        }
59
+    public void setCompletedTasks(Long completedTasks) {
60
+        this.completedTasks = completedTasks;
61
+    }
108 62
 
109
-        public Double getCompletionRate() {
110
-            return completionRate;
111
-        }
63
+    public Double getCompletionRate() {
64
+        return completionRate;
65
+    }
112 66
 
113
-        public void setCompletionRate(Double completionRate) {
114
-            this.completionRate = completionRate;
115
-        }
67
+    public void setCompletionRate(Double completionRate) {
68
+        this.completionRate = completionRate;
69
+    }
116 70
 
117
-        public Double getAvgScore() {
118
-            return avgScore;
119
-        }
71
+    public Double getAvgScore() {
72
+        return avgScore;
73
+    }
120 74
 
121
-        public void setAvgScore(Double avgScore) {
122
-            this.avgScore = avgScore;
123
-        }
75
+    public void setAvgScore(Double avgScore) {
76
+        this.avgScore = avgScore;
77
+    }
124 78
 
125
-        public Long getExpiredTasks() {
126
-            return expiredTasks;
127
-        }
79
+    public Long getExpiredTasks() {
80
+        return expiredTasks;
81
+    }
128 82
 
129
-        public void setExpiredTasks(Long expiredTasks) {
130
-            this.expiredTasks = expiredTasks;
131
-        }
83
+    public void setExpiredTasks(Long expiredTasks) {
84
+        this.expiredTasks = expiredTasks;
85
+    }
132 86
 
133
-        public GrowthData getGrowth() {
134
-            return growth;
135
-        }
87
+    public GrowthData getGrowth() {
88
+        return growth;
89
+    }
136 90
 
137
-        public void setGrowth(GrowthData growth) {
138
-            this.growth = growth;
139
-        }
91
+    public void setGrowth(GrowthData growth) {
92
+        this.growth = growth;
140 93
     }
141 94
 
142 95
     /**
@@ -189,9 +142,11 @@ public class DashboardOverviewDTO implements Serializable {
189 142
     @Override
190 143
     public String toString() {
191 144
         return "DashboardOverviewDTO{" +
192
-                "today=" + today +
193
-                ", week=" + week +
194
-                ", month=" + month +
145
+                "totalTasks=" + totalTasks +
146
+                ", completedTasks=" + completedTasks +
147
+                ", completionRate=" + completionRate +
148
+                ", avgScore=" + avgScore +
149
+                ", expiredTasks=" + expiredTasks +
195 150
                 '}';
196 151
     }
197 152
 }

+ 244 - 85
airport-exam/src/main/java/com/sundot/airport/exam/service/impl/DashboardServiceImpl.java

@@ -1,14 +1,22 @@
1 1
 package com.sundot.airport.exam.service.impl;
2 2
 
3 3
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.sundot.airport.common.cache.StatisticsCacheConfig;
4 5
 import com.sundot.airport.common.core.domain.entity.SysDept;
6
+import com.sundot.airport.common.core.domain.entity.SysRole;
7
+import com.sundot.airport.common.core.domain.model.LoginUser;
8
+import com.sundot.airport.common.enums.RoleTypeEnum;
9
+import com.sundot.airport.common.utils.SecurityUtils;
5 10
 import com.sundot.airport.exam.domain.DailyTask;
6 11
 import com.sundot.airport.exam.dto.*;
7 12
 import com.sundot.airport.exam.enums.DailyTaskStatusEnum;
8 13
 import com.sundot.airport.exam.mapper.DailyTaskMapper;
9 14
 import com.sundot.airport.exam.service.IDashboardService;
10 15
 import com.sundot.airport.system.mapper.SysDeptMapper;
16
+import org.slf4j.Logger;
17
+import org.slf4j.LoggerFactory;
11 18
 import org.springframework.beans.factory.annotation.Autowired;
19
+import org.springframework.cache.annotation.Cacheable;
12 20
 import org.springframework.stereotype.Service;
13 21
 
14 22
 import java.math.BigDecimal;
@@ -33,6 +41,8 @@ import java.util.stream.Collectors;
33 41
 @Service
34 42
 public class DashboardServiceImpl implements IDashboardService {
35 43
 
44
+    private static final Logger log = LoggerFactory.getLogger(DashboardServiceImpl.class);
45
+
36 46
     @Autowired
37 47
     private DailyTaskMapper dailyTaskMapper;
38 48
 
@@ -43,39 +53,53 @@ public class DashboardServiceImpl implements IDashboardService {
43 53
     private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
44 54
 
45 55
     @Override
56
+    @Cacheable(value = StatisticsCacheConfig.DASHBOARD_CACHE,
57
+            key = "'overview:' + T(com.sundot.airport.common.utils.SecurityUtils).getUserId() + ':' + (#query != null ? #query.toString() : 'default')")
46 58
     public DashboardOverviewDTO getOverview(DashboardQueryDTO query) {
47 59
         DashboardOverviewDTO dto = new DashboardOverviewDTO();
48 60
 
49
-        // 创建不带时间范围的查询条件(避免在queryTasks中重复筛选)
50
-        DashboardQueryDTO queryWithoutTimeRange = new DashboardQueryDTO();
51
-        if (query != null) {
52
-            queryWithoutTimeRange.setDeptIds(query.getDeptIds());
53
-            queryWithoutTimeRange.setRoles(query.getRoles());
54
-            queryWithoutTimeRange.setUserIds(query.getUserIds());
55
-            // 不设置timeRange,让buildPeriodStatistics来筛选
61
+        // 使用startDate/endDate查询,若未指定则默认今天
62
+        if (query == null) {
63
+            query = new DashboardQueryDTO();
56 64
         }
65
+        ensureDateRange(query);
57 66
 
58
-        // 查询所有任务(不带时间范围筛选)
59
-        List<DailyTask> allTasks = queryTasks(queryWithoutTimeRange);
67
+        List<DailyTask> allTasks = queryTasks(query);
68
+
69
+        dto.setTotalTasks((long) allTasks.size());
60 70
 
61
-        // 今日数据
62
-        LocalDate today = LocalDate.now();
63
-        dto.setToday(buildPeriodStatistics(allTasks, today, today, null));
71
+        long completed = allTasks.stream()
72
+                .filter(t -> DailyTaskStatusEnum.COMPLETED.getCode().equals(t.getDtStatus()))
73
+                .count();
74
+        dto.setCompletedTasks(completed);
64 75
 
65
-        // 本周数据
66
-        LocalDate weekStart = today.minusDays(today.getDayOfWeek().getValue() - 1);
67
-        LocalDate lastWeekStart = weekStart.minusWeeks(1);
68
-        dto.setWeek(buildPeriodStatistics(allTasks, weekStart, today, lastWeekStart));
76
+        double completionRate = allTasks.size() > 0
77
+                ? (completed * 100.0 / allTasks.size()) : 0.0;
78
+        dto.setCompletionRate(Math.round(completionRate * 100.0) / 100.0);
69 79
 
70
-        // 本月数据
71
-        LocalDate monthStart = today.withDayOfMonth(1);
72
-        LocalDate lastMonthStart = monthStart.minusMonths(1);
73
-        dto.setMonth(buildPeriodStatistics(allTasks, monthStart, today, lastMonthStart));
80
+        double avgScore = allTasks.stream()
81
+                .filter(t -> DailyTaskStatusEnum.COMPLETED.getCode().equals(t.getDtStatus()))
82
+                .filter(t -> t.getDtTotalScore() != null)
83
+                .mapToDouble(t -> t.getDtTotalScore().doubleValue())
84
+                .average()
85
+                .orElse(0.0);
86
+        dto.setAvgScore(Math.round(avgScore * 100.0) / 100.0);
87
+
88
+        long expired = allTasks.stream()
89
+                .filter(t -> DailyTaskStatusEnum.EXPIRED.getCode().equals(t.getDtStatus()))
90
+                .count();
91
+        dto.setExpiredTasks(expired);
92
+
93
+        // 环比增长(与相同长度的上一周期比较)
94
+        DashboardOverviewDTO.GrowthData growth = computeGrowth(query, allTasks);
95
+        dto.setGrowth(growth);
74 96
 
75 97
         return dto;
76 98
     }
77 99
 
78 100
     @Override
101
+    @Cacheable(value = StatisticsCacheConfig.DASHBOARD_CACHE,
102
+            key = "'trend:' + T(com.sundot.airport.common.utils.SecurityUtils).getUserId() + ':' + (#query != null ? #query.toString() : 'default')")
79 103
     public DashboardTrendDTO getTrend(DashboardQueryDTO query) {
80 104
         DashboardTrendDTO dto = new DashboardTrendDTO();
81 105
 
@@ -130,6 +154,8 @@ public class DashboardServiceImpl implements IDashboardService {
130 154
     }
131 155
 
132 156
     @Override
157
+    @Cacheable(value = StatisticsCacheConfig.DASHBOARD_CACHE,
158
+            key = "'deptRanking:' + T(com.sundot.airport.common.utils.SecurityUtils).getUserId() + ':' + (#query != null ? #query.toString() : 'default')")
133 159
     public List<DashboardDeptRankingDTO> getDeptRanking(DashboardQueryDTO query) {
134 160
         // 查询任务
135 161
         List<DailyTask> allTasks = queryTasks(query);
@@ -195,6 +221,8 @@ public class DashboardServiceImpl implements IDashboardService {
195 221
     }
196 222
 
197 223
     @Override
224
+    @Cacheable(value = StatisticsCacheConfig.DASHBOARD_CACHE,
225
+            key = "'roleStats:' + T(com.sundot.airport.common.utils.SecurityUtils).getUserId() + ':' + (#query != null ? #query.toString() : 'default')")
198 226
     public List<DashboardRoleStatsDTO> getRoleStats(DashboardQueryDTO query) {
199 227
         // 查询任务
200 228
         List<DailyTask> allTasks = queryTasks(query);
@@ -265,6 +293,8 @@ public class DashboardServiceImpl implements IDashboardService {
265 293
     }
266 294
 
267 295
     @Override
296
+    @Cacheable(value = StatisticsCacheConfig.DASHBOARD_CACHE,
297
+            key = "'expiredAnalysis:' + T(com.sundot.airport.common.utils.SecurityUtils).getUserId() + ':' + (#query != null ? #query.toString() : 'default')")
268 298
     public DashboardExpiredAnalysisDTO getExpiredAnalysis(DashboardQueryDTO query) {
269 299
         DashboardExpiredAnalysisDTO dto = new DashboardExpiredAnalysisDTO();
270 300
 
@@ -358,6 +388,8 @@ public class DashboardServiceImpl implements IDashboardService {
358 388
     }
359 389
 
360 390
     @Override
391
+    @Cacheable(value = StatisticsCacheConfig.DASHBOARD_CACHE,
392
+            key = "'userRanking:' + T(com.sundot.airport.common.utils.SecurityUtils).getUserId() + ':' + (#query != null ? #query.toString() : 'default')")
361 393
     public List<DashboardUserRankingDTO> getUserRanking(DashboardQueryDTO query) {
362 394
         // 查询任务
363 395
         List<DailyTask> allTasks = queryTasks(query);
@@ -438,9 +470,7 @@ public class DashboardServiceImpl implements IDashboardService {
438 470
         if (query == null) {
439 471
             query = new DashboardQueryDTO();
440 472
         }
441
-        if (query.getTimeRange() == null || query.getTimeRange().isEmpty()) {
442
-            query.setTimeRange("today");
443
-        }
473
+        ensureDateRange(query);
444 474
 
445 475
         // 查询任务
446 476
         List<DailyTask> allTasks = queryTasks(query);
@@ -567,11 +597,27 @@ public class DashboardServiceImpl implements IDashboardService {
567 597
     }
568 598
 
569 599
     @Override
600
+    @Cacheable(value = StatisticsCacheConfig.DASHBOARD_CACHE,
601
+            key = "'filterOptions:' + T(com.sundot.airport.common.utils.SecurityUtils).getUserId()")
570 602
     public FilterOptionsDTO getFilterOptions() {
571 603
         FilterOptionsDTO dto = new FilterOptionsDTO();
572 604
 
573
-        // 查询所有部门
574
-        List<SysDept> depts = sysDeptMapper.selectDeptList(new SysDept());
605
+        List<SysDept> depts;
606
+        Set<Long> permittedDeptIds = getPermittedDeptIds();
607
+        if (permittedDeptIds == null) {
608
+            // 全部数据权限,返回所有部门
609
+            depts = sysDeptMapper.selectDeptList(new SysDept());
610
+        } else if (permittedDeptIds.isEmpty()) {
611
+            // 无部门权限(安检员等),返回空
612
+            depts = Collections.emptyList();
613
+        } else {
614
+            // 有部门限制,只返回权限范围内的部门
615
+            List<SysDept> allDepts = sysDeptMapper.selectDeptList(new SysDept());
616
+            depts = allDepts.stream()
617
+                    .filter(d -> permittedDeptIds.contains(d.getDeptId()))
618
+                    .collect(Collectors.toList());
619
+        }
620
+
575 621
         List<FilterOptionsDTO.DeptOption> deptOptions = depts.stream()
576 622
                 .map(dept -> {
577 623
                     FilterOptionsDTO.DeptOption option = new FilterOptionsDTO.DeptOption();
@@ -591,22 +637,187 @@ public class DashboardServiceImpl implements IDashboardService {
591 637
     // ==================== 私有辅助方法 ====================
592 638
 
593 639
     /**
594
-     * 查询任务列表
640
+     * 确保query有日期范围,若未指定startDate/endDate且未指定timeRange,则默认今天
641
+     */
642
+    private void ensureDateRange(DashboardQueryDTO query) {
643
+        boolean hasDateRange = (query.getStartDate() != null && !query.getStartDate().isEmpty())
644
+                || (query.getEndDate() != null && !query.getEndDate().isEmpty());
645
+        boolean hasTimeRange = query.getTimeRange() != null && !query.getTimeRange().isEmpty();
646
+
647
+        if (!hasDateRange && !hasTimeRange) {
648
+            String today = LocalDate.now().format(DATE_FORMATTER);
649
+            query.setStartDate(today);
650
+            query.setEndDate(today);
651
+        }
652
+    }
653
+
654
+    /**
655
+     * 计算环比增长数据
656
+     */
657
+    private DashboardOverviewDTO.GrowthData computeGrowth(DashboardQueryDTO query, List<DailyTask> currentTasks) {
658
+        DashboardOverviewDTO.GrowthData growth = new DashboardOverviewDTO.GrowthData();
659
+        try {
660
+            LocalDate startDate = LocalDate.parse(query.getStartDate(), DATE_FORMATTER);
661
+            LocalDate endDate = LocalDate.parse(query.getEndDate(), DATE_FORMATTER);
662
+            long periodDays = ChronoUnit.DAYS.between(startDate, endDate) + 1;
663
+
664
+            // 上一周期
665
+            LocalDate prevEnd = startDate.minusDays(1);
666
+            LocalDate prevStart = prevEnd.minusDays(periodDays - 1);
667
+
668
+            DashboardQueryDTO prevQuery = new DashboardQueryDTO();
669
+            prevQuery.setDeptIds(query.getDeptIds());
670
+            prevQuery.setRoles(query.getRoles());
671
+            prevQuery.setUserIds(query.getUserIds());
672
+            prevQuery.setStartDate(prevStart.format(DATE_FORMATTER));
673
+            prevQuery.setEndDate(prevEnd.format(DATE_FORMATTER));
674
+
675
+            List<DailyTask> prevTasks = queryTasks(prevQuery);
676
+
677
+            // 任务数增长
678
+            long currentTotal = currentTasks.size();
679
+            long prevTotal = prevTasks.size();
680
+            growth.setTotalTasksGrowth(calcGrowthRate(currentTotal, prevTotal));
681
+
682
+            // 完成数增长
683
+            long currentCompleted = currentTasks.stream()
684
+                    .filter(t -> DailyTaskStatusEnum.COMPLETED.getCode().equals(t.getDtStatus())).count();
685
+            long prevCompleted = prevTasks.stream()
686
+                    .filter(t -> DailyTaskStatusEnum.COMPLETED.getCode().equals(t.getDtStatus())).count();
687
+            growth.setCompletedTasksGrowth(calcGrowthRate(currentCompleted, prevCompleted));
688
+
689
+            // 平均分增长
690
+            double currentAvg = currentTasks.stream()
691
+                    .filter(t -> DailyTaskStatusEnum.COMPLETED.getCode().equals(t.getDtStatus()))
692
+                    .filter(t -> t.getDtTotalScore() != null)
693
+                    .mapToDouble(t -> t.getDtTotalScore().doubleValue())
694
+                    .average().orElse(0.0);
695
+            double prevAvg = prevTasks.stream()
696
+                    .filter(t -> DailyTaskStatusEnum.COMPLETED.getCode().equals(t.getDtStatus()))
697
+                    .filter(t -> t.getDtTotalScore() != null)
698
+                    .mapToDouble(t -> t.getDtTotalScore().doubleValue())
699
+                    .average().orElse(0.0);
700
+            growth.setAvgScoreGrowth(calcGrowthRate((long) (currentAvg * 100), (long) (prevAvg * 100)));
701
+
702
+        } catch (Exception e) {
703
+            growth.setTotalTasksGrowth(0.0);
704
+            growth.setCompletedTasksGrowth(0.0);
705
+            growth.setAvgScoreGrowth(0.0);
706
+        }
707
+        return growth;
708
+    }
709
+
710
+    private double calcGrowthRate(long current, long previous) {
711
+        if (previous == 0) {
712
+            return current > 0 ? 100.0 : 0.0;
713
+        }
714
+        return Math.round((current - previous) * 10000.0 / previous) / 100.0;
715
+    }
716
+
717
+    /**
718
+     * 判断当前用户是否拥有全部数据权限(管理员、站长、质检科)
719
+     */
720
+    private boolean hasAllDataPermission() {
721
+        try {
722
+            LoginUser loginUser = SecurityUtils.getLoginUser();
723
+            if (loginUser == null) return false;
724
+            // userId=1 是超级管理员
725
+            if (loginUser.getUserId() == 1L) return true;
726
+            List<SysRole> roles = loginUser.getUser().getRoles();
727
+            if (roles == null || roles.isEmpty()) return false;
728
+            List<String> roleKeys = roles.stream().map(SysRole::getRoleKey).collect(Collectors.toList());
729
+            return roleKeys.contains(RoleTypeEnum.admin.getCode())
730
+                    || roleKeys.contains(RoleTypeEnum.test.getCode())
731
+                    || roleKeys.contains(RoleTypeEnum.zhijianke.getCode());
732
+        } catch (Exception e) {
733
+            log.warn("获取登录用户信息失败", e);
734
+            return false;
735
+        }
736
+    }
737
+
738
+    /**
739
+     * 获取当前用户有权限查看的部门ID集合,null 表示可查看全部
740
+     */
741
+    private Set<Long> getPermittedDeptIds() {
742
+        if (hasAllDataPermission()) {
743
+            return null; // 全部数据
744
+        }
745
+        try {
746
+            LoginUser loginUser = SecurityUtils.getLoginUser();
747
+            Long deptId = loginUser.getDeptId();
748
+            List<SysRole> roles = loginUser.getUser().getRoles();
749
+            List<String> roleKeys = roles.stream().map(SysRole::getRoleKey).collect(Collectors.toList());
750
+
751
+            // 科长:本科室 + 所有下级部门
752
+            if (roleKeys.contains(RoleTypeEnum.kezhang.getCode())) {
753
+                Set<Long> deptIds = new HashSet<>();
754
+                deptIds.add(deptId);
755
+                List<SysDept> children = sysDeptMapper.selectChildrenDeptById(deptId);
756
+                if (children != null) {
757
+                    children.forEach(d -> deptIds.add(d.getDeptId()));
758
+                }
759
+                return deptIds;
760
+            }
761
+            // 班组长:本班组
762
+            if (roleKeys.contains(RoleTypeEnum.banzuzhang.getCode())) {
763
+                Set<Long> deptIds = new HashSet<>();
764
+                deptIds.add(deptId);
765
+                return deptIds;
766
+            }
767
+            // 安检员或其他角色:只看自己(返回空集合,在queryTasks中走userId过滤)
768
+            return Collections.emptySet();
769
+        } catch (Exception e) {
770
+            log.warn("获取用户权限部门失败", e);
771
+            return Collections.emptySet();
772
+        }
773
+    }
774
+
775
+    /**
776
+     * 查询任务列表(含数据权限过滤)
595 777
      */
596 778
     private List<DailyTask> queryTasks(DashboardQueryDTO query) {
597 779
         LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
598 780
         wrapper.eq(DailyTask::getDelStatus, 0);
599 781
 
600
-        // 添加筛选条件
782
+        // ===== 数据权限过滤 =====
783
+        Set<Long> permittedDeptIds = getPermittedDeptIds();
784
+        if (permittedDeptIds != null) {
785
+            if (permittedDeptIds.isEmpty()) {
786
+                // 无部门权限,只能看自己的数据
787
+                try {
788
+                    wrapper.eq(DailyTask::getDtUserId, SecurityUtils.getUserId());
789
+                } catch (Exception e) {
790
+                    // 获取不到用户,返回空
791
+                    return Collections.emptyList();
792
+                }
793
+            } else {
794
+                wrapper.in(DailyTask::getDtDeptId, permittedDeptIds);
795
+            }
796
+        }
797
+        // permittedDeptIds == null 表示全部数据,不加部门过滤
798
+
799
+        // ===== 前端筛选条件 =====
601 800
         if (query != null) {
602
-            // 按部门筛选
801
+            // 按部门筛选(包含所有子部门),在权限范围内进一步缩小
603 802
             if (query.getDeptIds() != null && !query.getDeptIds().isEmpty()) {
604 803
                 String[] deptIdArray = query.getDeptIds().split(",");
605
-                List<Long> deptIds = Arrays.stream(deptIdArray)
606
-                        .map(String::trim)
607
-                        .map(Long::parseLong)
608
-                        .collect(Collectors.toList());
609
-                wrapper.in(DailyTask::getDtDeptId, deptIds);
804
+                Set<Long> selectedDeptIds = new HashSet<>();
805
+                for (String idStr : deptIdArray) {
806
+                    Long deptId = Long.parseLong(idStr.trim());
807
+                    selectedDeptIds.add(deptId);
808
+                    List<SysDept> children = sysDeptMapper.selectChildrenDeptById(deptId);
809
+                    if (children != null) {
810
+                        children.forEach(d -> selectedDeptIds.add(d.getDeptId()));
811
+                    }
812
+                }
813
+                // 如果有权限限制,取交集;否则直接用前端选择
814
+                if (permittedDeptIds != null && !permittedDeptIds.isEmpty()) {
815
+                    selectedDeptIds.retainAll(permittedDeptIds);
816
+                }
817
+                if (selectedDeptIds.isEmpty()) {
818
+                    return Collections.emptyList();
819
+                }
820
+                wrapper.in(DailyTask::getDtDeptId, selectedDeptIds);
610 821
             }
611 822
 
612 823
             // 按角色筛选
@@ -643,7 +854,6 @@ public class DashboardServiceImpl implements IDashboardService {
643 854
                 }
644 855
 
645 856
                 if (startDate != null) {
646
-                    // 将LocalDate转换为Date进行比较
647 857
                     Date startDateTime = Date.from(startDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
648 858
                     Date endDateTime = Date.from(endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
649 859
                     wrapper.ge(DailyTask::getDtBusinessDate, startDateTime);
@@ -677,57 +887,6 @@ public class DashboardServiceImpl implements IDashboardService {
677 887
     }
678 888
 
679 889
     /**
680
-     * 构建时间段统计数据
681
-     */
682
-    private DashboardOverviewDTO.PeriodStatistics buildPeriodStatistics(
683
-            List<DailyTask> allTasks, LocalDate startDate, LocalDate endDate, LocalDate compareStartDate) {
684
-
685
-        DashboardOverviewDTO.PeriodStatistics stats = new DashboardOverviewDTO.PeriodStatistics();
686
-
687
-        // 筛选时间段内的任务
688
-        List<DailyTask> periodTasks = allTasks.stream()
689
-                .filter(t -> isDateInRange(t.getDtBusinessDate(), startDate, endDate))
690
-                .collect(Collectors.toList());
691
-
692
-        stats.setTotalTasks((long) periodTasks.size());
693
-
694
-        long completed = periodTasks.stream()
695
-                .filter(t -> DailyTaskStatusEnum.COMPLETED.getCode().equals(t.getDtStatus()))
696
-                .count();
697
-        stats.setCompletedTasks(completed);
698
-
699
-        double completionRate = periodTasks.size() > 0
700
-                ? (completed * 100.0 / periodTasks.size()) : 0.0;
701
-        stats.setCompletionRate(Math.round(completionRate * 100.0) / 100.0);
702
-
703
-        // 计算平均分
704
-        double avgScore = periodTasks.stream()
705
-                .filter(t -> DailyTaskStatusEnum.COMPLETED.getCode().equals(t.getDtStatus()))
706
-                .filter(t -> t.getDtTotalScore() != null)
707
-                .mapToDouble(t -> t.getDtTotalScore().doubleValue())
708
-                .average()
709
-                .orElse(0.0);
710
-        stats.setAvgScore(Math.round(avgScore * 100.0) / 100.0);
711
-
712
-        long expired = periodTasks.stream()
713
-                .filter(t -> DailyTaskStatusEnum.EXPIRED.getCode().equals(t.getDtStatus()))
714
-                .count();
715
-        stats.setExpiredTasks(expired);
716
-
717
-        // 计算环比增长
718
-        if (compareStartDate != null) {
719
-            DashboardOverviewDTO.GrowthData growth = new DashboardOverviewDTO.GrowthData();
720
-            // TODO: 实现环比计算逻辑
721
-            growth.setTotalTasksGrowth(0.0);
722
-            growth.setCompletedTasksGrowth(0.0);
723
-            growth.setAvgScoreGrowth(0.0);
724
-            stats.setGrowth(growth);
725
-        }
726
-
727
-        return stats;
728
-    }
729
-
730
-    /**
731 890
      * 判断日期是否在指定范围内
732 891
      */
733 892
     private boolean isDateInRange(Date date, LocalDate startDate, LocalDate endDate) {