Browse Source

第一次提交

chenshudong 1 day ago
parent
commit
585ef44297
100 changed files with 21663 additions and 0 deletions
  1. 88 0
      .gitignore
  2. 20 0
      LICENSE
  3. 114 0
      airport-admin/pom.xml
  4. 30 0
      airport-admin/src/main/java/com/sundot/airport/Application.java
  5. 18 0
      airport-admin/src/main/java/com/sundot/airport/ServletInitializer.java
  6. 311 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/approval/ApprovalController.java
  7. 362 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/approval/CheckApprovalController.java
  8. 170 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/approval/dto/ApprovalDTO.java
  9. 81 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/approval/dto/PersonalCheckDTO.java
  10. 25 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/approval/dto/RejectDTO.java
  11. 79 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/approval/dto/SectionCheckDTO.java
  12. 93 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/approval/dto/SeizureReportDTO.java
  13. 52 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/attendance/AttendanceAreaController.java
  14. 91 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/attendance/AttendanceCheckRecordController.java
  15. 1181 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/attendance/AttendancePostRecordController.java
  16. 289 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/attendance/AttendanceRecordController.java
  17. 166 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/attendance/AttendanceTeamUserRecordController.java
  18. 87 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/attendance/api/AttendanceController.java
  19. 38 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/cache/StatisticsCacheController.java
  20. 196 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/check/CheckCorrectionController.java
  21. 256 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/check/CheckLargeScreenController.java
  22. 98 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/check/CheckProjectItemController.java
  23. 171 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/check/CheckRecordController.java
  24. 230 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/check/CheckTaskController.java
  25. 98 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/check/CheckUserController.java
  26. 94 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/common/CaptchaController.java
  27. 162 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/common/CommonController.java
  28. 52 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/common/DataConfigController.java
  29. 143 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/daily/DailyTaskConfigController.java
  30. 210 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/daily/DeptProfileController.java
  31. 208 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/daily/SiteProfileController.java
  32. 20 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/BaseController.java
  33. 395 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/DailyExamComparisonController.java
  34. 513 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/DailyExamController.java
  35. 755 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/DailyExamWrongAnalysisController.java
  36. 542 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/DailyExamWrongAnalysisPcController.java
  37. 109 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/EighteenController.java
  38. 139 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/ExamController.java
  39. 104 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/QuesCatController.java
  40. 143 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/QuesController.java
  41. 43 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/QuizAccuracyAnalysisController.java
  42. 109 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/TestPaperCatController.java
  43. 135 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/TestPaperController.java
  44. 138 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/exam/WeakModuleRuleController.java
  45. 554 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/home/AttendanceIndexStatsController.java
  46. 41 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/home/SeizureRankingController.java
  47. 63 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/home/SeizureReportController.java
  48. 227 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/item/FailedMatchItemController.java
  49. 161 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/item/ItemLargeScreenController.java
  50. 280 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/item/ItemSeizureItemsController.java
  51. 270 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/item/ItemSeizureRecordController.java
  52. 1711 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/item/PerformanceDimensionController.java
  53. 556 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/item/PerformanceIndicatorsController.java
  54. 940 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/item/PerformanceMetricsController.java
  55. 73 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/item/SeizureMessagePushController.java
  56. 115 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/monitor/CacheController.java
  57. 25 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/monitor/ServerController.java
  58. 77 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/monitor/SysLogininforController.java
  59. 65 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/monitor/SysOperlogController.java
  60. 73 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/monitor/SysUserOnlineController.java
  61. 209 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/portrait/AttendanceStatsController.java
  62. 309 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/portrait/ItemUserRankingController.java
  63. 121 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/portrait/UserBasicPortraitController.java
  64. 416 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/quality/ItemCategoryStatsController.java
  65. 541 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/quality/QualificationLevelStatsController.java
  66. 642 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/quality/SimpleAttendancePersonController.java
  67. 100 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseAttachmentController.java
  68. 108 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseCheckCategoryController.java
  69. 108 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseCheckPointController.java
  70. 109 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseDefaultChoiseController.java
  71. 107 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/BasePositionController.java
  72. 194 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseProjectController.java
  73. 100 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseReadController.java
  74. 109 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseSeizeCategoryController.java
  75. 109 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseSeizeItemController.java
  76. 54 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SmsController.java
  77. 132 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysAppController.java
  78. 68 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysCheckAnalysisReportController.java
  79. 133 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysConfigController.java
  80. 162 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysDeptController.java
  81. 121 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysDictDataController.java
  82. 121 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysDictTypeController.java
  83. 93 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysDynamicSqlController.java
  84. 563 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysHomePageController.java
  85. 654 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysHomeReportController.java
  86. 29 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysIndexController.java
  87. 175 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysLearningGrowthController.java
  88. 137 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysLoginController.java
  89. 125 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysMenuController.java
  90. 88 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysNoticeController.java
  91. 136 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysPostController.java
  92. 148 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysProfileController.java
  93. 38 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysRegisterController.java
  94. 239 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysRoleController.java
  95. 264 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysUsageReportController.java
  96. 927 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysUserController.java
  97. 102 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysWorkingDocumentController.java
  98. 183 0
      airport-admin/src/main/java/com/sundot/airport/web/controller/tool/TestController.java
  99. 100 0
      airport-admin/src/main/java/com/sundot/airport/web/core/cache/UserCache.java
  100. 0 0
      airport-admin/src/main/java/com/sundot/airport/web/core/config/SwaggerConfig.java

+ 88 - 0
.gitignore

@@ -0,0 +1,88 @@
1
+######################################################################
2
+# Build Tools
3
+
4
+.gradle
5
+/build/
6
+!gradle/wrapper/gradle-wrapper.jar
7
+
8
+target/
9
+!.mvn/wrapper/maven-wrapper.jar
10
+
11
+######################################################################
12
+# IDE
13
+
14
+### STS ###
15
+.apt_generated
16
+.classpath
17
+.factorypath
18
+.project
19
+.settings
20
+.springBeans
21
+
22
+### IntelliJ IDEA ###
23
+.idea
24
+*.iws
25
+*.iml
26
+*.ipr
27
+
28
+### JRebel ###
29
+rebel.xml
30
+
31
+### NetBeans ###
32
+nbproject/private/
33
+build/*
34
+nbbuild/
35
+dist/
36
+nbdist/
37
+.nb-gradle/
38
+
39
+######################################################################
40
+# Others
41
+*.log
42
+*.xml.versionsBackup
43
+*.swp
44
+
45
+# Node.js files
46
+node_modules/
47
+package-lock.json
48
+
49
+# Documentation and test files
50
+CLAUDE.md
51
+*.curl
52
+nul
53
+
54
+######################################################################
55
+# Project-specific ignores
56
+
57
+# Documentation files
58
+.claude/
59
+docs/
60
+DEPLOY-HAIKOU.md
61
+FailedMatchItem-API-DOC.md
62
+HAIKOU-DEPLOYMENT-README.md
63
+README_DAILY_EXAM_INTEGRATION.md
64
+安检分级质控系统云服务信息安全保障方案.md
65
+安检分级质控系统信息安全保障说明书(精简版).md
66
+安检分级质控系统信息安全说明文档.md
67
+
68
+# Configuration and deployment scripts
69
+application-haikou.yml
70
+deploy-haikou.sh
71
+nginx-dual-env.conf
72
+nginx.conf.new
73
+
74
+# SQL scripts
75
+create_tables.sql
76
+debug_query.sql
77
+sql/add_department_fields_to_daily_task.sql
78
+sql/daily_question_answer.sql
79
+sql/edu_cs_weak_module_rule.sql
80
+sql/quartz_daily_task_expire_job.sql
81
+
82
+# Testing and utilities
83
+postman/
84
+stress-test/
85
+
86
+!*/build/*.java
87
+!*/build/*.html
88
+!*/build/*.xml

+ 20 - 0
LICENSE

@@ -0,0 +1,20 @@
1
+The MIT License (MIT)
2
+
3
+Copyright (c) 2018 RuoYi
4
+
5
+Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+this software and associated documentation files (the "Software"), to deal in
7
+the Software without restriction, including without limitation the rights to
8
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+the Software, and to permit persons to whom the Software is furnished to do so,
10
+subject to the following conditions:
11
+
12
+The above copyright notice and this permission notice shall be included in all
13
+copies or substantial portions of the Software.
14
+
15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 114 - 0
airport-admin/pom.xml

@@ -0,0 +1,114 @@
1
+<?xml version="1.0" encoding="UTF-8"?>
2
+<project xmlns="http://maven.apache.org/POM/4.0.0"
3
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5
+    <parent>
6
+        <artifactId>airport</artifactId>
7
+        <groupId>com.sundot.airport</groupId>
8
+        <version>3.9.0</version>
9
+    </parent>
10
+    <modelVersion>4.0.0</modelVersion>
11
+    <packaging>jar</packaging>
12
+    <artifactId>airport-admin</artifactId>
13
+
14
+    <description>
15
+        web服务入口
16
+    </description>
17
+
18
+    <dependencies>
19
+
20
+        <!-- spring-boot-devtools -->
21
+        <dependency>
22
+            <groupId>org.springframework.boot</groupId>
23
+            <artifactId>spring-boot-devtools</artifactId>
24
+            <optional>true</optional> <!-- 表示依赖不会传递 -->
25
+        </dependency>
26
+
27
+        <!-- swagger3-->
28
+        <dependency>
29
+            <groupId>io.springfox</groupId>
30
+            <artifactId>springfox-boot-starter</artifactId>
31
+        </dependency>
32
+
33
+        <!-- 防止进入swagger页面报类型转换错误,排除3.0.0中的引用,手动增加1.6.2版本 -->
34
+        <dependency>
35
+            <groupId>io.swagger</groupId>
36
+            <artifactId>swagger-models</artifactId>
37
+            <version>1.6.2</version>
38
+        </dependency>
39
+
40
+        <!-- Mysql驱动包 -->
41
+        <dependency>
42
+            <groupId>mysql</groupId>
43
+            <artifactId>mysql-connector-java</artifactId>
44
+        </dependency>
45
+
46
+        <!-- 核心模块-->
47
+        <dependency>
48
+            <groupId>com.sundot.airport</groupId>
49
+            <artifactId>airport-framework</artifactId>
50
+        </dependency>
51
+
52
+        <!-- 定时任务-->
53
+        <dependency>
54
+            <groupId>com.sundot.airport</groupId>
55
+            <artifactId>airport-quartz</artifactId>
56
+        </dependency>
57
+
58
+        <!-- 代码生成-->
59
+        <dependency>
60
+            <groupId>com.sundot.airport</groupId>
61
+            <artifactId>airport-generator</artifactId>
62
+        </dependency>
63
+
64
+        <!-- 查获物品管理模块-->
65
+        <dependency>
66
+            <groupId>com.sundot.airport</groupId>
67
+            <artifactId>airport-item</artifactId>
68
+        </dependency>
69
+
70
+        <!-- 巡检管理模块-->
71
+        <dependency>
72
+            <groupId>com.sundot.airport</groupId>
73
+            <artifactId>airport-check</artifactId>
74
+        </dependency>
75
+
76
+        <dependency>
77
+            <groupId>org.projectlombok</groupId>
78
+            <artifactId>lombok</artifactId>
79
+            <scope>provided</scope>
80
+        </dependency>
81
+
82
+    </dependencies>
83
+
84
+    <build>
85
+        <plugins>
86
+            <plugin>
87
+                <groupId>org.springframework.boot</groupId>
88
+                <artifactId>spring-boot-maven-plugin</artifactId>
89
+                <version>2.5.15</version>
90
+                <configuration>
91
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
92
+                </configuration>
93
+                <executions>
94
+                    <execution>
95
+                        <goals>
96
+                            <goal>repackage</goal>
97
+                        </goals>
98
+                    </execution>
99
+                </executions>
100
+            </plugin>
101
+            <plugin>
102
+                <groupId>org.apache.maven.plugins</groupId>
103
+                <artifactId>maven-war-plugin</artifactId>
104
+                <version>3.1.0</version>
105
+                <configuration>
106
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
107
+                    <warName>${project.artifactId}</warName>
108
+                </configuration>
109
+            </plugin>
110
+        </plugins>
111
+        <finalName>${project.artifactId}</finalName>
112
+    </build>
113
+
114
+</project>

+ 30 - 0
airport-admin/src/main/java/com/sundot/airport/Application.java

@@ -0,0 +1,30 @@
1
+package com.sundot.airport;
2
+
3
+import org.springframework.boot.SpringApplication;
4
+import org.springframework.boot.autoconfigure.SpringBootApplication;
5
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
6
+
7
+/**
8
+ * 启动程序
9
+ * 
10
+ * @author ruoyi
11
+ */
12
+@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
13
+public class Application
14
+{
15
+    public static void main(String[] args)
16
+    {
17
+        // System.setProperty("spring.devtools.restart.enabled", "false");
18
+        SpringApplication.run(Application.class, args);
19
+        System.out.println("(♥◠‿◠)ノ゙  若依启动成功   ლ(´ڡ`ლ)゙  \n" +
20
+                " .-------.       ____     __        \n" +
21
+                " |  _ _   \\      \\   \\   /  /    \n" +
22
+                " | ( ' )  |       \\  _. /  '       \n" +
23
+                " |(_ o _) /        _( )_ .'         \n" +
24
+                " | (_,_).' __  ___(_ o _)'          \n" +
25
+                " |  |\\ \\  |  ||   |(_,_)'         \n" +
26
+                " |  | \\ `'   /|   `-'  /           \n" +
27
+                " |  |  \\    /  \\      /           \n" +
28
+                " ''-'   `'-'    `-..-'              ");
29
+    }
30
+}

+ 18 - 0
airport-admin/src/main/java/com/sundot/airport/ServletInitializer.java

@@ -0,0 +1,18 @@
1
+package com.sundot.airport;
2
+
3
+import org.springframework.boot.builder.SpringApplicationBuilder;
4
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
5
+
6
+/**
7
+ * web容器中进行部署
8
+ * 
9
+ * @author ruoyi
10
+ */
11
+public class ServletInitializer extends SpringBootServletInitializer
12
+{
13
+    @Override
14
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
15
+    {
16
+        return application.sources(Application.class);
17
+    }
18
+}

+ 311 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/approval/ApprovalController.java

@@ -0,0 +1,311 @@
1
+package com.sundot.airport.web.controller.approval;
2
+
3
+import java.util.List;
4
+import java.util.Map;
5
+import javax.servlet.http.HttpServletResponse;
6
+
7
+import com.sundot.airport.common.utils.SecurityUtils;
8
+import com.sundot.airport.system.domain.approval.ApprovalInstance;
9
+import com.sundot.airport.system.domain.approval.ApprovalTask;
10
+import com.sundot.airport.system.service.approval.IApprovalEngineService;
11
+import org.springframework.security.access.prepost.PreAuthorize;
12
+import org.springframework.beans.factory.annotation.Autowired;
13
+import org.springframework.web.bind.annotation.GetMapping;
14
+import org.springframework.web.bind.annotation.PostMapping;
15
+import org.springframework.web.bind.annotation.PutMapping;
16
+import org.springframework.web.bind.annotation.DeleteMapping;
17
+import org.springframework.web.bind.annotation.PathVariable;
18
+import org.springframework.web.bind.annotation.RequestBody;
19
+import org.springframework.web.bind.annotation.RequestMapping;
20
+import org.springframework.web.bind.annotation.RestController;
21
+import com.sundot.airport.common.annotation.Log;
22
+import com.sundot.airport.common.core.controller.BaseController;
23
+import com.sundot.airport.common.core.domain.AjaxResult;
24
+import com.sundot.airport.common.enums.BusinessType;
25
+import com.sundot.airport.common.utils.poi.ExcelUtil;
26
+import com.sundot.airport.common.core.page.TableDataInfo;
27
+
28
+/**
29
+ * 审批流程Controller
30
+ *
31
+ * @author simon lin
32
+ * @date 2025-09-06
33
+ */
34
+@RestController
35
+@RequestMapping("/system/approval")
36
+public class ApprovalController extends BaseController {
37
+    @Autowired
38
+    private IApprovalEngineService approvalEngineService;
39
+
40
+    /**
41
+     * 启动审批流程
42
+     */
43
+    @PreAuthorize("@ss.hasPermi('system:approval:start')")
44
+    @Log(title = "启动审批流程", businessType = BusinessType.INSERT)
45
+    @PostMapping("/start")
46
+    public AjaxResult startProcess(@RequestBody Map<String, Object> params) {
47
+        String workflowCode = (String) params.get("workflowCode");
48
+        String businessType = (String) params.get("businessType");
49
+        Long businessId = Long.valueOf(params.get("businessId").toString());
50
+        String title = (String) params.get("title");
51
+        Long submitterId = SecurityUtils.getUserId();
52
+        String submitterName = SecurityUtils.getUsername();
53
+        Map<String, Object> formData = (Map<String, Object>) params.get("formData");
54
+        Map<String, Object> businessData = (Map<String, Object>) params.get("businessData");
55
+
56
+        ApprovalInstance instance = approvalEngineService.startProcess(
57
+                workflowCode, businessType, businessId, title,
58
+                submitterId, submitterName, formData, businessData);
59
+
60
+        return AjaxResult.success("流程启动成功", instance);
61
+    }
62
+
63
+    /**
64
+     * 启动个人级别审批流程
65
+     */
66
+    @PreAuthorize("@ss.hasPermi('system:approval:start')")
67
+    @Log(title = "启动个人级别审批流程", businessType = BusinessType.INSERT)
68
+    @PostMapping("/start/personal")
69
+    public AjaxResult startPersonalLevelProcess(@RequestBody Map<String, Object> params) {
70
+        String businessType = (String) params.get("businessType");
71
+        Long businessId = Long.valueOf(params.get("businessId").toString());
72
+        String title = (String) params.get("title");
73
+        Long submitterId = SecurityUtils.getUserId();
74
+        String submitterName = SecurityUtils.getUsername();
75
+
76
+        // 新的个人级别检查流程参数
77
+        Long sectionLeaderId = null;
78
+        Object sectionLeaderIdObj = params.get("sectionLeaderId");
79
+        if (sectionLeaderIdObj != null) {
80
+            if (sectionLeaderIdObj instanceof Number) {
81
+                sectionLeaderId = ((Number) sectionLeaderIdObj).longValue();
82
+            } else if (sectionLeaderIdObj instanceof String) {
83
+                sectionLeaderId = Long.parseLong((String) sectionLeaderIdObj);
84
+            }
85
+        }
86
+
87
+        if (sectionLeaderId == null) {
88
+            return AjaxResult.error("sectionLeaderId参数不能为空");
89
+        }
90
+
91
+        Map<String, Object> formData = (Map<String, Object>) params.get("formData");
92
+        Map<String, Object> businessData = (Map<String, Object>) params.get("businessData");
93
+
94
+        ApprovalInstance instance = approvalEngineService.startPersonalLevelProcess(
95
+                businessType, businessId, title, submitterId, submitterName,
96
+                sectionLeaderId, formData, businessData);
97
+
98
+        return AjaxResult.success("个人级别流程启动成功", instance);
99
+    }
100
+
101
+    /**
102
+     * 启动科级审批流程
103
+     */
104
+    @PreAuthorize("@ss.hasPermi('system:approval:start')")
105
+    @Log(title = "启动科级审批流程", businessType = BusinessType.INSERT)
106
+    @PostMapping("/start/section")
107
+    public AjaxResult startSectionLevelProcess(@RequestBody Map<String, Object> params) {
108
+        String businessType = (String) params.get("businessType");
109
+        Long businessId = Long.valueOf(params.get("businessId").toString());
110
+        String title = (String) params.get("title");
111
+        Long submitterId = SecurityUtils.getUserId();
112
+        String submitterName = SecurityUtils.getUsername();
113
+
114
+        // 新的科级审批流程参数
115
+        Long sectionLeaderId = null;
116
+        Object sectionLeaderIdObj = params.get("sectionLeaderId");
117
+        if (sectionLeaderIdObj != null) {
118
+            if (sectionLeaderIdObj instanceof Number) {
119
+                sectionLeaderId = ((Number) sectionLeaderIdObj).longValue();
120
+            } else if (sectionLeaderIdObj instanceof String) {
121
+                sectionLeaderId = Long.parseLong((String) sectionLeaderIdObj);
122
+            }
123
+        }
124
+
125
+        if (sectionLeaderId == null) {
126
+            return AjaxResult.error("sectionLeaderId参数不能为空");
127
+        }
128
+
129
+        Map<String, Object> formData = (Map<String, Object>) params.get("formData");
130
+        Map<String, Object> businessData = (Map<String, Object>) params.get("businessData");
131
+
132
+        ApprovalInstance instance = approvalEngineService.startSectionLevelProcess(
133
+                businessType, businessId, title, submitterId, submitterName, sectionLeaderId, formData, businessData);
134
+
135
+        return AjaxResult.success("科级审批流程启动成功", instance);
136
+    }
137
+
138
+    /**
139
+     * 启动班组级审批流程
140
+     */
141
+    @PreAuthorize("@ss.hasPermi('system:approval:start')")
142
+    @Log(title = "启动班组级审批流程", businessType = BusinessType.INSERT)
143
+    @PostMapping("/start/group")
144
+    public AjaxResult startGroupLevelProcess(@RequestBody Map<String, Object> params) {
145
+        String businessType = (String) params.get("businessType");
146
+        Long businessId = Long.valueOf(params.get("businessId").toString());
147
+        String title = (String) params.get("title");
148
+        Long submitterId = SecurityUtils.getUserId();
149
+        String submitterName = SecurityUtils.getUsername();
150
+        Map<String, Object> formData = (Map<String, Object>) params.get("formData");
151
+        Map<String, Object> businessData = (Map<String, Object>) params.get("businessData");
152
+
153
+        ApprovalInstance instance = approvalEngineService.startGroupLevelProcess(
154
+                businessType, businessId, title, submitterId, submitterName, formData, businessData);
155
+
156
+        return AjaxResult.success("班组级审批流程启动成功", instance);
157
+    }
158
+
159
+    /**
160
+     * 启动查获上报审批流程
161
+     */
162
+    @PreAuthorize("@ss.hasPermi('system:approval:start')")
163
+    @Log(title = "启动查获上报审批流程", businessType = BusinessType.INSERT)
164
+    @PostMapping("/start/seizure")
165
+    public AjaxResult startSeizureReportProcess(@RequestBody Map<String, Object> params) {
166
+        String businessType = (String) params.get("businessType");
167
+        Long businessId = Long.valueOf(params.get("businessId").toString());
168
+        String title = (String) params.get("title");
169
+        Long submitterId = SecurityUtils.getUserId();
170
+        String submitterName = SecurityUtils.getUsername();
171
+        String submitterRole = (String) params.get("submitterRole");
172
+        Map<String, Object> formData = (Map<String, Object>) params.get("formData");
173
+        Map<String, Object> businessData = (Map<String, Object>) params.get("businessData");
174
+
175
+        ApprovalInstance instance = approvalEngineService.startSeizureReportProcess(
176
+                businessType, businessId, title, submitterId, submitterName,
177
+                submitterRole, formData, businessData);
178
+
179
+        return AjaxResult.success("查获上报审批流程启动成功", instance);
180
+    }
181
+
182
+    /**
183
+     * 审批任务(同意)
184
+     */
185
+    @PreAuthorize("@ss.hasPermi('system:approval:approve')")
186
+    @Log(title = "审批任务", businessType = BusinessType.UPDATE)
187
+    @PutMapping("/approve/{taskId}")
188
+    public AjaxResult approveTask(@PathVariable("taskId") Long taskId, @RequestBody Map<String, Object> params) {
189
+        Long operatorId = SecurityUtils.getUserId();
190
+        String operatorName = SecurityUtils.getUsername();
191
+        String comment = (String) params.get("comment");
192
+        Map<String, Object> formData = (Map<String, Object>) params.get("formData");
193
+
194
+        boolean result = approvalEngineService.approveTask(taskId, operatorId, operatorName, comment, formData);
195
+        return result ? AjaxResult.success("审批成功") : AjaxResult.error("审批失败");
196
+    }
197
+
198
+    /**
199
+     * 驳回任务
200
+     */
201
+    @PreAuthorize("@ss.hasPermi('system:approval:reject')")
202
+    @Log(title = "驳回任务", businessType = BusinessType.UPDATE)
203
+    @PutMapping("/reject/{taskId}")
204
+    public AjaxResult rejectTask(@PathVariable("taskId") Long taskId, @RequestBody Map<String, Object> params) {
205
+        Long operatorId = SecurityUtils.getUserId();
206
+        String operatorName = SecurityUtils.getUsername();
207
+        String comment = (String) params.get("comment");
208
+
209
+        boolean result = approvalEngineService.rejectTask(taskId, operatorId, operatorName, comment);
210
+        return result ? AjaxResult.success("驳回成功") : AjaxResult.error("驳回失败");
211
+    }
212
+
213
+    /**
214
+     * 取消审批流程
215
+     */
216
+    @PreAuthorize("@ss.hasPermi('system:approval:cancel')")
217
+    @Log(title = "取消审批流程", businessType = BusinessType.UPDATE)
218
+    @PutMapping("/cancel/{instanceId}")
219
+    public AjaxResult cancelProcess(@PathVariable("instanceId") Long instanceId, @RequestBody Map<String, Object> params) {
220
+        Long operatorId = SecurityUtils.getUserId();
221
+        String operatorName = SecurityUtils.getUsername();
222
+        String comment = (String) params.get("comment");
223
+
224
+        boolean result = approvalEngineService.cancelProcess(instanceId, operatorId, operatorName, comment);
225
+        return result ? AjaxResult.success("取消成功") : AjaxResult.error("取消失败");
226
+    }
227
+
228
+    /**
229
+     * 获取用户待办任务列表
230
+     */
231
+    @PreAuthorize("@ss.hasPermi('system:approval:query')")
232
+    @GetMapping("/tasks/pending")
233
+    public TableDataInfo getPendingTasks() {
234
+        Long userId = SecurityUtils.getUserId();
235
+        List<ApprovalTask> list = approvalEngineService.getUserPendingTasks(userId);
236
+        return getDataTable(list);
237
+    }
238
+
239
+    /**
240
+     * 获取用户已办任务列表
241
+     */
242
+    @PreAuthorize("@ss.hasPermi('system:approval:query')")
243
+    @GetMapping("/tasks/completed")
244
+    public TableDataInfo getCompletedTasks() {
245
+        Long userId = SecurityUtils.getUserId();
246
+        List<ApprovalTask> list = approvalEngineService.getUserCompletedTasks(userId);
247
+        return getDataTable(list);
248
+    }
249
+
250
+    /**
251
+     * 获取用户发起的审批实例列表
252
+     */
253
+    @PreAuthorize("@ss.hasPermi('system:approval:query')")
254
+    @GetMapping("/instances/submitted")
255
+    public TableDataInfo getSubmittedInstances(String status) {
256
+        Long userId = SecurityUtils.getUserId();
257
+        List<ApprovalInstance> list = approvalEngineService.getUserSubmittedInstances(userId, status);
258
+        return getDataTable(list);
259
+    }
260
+
261
+    /**
262
+     * 根据业务类型和级别获取流程代码
263
+     */
264
+    @GetMapping("/workflow/code")
265
+    public AjaxResult getWorkflowCode(String businessType, String level, String submitterRole) {
266
+        String workflowCode = approvalEngineService.getWorkflowCodeByBusinessAndLevel(businessType, level, submitterRole);
267
+        return AjaxResult.success(workflowCode);
268
+    }
269
+
270
+    /**
271
+     * 统计用户待办任务数量
272
+     */
273
+    @GetMapping("/tasks/pending/count")
274
+    public AjaxResult getPendingTasksCount() {
275
+        Long userId = SecurityUtils.getUserId();
276
+        List<ApprovalTask> tasks = approvalEngineService.getUserPendingTasks(userId);
277
+        return AjaxResult.success(tasks.size());
278
+    }
279
+
280
+    /**
281
+     * 批量审批任务(同意)
282
+     *
283
+     * @param ids 任务ID列表
284
+     */
285
+    @PreAuthorize("@ss.hasPermi('system:approval:approve')")
286
+    @Log(title = "审批任务", businessType = BusinessType.UPDATE)
287
+    @PutMapping("/approve/list")
288
+    public AjaxResult approveTaskList(@RequestBody List<Long> ids) {
289
+        if (ids == null || ids.isEmpty()) {
290
+            return AjaxResult.error("ID列表不能为空");
291
+        }
292
+        boolean result = approvalEngineService.approveTaskList(ids);
293
+        return result ? AjaxResult.success("审批成功") : AjaxResult.error("审批失败");
294
+    }
295
+
296
+    /**
297
+     * 批量驳回任务
298
+     *
299
+     * @param ids 任务ID列表
300
+     */
301
+    @PreAuthorize("@ss.hasPermi('system:approval:reject')")
302
+    @Log(title = "驳回任务", businessType = BusinessType.UPDATE)
303
+    @PutMapping("/reject/list")
304
+    public AjaxResult rejectTaskList(@RequestBody List<Long> ids) {
305
+        if (ids == null || ids.isEmpty()) {
306
+            return AjaxResult.error("ID列表不能为空");
307
+        }
308
+        boolean result = approvalEngineService.rejectTaskList(ids);
309
+        return result ? AjaxResult.success("驳回成功") : AjaxResult.error("驳回失败");
310
+    }
311
+}

+ 362 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/approval/CheckApprovalController.java

@@ -0,0 +1,362 @@
1
+package com.sundot.airport.web.controller.approval;
2
+
3
+import java.util.List;
4
+import java.util.Map;
5
+
6
+import javax.validation.Valid;
7
+
8
+import org.springframework.beans.factory.annotation.Autowired;
9
+import org.springframework.security.access.prepost.PreAuthorize;
10
+import org.springframework.web.bind.annotation.*;
11
+
12
+import com.sundot.airport.common.annotation.Log;
13
+import com.sundot.airport.common.core.controller.BaseController;
14
+import com.sundot.airport.common.core.domain.AjaxResult;
15
+import com.sundot.airport.common.core.page.TableDataInfo;
16
+import com.sundot.airport.common.enums.BusinessType;
17
+import com.sundot.airport.common.utils.SecurityUtils;
18
+import com.sundot.airport.system.domain.approval.ApprovalInstance;
19
+import com.sundot.airport.system.domain.approval.ApprovalTask;
20
+import com.sundot.airport.system.service.approval.IApprovalEngineService;
21
+import com.sundot.airport.system.service.approval.IApprovalUserService;
22
+import com.sundot.airport.system.service.approval.ICheckApprovalService;
23
+import com.sundot.airport.web.controller.approval.dto.*;
24
+import com.sundot.airport.system.domain.approval.dto.ApprovalCcDetailDTO;
25
+
26
+/**
27
+ * 安检审批流程Controller
28
+ *
29
+ * @author simon lin
30
+ * @date 2025-09-06
31
+ */
32
+@RestController
33
+@RequestMapping("/system/check")
34
+public class CheckApprovalController extends BaseController {
35
+    @Autowired
36
+    private IApprovalEngineService approvalEngineService;
37
+
38
+    @Autowired
39
+    private IApprovalUserService approvalUserService;
40
+
41
+    @Autowired
42
+    private ICheckApprovalService checkApprovalService;
43
+
44
+    /**
45
+     * 1. 启动个人级别审批流程
46
+     */
47
+    @PreAuthorize("@ss.hasPermi('system:approval:start')")
48
+    @Log(title = "个人级别安检检查", businessType = BusinessType.INSERT)
49
+    @PostMapping("/personal")
50
+    public AjaxResult submitPersonalCheck(@RequestBody Map<String, Object> requestData) {
51
+        // 获取备注参数
52
+        String remark = (String) requestData.get("remark");
53
+
54
+        try {
55
+            ApprovalInstance instance = checkApprovalService.submitPersonalCheck(
56
+                    requestData,
57
+                    SecurityUtils.getUserId(),
58
+                    SecurityUtils.getUsername(),
59
+                    remark
60
+            );
61
+            return AjaxResult.success("个人级别检查流程启动成功", instance);
62
+        } catch (Exception e) {
63
+            logger.error("启动个人级别检查流程失败", e);
64
+            return AjaxResult.error("启动流程失败:" + e.getMessage());
65
+        }
66
+    }
67
+
68
+    /**
69
+     * 2. 启动科级审批流程
70
+     */
71
+    @PreAuthorize("@ss.hasPermi('system:approval:start')")
72
+    @Log(title = "科级安检检查", businessType = BusinessType.INSERT)
73
+    @PostMapping("/section")
74
+    public AjaxResult submitSectionCheck(@RequestBody Map<String, Object> requestData) {
75
+        // 获取备注参数
76
+        String remark = (String) requestData.get("remark");
77
+
78
+        try {
79
+            ApprovalInstance instance = checkApprovalService.submitSectionCheck(
80
+                    requestData,
81
+                    SecurityUtils.getUserId(),
82
+                    SecurityUtils.getUsername(),
83
+                    remark
84
+            );
85
+            return AjaxResult.success("科级检查流程启动成功", instance);
86
+        } catch (Exception e) {
87
+            logger.error("启动科级检查流程失败", e);
88
+            return AjaxResult.error("启动流程失败:" + e.getMessage());
89
+        }
90
+    }
91
+
92
+    /**
93
+     * 3. 启动班组级审批流程
94
+     */
95
+    @PreAuthorize("@ss.hasPermi('system:approval:start')")
96
+    @Log(title = "班组级安检检查", businessType = BusinessType.INSERT)
97
+    @PostMapping("/group")
98
+    public AjaxResult submitGroupCheck(@RequestBody Map<String, Object> requestData) {
99
+        // 获取备注参数
100
+        String remark = (String) requestData.get("remark");
101
+
102
+        try {
103
+            ApprovalInstance instance = checkApprovalService.submitGroupCheck(
104
+                    requestData,
105
+                    SecurityUtils.getUserId(),
106
+                    SecurityUtils.getUsername(),
107
+                    remark
108
+            );
109
+            return AjaxResult.success("班组级检查流程启动成功", instance);
110
+        } catch (Exception e) {
111
+            logger.error("启动班组级检查流程失败", e);
112
+            return AjaxResult.error("启动流程失败:" + e.getMessage());
113
+        }
114
+    }
115
+
116
+    /**
117
+     * 4. 启动查获上报流程
118
+     */
119
+    @PreAuthorize("@ss.hasPermi('system:approval:start')")
120
+    @Log(title = "查获上报", businessType = BusinessType.INSERT)
121
+    @PostMapping("/seizure")
122
+    public AjaxResult submitSeizureReport(@RequestBody Map<String, Object> requestData) {
123
+        // 获取备注参数
124
+        String remark = (String) requestData.get("remark");
125
+
126
+        try {
127
+            ApprovalInstance instance = checkApprovalService.submitSeizureReport(
128
+                    requestData,
129
+                    SecurityUtils.getUserId(),
130
+                    SecurityUtils.getUsername(),
131
+                    remark
132
+            );
133
+            return AjaxResult.success("查获上报流程启动成功", instance);
134
+        } catch (Exception e) {
135
+            logger.error("启动查获上报流程失败", e);
136
+            return AjaxResult.error("启动流程失败:" + e.getMessage());
137
+        }
138
+    }
139
+
140
+    /**
141
+     * 5. 审批任务(同意)
142
+     */
143
+    @PreAuthorize("@ss.hasPermi('system:approval:approve')")
144
+    @Log(title = "审批任务", businessType = BusinessType.UPDATE)
145
+    @PutMapping("/approval/approve/{taskId}")
146
+    public AjaxResult approveTask(@PathVariable Long taskId, @Valid @RequestBody ApprovalDTO approvalData) {
147
+        try {
148
+            ApprovalTask task = checkApprovalService.approveTask(
149
+                    taskId,
150
+                    approvalData.getComment(),
151
+                    approvalData.getFormData(),
152
+                    SecurityUtils.getUserId(),
153
+                    SecurityUtils.getUsername()
154
+            );
155
+            return AjaxResult.success("任务审批成功", task);
156
+        } catch (Exception e) {
157
+            logger.error("审批任务失败", e);
158
+            return AjaxResult.error("审批失败:" + e.getMessage());
159
+        }
160
+    }
161
+
162
+    /**
163
+     * 6. 审批任务(驳回)
164
+     */
165
+    @PreAuthorize("@ss.hasPermi('system:approval:reject')")
166
+    @Log(title = "驳回任务", businessType = BusinessType.UPDATE)
167
+    @PutMapping("/approval/reject/{taskId}")
168
+    public AjaxResult rejectTask(@PathVariable Long taskId, @Valid @RequestBody RejectDTO rejectData) {
169
+        try {
170
+            ApprovalTask task = checkApprovalService.rejectTask(
171
+                    taskId,
172
+                    rejectData.getRejectReason(),
173
+                    SecurityUtils.getUserId(),
174
+                    SecurityUtils.getUsername()
175
+            );
176
+            return AjaxResult.success("任务驳回成功", task);
177
+        } catch (Exception e) {
178
+            logger.error("驳回任务失败", e);
179
+            return AjaxResult.error("驳回失败:" + e.getMessage());
180
+        }
181
+    }
182
+
183
+    /**
184
+     * 7. 获取待办任务列表
185
+     */
186
+    @PreAuthorize("@ss.hasPermi('system:approval:query')")
187
+    @GetMapping("/approval/tasks/pending")
188
+    public TableDataInfo getPendingTasks() {
189
+        try {
190
+            List<ApprovalTask> list = checkApprovalService.getPendingTasks(SecurityUtils.getUserId());
191
+            return getDataTable(list);
192
+        } catch (Exception e) {
193
+            logger.error("获取待办任务列表失败", e);
194
+            return getDataTable(null);
195
+        }
196
+    }
197
+
198
+    /**
199
+     * 8. 获取已办任务列表
200
+     */
201
+    @PreAuthorize("@ss.hasPermi('system:approval:query')")
202
+    @GetMapping("/approval/tasks/completed")
203
+    public TableDataInfo getCompletedTasks() {
204
+        try {
205
+            List<ApprovalTask> list = checkApprovalService.getCompletedTasks(SecurityUtils.getUserId());
206
+            return getDataTable(list);
207
+        } catch (Exception e) {
208
+            logger.error("获取已办任务列表失败", e);
209
+            return getDataTable(null);
210
+        }
211
+    }
212
+
213
+    /**
214
+     * 9. 获取我发起的审批实例
215
+     */
216
+    @PreAuthorize("@ss.hasPermi('system:approval:query')")
217
+    @GetMapping("/approval/instances/submitted")
218
+    public TableDataInfo getSubmittedInstances(@RequestParam(required = false) String status) {
219
+        try {
220
+            List<ApprovalInstance> list = checkApprovalService.getSubmittedInstances(SecurityUtils.getUserId(), status);
221
+            return getDataTable(list);
222
+        } catch (Exception e) {
223
+            logger.error("获取提交实例列表失败", e);
224
+            return getDataTable(null);
225
+        }
226
+    }
227
+
228
+    /**
229
+     * 10. 取消流程
230
+     */
231
+    @PreAuthorize("@ss.hasPermi('system:approval:cancel')")
232
+    @Log(title = "取消审批流程", businessType = BusinessType.UPDATE)
233
+    @PutMapping("/approval/cancel/{instanceId}")
234
+    public AjaxResult cancelProcess(@PathVariable Long instanceId, @RequestParam String comment) {
235
+        try {
236
+            boolean success = approvalEngineService.cancelProcess(instanceId, SecurityUtils.getUserId(), SecurityUtils.getUsername(), comment);
237
+            if (!success) {
238
+                return AjaxResult.error("取消失败");
239
+            }
240
+            ApprovalInstance instance = null; // 返回空对象或查询实例
241
+            return AjaxResult.success("流程取消成功", instance);
242
+        } catch (Exception e) {
243
+            logger.error("取消流程失败", e);
244
+            return AjaxResult.error("取消失败:" + e.getMessage());
245
+        }
246
+    }
247
+
248
+    /**
249
+     * 11. 根据业务类型获取工作流代码
250
+     */
251
+    @PreAuthorize("@ss.hasPermi('system:approval:query')")
252
+    @GetMapping("/approval/workflow-code")
253
+    public AjaxResult getWorkflowCode(@RequestParam String businessType,
254
+                                      @RequestParam(required = false) String submitterRole) {
255
+        try {
256
+            // 暂时返回空值,需要实现该方法
257
+            String workflowCode = null;
258
+            return AjaxResult.success("获取工作流代码成功", workflowCode);
259
+        } catch (Exception e) {
260
+            logger.error("获取工作流代码失败", e);
261
+            return AjaxResult.error("获取失败:" + e.getMessage());
262
+        }
263
+    }
264
+
265
+    /**
266
+     * 12. 获取待办任务数量
267
+     */
268
+    @PreAuthorize("@ss.hasPermi('system:approval:query')")
269
+    @GetMapping("/approval/tasks/pending/count")
270
+    public AjaxResult getPendingTasksCount() {
271
+        try {
272
+            Long count = checkApprovalService.getPendingTaskCount(SecurityUtils.getUserId());
273
+            return AjaxResult.success("获取待办任务数量成功", count);
274
+        } catch (Exception e) {
275
+            logger.error("获取待办任务数量失败", e);
276
+            return AjaxResult.error("获取失败:" + e.getMessage());
277
+        }
278
+    }
279
+
280
+    /**
281
+     * 13. 获取当前用户的抄送列表详情
282
+     */
283
+    @PreAuthorize("@ss.hasPermi('system:approval:query')")
284
+    @GetMapping("/approval/cc/details")
285
+    public TableDataInfo getCcDetails() {
286
+        try {
287
+            startPage();
288
+            List<ApprovalCcDetailDTO> list = checkApprovalService.getCcDetailsByUserId(SecurityUtils.getUserId());
289
+            return getDataTable(list);
290
+        } catch (Exception e) {
291
+            logger.error("获取抄送详情列表失败", e);
292
+            return getDataTable(null);
293
+        }
294
+    }
295
+
296
+    /**
297
+     * 14. 根据实例ID获取审批历史
298
+     */
299
+    @PreAuthorize("@ss.hasPermi('system:approval:query')")
300
+    @GetMapping("/approval/history/instance/{instanceId}")
301
+    public TableDataInfo getApprovalHistoryByInstanceId(@PathVariable Long instanceId) {
302
+        try {
303
+            List<com.sundot.airport.system.domain.approval.ApprovalHistory> list =
304
+                    checkApprovalService.getApprovalHistoryByInstanceId(instanceId);
305
+            return getDataTable(list);
306
+        } catch (Exception e) {
307
+            logger.error("根据实例ID获取审批历史失败", e);
308
+            return getDataTable(null);
309
+        }
310
+    }
311
+
312
+    /**
313
+     * 15. 根据任务ID获取审批历史
314
+     */
315
+    @PreAuthorize("@ss.hasPermi('system:approval:query')")
316
+    @GetMapping("/approval/history/task/{taskId}")
317
+    public TableDataInfo getApprovalHistoryByTaskId(@PathVariable Long taskId) {
318
+        try {
319
+            List<com.sundot.airport.system.domain.approval.ApprovalHistory> list =
320
+                    checkApprovalService.getApprovalHistoryByTaskId(taskId);
321
+            return getDataTable(list);
322
+        } catch (Exception e) {
323
+            logger.error("根据任务ID获取审批历史失败", e);
324
+            return getDataTable(null);
325
+        }
326
+    }
327
+
328
+    /**
329
+     * 16. 获取用户的审批历史记录
330
+     */
331
+    @PreAuthorize("@ss.hasPermi('system:approval:query')")
332
+    @GetMapping("/approval/history/user")
333
+    public TableDataInfo getUserApprovalHistory(@RequestParam(required = false) String action) {
334
+        try {
335
+            List<com.sundot.airport.system.domain.approval.ApprovalHistory> list =
336
+                    checkApprovalService.getUserApprovalHistory(SecurityUtils.getUserId(), action);
337
+            return getDataTable(list);
338
+        } catch (Exception e) {
339
+            logger.error("获取用户审批历史失败", e);
340
+            return getDataTable(null);
341
+        }
342
+    }
343
+
344
+    /**
345
+     * 17. 批量更新抄送消息已读状态
346
+     */
347
+    @PreAuthorize("@ss.hasPermi('system:approval:update')")
348
+    @Log(title = "批量更新抄送已读状态", businessType = BusinessType.UPDATE)
349
+    @PutMapping("/approval/cc/batch-read")
350
+    public AjaxResult batchUpdateCcReadStatus(@RequestBody List<Long> ids) {
351
+        try {
352
+            if (ids == null || ids.isEmpty()) {
353
+                return AjaxResult.error("ID列表不能为空");
354
+            }
355
+            int rows = checkApprovalService.batchUpdateCcReadStatus(ids);
356
+            return AjaxResult.success("批量更新成功", rows);
357
+        } catch (Exception e) {
358
+            logger.error("批量更新抄送已读状态失败", e);
359
+            return AjaxResult.error("更新失败:" + e.getMessage());
360
+        }
361
+    }
362
+}

+ 170 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/approval/dto/ApprovalDTO.java

@@ -0,0 +1,170 @@
1
+package com.sundot.airport.web.controller.approval.dto;
2
+
3
+import javax.validation.constraints.NotBlank;
4
+import java.util.Date;
5
+import java.util.Map;
6
+
7
+/**
8
+ * 审批DTO
9
+ *
10
+ * @author simon lin
11
+ * @date 2025-09-06
12
+ */
13
+public class ApprovalDTO {
14
+    /**
15
+     * 审批意见
16
+     */
17
+    @NotBlank(message = "审批意见不能为空")
18
+    private String comment;
19
+
20
+    /**
21
+     * 节点类型
22
+     */
23
+    private String nodeType;
24
+
25
+    /**
26
+     * 目标班组ID
27
+     */
28
+    private Long targetGroupId;
29
+
30
+    /**
31
+     * 整改要求
32
+     */
33
+    private String rectificationRequirement;
34
+
35
+    /**
36
+     * 整改详情
37
+     */
38
+    private String rectificationDetail;
39
+
40
+    /**
41
+     * 整改时间
42
+     */
43
+    private Date rectificationTime;
44
+
45
+    /**
46
+     * 整改结果
47
+     */
48
+    private String rectificationResult;
49
+
50
+    /**
51
+     * 复查结果
52
+     */
53
+    private String reviewResult;
54
+
55
+    /**
56
+     * 复查意见
57
+     */
58
+    private String reviewComment;
59
+
60
+    /**
61
+     * 最终决定
62
+     */
63
+    private String finalDecision;
64
+
65
+    /**
66
+     * 复查时间
67
+     */
68
+    private Date reviewTime;
69
+
70
+    /**
71
+     * 表单数据
72
+     */
73
+    private Map<String, Object> formData;
74
+
75
+    public String getComment() {
76
+        return comment;
77
+    }
78
+
79
+    public void setComment(String comment) {
80
+        this.comment = comment;
81
+    }
82
+
83
+    public String getNodeType() {
84
+        return nodeType;
85
+    }
86
+
87
+    public void setNodeType(String nodeType) {
88
+        this.nodeType = nodeType;
89
+    }
90
+
91
+    public Long getTargetGroupId() {
92
+        return targetGroupId;
93
+    }
94
+
95
+    public void setTargetGroupId(Long targetGroupId) {
96
+        this.targetGroupId = targetGroupId;
97
+    }
98
+
99
+    public String getRectificationRequirement() {
100
+        return rectificationRequirement;
101
+    }
102
+
103
+    public void setRectificationRequirement(String rectificationRequirement) {
104
+        this.rectificationRequirement = rectificationRequirement;
105
+    }
106
+
107
+    public String getRectificationDetail() {
108
+        return rectificationDetail;
109
+    }
110
+
111
+    public void setRectificationDetail(String rectificationDetail) {
112
+        this.rectificationDetail = rectificationDetail;
113
+    }
114
+
115
+    public Date getRectificationTime() {
116
+        return rectificationTime;
117
+    }
118
+
119
+    public void setRectificationTime(Date rectificationTime) {
120
+        this.rectificationTime = rectificationTime;
121
+    }
122
+
123
+    public String getRectificationResult() {
124
+        return rectificationResult;
125
+    }
126
+
127
+    public void setRectificationResult(String rectificationResult) {
128
+        this.rectificationResult = rectificationResult;
129
+    }
130
+
131
+    public String getReviewResult() {
132
+        return reviewResult;
133
+    }
134
+
135
+    public void setReviewResult(String reviewResult) {
136
+        this.reviewResult = reviewResult;
137
+    }
138
+
139
+    public String getReviewComment() {
140
+        return reviewComment;
141
+    }
142
+
143
+    public void setReviewComment(String reviewComment) {
144
+        this.reviewComment = reviewComment;
145
+    }
146
+
147
+    public String getFinalDecision() {
148
+        return finalDecision;
149
+    }
150
+
151
+    public void setFinalDecision(String finalDecision) {
152
+        this.finalDecision = finalDecision;
153
+    }
154
+
155
+    public Date getReviewTime() {
156
+        return reviewTime;
157
+    }
158
+
159
+    public void setReviewTime(Date reviewTime) {
160
+        this.reviewTime = reviewTime;
161
+    }
162
+
163
+    public Map<String, Object> getFormData() {
164
+        return formData;
165
+    }
166
+
167
+    public void setFormData(Map<String, Object> formData) {
168
+        this.formData = formData;
169
+    }
170
+}

+ 81 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/approval/dto/PersonalCheckDTO.java

@@ -0,0 +1,81 @@
1
+package com.sundot.airport.web.controller.approval.dto;
2
+
3
+import javax.validation.constraints.NotBlank;
4
+import javax.validation.constraints.NotNull;
5
+import java.util.List;
6
+
7
+/**
8
+ * 个人级别检查DTO
9
+ *
10
+ * @author simon lin
11
+ * @date 2025-09-06
12
+ */
13
+public class PersonalCheckDTO {
14
+    /**
15
+     * 业务ID
16
+     */
17
+    @NotNull(message = "业务ID不能为空")
18
+    private Long id;
19
+
20
+    /**
21
+     * 检查描述
22
+     */
23
+    @NotBlank(message = "检查描述不能为空")
24
+    private String description;
25
+
26
+    /**
27
+     * 检查类型
28
+     */
29
+    private String checkType;
30
+
31
+    /**
32
+     * 检查位置
33
+     */
34
+    private String location;
35
+
36
+    /**
37
+     * 目标用户ID列表
38
+     */
39
+    @NotNull(message = "目标用户ID列表不能为空")
40
+    private Long[] targetUserIds;
41
+
42
+    public Long getId() {
43
+        return id;
44
+    }
45
+
46
+    public void setId(Long id) {
47
+        this.id = id;
48
+    }
49
+
50
+    public String getDescription() {
51
+        return description;
52
+    }
53
+
54
+    public void setDescription(String description) {
55
+        this.description = description;
56
+    }
57
+
58
+    public String getCheckType() {
59
+        return checkType;
60
+    }
61
+
62
+    public void setCheckType(String checkType) {
63
+        this.checkType = checkType;
64
+    }
65
+
66
+    public String getLocation() {
67
+        return location;
68
+    }
69
+
70
+    public void setLocation(String location) {
71
+        this.location = location;
72
+    }
73
+
74
+    public Long[] getTargetUserIds() {
75
+        return targetUserIds;
76
+    }
77
+
78
+    public void setTargetUserIds(Long[] targetUserIds) {
79
+        this.targetUserIds = targetUserIds;
80
+    }
81
+}

+ 25 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/approval/dto/RejectDTO.java

@@ -0,0 +1,25 @@
1
+package com.sundot.airport.web.controller.approval.dto;
2
+
3
+import javax.validation.constraints.NotBlank;
4
+
5
+/**
6
+ * 驳回DTO
7
+ *
8
+ * @author simon lin
9
+ * @date 2025-09-06
10
+ */
11
+public class RejectDTO {
12
+    /**
13
+     * 驳回理由
14
+     */
15
+    @NotBlank(message = "驳回理由不能为空")
16
+    private String rejectReason;
17
+
18
+    public String getRejectReason() {
19
+        return rejectReason;
20
+    }
21
+
22
+    public void setRejectReason(String rejectReason) {
23
+        this.rejectReason = rejectReason;
24
+    }
25
+}

+ 79 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/approval/dto/SectionCheckDTO.java

@@ -0,0 +1,79 @@
1
+package com.sundot.airport.web.controller.approval.dto;
2
+
3
+import javax.validation.constraints.NotBlank;
4
+import javax.validation.constraints.NotNull;
5
+
6
+/**
7
+ * 科级检查DTO
8
+ *
9
+ * @author simon lin
10
+ * @date 2025-09-06
11
+ */
12
+public class SectionCheckDTO {
13
+    /**
14
+     * 业务ID
15
+     */
16
+    @NotNull(message = "业务ID不能为空")
17
+    private Long id;
18
+
19
+    /**
20
+     * 检查描述
21
+     */
22
+    @NotBlank(message = "检查描述不能为空")
23
+    private String description;
24
+
25
+    /**
26
+     * 检查类型
27
+     */
28
+    private String checkType;
29
+
30
+    /**
31
+     * 严重程度
32
+     */
33
+    private String severity;
34
+
35
+    /**
36
+     * 目标部门ID
37
+     */
38
+    private Long targetDeptId;
39
+
40
+    public Long getId() {
41
+        return id;
42
+    }
43
+
44
+    public void setId(Long id) {
45
+        this.id = id;
46
+    }
47
+
48
+    public String getDescription() {
49
+        return description;
50
+    }
51
+
52
+    public void setDescription(String description) {
53
+        this.description = description;
54
+    }
55
+
56
+    public String getCheckType() {
57
+        return checkType;
58
+    }
59
+
60
+    public void setCheckType(String checkType) {
61
+        this.checkType = checkType;
62
+    }
63
+
64
+    public String getSeverity() {
65
+        return severity;
66
+    }
67
+
68
+    public void setSeverity(String severity) {
69
+        this.severity = severity;
70
+    }
71
+
72
+    public Long getTargetDeptId() {
73
+        return targetDeptId;
74
+    }
75
+
76
+    public void setTargetDeptId(Long targetDeptId) {
77
+        this.targetDeptId = targetDeptId;
78
+    }
79
+}

+ 93 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/approval/dto/SeizureReportDTO.java

@@ -0,0 +1,93 @@
1
+package com.sundot.airport.web.controller.approval.dto;
2
+
3
+import javax.validation.constraints.NotBlank;
4
+import javax.validation.constraints.NotNull;
5
+import java.util.Date;
6
+
7
+/**
8
+ * 查获上报DTO
9
+ *
10
+ * @author simon lin
11
+ * @date 2025-09-06
12
+ */
13
+public class SeizureReportDTO {
14
+    /**
15
+     * 业务ID
16
+     */
17
+    @NotNull(message = "业务ID不能为空")
18
+    private Long id;
19
+
20
+    /**
21
+     * 物品类型
22
+     */
23
+    @NotBlank(message = "物品类型不能为空")
24
+    private String itemType;
25
+
26
+    /**
27
+     * 数量
28
+     */
29
+    private Integer quantity;
30
+
31
+    /**
32
+     * 位置
33
+     */
34
+    private String location;
35
+
36
+    /**
37
+     * 查获时间
38
+     */
39
+    private Date seizureTime;
40
+
41
+    /**
42
+     * 旅客信息
43
+     */
44
+    private String passengerInfo;
45
+
46
+    public Long getId() {
47
+        return id;
48
+    }
49
+
50
+    public void setId(Long id) {
51
+        this.id = id;
52
+    }
53
+
54
+    public String getItemType() {
55
+        return itemType;
56
+    }
57
+
58
+    public void setItemType(String itemType) {
59
+        this.itemType = itemType;
60
+    }
61
+
62
+    public Integer getQuantity() {
63
+        return quantity;
64
+    }
65
+
66
+    public void setQuantity(Integer quantity) {
67
+        this.quantity = quantity;
68
+    }
69
+
70
+    public String getLocation() {
71
+        return location;
72
+    }
73
+
74
+    public void setLocation(String location) {
75
+        this.location = location;
76
+    }
77
+
78
+    public Date getSeizureTime() {
79
+        return seizureTime;
80
+    }
81
+
82
+    public void setSeizureTime(Date seizureTime) {
83
+        this.seizureTime = seizureTime;
84
+    }
85
+
86
+    public String getPassengerInfo() {
87
+        return passengerInfo;
88
+    }
89
+
90
+    public void setPassengerInfo(String passengerInfo) {
91
+        this.passengerInfo = passengerInfo;
92
+    }
93
+}

+ 52 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/attendance/AttendanceAreaController.java

@@ -0,0 +1,52 @@
1
+package com.sundot.airport.web.controller.attendance;
2
+
3
+import cn.hutool.core.collection.CollectionUtil;
4
+import com.sundot.airport.attendance.domain.AttendanceArea;
5
+import com.sundot.airport.attendance.service.AttendanceAreaService;
6
+import com.sundot.airport.common.core.controller.BaseController;
7
+import com.sundot.airport.common.core.domain.AjaxResult;
8
+import org.springframework.beans.factory.annotation.Autowired;
9
+import org.springframework.security.access.prepost.PreAuthorize;
10
+import org.springframework.web.bind.annotation.*;
11
+
12
+import java.math.BigDecimal;
13
+import java.util.List;
14
+
15
+@RestController
16
+@RequestMapping("/attendance/area")
17
+public class AttendanceAreaController extends BaseController {
18
+
19
+    @Autowired
20
+    private AttendanceAreaService attendanceService;
21
+
22
+    /**
23
+     * 查询是否在考勤范围
24
+     *
25
+     * @param lng 经度
26
+     * @param lat 纬度
27
+     * @return
28
+     */
29
+    @PostMapping("/check-in")
30
+    public AjaxResult checkUserInValidArea(@RequestParam BigDecimal lng,
31
+                                           @RequestParam BigDecimal lat) {
32
+        List<AttendanceArea> validAreas = attendanceService.checkUserInValidArea(lng, lat);
33
+        if (CollectionUtil.isEmpty(validAreas)) {
34
+            return AjaxResult.error("不在任何考勤范围内");
35
+        }
36
+        return success(validAreas);
37
+    }
38
+
39
+    /**
40
+     * 考勤打卡列表
41
+     *
42
+     * @return
43
+     */
44
+    @PostMapping("/list")
45
+    public AjaxResult areaList() {
46
+        List<AttendanceArea> validAreas = attendanceService.areaList();
47
+        if (CollectionUtil.isEmpty(validAreas)) {
48
+            return AjaxResult.error("没有考勤范围");
49
+        }
50
+        return success(validAreas);
51
+    }
52
+}

+ 91 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/attendance/AttendanceCheckRecordController.java

@@ -0,0 +1,91 @@
1
+package com.sundot.airport.web.controller.attendance;
2
+
3
+import com.sundot.airport.attendance.domain.AttendanceCheckRecord;
4
+import com.sundot.airport.attendance.service.IAttendanceCheckRecordService;
5
+import com.sundot.airport.common.annotation.Log;
6
+import com.sundot.airport.common.core.controller.BaseController;
7
+import com.sundot.airport.common.core.domain.AjaxResult;
8
+import com.sundot.airport.common.core.page.TableDataInfo;
9
+import com.sundot.airport.common.enums.BusinessType;
10
+import com.sundot.airport.common.utils.poi.ExcelUtil;
11
+import org.springframework.beans.factory.annotation.Autowired;
12
+import org.springframework.security.access.prepost.PreAuthorize;
13
+import org.springframework.web.bind.annotation.*;
14
+
15
+import javax.servlet.http.HttpServletResponse;
16
+import java.util.List;
17
+
18
+/**
19
+ * 打卡记录Controller
20
+ *
21
+ * @author wangchong
22
+ * @date 2025-07-10
23
+ */
24
+@RestController
25
+@RequestMapping("/attendance/checkRecord")
26
+public class AttendanceCheckRecordController extends BaseController {
27
+    @Autowired
28
+    private IAttendanceCheckRecordService attendanceCheckRecordService;
29
+
30
+    /**
31
+     * 查询打卡记录列表
32
+     */
33
+    @PreAuthorize("@ss.hasPermi('attendance:checkRecord:list')")
34
+    @GetMapping("/list")
35
+    public TableDataInfo list(AttendanceCheckRecord attendanceCheckRecord) {
36
+        startPage();
37
+        List<AttendanceCheckRecord> list = attendanceCheckRecordService.selectAttendanceCheckRecordList(attendanceCheckRecord);
38
+        return getDataTable(list);
39
+    }
40
+
41
+    /**
42
+     * 导出打卡记录列表
43
+     */
44
+    @PreAuthorize("@ss.hasPermi('attendance:checkRecord:export')")
45
+    @Log(title = "打卡记录", businessType = BusinessType.EXPORT)
46
+    @PostMapping("/export")
47
+    public void export(HttpServletResponse response, AttendanceCheckRecord attendanceCheckRecord) {
48
+        List<AttendanceCheckRecord> list = attendanceCheckRecordService.selectAttendanceCheckRecordList(attendanceCheckRecord);
49
+        ExcelUtil<AttendanceCheckRecord> util = new ExcelUtil<AttendanceCheckRecord>(AttendanceCheckRecord.class);
50
+        util.exportExcel(response, list, "打卡记录数据");
51
+    }
52
+
53
+    /**
54
+     * 获取打卡记录详细信息
55
+     */
56
+    @PreAuthorize("@ss.hasPermi('attendance:checkRecord:query')")
57
+    @GetMapping(value = "/{id}")
58
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
59
+        return success(attendanceCheckRecordService.selectAttendanceCheckRecordById(id));
60
+    }
61
+
62
+    /**
63
+     * 新增打卡记录
64
+     */
65
+    @PreAuthorize("@ss.hasPermi('attendance:checkRecord:add')")
66
+    @Log(title = "打卡记录", businessType = BusinessType.INSERT)
67
+    @PostMapping
68
+    public AjaxResult add(@RequestBody AttendanceCheckRecord attendanceCheckRecord) {
69
+        return toAjax(attendanceCheckRecordService.insertAttendanceCheckRecord(attendanceCheckRecord));
70
+    }
71
+
72
+    /**
73
+     * 修改打卡记录
74
+     */
75
+    @PreAuthorize("@ss.hasPermi('attendance:checkRecord:edit')")
76
+    @Log(title = "打卡记录", businessType = BusinessType.UPDATE)
77
+    @PutMapping
78
+    public AjaxResult edit(@RequestBody AttendanceCheckRecord attendanceCheckRecord) {
79
+        return toAjax(attendanceCheckRecordService.updateAttendanceCheckRecord(attendanceCheckRecord));
80
+    }
81
+
82
+    /**
83
+     * 删除打卡记录
84
+     */
85
+    @PreAuthorize("@ss.hasPermi('attendance:checkRecord:remove')")
86
+    @Log(title = "打卡记录", businessType = BusinessType.DELETE)
87
+    @DeleteMapping("/{ids}")
88
+    public AjaxResult remove(@PathVariable Long[] ids) {
89
+        return toAjax(attendanceCheckRecordService.deleteAttendanceCheckRecordByIds(ids));
90
+    }
91
+}

File diff suppressed because it is too large
+ 1181 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/attendance/AttendancePostRecordController.java


+ 289 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/attendance/AttendanceRecordController.java

@@ -0,0 +1,289 @@
1
+package com.sundot.airport.web.controller.attendance;
2
+
3
+import cn.hutool.core.collection.CollectionUtil;
4
+import cn.hutool.core.date.DateUtil;
5
+import cn.hutool.core.util.ObjUtil;
6
+import cn.hutool.core.util.StrUtil;
7
+import com.sundot.airport.attendance.domain.AttendanceRecord;
8
+import com.sundot.airport.attendance.dto.AttendanceRecordImportVO;
9
+import com.sundot.airport.attendance.service.IAttendanceRecordService;
10
+import com.sundot.airport.common.annotation.Log;
11
+import com.sundot.airport.common.core.controller.BaseController;
12
+import com.sundot.airport.common.core.domain.AjaxResult;
13
+import com.sundot.airport.common.core.page.TableDataInfo;
14
+import com.sundot.airport.common.dto.UserInfo;
15
+import com.sundot.airport.common.enums.BusinessType;
16
+import com.sundot.airport.common.exception.ServiceException;
17
+import com.sundot.airport.common.utils.poi.ExcelUtil;
18
+import com.sundot.airport.web.core.utils.DataPermissionUtils;
19
+import com.sundot.airport.system.service.ISysDeptService;
20
+import com.sundot.airport.web.core.cache.UserCache;
21
+import com.sundot.airport.common.core.domain.DataPermissionResult;
22
+import com.sundot.airport.common.enums.DataPermissionType;
23
+import org.springframework.beans.factory.annotation.Autowired;
24
+import org.springframework.security.access.prepost.PreAuthorize;
25
+import org.springframework.web.bind.annotation.*;
26
+import org.springframework.web.multipart.MultipartFile;
27
+
28
+import javax.servlet.http.HttpServletResponse;
29
+import java.util.Date;
30
+import java.util.List;
31
+import java.util.Map;
32
+import java.util.Objects;
33
+
34
+/**
35
+ * 考勤记录Controller
36
+ *
37
+ * @author wangchong
38
+ * @date 2025-07-10
39
+ */
40
+@RestController
41
+@RequestMapping("/attendance/attendanceRecord")
42
+public class AttendanceRecordController extends BaseController {
43
+    @Autowired
44
+    private IAttendanceRecordService attendanceRecordService;
45
+
46
+    @Autowired
47
+    private UserCache userCache;
48
+
49
+    @Autowired
50
+    private ISysDeptService sysDeptService;
51
+
52
+    /**
53
+     * 查询考勤记录列表
54
+     */
55
+    @PreAuthorize("@ss.hasPermi('attendance:attendanceRecord:list')")
56
+    @GetMapping("/list")
57
+    public TableDataInfo list(AttendanceRecord attendanceRecord) {
58
+        // 应用数据权限过滤
59
+        DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
60
+        switch (dataPermission.getPermissionType()) {
61
+            case SELF:
62
+                attendanceRecord.setUserId(dataPermission.getValue());
63
+                break;
64
+            case STATION:
65
+                attendanceRecord.setStationCode(dataPermission.getValue().toString());
66
+                break;
67
+            case BRIGADE:
68
+                attendanceRecord.setBrigadeCode(dataPermission.getValue().toString());
69
+                break;
70
+            case DEPARTMENT:
71
+                attendanceRecord.setDepartmentCode(dataPermission.getValue().toString());
72
+                break;
73
+            case TEAM:
74
+                attendanceRecord.setTeamCode(dataPermission.getValue().toString());
75
+                break;
76
+            case ALL:
77
+            default:
78
+                // 不设置过滤条件,查看所有数据
79
+                break;
80
+        }
81
+
82
+        startPage();
83
+        List<AttendanceRecord> list = attendanceRecordService.selectAttendanceRecordList(attendanceRecord);
84
+        return getDataTable(list);
85
+    }
86
+
87
+    /**
88
+     * 导出考勤记录列表
89
+     */
90
+    @PreAuthorize("@ss.hasPermi('attendance:attendanceRecord:export')")
91
+    @Log(title = "考勤记录", businessType = BusinessType.EXPORT)
92
+    @PostMapping("/export")
93
+    public void export(HttpServletResponse response, AttendanceRecord attendanceRecord) {
94
+        // 应用数据权限过滤
95
+        DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
96
+        switch (dataPermission.getPermissionType()) {
97
+            case SELF:
98
+                attendanceRecord.setUserId(dataPermission.getValue());
99
+                break;
100
+            case STATION:
101
+                attendanceRecord.setStationCode(dataPermission.getValue().toString());
102
+                break;
103
+            case BRIGADE:
104
+                attendanceRecord.setBrigadeCode(dataPermission.getValue().toString());
105
+                break;
106
+            case DEPARTMENT:
107
+                attendanceRecord.setDepartmentCode(dataPermission.getValue().toString());
108
+                break;
109
+            case TEAM:
110
+                attendanceRecord.setTeamCode(dataPermission.getValue().toString());
111
+                break;
112
+            case ALL:
113
+            default:
114
+                // 不设置过滤条件,查看所有数据
115
+                break;
116
+        }
117
+
118
+        List<AttendanceRecord> list = attendanceRecordService.selectAttendanceRecordList(attendanceRecord);
119
+        ExcelUtil<AttendanceRecord> util = new ExcelUtil<AttendanceRecord>(AttendanceRecord.class);
120
+        util.exportExcel(response, list, "考勤记录数据");
121
+    }
122
+
123
+    /**
124
+     * 获取考勤记录详细信息
125
+     */
126
+    @PreAuthorize("@ss.hasPermi('attendance:attendanceRecord:query')")
127
+    @GetMapping(value = "/{id}")
128
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
129
+        return success(attendanceRecordService.selectAttendanceRecordById(id));
130
+    }
131
+
132
+    /**
133
+     * 新增考勤记录
134
+     */
135
+    @PreAuthorize("@ss.hasPermi('attendance:attendanceRecord:add')")
136
+    @Log(title = "考勤记录", businessType = BusinessType.INSERT)
137
+    @PostMapping
138
+    public AjaxResult add(@RequestBody AttendanceRecord attendanceRecord) {
139
+        UserInfo userInfo = userCache.getUserInfo(attendanceRecord.getUserId());
140
+        if (Objects.nonNull(userInfo.getTeamsId())) {
141
+            attendanceRecord.setTeamCode(userInfo.getTeamsId().toString());
142
+        }
143
+        attendanceRecord.setTeamName(userInfo.getTeamsName());
144
+        if (Objects.nonNull(userInfo.getDepartmentId())) {
145
+            attendanceRecord.setDepartmentCode(userInfo.getDepartmentId().toString());
146
+        }
147
+        attendanceRecord.setDepartmentName(userInfo.getDepartmentName());
148
+        if (Objects.nonNull(userInfo.getBrigadeId())) {
149
+            attendanceRecord.setBrigadeCode(userInfo.getBrigadeId().toString());
150
+        }
151
+        attendanceRecord.setBrigadeName(userInfo.getBrigadeName());
152
+        if (Objects.nonNull(userInfo.getStationId())) {
153
+            attendanceRecord.setStationCode(userInfo.getStationId().toString());
154
+        }
155
+        attendanceRecord.setStationName(userInfo.getStationName());
156
+        attendanceRecord.setUserName(userInfo.getNickName());
157
+        attendanceRecord.setRevision(1L);
158
+        attendanceRecord.setWorkDuration((attendanceRecord.getCheckOutTime().getTime() - attendanceRecord.getCheckInTime().getTime()) / 1000 / 60);
159
+        attendanceRecord.setAttendanceDate(DateUtil.date(attendanceRecord.getCheckInTime()));
160
+        attendanceRecord.setOverDuration(0L);
161
+        attendanceRecord.setCreateBy(getUserId().toString());
162
+        return toAjax(attendanceRecordService.insertAttendanceRecord(attendanceRecord));
163
+    }
164
+
165
+    /**
166
+     * 修改考勤记录
167
+     */
168
+    @PreAuthorize("@ss.hasPermi('attendance:attendanceRecord:edit')")
169
+    @Log(title = "考勤记录", businessType = BusinessType.UPDATE)
170
+    @PutMapping
171
+    public AjaxResult edit(@RequestBody AttendanceRecord attendanceRecord) {
172
+        attendanceRecord.setUpdateBy(getUserId().toString());
173
+        attendanceRecord.setRevision(attendanceRecord.getRevision() + 1);
174
+        attendanceRecord.setWorkDuration((attendanceRecord.getCheckOutTime().getTime() - attendanceRecord.getCheckInTime().getTime()) / 1000 / 60);
175
+        attendanceRecord.setAttendanceDate(DateUtil.date(attendanceRecord.getCheckInTime()));
176
+        return toAjax(attendanceRecordService.updateAttendanceRecord(attendanceRecord));
177
+    }
178
+
179
+    /**
180
+     * 删除考勤记录
181
+     */
182
+    @PreAuthorize("@ss.hasPermi('attendance:attendanceRecord:remove')")
183
+    @Log(title = "考勤记录", businessType = BusinessType.DELETE)
184
+    @DeleteMapping("/{ids}")
185
+    public AjaxResult remove(@PathVariable Long[] ids) {
186
+        return toAjax(attendanceRecordService.deleteAttendanceRecordByIds(ids));
187
+    }
188
+
189
+    @PostMapping("/importTemplate")
190
+    public void importTemplate(HttpServletResponse response) {
191
+        ExcelUtil<AttendanceRecordImportVO> util = new ExcelUtil<AttendanceRecordImportVO>(AttendanceRecordImportVO.class);
192
+        util.importTemplateExcel(response, "用户数据");
193
+    }
194
+
195
+    @Log(title = "考勤管理", businessType = BusinessType.IMPORT)
196
+    @PreAuthorize("@ss.hasPermi('attendance:attendanceRecord:import')")
197
+    @PostMapping("/importData")
198
+    public AjaxResult importData(MultipartFile file) throws Exception {
199
+        ExcelUtil<AttendanceRecordImportVO> util = new ExcelUtil<AttendanceRecordImportVO>(AttendanceRecordImportVO.class);
200
+        List<AttendanceRecordImportVO> list = util.importExcel(file.getInputStream());
201
+        if (CollectionUtil.isEmpty(list)) {
202
+            throw new ServiceException("导入用户数据不能为空");
203
+        }
204
+        validateData(list);
205
+        String message = importData(list);
206
+        return success(message);
207
+    }
208
+
209
+    private void validateData(List<AttendanceRecordImportVO> list) {
210
+        StringBuilder failureMsg = new StringBuilder();
211
+        int failureNum = 2;
212
+        for (AttendanceRecordImportVO vo : list) {
213
+            if (StrUtil.isBlank(vo.getUserName())) {
214
+                failureMsg.append("<br/>第 ").append(failureNum).append(" 行用户姓名不能为空");
215
+            }
216
+            UserInfo userInfo = userCache.getUserInfo(vo.getUserName());
217
+            if (Objects.isNull(userInfo) || Objects.isNull(userInfo.getUserId())) {
218
+                failureMsg.append("<br/>第 ").append(failureNum).append(" 行用户不存在");
219
+            }
220
+            if (ObjUtil.isNull(vo.getAttendanceDate())) {
221
+                failureMsg.append("<br/>第 ").append(failureNum).append(" 行考勤日期格式错误或者不能为空");
222
+            }
223
+            if (ObjUtil.isNull(vo.getCheckInTime())) {
224
+                failureMsg.append("<br/>第 ").append(failureNum).append(" 行签到时间格式错误或者不能为空");
225
+            }
226
+            if (ObjUtil.isNull(vo.getCheckOutTime())) {
227
+                failureMsg.append("<br/>第 ").append(failureNum).append(" 行签退时间格式错误或者不能为空");
228
+            }
229
+            failureNum++;
230
+        }
231
+        if (StrUtil.isNotBlank(failureMsg)) {
232
+            throw new ServiceException(failureMsg.toString());
233
+        }
234
+    }
235
+
236
+    private String importData(List<AttendanceRecordImportVO> list) {
237
+        int successNum = 0;
238
+        int failureNum = 0;
239
+        StringBuilder successMsg = new StringBuilder();
240
+        StringBuilder failureMsg = new StringBuilder();
241
+        for (AttendanceRecordImportVO vo : list) {
242
+            UserInfo userInfo = userCache.getUserInfo(vo.getUserName());
243
+            try {
244
+                AttendanceRecord attendanceRecord = new AttendanceRecord();
245
+                attendanceRecord.setRevision(1L);
246
+                attendanceRecord.setUserId(userInfo.getUserId());
247
+                attendanceRecord.setCheckInTime(vo.getCheckInTime());
248
+                attendanceRecord.setCheckOutTime(vo.getCheckOutTime());
249
+                attendanceRecord.setWorkDuration((attendanceRecord.getCheckOutTime().getTime() - attendanceRecord.getCheckInTime().getTime()) / 1000 / 60);
250
+                if (Objects.nonNull(userInfo.getTeamsId())) {
251
+                    attendanceRecord.setTeamCode(userInfo.getTeamsId().toString());
252
+                }
253
+                attendanceRecord.setTeamName(userInfo.getTeamsName());
254
+                if (Objects.nonNull(userInfo.getDepartmentId())) {
255
+                    attendanceRecord.setDepartmentCode(userInfo.getDepartmentId().toString());
256
+                }
257
+                attendanceRecord.setDepartmentName(userInfo.getDepartmentName());
258
+                if (Objects.nonNull(userInfo.getBrigadeId())) {
259
+                    attendanceRecord.setBrigadeCode(userInfo.getBrigadeId().toString());
260
+                }
261
+                attendanceRecord.setBrigadeName(userInfo.getBrigadeName());
262
+                if (Objects.nonNull(userInfo.getStationId())) {
263
+                    attendanceRecord.setStationCode(userInfo.getStationId().toString());
264
+                }
265
+                attendanceRecord.setStationName(userInfo.getStationName());
266
+                attendanceRecord.setUserName(userInfo.getNickName());
267
+                attendanceRecord.setRemark(vo.getRemark());
268
+                attendanceRecord.setAttendanceDate(vo.getAttendanceDate());
269
+                attendanceRecord.setOverDuration(0L);
270
+                attendanceRecord.setCreateBy(getUserId().toString());
271
+                attendanceRecord.setCreateTime(new Date());
272
+                attendanceRecordService.insertAttendanceRecord(attendanceRecord);
273
+                successNum++;
274
+                successMsg.append("<br/>" + successNum + "、账号 " + userInfo.getUserName() + " 导入成功");
275
+            } catch (Exception e) {
276
+                failureNum++;
277
+                String msg = "<br/>" + failureNum + "、账号 " + userInfo.getUserName() + " 导入失败:";
278
+                failureMsg.append(msg + e.getMessage());
279
+            }
280
+        }
281
+        if (failureNum > 0) {
282
+            failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
283
+            throw new ServiceException(failureMsg.toString());
284
+        } else {
285
+            successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
286
+        }
287
+        return successMsg.toString();
288
+    }
289
+}

+ 166 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/attendance/AttendanceTeamUserRecordController.java

@@ -0,0 +1,166 @@
1
+package com.sundot.airport.web.controller.attendance;
2
+
3
+import java.util.Date;
4
+import java.util.List;
5
+import javax.servlet.http.HttpServletResponse;
6
+
7
+import com.sundot.airport.common.core.domain.DataPermissionResult;
8
+import com.sundot.airport.web.core.utils.DataPermissionUtils;
9
+import org.springframework.security.access.prepost.PreAuthorize;
10
+import org.springframework.beans.factory.annotation.Autowired;
11
+import org.springframework.web.bind.annotation.GetMapping;
12
+import org.springframework.web.bind.annotation.PostMapping;
13
+import org.springframework.web.bind.annotation.PutMapping;
14
+import org.springframework.web.bind.annotation.DeleteMapping;
15
+import org.springframework.web.bind.annotation.PathVariable;
16
+import org.springframework.web.bind.annotation.RequestBody;
17
+import org.springframework.web.bind.annotation.RequestMapping;
18
+import org.springframework.web.bind.annotation.RestController;
19
+import com.sundot.airport.common.annotation.Log;
20
+import com.sundot.airport.common.core.controller.BaseController;
21
+import com.sundot.airport.common.core.domain.AjaxResult;
22
+import com.sundot.airport.common.enums.BusinessType;
23
+import com.sundot.airport.attendance.domain.AttendanceTeamUserRecord;
24
+import com.sundot.airport.attendance.service.IAttendanceTeamUserRecordService;
25
+import com.sundot.airport.common.utils.poi.ExcelUtil;
26
+import com.sundot.airport.common.core.page.TableDataInfo;
27
+
28
+/**
29
+ * 考勤班组成员Controller
30
+ *
31
+ * @author ruoyi
32
+ * @date 2025-09-06
33
+ */
34
+@RestController
35
+@RequestMapping("/attendance/record")
36
+public class AttendanceTeamUserRecordController extends BaseController {
37
+    @Autowired
38
+    private IAttendanceTeamUserRecordService attendanceTeamUserRecordService;
39
+
40
+    /**
41
+     * 查询考勤班组成员列表
42
+     *
43
+     * @param attendanceTeamUserRecord
44
+     * @return
45
+     */
46
+    @PreAuthorize("@ss.hasPermi('attendance:record:list')")
47
+    @GetMapping("/pageList")
48
+    public TableDataInfo pageList(AttendanceTeamUserRecord attendanceTeamUserRecord) {
49
+        startPage();
50
+        List<AttendanceTeamUserRecord> list = attendanceTeamUserRecordService.selectAttendanceTeamUserRecordPageList(attendanceTeamUserRecord);
51
+        return getDataTable(list);
52
+    }
53
+
54
+    /**
55
+     * 上岗记录中查询考勤班组成员列表
56
+     */
57
+    @PreAuthorize("@ss.hasPermi('attendance:record:list')")
58
+    @GetMapping("/list")
59
+    public AjaxResult list(AttendanceTeamUserRecord attendanceTeamUserRecord) {
60
+        DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
61
+        switch (dataPermission.getPermissionType()) {
62
+            case TEAM:
63
+                attendanceTeamUserRecord.setCreateBy(getUsername());
64
+                break;
65
+            default:
66
+                break;
67
+        }
68
+        attendanceTeamUserRecord.setCheckInType("1");
69
+        List<AttendanceTeamUserRecord> list = attendanceTeamUserRecordService.selectAttendanceTeamUserRecordList(attendanceTeamUserRecord, true);
70
+        return success(list);
71
+    }
72
+
73
+    /**
74
+     * 导出考勤班组成员列表
75
+     */
76
+    @PreAuthorize("@ss.hasPermi('attendance:record:export')")
77
+    @Log(title = "考勤班组成员", businessType = BusinessType.EXPORT)
78
+    @PostMapping("/export")
79
+    public void export(HttpServletResponse response, AttendanceTeamUserRecord attendanceTeamUserRecord) {
80
+        if (attendanceTeamUserRecord == null) {
81
+            attendanceTeamUserRecord = new AttendanceTeamUserRecord();
82
+        }
83
+        DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
84
+        switch (dataPermission.getPermissionType()) {
85
+            case TEAM:
86
+                attendanceTeamUserRecord.setCreateBy(getUsername());
87
+                break;
88
+            default:
89
+                break;
90
+        }
91
+        attendanceTeamUserRecord.setCheckInType("1");
92
+        List<AttendanceTeamUserRecord> list = attendanceTeamUserRecordService.selectAttendanceTeamUserRecordList(attendanceTeamUserRecord, true);
93
+        ExcelUtil<AttendanceTeamUserRecord> util = new ExcelUtil<AttendanceTeamUserRecord>(AttendanceTeamUserRecord.class);
94
+        util.exportExcel(response, list, "考勤班组成员数据");
95
+    }
96
+
97
+    /**
98
+     * 获取考勤班组成员详细信息
99
+     */
100
+    @PreAuthorize("@ss.hasPermi('attendance:record:query')")
101
+    @GetMapping(value = "/{userId}")
102
+    public AjaxResult getInfo(@PathVariable("userId") Long userId, @PathVariable("attendanceDate") Date attendanceDate) {
103
+        return success(attendanceTeamUserRecordService.selectAttendanceTeamUserRecordByUserId(userId, attendanceDate, null));
104
+    }
105
+
106
+    /**
107
+     * 新增考勤班组成员
108
+     */
109
+    @PreAuthorize("@ss.hasPermi('attendance:record:add')")
110
+    @Log(title = "考勤班组成员", businessType = BusinessType.INSERT)
111
+    @PostMapping
112
+    public AjaxResult add(@RequestBody AttendanceTeamUserRecord attendanceTeamUserRecord) {
113
+        return toAjax(attendanceTeamUserRecordService.insertAttendanceTeamUserRecord(attendanceTeamUserRecord));
114
+    }
115
+
116
+    /**
117
+     * 修改考勤班组成员
118
+     */
119
+    @PreAuthorize("@ss.hasPermi('attendance:record:edit')")
120
+    @Log(title = "考勤班组成员", businessType = BusinessType.UPDATE)
121
+    @PutMapping
122
+    public AjaxResult edit(@RequestBody AttendanceTeamUserRecord attendanceTeamUserRecord) {
123
+        return toAjax(attendanceTeamUserRecordService.updateAttendanceTeamUserRecord(attendanceTeamUserRecord));
124
+    }
125
+
126
+    /**
127
+     * 删除考勤班组成员
128
+     */
129
+    @PreAuthorize("@ss.hasPermi('attendance:record:remove')")
130
+    @Log(title = "考勤班组成员", businessType = BusinessType.DELETE)
131
+    @DeleteMapping("/{userIds}")
132
+    public AjaxResult remove(@PathVariable Long[] userIds) {
133
+        return toAjax(attendanceTeamUserRecordService.deleteAttendanceTeamUserRecordByUserIds(userIds));
134
+    }
135
+
136
+    /**
137
+     * 批量新增考勤班组成员
138
+     *
139
+     * @param attendanceTeamUserRecord
140
+     * @return
141
+     */
142
+    @PreAuthorize("@ss.hasPermi('attendance:record:add:list')")
143
+    @Log(title = "考勤班组成员", businessType = BusinessType.INSERT)
144
+    @PostMapping(value = "/add/list")
145
+    public AjaxResult addList(@RequestBody List<AttendanceTeamUserRecord> attendanceTeamUserRecord) {
146
+        return toAjax(attendanceTeamUserRecordService.insertAttendanceTeamUserRecordList(attendanceTeamUserRecord));
147
+    }
148
+
149
+    /**
150
+     * 获取考勤班组成员列表
151
+     */
152
+    @GetMapping("/list/teamLeader")
153
+    public AjaxResult listTeamLeader() {
154
+        return success(attendanceTeamUserRecordService.selectAttendanceTeamLeaderId());
155
+    }
156
+
157
+    /**
158
+     * 根据主键删除考勤班组成员
159
+     */
160
+    @PreAuthorize("@ss.hasPermi('attendance:record:remove')")
161
+    @Log(title = "考勤班组成员", businessType = BusinessType.DELETE)
162
+    @DeleteMapping("/removeBy/{ids}")
163
+    public AjaxResult removeBy(@PathVariable Long[] ids) {
164
+        return toAjax(attendanceTeamUserRecordService.deleteAttendanceTeamUserRecordByIds(ids));
165
+    }
166
+}

+ 87 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/attendance/api/AttendanceController.java

@@ -0,0 +1,87 @@
1
+package com.sundot.airport.web.controller.attendance.api;
2
+
3
+import cn.hutool.core.util.ObjectUtil;
4
+import cn.hutool.core.util.StrUtil;
5
+import com.alibaba.fastjson2.JSON;
6
+import com.sundot.airport.attendance.domain.AttendanceRecord;
7
+import com.sundot.airport.attendance.service.IAttendanceRecordService;
8
+import com.sundot.airport.common.annotation.Log;
9
+import com.sundot.airport.common.core.controller.BaseController;
10
+import com.sundot.airport.common.core.domain.AjaxResult;
11
+import com.sundot.airport.common.core.domain.entity.SysUser;
12
+import com.sundot.airport.common.dto.AttendanceRecordDTO;
13
+import com.sundot.airport.common.dto.AttendanceRecordReq;
14
+import com.sundot.airport.common.dto.UserInfo;
15
+import com.sundot.airport.common.enums.BusinessType;
16
+import com.sundot.airport.common.utils.DateUtils;
17
+import com.sundot.airport.system.service.ISysConfigService;
18
+import com.sundot.airport.web.core.cache.UserCache;
19
+import org.springframework.beans.factory.annotation.Autowired;
20
+import org.springframework.transaction.annotation.Transactional;
21
+import org.springframework.web.bind.annotation.GetMapping;
22
+import org.springframework.web.bind.annotation.PostMapping;
23
+import org.springframework.web.bind.annotation.RequestBody;
24
+import org.springframework.web.bind.annotation.RequestMapping;
25
+import org.springframework.web.bind.annotation.RestController;
26
+
27
+import java.util.Date;
28
+import java.util.List;
29
+
30
+/**
31
+ * 考勤管理 h5 api
32
+ *
33
+ * @Author: wangchong
34
+ * @Date: 2025/7/10 14:49
35
+ **/
36
+@RestController
37
+@RequestMapping("/attendance")
38
+public class AttendanceController extends BaseController {
39
+
40
+    @Autowired
41
+    private IAttendanceRecordService attendanceRecordService;
42
+
43
+    @Autowired
44
+    private UserCache userCache;
45
+
46
+    /**
47
+     * 打卡签到
48
+     *
49
+     * @param dto 打卡信息
50
+     * @return 今日打卡历史
51
+     */
52
+    @Log(title = "打卡签到", businessType = BusinessType.INSERT)
53
+    @PostMapping("/v1/record")
54
+    @Transactional(rollbackFor = Exception.class)
55
+    public AjaxResult record(@RequestBody AttendanceRecordDTO dto) {
56
+        UserInfo userInfo = userCache.getUserInfo(dto.getUserId());
57
+        return success(attendanceRecordService.record(dto, userInfo));
58
+    }
59
+
60
+    /**
61
+     * 打卡签到列表
62
+     */
63
+    @PostMapping("/v1/record-list")
64
+    public AjaxResult recordList(@RequestBody AttendanceRecordReq dto) {
65
+        return success(attendanceRecordService.recordList(dto));
66
+    }
67
+
68
+    /**
69
+     * 查询可打卡类型:1=签到,2=签退
70
+     */
71
+    @GetMapping("/v1/getRecordType")
72
+    public AjaxResult getRecordType() {
73
+        return AjaxResult.success("操作成功", attendanceRecordService.getRecordType());
74
+    }
75
+
76
+    /**
77
+     * 根据角色标识查询今日打卡用户列表
78
+     */
79
+    @PostMapping("/selectUserListByRoleKey")
80
+    public AjaxResult selectUserListByRoleKey(@RequestBody List<String> roleTypeList) {
81
+        // 计算当前时间对应的班次日期
82
+        Date queryDate = DateUtils.getNowDate();
83
+        List<SysUser> result = attendanceRecordService.selectUserListByRoleKey(queryDate, roleTypeList);
84
+        return AjaxResult.success(result);
85
+    }
86
+
87
+}

+ 38 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/cache/StatisticsCacheController.java

@@ -0,0 +1,38 @@
1
+package com.sundot.airport.web.controller.cache;
2
+
3
+import com.sundot.airport.common.core.controller.BaseController;
4
+import com.sundot.airport.common.core.domain.AjaxResult;
5
+import lombok.extern.slf4j.Slf4j;
6
+import org.springframework.cache.annotation.CacheEvict;
7
+import org.springframework.web.bind.annotation.PostMapping;
8
+import org.springframework.web.bind.annotation.RequestMapping;
9
+import org.springframework.web.bind.annotation.RestController;
10
+
11
+/**
12
+ * 统计缓存缓存管理控制器
13
+ */
14
+@Slf4j
15
+@RestController
16
+@RequestMapping("/statistics/cache")
17
+public class StatisticsCacheController extends BaseController {
18
+
19
+    /**
20
+     * 清理统计缓存
21
+     */
22
+    @PostMapping("/clearStatistics")
23
+    @CacheEvict(value = {
24
+            "statistics_data",
25
+            "statistics_list",
26
+            "item_station",
27
+            "item_ranking_detail",
28
+            "item_duration",
29
+            "item_seizure_trend",
30
+            "attendance_station",
31
+            "attendance_open_trend",
32
+            "base_module_info"
33
+    }, allEntries = true)
34
+    public AjaxResult clearStatisticsCache() {
35
+        return AjaxResult.success("统计缓存清理成功");
36
+    }
37
+
38
+}

+ 196 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/check/CheckCorrectionController.java

@@ -0,0 +1,196 @@
1
+package com.sundot.airport.web.controller.check;
2
+
3
+import java.util.Collections;
4
+import java.util.List;
5
+import javax.servlet.http.HttpServletResponse;
6
+
7
+import cn.hutool.core.collection.CollUtil;
8
+import com.sundot.airport.common.core.domain.ApprovalWorkflowBatchDto;
9
+import com.sundot.airport.common.core.domain.DataPermissionResult;
10
+import com.sundot.airport.web.core.utils.DataPermissionUtils;
11
+import org.springframework.security.access.prepost.PreAuthorize;
12
+import org.springframework.beans.factory.annotation.Autowired;
13
+import org.springframework.web.bind.annotation.GetMapping;
14
+import org.springframework.web.bind.annotation.PostMapping;
15
+import org.springframework.web.bind.annotation.PutMapping;
16
+import org.springframework.web.bind.annotation.DeleteMapping;
17
+import org.springframework.web.bind.annotation.PathVariable;
18
+import org.springframework.web.bind.annotation.RequestBody;
19
+import org.springframework.web.bind.annotation.RequestMapping;
20
+import org.springframework.web.bind.annotation.RestController;
21
+import com.sundot.airport.common.annotation.Log;
22
+import com.sundot.airport.common.core.controller.BaseController;
23
+import com.sundot.airport.common.core.domain.AjaxResult;
24
+import com.sundot.airport.common.enums.BusinessType;
25
+import com.sundot.airport.check.domain.CheckCorrection;
26
+import com.sundot.airport.check.service.ICheckCorrectionService;
27
+import com.sundot.airport.common.utils.poi.ExcelUtil;
28
+import com.sundot.airport.common.core.page.TableDataInfo;
29
+
30
+/**
31
+ * 问题整改Controller
32
+ *
33
+ * @author ruoyi
34
+ * @date 2025-09-08
35
+ */
36
+@RestController
37
+@RequestMapping("/check/checkCorrection")
38
+public class CheckCorrectionController extends BaseController {
39
+    @Autowired
40
+    private ICheckCorrectionService checkCorrectionService;
41
+
42
+    /**
43
+     * 查询问题整改列表
44
+     */
45
+    @PreAuthorize("@ss.hasPermi('check:checkCorrection:list')")
46
+    @GetMapping("/list")
47
+    public TableDataInfo list(CheckCorrection checkCorrection) {
48
+        DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
49
+        switch (dataPermission.getPermissionType()) {
50
+            case BRIGADE:
51
+                checkCorrection.setCheckedBrigadeId(dataPermission.getValue());
52
+                break;
53
+            case DEPARTMENT:
54
+                checkCorrection.setCheckedDepartmentId(dataPermission.getValue());
55
+                break;
56
+            case TEAM:
57
+            case SELF:
58
+                return getDataTable(Collections.emptyList());
59
+            default:
60
+                break;
61
+        }
62
+        startPage();
63
+        List<CheckCorrection> list = checkCorrectionService.selectCheckCorrectionList(checkCorrection);
64
+        return getDataTable(list);
65
+    }
66
+
67
+    /**
68
+     * 导出问题整改列表
69
+     */
70
+    @PreAuthorize("@ss.hasPermi('check:checkCorrection:export')")
71
+    @Log(title = "问题整改", businessType = BusinessType.EXPORT)
72
+    @PostMapping("/export")
73
+    public void export(HttpServletResponse response, CheckCorrection checkCorrection) {
74
+        List<CheckCorrection> list = Collections.emptyList();
75
+        DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
76
+        switch (dataPermission.getPermissionType()) {
77
+            case ALL:
78
+            case STATION:
79
+                list = checkCorrectionService.selectCheckCorrectionList(checkCorrection);
80
+                break;
81
+            case BRIGADE:
82
+                checkCorrection.setCheckedBrigadeId(dataPermission.getValue());
83
+                list = checkCorrectionService.selectCheckCorrectionList(checkCorrection);
84
+                break;
85
+            case DEPARTMENT:
86
+                checkCorrection.setCheckedDepartmentId(dataPermission.getValue());
87
+                list = checkCorrectionService.selectCheckCorrectionList(checkCorrection);
88
+                break;
89
+            default:
90
+                break;
91
+        }
92
+        ExcelUtil<CheckCorrection> util = new ExcelUtil<CheckCorrection>(CheckCorrection.class);
93
+        util.exportExcel(response, list, "问题整改数据");
94
+    }
95
+
96
+    /**
97
+     * 获取问题整改详细信息
98
+     */
99
+    @PreAuthorize("@ss.hasPermi('check:checkCorrection:query')")
100
+    @GetMapping(value = "/{id}")
101
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
102
+        return success(checkCorrectionService.selectCheckCorrectionById(id));
103
+    }
104
+
105
+    /**
106
+     * 新增问题整改
107
+     */
108
+    @PreAuthorize("@ss.hasPermi('check:checkCorrection:add')")
109
+    @Log(title = "问题整改", businessType = BusinessType.INSERT)
110
+    @PostMapping
111
+    public AjaxResult add(@RequestBody CheckCorrection checkCorrection) {
112
+        checkCorrection.setCreateBy(getUsername());
113
+        return toAjax(checkCorrectionService.insertCheckCorrection(checkCorrection));
114
+    }
115
+
116
+    /**
117
+     * 修改问题整改
118
+     */
119
+    @PreAuthorize("@ss.hasPermi('check:checkCorrection:edit')")
120
+    @Log(title = "问题整改", businessType = BusinessType.UPDATE)
121
+    @PutMapping
122
+    public AjaxResult edit(@RequestBody CheckCorrection checkCorrection) {
123
+        checkCorrection.setUpdateBy(getUsername());
124
+        if (CollUtil.isNotEmpty(checkCorrection.getCheckProjectItemList())) {
125
+            checkCorrection.getCheckProjectItemList().forEach(checkProjectItem -> {
126
+                checkProjectItem.setUpdateBy(getUsername());
127
+                if (CollUtil.isNotEmpty(checkProjectItem.getCheckUserList())) {
128
+                    checkProjectItem.getCheckUserList().forEach(checkUser -> {
129
+                        checkUser.setUpdateBy(getUsername());
130
+                    });
131
+                }
132
+            });
133
+        }
134
+        if (CollUtil.isNotEmpty(checkCorrection.getBaseAttachmentList())) {
135
+            checkCorrection.getBaseAttachmentList().forEach(baseAttachment -> {
136
+                baseAttachment.setUpdateBy(getUsername());
137
+            });
138
+        }
139
+        if (CollUtil.isNotEmpty(checkCorrection.getCheckRecordBaseAttachmentList())) {
140
+            checkCorrection.getCheckRecordBaseAttachmentList().forEach(baseAttachment -> {
141
+                baseAttachment.setUpdateBy(getUsername());
142
+            });
143
+        }
144
+        return toAjax(checkCorrectionService.updateCheckCorrection(checkCorrection));
145
+    }
146
+
147
+    /**
148
+     * 删除问题整改
149
+     */
150
+    @PreAuthorize("@ss.hasPermi('check:checkCorrection:remove')")
151
+    @Log(title = "问题整改", businessType = BusinessType.DELETE)
152
+    @DeleteMapping("/{ids}")
153
+    public AjaxResult remove(@PathVariable Long[] ids) {
154
+        return toAjax(checkCorrectionService.deleteCheckCorrectionByIds(ids));
155
+    }
156
+
157
+    /**
158
+     * 审批任务(同意)
159
+     */
160
+    @PreAuthorize("@ss.hasPermi('check:checkCorrection:edit')")
161
+    @Log(title = "问题整改", businessType = BusinessType.UPDATE)
162
+    @PostMapping("/approveTask")
163
+    public AjaxResult approveTask(@RequestBody CheckCorrection checkCorrection) {
164
+        return success(checkCorrectionService.approveTask(checkCorrection));
165
+    }
166
+
167
+    /**
168
+     * 审批任务(驳回)
169
+     */
170
+    @PreAuthorize("@ss.hasPermi('check:checkCorrection:edit')")
171
+    @Log(title = "问题整改", businessType = BusinessType.UPDATE)
172
+    @PostMapping("/rejectTask")
173
+    public AjaxResult rejectTask(@RequestBody CheckCorrection checkCorrection) {
174
+        return success(checkCorrectionService.rejectTask(checkCorrection));
175
+    }
176
+
177
+    /**
178
+     * 批量审批任务(同意)
179
+     */
180
+    @PreAuthorize("@ss.hasPermi('check:checkCorrection:edit')")
181
+    @Log(title = "批量审批任务(同意)", businessType = BusinessType.UPDATE)
182
+    @PostMapping("/approveTaskBatch")
183
+    public AjaxResult approveTaskBatch(@RequestBody ApprovalWorkflowBatchDto approvalWorkflowBatchDto) {
184
+        return success(checkCorrectionService.approveTaskBatch(approvalWorkflowBatchDto));
185
+    }
186
+
187
+    /**
188
+     * 批量审批任务(驳回)
189
+     */
190
+    @PreAuthorize("@ss.hasPermi('check:checkCorrection:edit')")
191
+    @Log(title = "批量审批任务(驳回)", businessType = BusinessType.UPDATE)
192
+    @PostMapping("/rejectTaskBatch")
193
+    public AjaxResult rejectTaskBatch(@RequestBody ApprovalWorkflowBatchDto approvalWorkflowBatchDto) {
194
+        return success(checkCorrectionService.rejectTaskBatch(approvalWorkflowBatchDto));
195
+    }
196
+}

+ 256 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/check/CheckLargeScreenController.java

@@ -0,0 +1,256 @@
1
+package com.sundot.airport.web.controller.check;
2
+
3
+import com.sundot.airport.check.domain.CheckLargeScreenCommonDto;
4
+import com.sundot.airport.check.domain.CheckLargeScreenCorrectionDto;
5
+import com.sundot.airport.check.domain.CheckLargeScreenCorrectionPortraitDto;
6
+import com.sundot.airport.check.domain.CheckLargeScreenCorrectionQueryParamDto;
7
+import com.sundot.airport.check.domain.CheckLargeScreenHomePageDto;
8
+import com.sundot.airport.check.domain.CheckLargeScreenInspectionExecuteDto;
9
+import com.sundot.airport.check.domain.CheckLargeScreenPlanOverviewDto;
10
+import com.sundot.airport.check.domain.CheckLargeScreenPlanQueryParamDto;
11
+import com.sundot.airport.check.domain.CheckLargeScreenProblemDto;
12
+import com.sundot.airport.check.domain.CheckLargeScreenProblemQueryParamDto;
13
+import com.sundot.airport.check.domain.CheckLargeScreenProblemTrendDto;
14
+import com.sundot.airport.check.domain.CheckTask;
15
+import com.sundot.airport.check.service.ICheckLargeScreenService;
16
+import com.sundot.airport.common.core.controller.BaseController;
17
+import com.sundot.airport.common.core.domain.AjaxResult;
18
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
19
+import com.sundot.airport.common.core.domain.CheckLargeScreenHomePageRankingDto;
20
+import org.springframework.beans.factory.annotation.Autowired;
21
+import org.springframework.cache.annotation.Cacheable;
22
+import org.springframework.security.access.prepost.PreAuthorize;
23
+import org.springframework.web.bind.annotation.GetMapping;
24
+import org.springframework.web.bind.annotation.RequestHeader;
25
+import org.springframework.web.bind.annotation.RequestMapping;
26
+import org.springframework.web.bind.annotation.RestController;
27
+
28
+import java.math.BigDecimal;
29
+import java.util.List;
30
+
31
+/**
32
+ * 巡检大屏Controller
33
+ *
34
+ * @author ruoyi
35
+ * @date 2025-09-07
36
+ */
37
+@RestController
38
+@RequestMapping("/check/largeScreen")
39
+public class CheckLargeScreenController extends BaseController {
40
+
41
+    @Autowired
42
+    private ICheckLargeScreenService checkLargeScreenService;
43
+
44
+    /**
45
+     * 巡检计划-计划安排总览
46
+     */
47
+    @PreAuthorize("@ss.hasPermi('check:checkTask:list')")
48
+    @GetMapping("/planOverview")
49
+    public AjaxResult planOverview(CheckLargeScreenPlanQueryParamDto dto, @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
50
+        List<CheckLargeScreenPlanOverviewDto> result = checkLargeScreenService.planOverview(dto, source);
51
+        return success(result);
52
+    }
53
+
54
+    /**
55
+     * 巡检计划-日常任务检查指标累积分布
56
+     */
57
+    @PreAuthorize("@ss.hasPermi('check:checkTask:list')")
58
+    @GetMapping("/planDistribution")
59
+    public AjaxResult planDistribution(CheckLargeScreenPlanQueryParamDto dto, @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
60
+        List<CheckLargeScreenCommonDto> result = checkLargeScreenService.planDistribution(dto, source);
61
+        return success(result);
62
+    }
63
+
64
+    /**
65
+     * 巡检计划-任务明细统计表
66
+     */
67
+    @PreAuthorize("@ss.hasPermi('check:checkTask:list')")
68
+    @GetMapping("/planStatistics")
69
+    public AjaxResult planStatistics(CheckLargeScreenPlanQueryParamDto dto, @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
70
+        List<CheckTask> result = checkLargeScreenService.planStatistics(dto, source);
71
+        return success(result);
72
+    }
73
+
74
+    /**
75
+     * 问题发现-总体问题分布
76
+     */
77
+    @PreAuthorize("@ss.hasPermi('check:checkTask:list')")
78
+    @GetMapping("/problemDistribution")
79
+    public AjaxResult problemDistribution(CheckLargeScreenProblemQueryParamDto dto, @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
80
+        List<CheckLargeScreenProblemDto> result = checkLargeScreenService.problemDistribution(dto, source);
81
+        return success(result);
82
+    }
83
+
84
+    /**
85
+     * 问题发现-问题分布对比
86
+     */
87
+    @PreAuthorize("@ss.hasPermi('check:checkTask:list')")
88
+    @GetMapping("/problemComparison")
89
+    public AjaxResult problemComparison(CheckLargeScreenProblemQueryParamDto dto, @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
90
+        List<CheckLargeScreenProblemDto> result = checkLargeScreenService.problemComparison(dto, source);
91
+        return success(result);
92
+    }
93
+
94
+    /**
95
+     * 问题发现-通道面貌-问题对比
96
+     */
97
+    @PreAuthorize("@ss.hasPermi('check:checkTask:list')")
98
+    @GetMapping("/problemComparisonTwo")
99
+    public AjaxResult problemComparisonTwo(CheckLargeScreenProblemQueryParamDto dto, @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
100
+        List<CheckLargeScreenProblemDto> result = checkLargeScreenService.problemComparisonTwo(dto, source);
101
+        return success(result);
102
+    }
103
+
104
+    /**
105
+     * 问题发现-通道面貌-问题趋势
106
+     */
107
+    @PreAuthorize("@ss.hasPermi('check:checkTask:list')")
108
+    @GetMapping("/problemTrend")
109
+    public AjaxResult problemTrend(CheckLargeScreenProblemQueryParamDto dto, @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
110
+        List<CheckLargeScreenProblemTrendDto> result = checkLargeScreenService.problemTrend(dto, source);
111
+        return success(result);
112
+    }
113
+
114
+    /**
115
+     * 问题整改-整改状态总计
116
+     */
117
+    @PreAuthorize("@ss.hasPermi('check:checkTask:list')")
118
+    @GetMapping("/correction")
119
+    public AjaxResult correction(CheckLargeScreenCorrectionQueryParamDto dto, @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
120
+        CheckLargeScreenCorrectionDto result = checkLargeScreenService.correction(dto, source);
121
+        return success(result);
122
+    }
123
+
124
+    /**
125
+     * 问题整改-整改状态分布-科级对比
126
+     */
127
+    @PreAuthorize("@ss.hasPermi('check:checkTask:list')")
128
+    @GetMapping("/correctionDistribution")
129
+    public AjaxResult correctionDistribution(CheckLargeScreenCorrectionQueryParamDto dto, @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
130
+        List<CheckLargeScreenCorrectionDto> result = checkLargeScreenService.correctionDistribution(dto, source);
131
+        return success(result);
132
+    }
133
+
134
+    /**
135
+     * 巡检执行
136
+     */
137
+    @PreAuthorize("@ss.hasPermi('check:checkTask:list')")
138
+    @GetMapping("/inspectionExecute")
139
+    public AjaxResult inspectionExecute(BaseLargeScreenQueryParamDto dto) {
140
+        List<CheckLargeScreenInspectionExecuteDto> result = checkLargeScreenService.inspectionExecute(dto);
141
+        return success(result);
142
+    }
143
+
144
+    /**
145
+     * 巡检画像
146
+     */
147
+    @Cacheable(
148
+            value = "statistics_data",
149
+            keyGenerator = "statisticsKeyGenerator",
150
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
151
+    )
152
+    @GetMapping("/portrait")
153
+    public AjaxResult portrait(CheckLargeScreenCorrectionQueryParamDto dto) {
154
+        CheckLargeScreenCorrectionPortraitDto result = checkLargeScreenService.portrait(dto);
155
+        return success(result);
156
+    }
157
+
158
+    /**
159
+     * 工作画像-管理推动-检查单
160
+     */
161
+    @Cacheable(
162
+            value = "statistics_list",
163
+            keyGenerator = "statisticsKeyGenerator",
164
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
165
+    )
166
+    @GetMapping("/managementPromotionRecord")
167
+    public AjaxResult managementPromotionRecord(CheckLargeScreenCorrectionQueryParamDto dto) {
168
+        List<CheckLargeScreenPlanOverviewDto> result = checkLargeScreenService.managementPromotionRecord(dto);
169
+        return success(result);
170
+    }
171
+
172
+    /**
173
+     * 工作画像-管理推动-整改单
174
+     */
175
+    @Cacheable(
176
+            value = "statistics_data",
177
+            keyGenerator = "statisticsKeyGenerator",
178
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
179
+    )
180
+    @GetMapping("/managementPromotionCorrection")
181
+    public AjaxResult managementPromotionCorrection(CheckLargeScreenCorrectionQueryParamDto dto) {
182
+        CheckLargeScreenCorrectionDto result = checkLargeScreenService.managementPromotionCorrection(dto);
183
+        return success(result);
184
+    }
185
+
186
+    /**
187
+     * 工作画像-工作产出-巡检问题统计图
188
+     */
189
+    @Cacheable(
190
+            value = "statistics_list",
191
+            keyGenerator = "statisticsKeyGenerator",
192
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
193
+    )
194
+    @GetMapping("/workOutputCheck")
195
+    public AjaxResult workOutputCheck(CheckLargeScreenCorrectionQueryParamDto dto) {
196
+        List<CheckLargeScreenCommonDto> result = checkLargeScreenService.workOutputCheck(dto);
197
+        return success(result);
198
+    }
199
+
200
+    /**
201
+     * 首页
202
+     */
203
+//    @Cacheable(
204
+//            value = "statistics_check_homePage",
205
+//            keyGenerator = "statisticsKeyGenerator",
206
+//            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
207
+//    )
208
+    @GetMapping("/homePage")
209
+    public AjaxResult homePage(BaseLargeScreenQueryParamDto dto, @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
210
+        CheckLargeScreenHomePageDto result = checkLargeScreenService.homePage(dto, source);
211
+        return success(result);
212
+    }
213
+
214
+    /**
215
+     * 首页-巡检-巡检合格率
216
+     */
217
+//    @Cacheable(
218
+//            value = "statistics_check_passRate",
219
+//            keyGenerator = "statisticsKeyGenerator",
220
+//            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
221
+//    )
222
+    @GetMapping("/checkPassRate")
223
+    public AjaxResult checkPassRate(BaseLargeScreenQueryParamDto dto) {
224
+        BigDecimal result = checkLargeScreenService.checkPassRate(dto);
225
+        return success(result);
226
+    }
227
+
228
+    /**
229
+     * 首页-巡检-巡检合格率排名
230
+     */
231
+//    @Cacheable(
232
+//            value = "statistics_check_ranking",
233
+//            keyGenerator = "statisticsKeyGenerator",
234
+//            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
235
+//    )
236
+    @GetMapping("/checkRanking")
237
+    public AjaxResult checkRanking(BaseLargeScreenQueryParamDto dto) {
238
+        CheckLargeScreenHomePageRankingDto result = checkLargeScreenService.checkRanking(dto);
239
+        return success(result);
240
+    }
241
+
242
+    /**
243
+     * 首页-巡检-今日巡检问题数
244
+     */
245
+//    @Cacheable(
246
+//            value = "statistics_check_correction_count",
247
+//            keyGenerator = "statisticsKeyGenerator",
248
+//            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
249
+//    )
250
+    @GetMapping("/checkCorrectionCount")
251
+    public AjaxResult checkCorrectionCount(BaseLargeScreenQueryParamDto dto) {
252
+        Integer result = checkLargeScreenService.checkCorrectionCount(dto);
253
+        return success(result);
254
+    }
255
+
256
+}

+ 98 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/check/CheckProjectItemController.java

@@ -0,0 +1,98 @@
1
+package com.sundot.airport.web.controller.check;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.security.access.prepost.PreAuthorize;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.PostMapping;
10
+import org.springframework.web.bind.annotation.PutMapping;
11
+import org.springframework.web.bind.annotation.DeleteMapping;
12
+import org.springframework.web.bind.annotation.PathVariable;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.RestController;
16
+import com.sundot.airport.common.annotation.Log;
17
+import com.sundot.airport.common.core.controller.BaseController;
18
+import com.sundot.airport.common.core.domain.AjaxResult;
19
+import com.sundot.airport.common.enums.BusinessType;
20
+import com.sundot.airport.check.domain.CheckProjectItem;
21
+import com.sundot.airport.check.service.ICheckProjectItemService;
22
+import com.sundot.airport.common.utils.poi.ExcelUtil;
23
+import com.sundot.airport.common.core.page.TableDataInfo;
24
+
25
+/**
26
+ * 检查单和检查记录和问题整改的检查项明细Controller
27
+ *
28
+ * @author ruoyi
29
+ * @date 2025-07-11
30
+ */
31
+@RestController
32
+@RequestMapping("/check/projectItem")
33
+public class CheckProjectItemController extends BaseController {
34
+    @Autowired
35
+    private ICheckProjectItemService checkProjectItemService;
36
+
37
+    /**
38
+     * 查询检查单和检查记录和问题整改的检查项明细列表
39
+     */
40
+    @PreAuthorize("@ss.hasPermi('check:projectItem:list')")
41
+    @GetMapping("/list")
42
+    public TableDataInfo list(CheckProjectItem checkProjectItem) {
43
+        startPage();
44
+        List<CheckProjectItem> list = checkProjectItemService.selectCheckProjectItemList(checkProjectItem);
45
+        return getDataTable(list);
46
+    }
47
+
48
+    /**
49
+     * 导出检查单和检查记录和问题整改的检查项明细列表
50
+     */
51
+    @PreAuthorize("@ss.hasPermi('check:projectItem:export')")
52
+    @Log(title = "检查单和检查记录和问题整改的检查项明细", businessType = BusinessType.EXPORT)
53
+    @PostMapping("/export")
54
+    public void export(HttpServletResponse response, CheckProjectItem checkProjectItem) {
55
+        List<CheckProjectItem> list = checkProjectItemService.selectCheckProjectItemList(checkProjectItem);
56
+        ExcelUtil<CheckProjectItem> util = new ExcelUtil<CheckProjectItem>(CheckProjectItem.class);
57
+        util.exportExcel(response, list, "检查单和检查记录和问题整改的检查项明细数据");
58
+    }
59
+
60
+    /**
61
+     * 获取检查单和检查记录和问题整改的检查项明细详细信息
62
+     */
63
+    @PreAuthorize("@ss.hasPermi('check:projectItem:query')")
64
+    @GetMapping(value = "/{id}")
65
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
66
+        return success(checkProjectItemService.selectCheckProjectItemById(id));
67
+    }
68
+
69
+    /**
70
+     * 新增检查单和检查记录和问题整改的检查项明细
71
+     */
72
+    @PreAuthorize("@ss.hasPermi('check:projectItem:add')")
73
+    @Log(title = "检查单和检查记录和问题整改的检查项明细", businessType = BusinessType.INSERT)
74
+    @PostMapping
75
+    public AjaxResult add(@RequestBody CheckProjectItem checkProjectItem) {
76
+        return toAjax(checkProjectItemService.insertCheckProjectItem(checkProjectItem));
77
+    }
78
+
79
+    /**
80
+     * 修改检查单和检查记录和问题整改的检查项明细
81
+     */
82
+    @PreAuthorize("@ss.hasPermi('check:projectItem:edit')")
83
+    @Log(title = "检查单和检查记录和问题整改的检查项明细", businessType = BusinessType.UPDATE)
84
+    @PutMapping
85
+    public AjaxResult edit(@RequestBody CheckProjectItem checkProjectItem) {
86
+        return toAjax(checkProjectItemService.updateCheckProjectItem(checkProjectItem));
87
+    }
88
+
89
+    /**
90
+     * 删除检查单和检查记录和问题整改的检查项明细
91
+     */
92
+    @PreAuthorize("@ss.hasPermi('check:projectItem:remove')")
93
+    @Log(title = "检查单和检查记录和问题整改的检查项明细", businessType = BusinessType.DELETE)
94
+    @DeleteMapping("/{ids}")
95
+    public AjaxResult remove(@PathVariable Long[] ids) {
96
+        return toAjax(checkProjectItemService.deleteCheckProjectItemByIds(ids));
97
+    }
98
+}

+ 171 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/check/CheckRecordController.java

@@ -0,0 +1,171 @@
1
+package com.sundot.airport.web.controller.check;
2
+
3
+import java.util.Collections;
4
+import java.util.List;
5
+import java.util.stream.Collectors;
6
+import javax.servlet.http.HttpServletResponse;
7
+
8
+import cn.hutool.core.collection.CollUtil;
9
+import cn.hutool.core.util.StrUtil;
10
+import com.sundot.airport.common.core.domain.DataPermissionResult;
11
+import com.sundot.airport.common.core.domain.entity.SysUser;
12
+import com.sundot.airport.common.enums.CheckRecordStatusEnum;
13
+import com.sundot.airport.common.enums.SourceTypeEnum;
14
+import com.sundot.airport.system.service.ISysUserService;
15
+import com.sundot.airport.web.core.utils.DataPermissionUtils;
16
+import org.springframework.security.access.prepost.PreAuthorize;
17
+import org.springframework.beans.factory.annotation.Autowired;
18
+import org.springframework.web.bind.annotation.GetMapping;
19
+import org.springframework.web.bind.annotation.PostMapping;
20
+import org.springframework.web.bind.annotation.PutMapping;
21
+import org.springframework.web.bind.annotation.DeleteMapping;
22
+import org.springframework.web.bind.annotation.PathVariable;
23
+import org.springframework.web.bind.annotation.RequestBody;
24
+import org.springframework.web.bind.annotation.RequestHeader;
25
+import org.springframework.web.bind.annotation.RequestMapping;
26
+import org.springframework.web.bind.annotation.RestController;
27
+import com.sundot.airport.common.annotation.Log;
28
+import com.sundot.airport.common.core.controller.BaseController;
29
+import com.sundot.airport.common.core.domain.AjaxResult;
30
+import com.sundot.airport.common.enums.BusinessType;
31
+import com.sundot.airport.check.domain.CheckRecord;
32
+import com.sundot.airport.check.service.ICheckRecordService;
33
+import com.sundot.airport.common.utils.poi.ExcelUtil;
34
+import com.sundot.airport.common.core.page.TableDataInfo;
35
+
36
+/**
37
+ * 检查记录Controller
38
+ *
39
+ * @author ruoyi
40
+ * @date 2025-07-11
41
+ */
42
+@RestController
43
+@RequestMapping("/check/checkRecord")
44
+public class CheckRecordController extends BaseController {
45
+    @Autowired
46
+    private ICheckRecordService checkRecordService;
47
+    @Autowired
48
+    private ISysUserService sysUserService;
49
+
50
+    /**
51
+     * 查询检查记录列表
52
+     */
53
+    @PreAuthorize("@ss.hasPermi('check:checkRecord:list')")
54
+    @GetMapping("/list")
55
+    public TableDataInfo list(CheckRecord checkRecord, @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
56
+        DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
57
+        List<CheckRecord> list;
58
+        if (StrUtil.equals(SourceTypeEnum.mobile.getCode(), source)) {
59
+            startPage();
60
+            list = checkRecordService.selectCheckRecordList(checkRecord);
61
+        } else if (StrUtil.equals(SourceTypeEnum.web.getCode(), source)) {
62
+            switch (dataPermission.getPermissionType()) {
63
+                case ALL:
64
+                case STATION:
65
+                    break;
66
+                case BRIGADE:
67
+                case DEPARTMENT:
68
+                    List<SysUser> sysUserList = sysUserService.selectUserByDeptId(dataPermission.getValue());
69
+                    if (CollUtil.isEmpty(sysUserList)) {
70
+                        return getDataTable(Collections.emptyList());
71
+                    }
72
+                    List<Long> userIdList = sysUserList.stream().map(SysUser::getUserId).distinct().collect(Collectors.toList());
73
+                    checkRecord.setQueryCheckerIdList(userIdList);
74
+                    break;
75
+                default:
76
+                    return getDataTable(Collections.emptyList());
77
+            }
78
+            startPage();
79
+            list = checkRecordService.selectCheckRecordList(checkRecord);
80
+        } else {
81
+            return getDataTable(Collections.emptyList());
82
+        }
83
+        return getDataTable(list);
84
+    }
85
+
86
+    /**
87
+     * 导出检查记录列表
88
+     */
89
+    @PreAuthorize("@ss.hasPermi('check:checkRecord:export')")
90
+    @Log(title = "检查记录", businessType = BusinessType.EXPORT)
91
+    @PostMapping("/export")
92
+    public void export(HttpServletResponse response, CheckRecord checkRecord) {
93
+        DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
94
+        List<CheckRecord> list = Collections.emptyList();
95
+        switch (dataPermission.getPermissionType()) {
96
+            case ALL:
97
+            case STATION:
98
+                list = checkRecordService.selectCheckRecordList(checkRecord);
99
+                break;
100
+            case BRIGADE:
101
+            case DEPARTMENT:
102
+                List<SysUser> sysUserList = sysUserService.selectUserByDeptId(dataPermission.getValue());
103
+                if (CollUtil.isNotEmpty(sysUserList)) {
104
+                    List<Long> userIdList = sysUserList.stream().map(SysUser::getUserId).distinct().collect(Collectors.toList());
105
+                    checkRecord.setQueryCheckerIdList(userIdList);
106
+                    list = checkRecordService.selectCheckRecordList(checkRecord);
107
+                }
108
+                break;
109
+            default:
110
+                break;
111
+        }
112
+        ExcelUtil<CheckRecord> util = new ExcelUtil<CheckRecord>(CheckRecord.class);
113
+        util.exportExcel(response, list, "检查记录数据");
114
+    }
115
+
116
+    /**
117
+     * 获取检查记录详细信息
118
+     */
119
+    @PreAuthorize("@ss.hasPermi('check:checkRecord:query')")
120
+    @GetMapping(value = "/{id}")
121
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
122
+        return success(checkRecordService.selectCheckRecordById(id));
123
+    }
124
+
125
+    /**
126
+     * 新增检查记录
127
+     */
128
+    @PreAuthorize("@ss.hasPermi('check:checkRecord:add')")
129
+    @Log(title = "检查记录", businessType = BusinessType.INSERT)
130
+    @PostMapping("/add")
131
+    public AjaxResult add(@RequestBody CheckRecord checkRecord) {
132
+        checkRecord.setCreateBy(getUsername());
133
+        checkRecord.setCheckRecordStatus(CheckRecordStatusEnum.FORMAL.getCode());
134
+        checkRecord.setCheckRecordStatusDesc(CheckRecordStatusEnum.FORMAL.getDesc());
135
+        return toAjax(checkRecordService.insertOrUpdateCheckRecord(checkRecord, true));
136
+    }
137
+
138
+    /**
139
+     * 新增草稿检查记录
140
+     */
141
+    @PreAuthorize("@ss.hasPermi('check:checkRecord:add')")
142
+    @Log(title = "草稿检查记录", businessType = BusinessType.INSERT)
143
+    @PostMapping("/draft")
144
+    public AjaxResult draft(@RequestBody CheckRecord checkRecord) {
145
+        checkRecord.setCreateBy(getUsername());
146
+        checkRecord.setCheckRecordStatus(CheckRecordStatusEnum.DRAFT.getCode());
147
+        checkRecord.setCheckRecordStatusDesc(CheckRecordStatusEnum.DRAFT.getDesc());
148
+        return toAjax(checkRecordService.insertOrUpdateCheckRecord(checkRecord, false));
149
+    }
150
+
151
+    /**
152
+     * 修改检查记录
153
+     */
154
+    @PreAuthorize("@ss.hasPermi('check:checkRecord:edit')")
155
+    @Log(title = "检查记录", businessType = BusinessType.UPDATE)
156
+    @PutMapping
157
+    public AjaxResult edit(@RequestBody CheckRecord checkRecord) {
158
+        checkRecord.setUpdateBy(getUsername());
159
+        return toAjax(checkRecordService.updateCheckRecord(checkRecord));
160
+    }
161
+
162
+    /**
163
+     * 删除检查记录
164
+     */
165
+    @PreAuthorize("@ss.hasPermi('check:checkRecord:remove')")
166
+    @Log(title = "检查记录", businessType = BusinessType.DELETE)
167
+    @DeleteMapping("/{ids}")
168
+    public AjaxResult remove(@PathVariable Long[] ids) {
169
+        return toAjax(checkRecordService.deleteCheckRecordByIds(ids));
170
+    }
171
+}

+ 230 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/check/CheckTaskController.java

@@ -0,0 +1,230 @@
1
+package com.sundot.airport.web.controller.check;
2
+
3
+import java.util.Collections;
4
+import java.util.List;
5
+import java.util.stream.Collectors;
6
+import javax.servlet.http.HttpServletResponse;
7
+
8
+import cn.hutool.core.collection.CollUtil;
9
+import cn.hutool.core.util.StrUtil;
10
+import com.sundot.airport.common.core.domain.DataPermissionResult;
11
+import com.sundot.airport.common.core.domain.entity.SysDept;
12
+import com.sundot.airport.common.core.domain.entity.SysRole;
13
+import com.sundot.airport.common.enums.CheckLevelEnum;
14
+import com.sundot.airport.common.enums.CheckTaskStatusEnum;
15
+import com.sundot.airport.common.enums.RoleTypeEnum;
16
+import com.sundot.airport.common.enums.SourceTypeEnum;
17
+import com.sundot.airport.common.utils.SecurityUtils;
18
+import com.sundot.airport.system.service.ISysDeptService;
19
+import com.sundot.airport.web.core.utils.DataPermissionUtils;
20
+import org.springframework.security.access.prepost.PreAuthorize;
21
+import org.springframework.beans.factory.annotation.Autowired;
22
+import org.springframework.web.bind.annotation.GetMapping;
23
+import org.springframework.web.bind.annotation.PostMapping;
24
+import org.springframework.web.bind.annotation.PutMapping;
25
+import org.springframework.web.bind.annotation.DeleteMapping;
26
+import org.springframework.web.bind.annotation.PathVariable;
27
+import org.springframework.web.bind.annotation.RequestBody;
28
+import org.springframework.web.bind.annotation.RequestHeader;
29
+import org.springframework.web.bind.annotation.RequestMapping;
30
+import org.springframework.web.bind.annotation.RestController;
31
+import com.sundot.airport.common.annotation.Log;
32
+import com.sundot.airport.common.core.controller.BaseController;
33
+import com.sundot.airport.common.core.domain.AjaxResult;
34
+import com.sundot.airport.common.enums.BusinessType;
35
+import com.sundot.airport.check.domain.CheckTask;
36
+import com.sundot.airport.check.service.ICheckTaskService;
37
+import com.sundot.airport.common.utils.poi.ExcelUtil;
38
+import com.sundot.airport.common.core.page.TableDataInfo;
39
+
40
+/**
41
+ * 检查任务Controller
42
+ *
43
+ * @author ruoyi
44
+ * @date 2025-09-07
45
+ */
46
+@RestController
47
+@RequestMapping("/check/checkTask")
48
+public class CheckTaskController extends BaseController {
49
+    @Autowired
50
+    private ICheckTaskService checkTaskService;
51
+    @Autowired
52
+    private ISysDeptService sysDeptService;
53
+
54
+    /**
55
+     * 查询检查任务列表
56
+     */
57
+    @PreAuthorize("@ss.hasPermi('check:checkTask:list')")
58
+    @GetMapping("/list")
59
+    public TableDataInfo list(CheckTask checkTask, @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
60
+        DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
61
+        startPage();
62
+        List<CheckTask> list;
63
+        if (StrUtil.equals(SourceTypeEnum.mobile.getCode(), source)) {
64
+            switch (dataPermission.getPermissionType()) {
65
+                case ALL:
66
+                    checkTask.setCheckLevel(null);
67
+                    break;
68
+                case STATION:
69
+                    checkTask.setCheckLevel(CheckLevelEnum.STATION_LEVEL.getCode());
70
+                    break;
71
+                case BRIGADE:
72
+                    List<SysRole> roles = SecurityUtils.getLoginUser().getUser().getRoles();
73
+                    List<String> roleKeyList = roles.stream().map(SysRole::getRoleKey).collect(Collectors.toList());
74
+                    if (roleKeyList.contains(RoleTypeEnum.xingzheng.getCode())) {
75
+                        checkTask.setCheckLevel(CheckLevelEnum.BRIGADE_LEVEL.getCode());
76
+                        checkTask.setSelfCheckDeptId(SecurityUtils.getLoginUser().getUser().getDept().getDeptId());
77
+                    } else {
78
+                        return getDataTable(Collections.emptyList());
79
+                    }
80
+                    break;
81
+                case DEPARTMENT:
82
+                    checkTask.setCheckLevel(CheckLevelEnum.DEPARTMENT_LEVEL.getCode());
83
+                    checkTask.setSelfCheckDeptId(SecurityUtils.getLoginUser().getUser().getDept().getParentId());
84
+                    break;
85
+                case TEAM:
86
+                    checkTask.setCheckLevel(CheckLevelEnum.TEAM_LEVEL.getCode());
87
+                    SysDept teamDept = SecurityUtils.getLoginUser().getUser().getDept();
88
+                    SysDept departmentDept = sysDeptService.selectDeptById(teamDept.getParentId());
89
+                    checkTask.setSelfCheckDeptId(departmentDept.getParentId());
90
+                    break;
91
+                default:
92
+                    return getDataTable(Collections.emptyList());
93
+            }
94
+            list = checkTaskService.selectCheckTaskList(checkTask);
95
+        } else if (StrUtil.equals(SourceTypeEnum.web.getCode(), source)) {
96
+            switch (dataPermission.getPermissionType()) {
97
+                case ALL:
98
+                case STATION:
99
+                    list = checkTaskService.selectCheckTaskList(checkTask);
100
+                    break;
101
+                case BRIGADE:
102
+                    checkTask.setSelfCheckDeptId(SecurityUtils.getLoginUser().getUser().getDept().getDeptId());
103
+                    list = checkTaskService.selectCheckTaskListBrigadeWeb(checkTask);
104
+                    break;
105
+                case DEPARTMENT:
106
+                    checkTask.setSelfCheckDeptId(SecurityUtils.getLoginUser().getUser().getDept().getParentId());
107
+                    list = checkTaskService.selectCheckTaskListWeb(checkTask);
108
+                    break;
109
+                default:
110
+                    return getDataTable(Collections.emptyList());
111
+            }
112
+        } else {
113
+            return getDataTable(Collections.emptyList());
114
+        }
115
+        return getDataTable(list);
116
+    }
117
+
118
+    /**
119
+     * 导出检查任务列表
120
+     */
121
+    @PreAuthorize("@ss.hasPermi('check:checkTask:export')")
122
+    @Log(title = "检查任务", businessType = BusinessType.EXPORT)
123
+    @PostMapping("/export")
124
+    public void export(HttpServletResponse response, CheckTask checkTask) {
125
+        DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
126
+        List<CheckTask> list;
127
+        switch (dataPermission.getPermissionType()) {
128
+            case ALL:
129
+            case STATION:
130
+                list = checkTaskService.selectCheckTaskList(checkTask);
131
+                break;
132
+            case BRIGADE:
133
+                checkTask.setSelfCheckDeptId(SecurityUtils.getLoginUser().getUser().getDept().getDeptId());
134
+                list = checkTaskService.selectCheckTaskListBrigadeWeb(checkTask);
135
+                break;
136
+            case DEPARTMENT:
137
+                checkTask.setSelfCheckDeptId(SecurityUtils.getLoginUser().getUser().getDept().getParentId());
138
+                list = checkTaskService.selectCheckTaskListWeb(checkTask);
139
+                break;
140
+            default:
141
+                list = Collections.emptyList();
142
+                break;
143
+        }
144
+        ExcelUtil<CheckTask> util = new ExcelUtil<CheckTask>(CheckTask.class);
145
+        util.exportExcel(response, list, "检查任务数据");
146
+    }
147
+
148
+    /**
149
+     * 获取检查任务详细信息
150
+     */
151
+    @PreAuthorize("@ss.hasPermi('check:checkTask:query')")
152
+    @GetMapping(value = "/{id}")
153
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
154
+        return success(checkTaskService.selectCheckTaskById(id));
155
+    }
156
+
157
+    /**
158
+     * 新增检查任务
159
+     */
160
+    @PreAuthorize("@ss.hasPermi('check:checkTask:add')")
161
+    @Log(title = "检查任务", businessType = BusinessType.INSERT)
162
+    @PostMapping
163
+    public AjaxResult add(@RequestBody CheckTask checkTask) {
164
+        checkTask.setCreateId(getUserId());
165
+        checkTask.setCreateBy(getUsername());
166
+        return toAjax(checkTaskService.insertCheckTask(checkTask));
167
+    }
168
+
169
+    /**
170
+     * 修改检查任务
171
+     */
172
+    @PreAuthorize("@ss.hasPermi('check:checkTask:edit')")
173
+    @Log(title = "检查任务", businessType = BusinessType.UPDATE)
174
+    @PutMapping
175
+    public AjaxResult edit(@RequestBody CheckTask checkTask) {
176
+        checkTask.setUpdateId(getUserId());
177
+        checkTask.setUpdateBy(getUsername());
178
+        return toAjax(checkTaskService.updateCheckTask(checkTask));
179
+    }
180
+
181
+    /**
182
+     * 删除检查任务
183
+     */
184
+    @PreAuthorize("@ss.hasPermi('check:checkTask:remove')")
185
+    @Log(title = "检查任务", businessType = BusinessType.DELETE)
186
+    @DeleteMapping("/{ids}")
187
+    public AjaxResult remove(@PathVariable Long[] ids) {
188
+        return toAjax(checkTaskService.deleteCheckTaskByIds(ids));
189
+    }
190
+
191
+    /**
192
+     * 查询检查任务未读数量
193
+     */
194
+    @PreAuthorize("@ss.hasPermi('check:checkTask:list')")
195
+    @GetMapping("/unReadNum")
196
+    public AjaxResult unReadNum() {
197
+        DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
198
+        CheckTask checkTask = new CheckTask();
199
+        checkTask.setStatus(CheckTaskStatusEnum.IN_PROGRESS.getCode());
200
+        switch (dataPermission.getPermissionType()) {
201
+            case ALL:
202
+                checkTask.setCheckLevel(null);
203
+                break;
204
+            case STATION:
205
+                checkTask.setCheckLevel(CheckLevelEnum.STATION_LEVEL.getCode());
206
+                break;
207
+            case BRIGADE:
208
+                checkTask.setCheckLevel(CheckLevelEnum.BRIGADE_LEVEL.getCode());
209
+                checkTask.setSelfCheckDeptId(SecurityUtils.getLoginUser().getUser().getDept().getDeptId());
210
+                break;
211
+            case DEPARTMENT:
212
+                checkTask.setCheckLevel(CheckLevelEnum.DEPARTMENT_LEVEL.getCode());
213
+                checkTask.setSelfCheckDeptId(SecurityUtils.getLoginUser().getUser().getDept().getParentId());
214
+                break;
215
+            case TEAM:
216
+                checkTask.setCheckLevel(CheckLevelEnum.TEAM_LEVEL.getCode());
217
+                SysDept teamDept = SecurityUtils.getLoginUser().getUser().getDept();
218
+                SysDept departmentDept = sysDeptService.selectDeptById(teamDept.getParentId());
219
+                checkTask.setSelfCheckDeptId(departmentDept.getParentId());
220
+                break;
221
+            default:
222
+                return success(0);
223
+        }
224
+        List<CheckTask> list = checkTaskService.selectCheckTaskList(checkTask);
225
+        if (CollUtil.isEmpty(list)) {
226
+            return success(0);
227
+        }
228
+        return success(list.stream().filter(item -> !item.isRead()).count());
229
+    }
230
+}

+ 98 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/check/CheckUserController.java

@@ -0,0 +1,98 @@
1
+package com.sundot.airport.web.controller.check;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.security.access.prepost.PreAuthorize;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.PostMapping;
10
+import org.springframework.web.bind.annotation.PutMapping;
11
+import org.springframework.web.bind.annotation.DeleteMapping;
12
+import org.springframework.web.bind.annotation.PathVariable;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.RestController;
16
+import com.sundot.airport.common.annotation.Log;
17
+import com.sundot.airport.common.core.controller.BaseController;
18
+import com.sundot.airport.common.core.domain.AjaxResult;
19
+import com.sundot.airport.common.enums.BusinessType;
20
+import com.sundot.airport.check.domain.CheckUser;
21
+import com.sundot.airport.check.service.ICheckUserService;
22
+import com.sundot.airport.common.utils.poi.ExcelUtil;
23
+import com.sundot.airport.common.core.page.TableDataInfo;
24
+
25
+/**
26
+ * 检查单检查人员和检查记录责任人和检查记录及问题整改检查项责任人Controller
27
+ *
28
+ * @author ruoyi
29
+ * @date 2025-07-11
30
+ */
31
+@RestController
32
+@RequestMapping("/check/checkUser")
33
+public class CheckUserController extends BaseController {
34
+    @Autowired
35
+    private ICheckUserService checkUserService;
36
+
37
+    /**
38
+     * 查询检查单检查人员和检查记录责任人和检查记录及问题整改检查项责任人列表
39
+     */
40
+    @PreAuthorize("@ss.hasPermi('check:checkUser:list')")
41
+    @GetMapping("/list")
42
+    public TableDataInfo list(CheckUser checkUser) {
43
+        startPage();
44
+        List<CheckUser> list = checkUserService.selectCheckUserList(checkUser);
45
+        return getDataTable(list);
46
+    }
47
+
48
+    /**
49
+     * 导出检查单检查人员和检查记录责任人和检查记录及问题整改检查项责任人列表
50
+     */
51
+    @PreAuthorize("@ss.hasPermi('check:checkUser:export')")
52
+    @Log(title = "检查单检查人员和检查记录责任人和检查记录及问题整改检查项责任人", businessType = BusinessType.EXPORT)
53
+    @PostMapping("/export")
54
+    public void export(HttpServletResponse response, CheckUser checkUser) {
55
+        List<CheckUser> list = checkUserService.selectCheckUserList(checkUser);
56
+        ExcelUtil<CheckUser> util = new ExcelUtil<CheckUser>(CheckUser.class);
57
+        util.exportExcel(response, list, "检查单检查人员和检查记录责任人和检查记录及问题整改检查项责任人数据");
58
+    }
59
+
60
+    /**
61
+     * 获取检查单检查人员和检查记录责任人和检查记录及问题整改检查项责任人详细信息
62
+     */
63
+    @PreAuthorize("@ss.hasPermi('check:checkUser:query')")
64
+    @GetMapping(value = "/{id}")
65
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
66
+        return success(checkUserService.selectCheckUserById(id));
67
+    }
68
+
69
+    /**
70
+     * 新增检查单检查人员和检查记录责任人和检查记录及问题整改检查项责任人
71
+     */
72
+    @PreAuthorize("@ss.hasPermi('check:checkUser:add')")
73
+    @Log(title = "检查单检查人员和检查记录责任人和检查记录及问题整改检查项责任人", businessType = BusinessType.INSERT)
74
+    @PostMapping
75
+    public AjaxResult add(@RequestBody CheckUser checkUser) {
76
+        return toAjax(checkUserService.insertCheckUser(checkUser));
77
+    }
78
+
79
+    /**
80
+     * 修改检查单检查人员和检查记录责任人和检查记录及问题整改检查项责任人
81
+     */
82
+    @PreAuthorize("@ss.hasPermi('check:checkUser:edit')")
83
+    @Log(title = "检查单检查人员和检查记录责任人和检查记录及问题整改检查项责任人", businessType = BusinessType.UPDATE)
84
+    @PutMapping
85
+    public AjaxResult edit(@RequestBody CheckUser checkUser) {
86
+        return toAjax(checkUserService.updateCheckUser(checkUser));
87
+    }
88
+
89
+    /**
90
+     * 删除检查单检查人员和检查记录责任人和检查记录及问题整改检查项责任人
91
+     */
92
+    @PreAuthorize("@ss.hasPermi('check:checkUser:remove')")
93
+    @Log(title = "检查单检查人员和检查记录责任人和检查记录及问题整改检查项责任人", businessType = BusinessType.DELETE)
94
+    @DeleteMapping("/{ids}")
95
+    public AjaxResult remove(@PathVariable Long[] ids) {
96
+        return toAjax(checkUserService.deleteCheckUserByIds(ids));
97
+    }
98
+}

+ 94 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/common/CaptchaController.java

@@ -0,0 +1,94 @@
1
+package com.sundot.airport.web.controller.common;
2
+
3
+import java.awt.image.BufferedImage;
4
+import java.io.IOException;
5
+import java.util.concurrent.TimeUnit;
6
+import javax.annotation.Resource;
7
+import javax.imageio.ImageIO;
8
+import javax.servlet.http.HttpServletResponse;
9
+import org.springframework.beans.factory.annotation.Autowired;
10
+import org.springframework.util.FastByteArrayOutputStream;
11
+import org.springframework.web.bind.annotation.GetMapping;
12
+import org.springframework.web.bind.annotation.RestController;
13
+import com.google.code.kaptcha.Producer;
14
+import com.sundot.airport.common.config.RuoYiConfig;
15
+import com.sundot.airport.common.constant.CacheConstants;
16
+import com.sundot.airport.common.constant.Constants;
17
+import com.sundot.airport.common.core.domain.AjaxResult;
18
+import com.sundot.airport.common.core.redis.RedisCache;
19
+import com.sundot.airport.common.utils.sign.Base64;
20
+import com.sundot.airport.common.utils.uuid.IdUtils;
21
+import com.sundot.airport.system.service.ISysConfigService;
22
+
23
+/**
24
+ * 验证码操作处理
25
+ * 
26
+ * @author ruoyi
27
+ */
28
+@RestController
29
+public class CaptchaController
30
+{
31
+    @Resource(name = "captchaProducer")
32
+    private Producer captchaProducer;
33
+
34
+    @Resource(name = "captchaProducerMath")
35
+    private Producer captchaProducerMath;
36
+
37
+    @Autowired
38
+    private RedisCache redisCache;
39
+    
40
+    @Autowired
41
+    private ISysConfigService configService;
42
+    /**
43
+     * 生成验证码
44
+     */
45
+    @GetMapping("/captchaImage")
46
+    public AjaxResult getCode(HttpServletResponse response) throws IOException
47
+    {
48
+        AjaxResult ajax = AjaxResult.success();
49
+        boolean captchaEnabled = configService.selectCaptchaEnabled();
50
+        ajax.put("captchaEnabled", captchaEnabled);
51
+        if (!captchaEnabled)
52
+        {
53
+            return ajax;
54
+        }
55
+
56
+        // 保存验证码信息
57
+        String uuid = IdUtils.simpleUUID();
58
+        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
59
+
60
+        String capStr = null, code = null;
61
+        BufferedImage image = null;
62
+
63
+        // 生成验证码
64
+        String captchaType = RuoYiConfig.getCaptchaType();
65
+        if ("math".equals(captchaType))
66
+        {
67
+            String capText = captchaProducerMath.createText();
68
+            capStr = capText.substring(0, capText.lastIndexOf("@"));
69
+            code = capText.substring(capText.lastIndexOf("@") + 1);
70
+            image = captchaProducerMath.createImage(capStr);
71
+        }
72
+        else if ("char".equals(captchaType))
73
+        {
74
+            capStr = code = captchaProducer.createText();
75
+            image = captchaProducer.createImage(capStr);
76
+        }
77
+
78
+        redisCache.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
79
+        // 转换流信息写出
80
+        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
81
+        try
82
+        {
83
+            ImageIO.write(image, "jpg", os);
84
+        }
85
+        catch (IOException e)
86
+        {
87
+            return AjaxResult.error(e.getMessage());
88
+        }
89
+
90
+        ajax.put("uuid", uuid);
91
+        ajax.put("img", Base64.encode(os.toByteArray()));
92
+        return ajax;
93
+    }
94
+}

+ 162 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/common/CommonController.java

@@ -0,0 +1,162 @@
1
+package com.sundot.airport.web.controller.common;
2
+
3
+import java.util.ArrayList;
4
+import java.util.List;
5
+import javax.servlet.http.HttpServletRequest;
6
+import javax.servlet.http.HttpServletResponse;
7
+import org.slf4j.Logger;
8
+import org.slf4j.LoggerFactory;
9
+import org.springframework.beans.factory.annotation.Autowired;
10
+import org.springframework.http.MediaType;
11
+import org.springframework.web.bind.annotation.GetMapping;
12
+import org.springframework.web.bind.annotation.PostMapping;
13
+import org.springframework.web.bind.annotation.RequestMapping;
14
+import org.springframework.web.bind.annotation.RestController;
15
+import org.springframework.web.multipart.MultipartFile;
16
+import com.sundot.airport.common.config.RuoYiConfig;
17
+import com.sundot.airport.common.core.domain.AjaxResult;
18
+import com.sundot.airport.common.utils.StringUtils;
19
+import com.sundot.airport.common.utils.file.FileUploadUtils;
20
+import com.sundot.airport.common.utils.file.FileUtils;
21
+import com.sundot.airport.framework.config.ServerConfig;
22
+
23
+/**
24
+ * 通用请求处理
25
+ * 
26
+ * @author ruoyi
27
+ */
28
+@RestController
29
+@RequestMapping("/common")
30
+public class CommonController
31
+{
32
+    private static final Logger log = LoggerFactory.getLogger(CommonController.class);
33
+
34
+    @Autowired
35
+    private ServerConfig serverConfig;
36
+
37
+    private static final String FILE_DELIMETER = ",";
38
+
39
+    /**
40
+     * 通用下载请求
41
+     * 
42
+     * @param fileName 文件名称
43
+     * @param delete 是否删除
44
+     */
45
+    @GetMapping("/download")
46
+    public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
47
+    {
48
+        try
49
+        {
50
+            if (!FileUtils.checkAllowDownload(fileName))
51
+            {
52
+                throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
53
+            }
54
+            String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
55
+            String filePath = RuoYiConfig.getDownloadPath() + fileName;
56
+
57
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
58
+            FileUtils.setAttachmentResponseHeader(response, realFileName);
59
+            FileUtils.writeBytes(filePath, response.getOutputStream());
60
+            if (delete)
61
+            {
62
+                FileUtils.deleteFile(filePath);
63
+            }
64
+        }
65
+        catch (Exception e)
66
+        {
67
+            log.error("下载文件失败", e);
68
+        }
69
+    }
70
+
71
+    /**
72
+     * 通用上传请求(单个)
73
+     */
74
+    @PostMapping("/upload")
75
+    public AjaxResult uploadFile(MultipartFile file) throws Exception
76
+    {
77
+        try
78
+        {
79
+            // 上传文件路径
80
+            String filePath = RuoYiConfig.getUploadPath();
81
+            // 上传并返回新文件名称
82
+            String fileName = FileUploadUtils.upload(filePath, file);
83
+            String url = serverConfig.getUrl() + fileName;
84
+            AjaxResult ajax = AjaxResult.success();
85
+            ajax.put("url", url);
86
+            ajax.put("fileName", fileName);
87
+            ajax.put("newFileName", FileUtils.getName(fileName));
88
+            ajax.put("originalFilename", file.getOriginalFilename());
89
+            return ajax;
90
+        }
91
+        catch (Exception e)
92
+        {
93
+            return AjaxResult.error(e.getMessage());
94
+        }
95
+    }
96
+
97
+    /**
98
+     * 通用上传请求(多个)
99
+     */
100
+    @PostMapping("/uploads")
101
+    public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception
102
+    {
103
+        try
104
+        {
105
+            // 上传文件路径
106
+            String filePath = RuoYiConfig.getUploadPath();
107
+            List<String> urls = new ArrayList<String>();
108
+            List<String> fileNames = new ArrayList<String>();
109
+            List<String> newFileNames = new ArrayList<String>();
110
+            List<String> originalFilenames = new ArrayList<String>();
111
+            for (MultipartFile file : files)
112
+            {
113
+                // 上传并返回新文件名称
114
+                String fileName = FileUploadUtils.upload(filePath, file);
115
+                String url = serverConfig.getUrl() + fileName;
116
+                urls.add(url);
117
+                fileNames.add(fileName);
118
+                newFileNames.add(FileUtils.getName(fileName));
119
+                originalFilenames.add(file.getOriginalFilename());
120
+            }
121
+            AjaxResult ajax = AjaxResult.success();
122
+            ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
123
+            ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
124
+            ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
125
+            ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
126
+            return ajax;
127
+        }
128
+        catch (Exception e)
129
+        {
130
+            return AjaxResult.error(e.getMessage());
131
+        }
132
+    }
133
+
134
+    /**
135
+     * 本地资源通用下载
136
+     */
137
+    @GetMapping("/download/resource")
138
+    public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
139
+            throws Exception
140
+    {
141
+        try
142
+        {
143
+            if (!FileUtils.checkAllowDownload(resource))
144
+            {
145
+                throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
146
+            }
147
+            // 本地资源路径
148
+            String localPath = RuoYiConfig.getProfile();
149
+            // 数据库资源地址
150
+            String downloadPath = localPath + FileUtils.stripPrefix(resource);
151
+            // 下载名称
152
+            String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
153
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
154
+            FileUtils.setAttachmentResponseHeader(response, downloadName);
155
+            FileUtils.writeBytes(downloadPath, response.getOutputStream());
156
+        }
157
+        catch (Exception e)
158
+        {
159
+            log.error("下载文件失败", e);
160
+        }
161
+    }
162
+}

+ 52 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/common/DataConfigController.java

@@ -0,0 +1,52 @@
1
+package com.sundot.airport.web.controller.common;
2
+
3
+import com.sundot.airport.common.config.RuoYiConfig;
4
+import com.sundot.airport.common.core.domain.AjaxResult;
5
+import com.sundot.airport.common.core.domain.TreeQuery;
6
+import com.sundot.airport.common.core.domain.entity.SysDept;
7
+import com.sundot.airport.common.utils.StringUtils;
8
+import com.sundot.airport.common.utils.file.FileUploadUtils;
9
+import com.sundot.airport.common.utils.file.FileUtils;
10
+import com.sundot.airport.framework.config.ServerConfig;
11
+import com.sundot.airport.system.service.ISysTreeService;
12
+import org.slf4j.Logger;
13
+import org.slf4j.LoggerFactory;
14
+import org.springframework.beans.factory.annotation.Autowired;
15
+import org.springframework.http.MediaType;
16
+import org.springframework.security.access.prepost.PreAuthorize;
17
+import org.springframework.web.bind.annotation.GetMapping;
18
+import org.springframework.web.bind.annotation.PostMapping;
19
+import org.springframework.web.bind.annotation.RequestMapping;
20
+import org.springframework.web.bind.annotation.RestController;
21
+import org.springframework.web.multipart.MultipartFile;
22
+
23
+import javax.servlet.http.HttpServletRequest;
24
+import javax.servlet.http.HttpServletResponse;
25
+import java.util.ArrayList;
26
+import java.util.List;
27
+
28
+import static com.sundot.airport.common.core.domain.AjaxResult.success;
29
+
30
+/**
31
+ * 通用数据配置树结构请求处理
32
+ * 
33
+ * @author ruoyi
34
+ */
35
+@RestController
36
+@RequestMapping("/dataConfig")
37
+public class DataConfigController
38
+{
39
+    private static final Logger log = LoggerFactory.getLogger(DataConfigController.class);
40
+
41
+    @Autowired
42
+    private ISysTreeService sysTreeService ;
43
+
44
+    /**
45
+     * 获取部门树列表
46
+     */
47
+    @GetMapping("/dataConfigTree")
48
+    public AjaxResult dataConfigTree(TreeQuery treeQuery)
49
+    {
50
+        return success(sysTreeService.selectTreeList(treeQuery));
51
+    }
52
+}

+ 143 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/daily/DailyTaskConfigController.java

@@ -0,0 +1,143 @@
1
+package com.sundot.airport.web.controller.daily;
2
+
3
+import com.sundot.airport.common.annotation.Log;
4
+import com.sundot.airport.common.core.controller.BaseController;
5
+import com.sundot.airport.common.core.domain.AjaxResult;
6
+import com.sundot.airport.common.core.page.TableDataInfo;
7
+import com.sundot.airport.common.enums.BusinessType;
8
+import com.sundot.airport.exam.domain.DailyTaskConfig;
9
+import com.sundot.airport.exam.dto.DailyTaskConfigDTO;
10
+import com.sundot.airport.exam.dto.DailyTaskConfigQueryDTO;
11
+import com.sundot.airport.exam.service.IDailyTaskConfigService;
12
+import org.springframework.beans.factory.annotation.Autowired;
13
+import org.springframework.security.access.prepost.PreAuthorize;
14
+import org.springframework.validation.annotation.Validated;
15
+import org.springframework.web.bind.annotation.*;
16
+
17
+/**
18
+ * <b>功能名:</b>DailyTaskConfigController<br>
19
+ * <b>说明:</b> 每日答题任务配置Controller <br>
20
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
21
+ * <b>修改履历:
22
+ *
23
+ * @author Simon Lin
24
+ */
25
+@RestController
26
+@RequestMapping("/exam/daily/config")
27
+public class DailyTaskConfigController extends BaseController {
28
+
29
+    @Autowired
30
+    private IDailyTaskConfigService dailyTaskConfigService;
31
+
32
+    /**
33
+     * 查询每日答题任务配置列表
34
+     */
35
+    @PreAuthorize("@ss.hasPermi('exam:daily:config:list')")
36
+    @PostMapping("/list")
37
+    public TableDataInfo list(@RequestBody DailyTaskConfigQueryDTO queryDTO) {
38
+        return dailyTaskConfigService.selectConfigList(queryDTO);
39
+    }
40
+
41
+    /**
42
+     * 获取每日答题任务配置详细信息
43
+     */
44
+    @PreAuthorize("@ss.hasPermi('exam:daily:config:query')")
45
+    @GetMapping("/{dtcId}")
46
+    public AjaxResult getInfo(@PathVariable("dtcId") String dtcId) {
47
+        return success(dailyTaskConfigService.selectConfigById(dtcId));
48
+    }
49
+
50
+    /**
51
+     * 新增每日答题任务配置
52
+     */
53
+    @PreAuthorize("@ss.hasPermi('exam:daily:config:add')")
54
+    @Log(title = "每日答题任务配置", businessType = BusinessType.INSERT)
55
+    @PostMapping
56
+    public AjaxResult add(@Validated @RequestBody DailyTaskConfigDTO dto) {
57
+        return toAjax(dailyTaskConfigService.insertConfig(dto));
58
+    }
59
+
60
+    /**
61
+     * 修改每日答题任务配置
62
+     */
63
+    @PreAuthorize("@ss.hasPermi('exam:daily:config:edit')")
64
+    @Log(title = "每日答题任务配置", businessType = BusinessType.UPDATE)
65
+    @PutMapping
66
+    public AjaxResult edit(@Validated @RequestBody DailyTaskConfigDTO dto) {
67
+        return toAjax(dailyTaskConfigService.updateConfig(dto));
68
+    }
69
+
70
+    /**
71
+     * 删除每日答题任务配置
72
+     */
73
+    @PreAuthorize("@ss.hasPermi('exam:daily:config:remove')")
74
+    @Log(title = "每日答题任务配置", businessType = BusinessType.DELETE)
75
+    @DeleteMapping("/{dtcIds}")
76
+    public AjaxResult remove(@PathVariable String[] dtcIds) {
77
+        return toAjax(dailyTaskConfigService.deleteConfigByIds(dtcIds));
78
+    }
79
+
80
+    /**
81
+     * 启用/停用每日答题任务配置
82
+     */
83
+    @PreAuthorize("@ss.hasPermi('exam:daily:config:edit')")
84
+    @Log(title = "每日答题任务配置", businessType = BusinessType.UPDATE)
85
+    @PutMapping("/changeStatus")
86
+    public AjaxResult changeStatus(@RequestParam("dtcId") String dtcId,
87
+                                    @RequestParam("enableStatus") Boolean enableStatus) {
88
+        return toAjax(dailyTaskConfigService.updateConfigStatus(dtcId, enableStatus));
89
+    }
90
+
91
+    /**
92
+     * 根据当前用户获取有效配置
93
+     * 遍历用户的所有角色,收集所有匹配的有效配置,并选择最新的配置
94
+     */
95
+    @GetMapping("/effective")
96
+    public AjaxResult getEffectiveConfig() {
97
+        // 获取当前用户的角色ID列表和部门
98
+        Long deptId = getDeptId();
99
+
100
+        // 获取用户的所有角色ID
101
+        java.util.List<Long> roleIds = getLoginUser().getUser().getRoles().stream()
102
+                .map(role -> role.getRoleId())
103
+                .collect(java.util.stream.Collectors.toList());
104
+
105
+        if (roleIds.isEmpty()) {
106
+            return AjaxResult.error("用户未分配角色");
107
+        }
108
+
109
+        // 收集所有角色匹配的有效配置
110
+        java.util.List<DailyTaskConfig> matchedConfigs = new java.util.ArrayList<>();
111
+        for (Long roleId : roleIds) {
112
+            DailyTaskConfig config = dailyTaskConfigService.getEffectiveConfig(roleId, deptId);
113
+            if (config != null) {
114
+                matchedConfigs.add(config);
115
+            }
116
+        }
117
+
118
+        if (matchedConfigs.isEmpty()) {
119
+            return AjaxResult.error("未找到适用的答题配置");
120
+        }
121
+
122
+        // 选择最新的配置(根据创建时间降序排序,取第一个)
123
+        DailyTaskConfig latestConfig = matchedConfigs.stream()
124
+                .sorted((c1, c2) -> c2.getCreateTime().compareTo(c1.getCreateTime()))
125
+                .findFirst()
126
+                .orElse(null);
127
+
128
+        return success(latestConfig);
129
+    }
130
+
131
+    /**
132
+     * 获取默认配置(第一条配置)
133
+     * 用于单配置管理页面
134
+     */
135
+    @GetMapping("/default")
136
+    public AjaxResult getDefaultConfig() {
137
+        DailyTaskConfig config = dailyTaskConfigService.getDefaultConfig();
138
+        if (config == null) {
139
+            return AjaxResult.error("暂无配置数据");
140
+        }
141
+        return success(config);
142
+    }
143
+}

+ 210 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/daily/DeptProfileController.java

@@ -0,0 +1,210 @@
1
+package com.sundot.airport.web.controller.daily;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.sundot.airport.common.core.controller.BaseController;
5
+import com.sundot.airport.common.core.domain.AjaxResult;
6
+import com.sundot.airport.common.core.domain.entity.SysDept;
7
+import com.sundot.airport.exam.domain.DailyTask;
8
+import com.sundot.airport.exam.dto.DeptProfileDTO;
9
+import com.sundot.airport.exam.dto.DeptProfileQueryDTO;
10
+import com.sundot.airport.exam.mapper.DailyTaskMapper;
11
+import com.sundot.airport.exam.service.IDeptProfileService;
12
+import com.sundot.airport.system.service.ISysDeptService;
13
+import io.swagger.annotations.Api;
14
+import io.swagger.annotations.ApiOperation;
15
+import io.swagger.annotations.ApiParam;
16
+import org.springframework.beans.factory.annotation.Autowired;
17
+import org.springframework.web.bind.annotation.GetMapping;
18
+import org.springframework.web.bind.annotation.RequestMapping;
19
+import org.springframework.web.bind.annotation.RequestParam;
20
+import org.springframework.web.bind.annotation.RestController;
21
+
22
+import java.math.BigDecimal;
23
+import java.math.RoundingMode;
24
+import java.sql.Date;
25
+import java.text.SimpleDateFormat;
26
+import java.time.LocalDate;
27
+import java.util.*;
28
+import java.util.stream.Collectors;
29
+
30
+/**
31
+ * <b>功能名:</b>DeptProfileController<br>
32
+ * <b>说明:</b> 组织画像Controller <br>
33
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
34
+ * <b>修改履历:
35
+ *
36
+ * @author Claude
37
+ */
38
+@Api(tags = "组织画像")
39
+@RestController
40
+@RequestMapping("/exam/daily/dept-profile")
41
+public class DeptProfileController extends BaseController {
42
+
43
+    @Autowired
44
+    private IDeptProfileService deptProfileService;
45
+
46
+    @Autowired
47
+    private DailyTaskMapper dailyTaskMapper;
48
+
49
+    @Autowired
50
+    private ISysDeptService deptService;
51
+
52
+    /**
53
+     * 获取组织画像
54
+     */
55
+    @ApiOperation("获取组织画像")
56
+    @GetMapping
57
+    public AjaxResult getDeptProfile(
58
+            @ApiParam(value = "部门ID", required = true)
59
+            @RequestParam Long deptId,
60
+
61
+            @ApiParam(value = "开始日期", required = false, example = "2025-01-01")
62
+            @RequestParam(required = false) String startDate,
63
+
64
+            @ApiParam(value = "结束日期", required = false, example = "2025-01-31")
65
+            @RequestParam(required = false) String endDate) {
66
+
67
+        // 如果没有传日期范围,则默认为91天
68
+        if (startDate == null || endDate == null) {
69
+            java.time.LocalDate today = java.time.LocalDate.now();
70
+            endDate = today.toString();
71
+            startDate = today.minusDays(91).toString();
72
+        }
73
+
74
+        DeptProfileQueryDTO query = new DeptProfileQueryDTO();
75
+        query.setDeptId(deptId);
76
+        query.setStartDate(startDate);
77
+        query.setEndDate(endDate);
78
+
79
+        DeptProfileDTO profile = deptProfileService.getDeptProfile(query);
80
+
81
+        return success(profile);
82
+    }
83
+
84
+    /**
85
+     * 获取部门每日抽问抽答完成率
86
+     * 统计该部门及其所有下级部门成员的抽问抽答完成率
87
+     * 例如:传入科级ID,统计该科级下所有班组成员的完成率
88
+     */
89
+    @ApiOperation("获取部门每日抽问抽答完成率")
90
+    @GetMapping("/daily-completion-rate")
91
+    public AjaxResult getDailyCompletionRate(
92
+            @ApiParam(value = "部门ID(可以是科级ID、班组ID等)", required = true)
93
+            @RequestParam Long deptId,
94
+
95
+            @ApiParam(value = "开始日期", required = false, example = "2025-01-01")
96
+            @RequestParam(required = false) String startDate,
97
+
98
+            @ApiParam(value = "结束日期", required = false, example = "2025-01-31")
99
+            @RequestParam(required = false) String endDate) {
100
+
101
+        try {
102
+            // 如果没有传日期范围,则默认为91天
103
+            if (startDate == null || endDate == null) {
104
+                LocalDate today = LocalDate.now();
105
+                endDate = today.toString();
106
+                startDate = today.minusDays(91).toString();
107
+            }
108
+
109
+            // 获取该部门及所有下级部门的ID列表(例如:科级 -> 所有班组)
110
+            List<Long> deptIds = getAllSubDeptIds(deptId);
111
+            logger.info("查询部门ID={} 的下级部门,共找到 {} 个部门: {}", deptId, deptIds.size(), deptIds);
112
+
113
+            if (deptIds.isEmpty()) {
114
+                return success(new ArrayList<>());
115
+            }
116
+
117
+            // 转换日期
118
+            LocalDate start = LocalDate.parse(startDate);
119
+            LocalDate end = LocalDate.parse(endDate);
120
+
121
+            // 构建日期范围的Map,存储每一天的完成率
122
+            Map<String, Map<String, Object>> dailyStats = new LinkedHashMap<>();
123
+
124
+            // 初始化每一天的数据
125
+            LocalDate currentDate = start;
126
+            while (!currentDate.isAfter(end)) {
127
+                String dateStr = currentDate.toString();
128
+                Map<String, Object> dayData = new HashMap<>();
129
+                dayData.put("date", dateStr);
130
+                dayData.put("completionRate", BigDecimal.ZERO);
131
+                dayData.put("totalTasks", 0);
132
+                dayData.put("completedTasks", 0);
133
+                dailyStats.put(dateStr, dayData);
134
+                currentDate = currentDate.plusDays(1);
135
+            }
136
+
137
+            // 查询该时间段内所有任务
138
+            LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
139
+            wrapper.in(DailyTask::getDtDeptId, deptIds);
140
+            wrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(startDate));
141
+            wrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(endDate));
142
+            wrapper.eq(DailyTask::getDelStatus, 0);
143
+
144
+            List<DailyTask> tasks = dailyTaskMapper.selectList(wrapper);
145
+            logger.info("查询到 {} 条任务记录", tasks.size());
146
+
147
+            // 按日期分组统计(使用SimpleDateFormat格式化日期)
148
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
149
+            Map<String, List<DailyTask>> tasksByDate = tasks.stream()
150
+                    .collect(Collectors.groupingBy(task -> sdf.format(task.getDtBusinessDate())));
151
+
152
+            // 计算每一天的完成率
153
+            for (Map.Entry<String, List<DailyTask>> entry : tasksByDate.entrySet()) {
154
+                String dateStr = entry.getKey();
155
+                List<DailyTask> dayTasks = entry.getValue();
156
+
157
+                int totalTasks = dayTasks.size();
158
+                long completedTasks = dayTasks.stream()
159
+                        .filter(task -> "COMPLETED".equals(task.getDtStatus()))
160
+                        .count();
161
+
162
+                BigDecimal completionRate = BigDecimal.ZERO;
163
+                if (totalTasks > 0) {
164
+                    completionRate = new BigDecimal(completedTasks)
165
+                            .multiply(new BigDecimal(100))
166
+                            .divide(new BigDecimal(totalTasks), 2, RoundingMode.HALF_UP);
167
+                }
168
+
169
+                if (dailyStats.containsKey(dateStr)) {
170
+                    Map<String, Object> dayData = dailyStats.get(dateStr);
171
+                    dayData.put("completionRate", completionRate);
172
+                    dayData.put("totalTasks", totalTasks);
173
+                    dayData.put("completedTasks", completedTasks);
174
+                }
175
+            }
176
+
177
+            // 转换为数组返回
178
+            List<Map<String, Object>> result = new ArrayList<>(dailyStats.values());
179
+
180
+            return success(result);
181
+        } catch (Exception e) {
182
+            logger.error("获取部门每日完成率失败", e);
183
+            return error("获取部门每日完成率失败:" + e.getMessage());
184
+        }
185
+    }
186
+
187
+    /**
188
+     * 获取指定部门及其所有下级部门的ID列表
189
+     * 根据sys_dept表的层级关系递归查找
190
+     * 例如:传入科级ID,返回该科级ID及其所有下级班组ID
191
+     */
192
+    private List<Long> getAllSubDeptIds(Long deptId) {
193
+        List<Long> result = new ArrayList<>();
194
+        result.add(deptId); // 包含自己
195
+
196
+        // 查询直接下级部门
197
+        SysDept queryDept = new SysDept();
198
+        queryDept.setParentId(deptId);
199
+        List<SysDept> subDepts = deptService.selectDeptInfoAll(queryDept);
200
+
201
+        // 递归查询所有下级部门
202
+        for (SysDept subDept : subDepts) {
203
+            if ("0".equals(subDept.getDelFlag())) { // 只包含未删除的部门
204
+                result.addAll(getAllSubDeptIds(subDept.getDeptId()));
205
+            }
206
+        }
207
+
208
+        return result;
209
+    }
210
+}

+ 208 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/daily/SiteProfileController.java

@@ -0,0 +1,208 @@
1
+package com.sundot.airport.web.controller.daily;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.sundot.airport.common.core.controller.BaseController;
5
+import com.sundot.airport.common.core.domain.AjaxResult;
6
+import com.sundot.airport.common.core.domain.entity.SysDept;
7
+import com.sundot.airport.exam.domain.DailyTask;
8
+import com.sundot.airport.exam.dto.SiteProfileDTO;
9
+import com.sundot.airport.exam.dto.SiteProfileQueryDTO;
10
+import com.sundot.airport.exam.mapper.DailyTaskMapper;
11
+import com.sundot.airport.exam.service.ISiteProfileService;
12
+import com.sundot.airport.system.service.ISysDeptService;
13
+import io.swagger.annotations.Api;
14
+import io.swagger.annotations.ApiOperation;
15
+import io.swagger.annotations.ApiParam;
16
+import org.springframework.beans.factory.annotation.Autowired;
17
+import org.springframework.web.bind.annotation.GetMapping;
18
+import org.springframework.web.bind.annotation.RequestMapping;
19
+import org.springframework.web.bind.annotation.RequestParam;
20
+import org.springframework.web.bind.annotation.RestController;
21
+
22
+import java.math.BigDecimal;
23
+import java.math.RoundingMode;
24
+import java.sql.Date;
25
+import java.text.SimpleDateFormat;
26
+import java.time.LocalDate;
27
+import java.util.*;
28
+import java.util.stream.Collectors;
29
+
30
+/**
31
+ * <b>功能名:</b>SiteProfileController<br>
32
+ * <b>说明:</b> 站级画像Controller <br>
33
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
34
+ * <b>修改履历:
35
+ *
36
+ * @author Claude
37
+ */
38
+@Api(tags = "站级画像")
39
+@RestController
40
+@RequestMapping("/exam/daily/site-profile")
41
+public class SiteProfileController extends BaseController {
42
+
43
+    @Autowired
44
+    private ISiteProfileService siteProfileService;
45
+
46
+    @Autowired
47
+    private DailyTaskMapper dailyTaskMapper;
48
+
49
+    @Autowired
50
+    private ISysDeptService deptService;
51
+
52
+    /**
53
+     * 获取站级画像
54
+     */
55
+    @ApiOperation("获取站级画像")
56
+    @GetMapping
57
+    public AjaxResult getSiteProfile(
58
+            @ApiParam(value = "站级ID", required = true)
59
+            @RequestParam Long siteId,
60
+
61
+            @ApiParam(value = "开始日期", required = false, example = "2025-01-01")
62
+            @RequestParam(required = false) String startDate,
63
+
64
+            @ApiParam(value = "结束日期", required = false, example = "2025-01-31")
65
+            @RequestParam(required = false) String endDate) {
66
+
67
+        // 如果没有传日期范围,则默认为91天
68
+        if (startDate == null || endDate == null) {
69
+            java.time.LocalDate today = java.time.LocalDate.now();
70
+            endDate = today.toString();
71
+            startDate = today.minusDays(91).toString();
72
+        }
73
+
74
+        SiteProfileQueryDTO query = new SiteProfileQueryDTO();
75
+        query.setSiteId(siteId);
76
+        query.setStartDate(startDate);
77
+        query.setEndDate(endDate);
78
+
79
+        SiteProfileDTO profile = siteProfileService.getSiteProfile(query);
80
+
81
+        return success(profile);
82
+    }
83
+
84
+    /**
85
+     * 获取站级每日抽问抽答完成率
86
+     * 统计该站级及其所有下级部门(科级、班组等)成员的抽问抽答完成率
87
+     * 例如:传入站级ID,统计该站级下所有科级、班组成员的完成率
88
+     */
89
+    @ApiOperation("获取站级每日抽问抽答完成率")
90
+    @GetMapping("/daily-completion-rate")
91
+    public AjaxResult getDailyCompletionRate(
92
+            @ApiParam(value = "站级ID", required = true)
93
+            @RequestParam Long siteId,
94
+
95
+            @ApiParam(value = "开始日期", required = false, example = "2025-01-01")
96
+            @RequestParam(required = false) String startDate,
97
+
98
+            @ApiParam(value = "结束日期", required = false, example = "2025-01-31")
99
+            @RequestParam(required = false) String endDate) {
100
+
101
+        try {
102
+            // 如果没有传日期范围,则默认为91天
103
+            if (startDate == null || endDate == null) {
104
+                LocalDate today = LocalDate.now();
105
+                endDate = today.toString();
106
+                startDate = today.minusDays(91).toString();
107
+            }
108
+
109
+            // 获取该站级及所有下级部门的ID列表(站级 -> 科级 -> 班组)
110
+            List<Long> deptIds = getAllSubDeptIds(siteId);
111
+
112
+            if (deptIds.isEmpty()) {
113
+                return success(new ArrayList<>());
114
+            }
115
+
116
+            // 转换日期
117
+            LocalDate start = LocalDate.parse(startDate);
118
+            LocalDate end = LocalDate.parse(endDate);
119
+
120
+            // 构建日期范围的Map,存储每一天的完成率
121
+            Map<String, Map<String, Object>> dailyStats = new LinkedHashMap<>();
122
+
123
+            // 初始化每一天的数据
124
+            LocalDate currentDate = start;
125
+            while (!currentDate.isAfter(end)) {
126
+                String dateStr = currentDate.toString();
127
+                Map<String, Object> dayData = new HashMap<>();
128
+                dayData.put("date", dateStr);
129
+                dayData.put("completionRate", BigDecimal.ZERO);
130
+                dayData.put("totalTasks", 0);
131
+                dayData.put("completedTasks", 0);
132
+                dailyStats.put(dateStr, dayData);
133
+                currentDate = currentDate.plusDays(1);
134
+            }
135
+
136
+            // 查询该时间段内所有任务
137
+            LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
138
+            wrapper.in(DailyTask::getDtDeptId, deptIds);
139
+            wrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(startDate));
140
+            wrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(endDate));
141
+            wrapper.eq(DailyTask::getDelStatus, 0);
142
+
143
+            List<DailyTask> tasks = dailyTaskMapper.selectList(wrapper);
144
+
145
+            // 按日期分组统计(使用SimpleDateFormat格式化日期)
146
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
147
+            Map<String, List<DailyTask>> tasksByDate = tasks.stream()
148
+                    .collect(Collectors.groupingBy(task -> sdf.format(task.getDtBusinessDate())));
149
+
150
+            // 计算每一天的完成率
151
+            for (Map.Entry<String, List<DailyTask>> entry : tasksByDate.entrySet()) {
152
+                String dateStr = entry.getKey();
153
+                List<DailyTask> dayTasks = entry.getValue();
154
+
155
+                int totalTasks = dayTasks.size();
156
+                long completedTasks = dayTasks.stream()
157
+                        .filter(task -> "COMPLETED".equals(task.getDtStatus()))
158
+                        .count();
159
+
160
+                BigDecimal completionRate = BigDecimal.ZERO;
161
+                if (totalTasks > 0) {
162
+                    completionRate = new BigDecimal(completedTasks)
163
+                            .multiply(new BigDecimal(100))
164
+                            .divide(new BigDecimal(totalTasks), 2, RoundingMode.HALF_UP);
165
+                }
166
+
167
+                if (dailyStats.containsKey(dateStr)) {
168
+                    Map<String, Object> dayData = dailyStats.get(dateStr);
169
+                    dayData.put("completionRate", completionRate);
170
+                    dayData.put("totalTasks", totalTasks);
171
+                    dayData.put("completedTasks", completedTasks);
172
+                }
173
+            }
174
+
175
+            // 转换为数组返回
176
+            List<Map<String, Object>> result = new ArrayList<>(dailyStats.values());
177
+
178
+            return success(result);
179
+        } catch (Exception e) {
180
+            logger.error("获取站级每日完成率失败", e);
181
+            return error("获取站级每日完成率失败:" + e.getMessage());
182
+        }
183
+    }
184
+
185
+    /**
186
+     * 获取指定站级及其所有下级部门的ID列表
187
+     * 根据sys_dept表的层级关系递归查找
188
+     * 例如:传入站级ID,返回该站级ID及其所有下级科级、班组ID
189
+     */
190
+    private List<Long> getAllSubDeptIds(Long siteId) {
191
+        List<Long> result = new ArrayList<>();
192
+        result.add(siteId); // 包含自己
193
+
194
+        // 查询直接下级部门
195
+        SysDept queryDept = new SysDept();
196
+        queryDept.setParentId(siteId);
197
+        List<SysDept> subDepts = deptService.selectDeptInfoAll(queryDept);
198
+
199
+        // 递归查询所有下级部门
200
+        for (SysDept subDept : subDepts) {
201
+            if ("0".equals(subDept.getDelFlag())) { // 只包含未删除的部门
202
+                result.addAll(getAllSubDeptIds(subDept.getDeptId()));
203
+            }
204
+        }
205
+
206
+        return result;
207
+    }
208
+}

+ 20 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/BaseController.java

@@ -0,0 +1,20 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import org.slf4j.Logger;
4
+import org.slf4j.LoggerFactory;
5
+
6
+/**
7
+ * <b>功能名:</b>BaseController<br>
8
+ * <b>说明:</b> 基本controller管理抽象类 <br>
9
+ * <b>著作权:</b> Copyright (C) 2021 HUIFANEDU  CORPORATION<br>
10
+ * <b>修改履历:
11
+ *
12
+ * @author 2021-06-17 liangxiao
13
+ */
14
+public abstract class BaseController {
15
+
16
+    /**
17
+     * 日志记录
18
+     */
19
+    protected final Logger log = LoggerFactory.getLogger(this.getClass());
20
+}

+ 395 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/DailyExamComparisonController.java

@@ -0,0 +1,395 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.sundot.airport.common.core.domain.entity.SysDept;
5
+import com.sundot.airport.common.core.domain.entity.SysRole;
6
+import com.sundot.airport.common.core.domain.model.LoginUser;
7
+import com.sundot.airport.common.enums.RoleTypeEnum;
8
+import com.sundot.airport.common.utils.SecurityUtils;
9
+import com.sundot.airport.exam.common.HttpResult;
10
+import com.sundot.airport.exam.domain.DailyTask;
11
+import com.sundot.airport.exam.mapper.DailyTaskMapper;
12
+import com.sundot.airport.system.mapper.SysDeptMapper;
13
+import org.slf4j.Logger;
14
+import org.slf4j.LoggerFactory;
15
+import org.springframework.beans.factory.annotation.Autowired;
16
+import org.springframework.web.bind.annotation.GetMapping;
17
+import org.springframework.web.bind.annotation.RequestMapping;
18
+import org.springframework.web.bind.annotation.RequestParam;
19
+import org.springframework.web.bind.annotation.RestController;
20
+
21
+import java.math.BigDecimal;
22
+import java.math.RoundingMode;
23
+import java.sql.Date;
24
+import java.time.LocalDate;
25
+import java.util.*;
26
+import java.util.stream.Collectors;
27
+
28
+/**
29
+ * 抽问抽答完成趋势对比接口(PC管理端,美兰4级架构)
30
+ * STATION(安检站)> BRIGADE(大队)> MANAGER(主管)> 班组
31
+ *
32
+ * xAxis 为周期标签,每条 series 的 data 为各周期聚合完成数:
33
+ *   YEAR    → [去年总数, 当年总数]               xAxis: ["2025年","2026年"]
34
+ *   QUARTER → [去年同季度, 上季度, 当季度]        xAxis: ["2025年第1季度","2025年第4季度","2026年第1季度"]
35
+ *   MONTH   → [去年同月, 上月, 当月]              xAxis: ["2025年3月","2026年2月","2026年3月"]
36
+ *
37
+ * 同比 yoyRate   = (当期 - 去年同期) / 去年同期 × 100
38
+ * 环比 chainRatio = (当期 - 上期)   / 上期     × 100(仅 QUARTER/MONTH)
39
+ */
40
+@RestController
41
+@RequestMapping("/v1/cs/app/daily-exam")
42
+public class DailyExamComparisonController {
43
+
44
+    private static final Logger log = LoggerFactory.getLogger(DailyExamComparisonController.class);
45
+
46
+    private static final String[] QUARTER_NAMES = {"第一", "第二", "第三", "第四"};
47
+
48
+    @Autowired
49
+    private DailyTaskMapper dailyTaskMapper;
50
+
51
+    @Autowired
52
+    private SysDeptMapper sysDeptMapper;
53
+
54
+    /**
55
+     * 抽问抽答完成趋势对比
56
+     *
57
+     * @param dateRangeQueryType 时间类型:YEAR/QUARTER/MONTH,默认 MONTH
58
+     * @param year               年份,默认当前年
59
+     * @param quarter            季度 1-4,dateRangeQueryType=QUARTER 时有效
60
+     * @param month              月份 1-12,dateRangeQueryType=MONTH 时有效
61
+     * @param scopeType          统计范围:STATION/BRIGADE/MANAGER/TEAM/USER,不传则按登录角色自动判断
62
+     * @param scopeId            统计范围ID(deptId 或 userId)
63
+     */
64
+    @GetMapping("/completion-comparison")
65
+    public HttpResult<Map<String, Object>> getCompletionComparison(
66
+            @RequestParam(required = false, defaultValue = "MONTH") String dateRangeQueryType,
67
+            @RequestParam(required = false) Integer year,
68
+            @RequestParam(required = false) Integer quarter,
69
+            @RequestParam(required = false) Integer month,
70
+            @RequestParam(required = false) String scopeType,
71
+            @RequestParam(required = false) Long scopeId) {
72
+
73
+        try {
74
+            LoginUser loginUser = SecurityUtils.getLoginUser();
75
+            List<String> roleKeys = loginUser.getUser().getRoles().stream()
76
+                    .map(SysRole::getRoleKey).collect(Collectors.toList());
77
+            Long userDeptId = loginUser.getDeptId();
78
+            Long userId = loginUser.getUserId();
79
+
80
+            boolean isStation = userId == 1L
81
+                    || roleKeys.contains(RoleTypeEnum.admin.getCode())
82
+                    || roleKeys.contains(RoleTypeEnum.test.getCode())
83
+                    || roleKeys.contains(RoleTypeEnum.zhijianke.getCode());
84
+            boolean isBrigade = !isStation && (roleKeys.contains(RoleTypeEnum.jingli.getCode())
85
+                    || roleKeys.contains(RoleTypeEnum.xingzheng.getCode()));
86
+            boolean isManager = !isStation && !isBrigade && roleKeys.contains(RoleTypeEnum.kezhang.getCode());
87
+
88
+            if (scopeType == null && !isStation && !isBrigade && !isManager) {
89
+                Map<String, Object> noShow = new HashMap<>();
90
+                noShow.put("show", false);
91
+                return HttpResult.success(noShow);
92
+            }
93
+
94
+            LocalDate today = LocalDate.now();
95
+            int curYear = (year != null) ? year : today.getYear();
96
+            int curQuarter = (quarter != null) ? quarter : ((today.getMonthValue() - 1) / 3 + 1);
97
+            int curMonth = (month != null) ? month : today.getMonthValue();
98
+
99
+            List<String> xAxis = buildXAxisLabels(dateRangeQueryType, curYear, curQuarter, curMonth);
100
+            List<LocalDate[]> periods = buildPeriods(dateRangeQueryType, curYear, curQuarter, curMonth);
101
+            boolean hasChain = !"YEAR".equals(dateRangeQueryType);
102
+
103
+            List<Map<String, Object>> series = buildSeries(
104
+                    scopeType, scopeId, xAxis, periods, hasChain,
105
+                    isStation, isBrigade, isManager, userDeptId, loginUser);
106
+
107
+            Map<String, Object> result = new LinkedHashMap<>();
108
+            result.put("show", true);
109
+            result.put("xAxis", xAxis);
110
+            result.put("series", series);
111
+            return HttpResult.success(result);
112
+
113
+        } catch (Exception e) {
114
+            log.error("获取抽问抽答完成趋势对比失败", e);
115
+            return HttpResult.error("获取完成趋势对比失败:" + e.getMessage());
116
+        }
117
+    }
118
+
119
+    // -------------------------------------------------------------------------
120
+    // xAxis 标签
121
+    // -------------------------------------------------------------------------
122
+
123
+    /**
124
+     * YEAR    → ["2025年", "2026年"]
125
+     * QUARTER → ["2025年第1季度", "2025年第4季度", "2026年第1季度"]
126
+     * MONTH   → ["2025年3月", "2026年2月", "2026年3月"]
127
+     */
128
+    private List<String> buildXAxisLabels(String type, int year, int quarter, int month) {
129
+        List<String> labels = new ArrayList<>();
130
+        switch (type == null ? "MONTH" : type) {
131
+            case "YEAR":
132
+                labels.add((year - 1) + "年");
133
+                labels.add(year + "年");
134
+                break;
135
+            case "QUARTER": {
136
+                labels.add((year - 1) + "年" + QUARTER_NAMES[quarter - 1] + "季度");
137
+                int prevQ = quarter - 1, prevYear = year;
138
+                if (prevQ < 1) { prevQ = 4; prevYear = year - 1; }
139
+                labels.add(prevYear + "年" + QUARTER_NAMES[prevQ - 1] + "季度");
140
+                labels.add(year + "年" + QUARTER_NAMES[quarter - 1] + "季度");
141
+                break;
142
+            }
143
+            default: {
144
+                labels.add((year - 1) + "年" + month + "月");
145
+                int prevM = month - 1, prevYear = year;
146
+                if (prevM < 1) { prevM = 12; prevYear = year - 1; }
147
+                labels.add(prevYear + "年" + prevM + "月");
148
+                labels.add(year + "年" + month + "月");
149
+                break;
150
+            }
151
+        }
152
+        return labels;
153
+    }
154
+
155
+    // -------------------------------------------------------------------------
156
+    // 周期日期范围
157
+    // -------------------------------------------------------------------------
158
+
159
+    /**
160
+     * 返回各周期日期范围(顺序与 xAxis 对应)
161
+     * YEAR    → [去年全年, 今年全年]           (size=2)
162
+     * QUARTER → [去年同季度, 上季度, 当季度]    (size=3)
163
+     * MONTH   → [去年同月, 上月, 当月]          (size=3)
164
+     */
165
+    private List<LocalDate[]> buildPeriods(String type, int year, int quarter, int month) {
166
+        List<LocalDate[]> periods = new ArrayList<>();
167
+        switch (type == null ? "MONTH" : type) {
168
+            case "YEAR":
169
+                periods.add(yearRange(year - 1));
170
+                periods.add(yearRange(year));
171
+                break;
172
+            case "QUARTER": {
173
+                periods.add(quarterRange(year - 1, quarter));
174
+                int prevQ = quarter - 1, prevYear = year;
175
+                if (prevQ < 1) { prevQ = 4; prevYear = year - 1; }
176
+                periods.add(quarterRange(prevYear, prevQ));
177
+                periods.add(quarterRange(year, quarter));
178
+                break;
179
+            }
180
+            default: {
181
+                periods.add(monthRange(year - 1, month));
182
+                int prevM = month - 1, prevYear = year;
183
+                if (prevM < 1) { prevM = 12; prevYear = year - 1; }
184
+                periods.add(monthRange(prevYear, prevM));
185
+                periods.add(monthRange(year, month));
186
+                break;
187
+            }
188
+        }
189
+        return periods;
190
+    }
191
+
192
+    private LocalDate[] yearRange(int y) {
193
+        return new LocalDate[]{LocalDate.of(y, 1, 1), LocalDate.of(y, 12, 31)};
194
+    }
195
+
196
+    private LocalDate[] quarterRange(int y, int q) {
197
+        LocalDate start = LocalDate.of(y, (q - 1) * 3 + 1, 1);
198
+        return new LocalDate[]{start, start.plusMonths(3).minusDays(1)};
199
+    }
200
+
201
+    private LocalDate[] monthRange(int y, int m) {
202
+        LocalDate start = LocalDate.of(y, m, 1);
203
+        return new LocalDate[]{start, start.withDayOfMonth(start.lengthOfMonth())};
204
+    }
205
+
206
+    // -------------------------------------------------------------------------
207
+    // series 构建
208
+    // -------------------------------------------------------------------------
209
+
210
+    private List<Map<String, Object>> buildSeries(
211
+            String scopeType, Long scopeId,
212
+            List<String> xAxis, List<LocalDate[]> periods, boolean hasChain,
213
+            boolean isStation, boolean isBrigade, boolean isManager,
214
+            Long userDeptId, LoginUser loginUser) {
215
+
216
+        int currentIdx = periods.size() - 1;
217
+
218
+        if ("MANAGER".equals(scopeType) && scopeId != null) {
219
+            List<Integer> counts = countByPeriods(periods, r -> countCompletedByDepartmentId(scopeId, r[0], r[1]));
220
+            return Collections.singletonList(
221
+                    buildSeriesItem(getDeptName(scopeId), scopeId, null, counts, currentIdx, hasChain));
222
+
223
+        } else if ("USER".equals(scopeType) && scopeId != null) {
224
+            List<Integer> counts = countByPeriods(periods, r -> countCompletedByUserId(scopeId, r[0], r[1]));
225
+            return Collections.singletonList(
226
+                    buildSeriesItem("用户" + scopeId, null, scopeId, counts, currentIdx, hasChain));
227
+
228
+        } else if ("BRIGADE".equals(scopeType) && scopeId != null) {
229
+            return buildBrigadeSeriesList(scopeId, periods, hasChain, currentIdx);
230
+
231
+        } else if ("TEAM".equals(scopeType) && scopeId != null) {
232
+            List<Integer> counts = countByPeriods(periods, r -> countCompletedByDepartmentId(scopeId, r[0], r[1]));
233
+            return Collections.singletonList(
234
+                    buildSeriesItem(getDeptName(scopeId), scopeId, null, counts, currentIdx, hasChain));
235
+
236
+        } else {
237
+            Long stationDeptId = ("STATION".equals(scopeType) && scopeId != null) ? scopeId : userDeptId;
238
+            if (isStation || "STATION".equals(scopeType)) {
239
+                return buildStationSeriesList(stationDeptId, periods, hasChain, currentIdx);
240
+            } else if (isBrigade) {
241
+                return buildBrigadeSeriesList(userDeptId, periods, hasChain, currentIdx);
242
+            } else {
243
+                // 主管:本主管室单条 series
244
+                String deptName = loginUser.getUser().getDept() != null
245
+                        ? loginUser.getUser().getDept().getDeptName() : "本主管";
246
+                List<Integer> counts = countByPeriods(periods,
247
+                        r -> countCompletedByDepartmentId(userDeptId, r[0], r[1]));
248
+                return Collections.singletonList(
249
+                        buildSeriesItem(deptName, userDeptId, null, counts, currentIdx, hasChain));
250
+            }
251
+        }
252
+    }
253
+
254
+    /**
255
+     * 站长视角:各大队独立 series + 全站汇总
256
+     */
257
+    private List<Map<String, Object>> buildStationSeriesList(
258
+            Long stationDeptId, List<LocalDate[]> periods, boolean hasChain, int currentIdx) {
259
+
260
+        List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(stationDeptId);
261
+        if (subDepts == null) subDepts = Collections.emptyList();
262
+        List<SysDept> brigades = subDepts.stream()
263
+                .filter(d -> stationDeptId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
264
+                .collect(Collectors.toList());
265
+
266
+        List<Map<String, Object>> series = new ArrayList<>();
267
+        int[] stationTotals = new int[periods.size()];
268
+
269
+        for (SysDept brigade : brigades) {
270
+            List<Integer> counts = countByPeriods(periods,
271
+                    r -> countCompletedByBrigadeId(brigade.getDeptId(), r[0], r[1]));
272
+            for (int i = 0; i < counts.size(); i++) stationTotals[i] += counts.get(i);
273
+            series.add(buildSeriesItem(brigade.getDeptName(), brigade.getDeptId(), null, counts, currentIdx, hasChain));
274
+        }
275
+
276
+        List<Integer> totalCounts = new ArrayList<>();
277
+        for (int c : stationTotals) totalCounts.add(c);
278
+        series.add(buildSeriesItem("全站", null, null, totalCounts, currentIdx, hasChain));
279
+        return series;
280
+    }
281
+
282
+    /**
283
+     * 大队视角:各主管独立 series
284
+     */
285
+    private List<Map<String, Object>> buildBrigadeSeriesList(
286
+            Long brigadeId, List<LocalDate[]> periods, boolean hasChain, int currentIdx) {
287
+
288
+        List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(brigadeId);
289
+        if (subDepts == null) subDepts = Collections.emptyList();
290
+        List<SysDept> managers = subDepts.stream()
291
+                .filter(d -> brigadeId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
292
+                .collect(Collectors.toList());
293
+
294
+        List<Map<String, Object>> series = new ArrayList<>();
295
+        for (SysDept manager : managers) {
296
+            List<Integer> counts = countByPeriods(periods,
297
+                    r -> countCompletedByDepartmentId(manager.getDeptId(), r[0], r[1]));
298
+            series.add(buildSeriesItem(manager.getDeptName(), manager.getDeptId(), null, counts, currentIdx, hasChain));
299
+        }
300
+        return series;
301
+    }
302
+
303
+    // -------------------------------------------------------------------------
304
+    // series item 构建
305
+    // -------------------------------------------------------------------------
306
+
307
+    /**
308
+     * 构建单条 series
309
+     *
310
+     * @param counts     各周期完成数,顺序与 xAxis 对应
311
+     * @param currentIdx 当期在 counts 中的下标(最后一个)
312
+     * @param hasChain   是否计算环比
313
+     */
314
+    private Map<String, Object> buildSeriesItem(String name, Long deptId, Long userId,
315
+                                                 List<Integer> counts, int currentIdx, boolean hasChain) {
316
+        int currentVal = counts.get(currentIdx);
317
+        int yoyVal = counts.get(0);
318
+        Integer prevVal = hasChain ? counts.get(1) : null;
319
+
320
+        Map<String, Object> s = new LinkedHashMap<>();
321
+        s.put("name", name);
322
+        if (deptId != null) s.put("deptId", deptId);
323
+        if (userId != null) s.put("userId", userId);
324
+        s.put("data", counts);
325
+        s.put("yoyRate", calcRate(currentVal, yoyVal));
326
+        s.put("chainRatio", prevVal != null ? calcRate(currentVal, prevVal) : null);
327
+        return s;
328
+    }
329
+
330
+    // -------------------------------------------------------------------------
331
+    // 统计辅助
332
+    // -------------------------------------------------------------------------
333
+
334
+    @FunctionalInterface
335
+    interface PeriodCounter {
336
+        int count(LocalDate[] range);
337
+    }
338
+
339
+    private List<Integer> countByPeriods(List<LocalDate[]> periods, PeriodCounter counter) {
340
+        List<Integer> result = new ArrayList<>();
341
+        for (LocalDate[] range : periods) {
342
+            result.add(counter.count(range));
343
+        }
344
+        return result;
345
+    }
346
+
347
+    private int countCompletedByDepartmentId(Long deptId, LocalDate start, LocalDate end) {
348
+        LambdaQueryWrapper<DailyTask> w = new LambdaQueryWrapper<>();
349
+        w.eq(DailyTask::getDtDepartmentId, deptId);
350
+        w.eq(DailyTask::getDtStatus, "COMPLETED");
351
+        w.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
352
+        w.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
353
+        w.eq(DailyTask::getDelStatus, 0);
354
+        return Math.toIntExact(dailyTaskMapper.selectCount(w));
355
+    }
356
+
357
+    private int countCompletedByUserId(Long userId, LocalDate start, LocalDate end) {
358
+        LambdaQueryWrapper<DailyTask> w = new LambdaQueryWrapper<>();
359
+        w.eq(DailyTask::getDtUserId, userId);
360
+        w.eq(DailyTask::getDtStatus, "COMPLETED");
361
+        w.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
362
+        w.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
363
+        w.eq(DailyTask::getDelStatus, 0);
364
+        return Math.toIntExact(dailyTaskMapper.selectCount(w));
365
+    }
366
+
367
+    private int countCompletedByBrigadeId(Long brigadeId, LocalDate start, LocalDate end) {
368
+        List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(brigadeId);
369
+        if (subDepts == null) subDepts = Collections.emptyList();
370
+        Set<Long> managerIds = subDepts.stream()
371
+                .filter(d -> brigadeId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
372
+                .map(SysDept::getDeptId)
373
+                .collect(Collectors.toSet());
374
+        if (managerIds.isEmpty()) return 0;
375
+        LambdaQueryWrapper<DailyTask> w = new LambdaQueryWrapper<>();
376
+        w.in(DailyTask::getDtDepartmentId, managerIds);
377
+        w.eq(DailyTask::getDtStatus, "COMPLETED");
378
+        w.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
379
+        w.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
380
+        w.eq(DailyTask::getDelStatus, 0);
381
+        return Math.toIntExact(dailyTaskMapper.selectCount(w));
382
+    }
383
+
384
+    private String getDeptName(Long deptId) {
385
+        SysDept dept = sysDeptMapper.selectDeptById(deptId);
386
+        return dept != null ? dept.getDeptName() : "部门" + deptId;
387
+    }
388
+
389
+    private BigDecimal calcRate(int current, int base) {
390
+        if (base == 0) return null;
391
+        return BigDecimal.valueOf(current - base)
392
+                .multiply(BigDecimal.valueOf(100))
393
+                .divide(BigDecimal.valueOf(base), 2, RoundingMode.HALF_UP);
394
+    }
395
+}

+ 513 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/DailyExamController.java

@@ -0,0 +1,513 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.sundot.airport.common.core.domain.entity.SysDept;
5
+import com.sundot.airport.common.core.domain.entity.SysRole;
6
+import com.sundot.airport.common.core.domain.model.LoginUser;
7
+import com.sundot.airport.common.enums.RoleTypeEnum;
8
+import com.sundot.airport.common.core.domain.SysAnalysisReportParamDto;
9
+import com.sundot.airport.common.utils.DateRangeQueryUtils;
10
+import com.sundot.airport.common.utils.SecurityUtils;
11
+import com.sundot.airport.exam.adapter.DailyTaskToExamAdapter;
12
+import com.sundot.airport.exam.common.ClientTypeConstant;
13
+import com.sundot.airport.exam.common.HttpResult;
14
+import com.sundot.airport.exam.common.PageData;
15
+import com.sundot.airport.exam.domain.DailyTask;
16
+import com.sundot.airport.exam.domain.DailyTaskDetail;
17
+import com.sundot.airport.exam.dto.SubmitAnswerDTO;
18
+import com.sundot.airport.exam.dto.SubmitResultDTO;
19
+import com.sundot.airport.exam.logic.eighteen.model.params.ExamRecAddParam;
20
+import com.sundot.airport.exam.logic.eighteen.model.params.Get18ExamIntroParams;
21
+import com.sundot.airport.exam.logic.eighteen.model.params.SubmitExamParam;
22
+import com.sundot.airport.exam.logic.eighteen.model.result.ExamGetResult;
23
+import com.sundot.airport.exam.logic.eighteen.model.result.Get18ExamIntroResult;
24
+import com.sundot.airport.exam.logic.eighteen.model.result.SubmitExamResult;
25
+import com.sundot.airport.exam.logic.eighteen.model.result.subresult.ItemIn18ExamRecordListResult;
26
+import com.sundot.airport.exam.mapper.DailyTaskDetailMapper;
27
+import com.sundot.airport.exam.mapper.DailyTaskMapper;
28
+import com.sundot.airport.exam.service.IDailyAnswerService;
29
+import com.sundot.airport.system.mapper.SysDeptMapper;
30
+import org.slf4j.Logger;
31
+import org.slf4j.LoggerFactory;
32
+import org.springframework.beans.factory.annotation.Autowired;
33
+import org.springframework.validation.annotation.Validated;
34
+import org.springframework.web.bind.annotation.*;
35
+
36
+import javax.annotation.Resource;
37
+import java.sql.Date;
38
+import java.text.SimpleDateFormat;
39
+import java.time.LocalDate;
40
+import java.time.format.DateTimeFormatter;
41
+import java.util.*;
42
+import java.util.stream.Collectors;
43
+
44
+/**
45
+ * <b>功能名:</b>DailyExamController<br>
46
+ * <b>说明:</b> 每日抽问抽答接口控制层(兼容十八大考试格式) <br>
47
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
48
+ * <b>修改履历:</b><br>
49
+ *
50
+ * @author 2025-01-16
51
+ */
52
+@RestController
53
+@RequestMapping("/v1/cs/app/daily-exam")
54
+public class DailyExamController {
55
+
56
+    private static final Logger log = LoggerFactory.getLogger(DailyExamController.class);
57
+
58
+    @Autowired
59
+    private DailyTaskMapper dailyTaskMapper;
60
+
61
+    @Autowired
62
+    private DailyTaskDetailMapper dailyTaskDetailMapper;
63
+
64
+    @Autowired
65
+    private IDailyAnswerService dailyAnswerService;
66
+
67
+    @Autowired
68
+    private DailyTaskToExamAdapter dailyTaskAdapter;
69
+
70
+    @Autowired
71
+    private SysDeptMapper sysDeptMapper;
72
+
73
+    /**
74
+     * <b>方法名: </b> getExamIntro <br>
75
+     * <b>说明: </b> 查询考试基本信息(兼容接口,实际返回当日任务信息) <br>
76
+     *
77
+     * @param param Get18ExamIntroParams 查询考试基本信息参数类
78
+     * @return HttpResult<Get18ExamIntroResult>
79
+     * <b>修改履历: </b>
80
+     * @author 2025-01-16
81
+     */
82
+    @PostMapping("/get_exam_intro")
83
+    public HttpResult<Get18ExamIntroResult> getExamIntro(@Validated @RequestBody Get18ExamIntroParams param) {
84
+        log.info("获取每日任务基本信息: userId={}", SecurityUtils.getUserId());
85
+
86
+        // 构造返回结果(固定信息)
87
+        Get18ExamIntroResult result = new Get18ExamIntroResult();
88
+        result.setExId("DAILY_TASK");
89
+        result.setExName("每日抽问抽答");
90
+        result.setExDetailImg("");
91
+        result.setExBgImg("");
92
+
93
+        return HttpResult.success(result);
94
+    }
95
+
96
+    /**
97
+     * <b>方法名: </b> applyExam <br>
98
+     * <b>说明: </b> 开始答题(获取今日待完成任务) <br>
99
+     *
100
+     * @param token java.lang.String token信息
101
+     * @param param com.sundot.airport.exam.logic.eighteen.model.params.ExamRecAddParam 获取考试信息和试题请求参数
102
+     * @return com.sundot.airport.exam.common.HttpResult<com.sundot.airport.exam.logic.eighteen.model.result.ExamGetResult><br>
103
+     * <b>修改履历: </b>
104
+     * @author 2025-01-16
105
+     */
106
+    @PostMapping("/get_exam")
107
+    public HttpResult<ExamGetResult> applyExam(
108
+            @RequestHeader(value = "Authorization", required = true) String token,
109
+            @Validated @RequestBody ExamRecAddParam param) {
110
+
111
+        Long userId = SecurityUtils.getUserId();
112
+        log.info("用户申请每日答题: userId={}, param={}", userId, param);
113
+
114
+        try {
115
+            // 1. 查询今日待完成任务
116
+            LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
117
+            wrapper.eq(DailyTask::getDtUserId, userId);
118
+            wrapper.eq(DailyTask::getDtStatus, "PENDING");
119
+            wrapper.eq(DailyTask::getDelStatus, 0);
120
+            wrapper.orderByAsc(DailyTask::getCreateTime);
121
+            wrapper.last("LIMIT 1");
122
+
123
+            DailyTask task = dailyTaskMapper.selectOne(wrapper);
124
+
125
+            if (task == null) {
126
+                log.warn("用户今日无待完成任务: userId={}", userId);
127
+                return HttpResult.error("今日暂无答题任务");
128
+            }
129
+
130
+            // 2. 更新任务状态为答题中
131
+            task.setDtStatus("IN_PROGRESS");
132
+            task.setDtStartTime(new java.util.Date());
133
+            dailyTaskMapper.updateById(task);
134
+
135
+            // 3. 查询任务明细(题目列表)
136
+            LambdaQueryWrapper<DailyTaskDetail> detailWrapper = new LambdaQueryWrapper<>();
137
+            detailWrapper.eq(DailyTaskDetail::getDtdTaskId, task.getDtId());
138
+            detailWrapper.eq(DailyTaskDetail::getDelStatus, 0);
139
+            detailWrapper.orderByAsc(DailyTaskDetail::getDtdQuestionNo);
140
+            List<DailyTaskDetail> details = dailyTaskDetailMapper.selectList(detailWrapper);
141
+
142
+            // 4. 转换为考试格式返回
143
+            ExamGetResult result = dailyTaskAdapter.convertDailyTaskToExam(task, details);
144
+
145
+            log.info("每日答题获取成功: userId={}, taskId={}, questionCount={}",
146
+                    userId, task.getDtId(), details.size());
147
+
148
+            return HttpResult.success(result);
149
+
150
+        } catch (Exception e) {
151
+            log.error("获取每日答题失败: userId={}", userId, e);
152
+            return HttpResult.error("获取每日答题失败:" + e.getMessage());
153
+        }
154
+    }
155
+
156
+    /**
157
+     * <b>方法名: </b> getExamRecordList <br>
158
+     * <b>说明: </b> 查询测试记录列表(用户历史任务列表) <br>
159
+     *
160
+     * @return HttpResult<PageData < ItemIn18ExamRecordListResult>>
161
+     * <b>修改履历: </b>
162
+     * @author 2025-01-16
163
+     */
164
+    @PostMapping("/get_exam_record_list")
165
+    public HttpResult<PageData<ItemIn18ExamRecordListResult>> getExamRecordList() {
166
+        Long userId = SecurityUtils.getUserId();
167
+        log.info("查询用户任务记录列表: userId={}", userId);
168
+
169
+        try {
170
+            // 查询用户所有任务记录
171
+            LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
172
+            wrapper.eq(DailyTask::getDtUserId, userId);
173
+            wrapper.eq(DailyTask::getDelStatus, 0);
174
+            wrapper.orderByDesc(DailyTask::getDtBusinessDate);
175
+            wrapper.orderByDesc(DailyTask::getCreateTime);
176
+
177
+            List<DailyTask> tasks = dailyTaskMapper.selectList(wrapper);
178
+
179
+            // 转换为考试记录列表格式
180
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
181
+            List<ItemIn18ExamRecordListResult> results = new ArrayList<>();
182
+
183
+            for (DailyTask task : tasks) {
184
+                ItemIn18ExamRecordListResult item = new ItemIn18ExamRecordListResult();
185
+                item.setErId(task.getDtId());
186
+                item.setErExId("DAILY_TASK_" + task.getDtBusinessDate());
187
+                item.setErExName("每日抽问抽答");
188
+                item.setErScore(task.getDtTotalScore() != null ? task.getDtTotalScore().intValue() : 0);
189
+                item.setCreateTime(task.getCreateTime() != null ? sdf.format(task.getCreateTime()) : "");
190
+                item.setErExEnableCert(false); // 每日任务不启用证书
191
+                item.setGetCert(false);
192
+
193
+                results.add(item);
194
+            }
195
+
196
+            PageData<ItemIn18ExamRecordListResult> pageData = new PageData<>();
197
+            pageData.setList(results);
198
+            pageData.setTotal(results.size());
199
+
200
+            return HttpResult.success(pageData);
201
+
202
+        } catch (Exception e) {
203
+            log.error("查询任务记录列表失败: userId={}", userId, e);
204
+            return HttpResult.error("查询记录列表失败:" + e.getMessage());
205
+        }
206
+    }
207
+
208
+    /**
209
+     * <b>方法名: </b> getExamRecord <br>
210
+     * <b>说明: </b> 查询测试记录详情(任务详情) <br>
211
+     *
212
+     * @param erId 任务ID
213
+     * @return HttpResult
214
+     * <b>修改履历: </b>
215
+     * @author 2025-01-16
216
+     */
217
+    @PostMapping("/get_exam_record")
218
+    public HttpResult getExamRecord(@RequestParam String erId) {
219
+        Long userId = SecurityUtils.getUserId();
220
+        log.info("查询任务记录详情: userId={}, taskId={}", userId, erId);
221
+
222
+        try {
223
+            // 查询任务主记录
224
+            LambdaQueryWrapper<DailyTask> taskWrapper = new LambdaQueryWrapper<>();
225
+            taskWrapper.eq(DailyTask::getDtId, erId);
226
+            taskWrapper.eq(DailyTask::getDelStatus, 0);
227
+            DailyTask task = dailyTaskMapper.selectOne(taskWrapper);
228
+
229
+            if (task == null) {
230
+                return HttpResult.error("任务不存在");
231
+            }
232
+
233
+            // 验证权限
234
+            if (!task.getDtUserId().equals(userId)) {
235
+                return HttpResult.error("无权查看该任务");
236
+            }
237
+
238
+            // 查询任务明细
239
+            LambdaQueryWrapper<DailyTaskDetail> detailWrapper = new LambdaQueryWrapper<>();
240
+            detailWrapper.eq(DailyTaskDetail::getDtdTaskId, erId);
241
+            detailWrapper.eq(DailyTaskDetail::getDelStatus, 0);
242
+            detailWrapper.orderByAsc(DailyTaskDetail::getDtdQuestionNo);
243
+            List<DailyTaskDetail> details = dailyTaskDetailMapper.selectList(detailWrapper);
244
+
245
+            // 组装返回结果
246
+            Map<String, Object> data = new HashMap<>();
247
+            data.put("task", task);
248
+            data.put("details", details);
249
+
250
+            return HttpResult.success(data);
251
+
252
+        } catch (Exception e) {
253
+            log.error("查询任务记录详情失败: userId={}, taskId={}", userId, erId, e);
254
+            return HttpResult.error("查询记录详情失败:" + e.getMessage());
255
+        }
256
+    }
257
+
258
+    /**
259
+     * <b>方法名: </b> submitExam <br>
260
+     * <b>说明: </b> 提交考试答案 <br>
261
+     *
262
+     * @param param SubmitExamParam 提交考试参数类
263
+     * @return HttpResult<SubmitExamResult>
264
+     * <b>修改履历: </b>
265
+     * @author 2025-01-16
266
+     */
267
+    @PostMapping("/submit_exam")
268
+    public HttpResult<SubmitExamResult> submitExam(@Validated @RequestBody SubmitExamParam param) {
269
+        Long userId = SecurityUtils.getUserId();
270
+        log.info("提交每日答题: userId={}, param={}", userId, param);
271
+
272
+        try {
273
+            // 1. 转换参数格式
274
+            SubmitAnswerDTO taskDto = dailyTaskAdapter.convertExamParamToTaskSubmit(param);
275
+
276
+            // 2. 调用答题服务提交
277
+            SubmitResultDTO taskResult = dailyAnswerService.submitAnswer(userId, taskDto);
278
+
279
+            // 3. 转换为考试提交结果格式
280
+            SubmitExamResult examResult = dailyTaskAdapter.convertTaskResultToExamResult(taskResult);
281
+
282
+            log.info("每日答题提交成功: userId={}, taskId={}, score={}",
283
+                    userId, taskResult.getTaskId(), taskResult.getTotalScore());
284
+
285
+            return HttpResult.success(examResult);
286
+
287
+        } catch (Exception e) {
288
+            log.error("提交每日答题失败: userId={}, erId={}", userId, param.getErId(), e);
289
+            return HttpResult.error("提交答题失败:" + e.getMessage());
290
+        }
291
+    }
292
+
293
+    /**
294
+     * 抽问抽答完成趋势
295
+     * 美兰4级架构:STATION > 大队(BRIGADE) > 主管(MANAGER) > 班组(TEAMS)
296
+     * 站长/管理员/质检科:各大队完成趋势折线
297
+     * 大队长(经理/行政):本大队各主管完成趋势折线
298
+     * 主管:本主管室完成趋势(单条折线)
299
+     * 班组长/安检员:show=false
300
+     *
301
+     * @param timeType  时间类型:year/month/custom,默认 month
302
+     * @param startDate 自定义开始日期(yyyy-MM-dd),timeType=custom 时有效
303
+     * @param endDate   自定义结束日期(yyyy-MM-dd),timeType=custom 时有效
304
+     */
305
+    @GetMapping("/completion-trend")
306
+    public HttpResult<Map<String, Object>> getCompletionTrend(
307
+            @RequestParam(required = false, defaultValue = "month") String timeType,
308
+            @RequestParam(required = false) String startDate,
309
+            @RequestParam(required = false) String endDate) {
310
+
311
+        try {
312
+            LoginUser loginUser = SecurityUtils.getLoginUser();
313
+            List<String> roleKeys = loginUser.getUser().getRoles().stream()
314
+                    .map(SysRole::getRoleKey).collect(Collectors.toList());
315
+            Long userDeptId = loginUser.getDeptId();
316
+            Long userId = loginUser.getUserId();
317
+
318
+            boolean isStation = userId == 1L
319
+                    || roleKeys.contains(RoleTypeEnum.admin.getCode())
320
+                    || roleKeys.contains(RoleTypeEnum.test.getCode())
321
+                    || roleKeys.contains(RoleTypeEnum.zhijianke.getCode());
322
+            boolean isBrigade = !isStation && (roleKeys.contains(RoleTypeEnum.jingli.getCode())
323
+                    || roleKeys.contains(RoleTypeEnum.xingzheng.getCode()));
324
+            boolean isKezhang = !isStation && !isBrigade && roleKeys.contains(RoleTypeEnum.kezhang.getCode());
325
+
326
+            // 班组长及以下不显示此tab
327
+            if (!isStation && !isBrigade && !isKezhang) {
328
+                Map<String, Object> noShow = new HashMap<>();
329
+                noShow.put("show", false);
330
+                return HttpResult.success(noShow);
331
+            }
332
+
333
+            LocalDate[] range = resolveDateRange(timeType, startDate, endDate);
334
+            LocalDate start = range[0];
335
+            LocalDate end = range[1];
336
+            boolean byMonth = "year".equals(timeType);
337
+            List<String> xAxis = buildXAxis(start, end, byMonth);
338
+            List<Map<String, Object>> series = new ArrayList<>();
339
+
340
+            if (isStation) {
341
+                // 站长:查该站所有子部门,按大队分组
342
+                List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(userDeptId);
343
+                if (subDepts == null) subDepts = Collections.emptyList();
344
+
345
+                // 大队(站的直接下级)
346
+                List<SysDept> brigadeDepts = subDepts.stream()
347
+                        .filter(d -> userDeptId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
348
+                        .collect(Collectors.toList());
349
+
350
+                // 构建 主管deptId -> 所属大队deptId 的映射
351
+                Map<Long, Long> managerToBrigadeId = new HashMap<>();
352
+                for (SysDept d : subDepts) {
353
+                    if ("0".equals(d.getDelFlag()) && d.getParentId() != null && !userDeptId.equals(d.getParentId())) {
354
+                        // 非直接下级,其parentId为某个大队,记录映射(主管级别)
355
+                        managerToBrigadeId.put(d.getDeptId(), d.getParentId());
356
+                    }
357
+                }
358
+
359
+                // 查全站所有任务
360
+                Set<Long> allDeptIds = new HashSet<>();
361
+                allDeptIds.add(userDeptId);
362
+                subDepts.forEach(d -> allDeptIds.add(d.getDeptId()));
363
+                List<DailyTask> allTasks = queryTasksByDeptIds(allDeptIds, start, end);
364
+
365
+                // 按大队分组生成折线
366
+                for (SysDept brigade : brigadeDepts) {
367
+                    Long brigadeId = brigade.getDeptId();
368
+                    List<DailyTask> brigadeTasks = allTasks.stream()
369
+                            .filter(t -> {
370
+                                Long managerId = t.getDtDepartmentId();
371
+                                if (managerId == null) return false;
372
+                                Long parentBrigadeId = managerToBrigadeId.get(managerId);
373
+                                return brigadeId.equals(parentBrigadeId);
374
+                            })
375
+                            .collect(Collectors.toList());
376
+                    Map<String, Object> item = new LinkedHashMap<>();
377
+                    item.put("name", brigade.getDeptName());
378
+                    item.put("deptId", brigadeId);
379
+                    item.put("data", buildSeriesData(xAxis, brigadeTasks, byMonth));
380
+                    series.add(item);
381
+                }
382
+
383
+            } else if (isBrigade) {
384
+                // 大队长:查本大队下各主管的任务
385
+                List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(userDeptId);
386
+                if (subDepts == null) subDepts = Collections.emptyList();
387
+
388
+                // 主管(大队的直接下级)
389
+                List<SysDept> managerDepts = subDepts.stream()
390
+                        .filter(d -> userDeptId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
391
+                        .collect(Collectors.toList());
392
+
393
+                for (SysDept manager : managerDepts) {
394
+                    List<DailyTask> tasks = queryTasksByDepartmentId(manager.getDeptId(), start, end);
395
+                    Map<String, Object> item = new LinkedHashMap<>();
396
+                    item.put("name", manager.getDeptName());
397
+                    item.put("deptId", manager.getDeptId());
398
+                    item.put("data", buildSeriesData(xAxis, tasks, byMonth));
399
+                    series.add(item);
400
+                }
401
+
402
+            } else {
403
+                // 主管:本主管室所有任务,一条折线
404
+                List<DailyTask> tasks = queryTasksByDepartmentId(userDeptId, start, end);
405
+                String deptName = loginUser.getUser().getDept() != null
406
+                        ? loginUser.getUser().getDept().getDeptName() : "本主管";
407
+                Map<String, Object> item = new LinkedHashMap<>();
408
+                item.put("name", deptName);
409
+                item.put("deptId", userDeptId);
410
+                item.put("data", buildSeriesData(xAxis, tasks, byMonth));
411
+                series.add(item);
412
+            }
413
+
414
+            Map<String, Object> result = new LinkedHashMap<>();
415
+            result.put("show", true);
416
+            result.put("xAxis", xAxis);
417
+            result.put("series", series);
418
+            return HttpResult.success(result);
419
+
420
+        } catch (Exception e) {
421
+            log.error("获取抽问抽答完成趋势失败", e);
422
+            return HttpResult.error("获取完成趋势失败:" + e.getMessage());
423
+        }
424
+    }
425
+
426
+    /**
427
+     * 根据 timeType 计算起止日期
428
+     * year: 当年全年;month: 当月;custom: 自定义范围
429
+     */
430
+    private LocalDate[] resolveDateRange(String timeType, String startDate, String endDate) {
431
+        LocalDate today = LocalDate.now();
432
+        LocalDate start;
433
+        LocalDate end = today;
434
+        switch (timeType == null ? "month" : timeType) {
435
+            case "year":
436
+                start = today.withDayOfYear(1);
437
+                end = today.withMonth(12).withDayOfMonth(31);
438
+                break;
439
+            case "custom":
440
+                start = (startDate != null) ? LocalDate.parse(startDate) : today.withDayOfMonth(1);
441
+                end = (endDate != null) ? LocalDate.parse(endDate) : today;
442
+                break;
443
+            default: // month
444
+                start = today.withDayOfMonth(1);
445
+                break;
446
+        }
447
+        return new LocalDate[]{start, end};
448
+    }
449
+
450
+    /**
451
+     * 生成X轴标签列表
452
+     * byMonth=true:每月一个标签(格式 yyyy-MM);false:每天一个标签(格式 yyyy-MM-dd)
453
+     */
454
+    private List<String> buildXAxis(LocalDate start, LocalDate end, boolean byMonth) {
455
+        List<String> xAxis = new ArrayList<>();
456
+        if (byMonth) {
457
+            LocalDate cursor = start.withDayOfMonth(1);
458
+            LocalDate endMonth = end.withDayOfMonth(1);
459
+            while (!cursor.isAfter(endMonth)) {
460
+                xAxis.add(cursor.format(DateTimeFormatter.ofPattern("yyyy-MM")));
461
+                cursor = cursor.plusMonths(1);
462
+            }
463
+        } else {
464
+            LocalDate cursor = start;
465
+            while (!cursor.isAfter(end)) {
466
+                xAxis.add(cursor.toString());
467
+                cursor = cursor.plusDays(1);
468
+            }
469
+        }
470
+        return xAxis;
471
+    }
472
+
473
+    /**
474
+     * 构建某条折线的数据数组,与 xAxis 对齐
475
+     */
476
+    private List<Integer> buildSeriesData(List<String> xAxis, List<DailyTask> tasks, boolean byMonth) {
477
+        SimpleDateFormat sdf = new SimpleDateFormat(byMonth ? "yyyy-MM" : "yyyy-MM-dd");
478
+        Map<String, Long> countMap = tasks.stream()
479
+                .filter(t -> "COMPLETED".equals(t.getDtStatus()) && t.getDtBusinessDate() != null)
480
+                .collect(Collectors.groupingBy(
481
+                        t -> sdf.format(t.getDtBusinessDate()),
482
+                        Collectors.counting()));
483
+        List<Integer> data = new ArrayList<>();
484
+        for (String label : xAxis) {
485
+            data.add(countMap.getOrDefault(label, 0L).intValue());
486
+        }
487
+        return data;
488
+    }
489
+
490
+    /**
491
+     * 按部门ID集合查询任务(用于站长视角:通过dtDeptId匹配所有子部门)
492
+     */
493
+    private List<DailyTask> queryTasksByDeptIds(Set<Long> deptIds, LocalDate start, LocalDate end) {
494
+        LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
495
+        wrapper.in(DailyTask::getDtDeptId, deptIds);
496
+        wrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
497
+        wrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
498
+        wrapper.eq(DailyTask::getDelStatus, 0);
499
+        return dailyTaskMapper.selectList(wrapper);
500
+    }
501
+
502
+    /**
503
+     * 按主管ID查询任务(通过dtDepartmentId匹配)
504
+     */
505
+    private List<DailyTask> queryTasksByDepartmentId(Long departmentId, LocalDate start, LocalDate end) {
506
+        LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
507
+        wrapper.eq(DailyTask::getDtDepartmentId, departmentId);
508
+        wrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
509
+        wrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
510
+        wrapper.eq(DailyTask::getDelStatus, 0);
511
+        return dailyTaskMapper.selectList(wrapper);
512
+    }
513
+}

+ 755 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/DailyExamWrongAnalysisController.java

@@ -0,0 +1,755 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.sundot.airport.common.core.domain.entity.SysDept;
5
+import com.sundot.airport.common.core.domain.entity.SysRole;
6
+import com.sundot.airport.common.core.domain.model.LoginUser;
7
+import com.sundot.airport.common.enums.RoleTypeEnum;
8
+import com.sundot.airport.common.core.domain.SysAnalysisReportParamDto;
9
+import com.sundot.airport.common.utils.DateRangeQueryUtils;
10
+import com.sundot.airport.common.utils.SecurityUtils;
11
+import com.sundot.airport.exam.common.HttpResult;
12
+import com.sundot.airport.exam.common.service.QuesCatService;
13
+import com.sundot.airport.exam.domain.DailyTask;
14
+import com.sundot.airport.exam.domain.DailyTaskDetail;
15
+import com.sundot.airport.exam.domain.QuesCat;
16
+import com.sundot.airport.exam.mapper.DailyTaskDetailMapper;
17
+import com.sundot.airport.exam.mapper.DailyTaskMapper;
18
+import com.sundot.airport.system.domain.BaseCheckCategory;
19
+import com.sundot.airport.system.mapper.SysDeptMapper;
20
+import com.sundot.airport.system.service.IBaseCheckCategoryService;
21
+import org.slf4j.Logger;
22
+import org.slf4j.LoggerFactory;
23
+import org.springframework.beans.factory.annotation.Autowired;
24
+import org.springframework.web.bind.annotation.GetMapping;
25
+import org.springframework.web.bind.annotation.RequestMapping;
26
+import org.springframework.web.bind.annotation.RequestParam;
27
+import org.springframework.web.bind.annotation.RestController;
28
+
29
+import java.sql.Date;
30
+import java.text.SimpleDateFormat;
31
+import java.time.LocalDate;
32
+import java.time.format.DateTimeFormatter;
33
+import java.util.*;
34
+import java.util.stream.Collectors;
35
+
36
+/**
37
+ * 抽问抽答错题分析接口(美兰4级架构适配版)
38
+ * <p>
39
+ * 美兰组织架构:STATION(安检站)> BRIGADE(大队)> MANAGER(主管)> TEAMS(班组)
40
+ * <p>
41
+ * 接口1: GET /wrong-analysis/overview      总体问题分布(饼图,所有角色)
42
+ * 接口2: GET /wrong-analysis/radar         问题分布对比(雷达图,仅站长)
43
+ * 接口3: GET /wrong-analysis/category-detail  大类详情
44
+ *   - 站长:各大队对比表格 + 各大队趋势
45
+ *   - 大队长:各主管对比表格 + 各主管趋势
46
+ *   - 主管:本主管各小类甜甜圈 + 本主管趋势
47
+ *   - 班组长及以下:show=false
48
+ */
49
+@RestController
50
+@RequestMapping("/v1/cs/app/daily-exam/wrong-analysis")
51
+public class DailyExamWrongAnalysisController {
52
+
53
+    private static final Logger log = LoggerFactory.getLogger(DailyExamWrongAnalysisController.class);
54
+
55
+    @Autowired
56
+    private DailyTaskMapper dailyTaskMapper;
57
+
58
+    @Autowired
59
+    private DailyTaskDetailMapper dailyTaskDetailMapper;
60
+
61
+    @Autowired
62
+    private SysDeptMapper sysDeptMapper;
63
+
64
+    @Autowired
65
+    private QuesCatService quesCatService;
66
+
67
+    @Autowired
68
+    private IBaseCheckCategoryService baseCheckCategoryService;
69
+
70
+    // =====================================================================
71
+    // 接口1:总体问题分布(饼图)
72
+    // =====================================================================
73
+
74
+    /**
75
+     * 总体问题分布(按大类统计错题数)
76
+     * 站长/管理员/质检科:全站;大队长:本大队;主管:本主管室;班组长:本班组
77
+     *
78
+     * @param timeType  year/month/custom,默认 month
79
+     * @param startDate 自定义开始日期 yyyy-MM-dd
80
+     * @param endDate   自定义结束日期 yyyy-MM-dd
81
+     */
82
+    @GetMapping("/overview")
83
+    public HttpResult<Map<String, Object>> getOverview(
84
+            @RequestParam(required = false, defaultValue = "month") String timeType,
85
+            @RequestParam(required = false) String startDate,
86
+            @RequestParam(required = false) String endDate) {
87
+
88
+        try {
89
+            RoleInfo role = getRoleInfo();
90
+            LocalDate[] range = resolveDateRange(timeType, startDate, endDate);
91
+
92
+            List<DailyTaskDetail> errorDetails = getErrorDetails(role, range[0], range[1]);
93
+
94
+            Map<String, BaseCheckCategory> qcIdToLevel1 = buildQcIdToLevel1Map(errorDetails);
95
+
96
+            Map<Long, long[]> countByCategory = new LinkedHashMap<>();
97
+            Map<Long, String> categoryNames = new LinkedHashMap<>();
98
+
99
+            for (DailyTaskDetail detail : errorDetails) {
100
+                BaseCheckCategory cat = qcIdToLevel1.get(detail.getDtdModuleId());
101
+                if (cat != null) {
102
+                    countByCategory.merge(cat.getId(), new long[]{1, cat.getOrderNum() != null ? cat.getOrderNum() : 99},
103
+                            (a, b) -> new long[]{a[0] + 1, a[1]});
104
+                    categoryNames.put(cat.getId(), cat.getName());
105
+                }
106
+            }
107
+
108
+            List<Map<String, Object>> categories = countByCategory.entrySet().stream()
109
+                    .sorted(Comparator.comparingLong(e -> e.getValue()[1]))
110
+                    .map(e -> {
111
+                        Map<String, Object> item = new LinkedHashMap<>();
112
+                        item.put("id", e.getKey());
113
+                        item.put("name", categoryNames.get(e.getKey()));
114
+                        item.put("count", e.getValue()[0]);
115
+                        return item;
116
+                    })
117
+                    .collect(Collectors.toList());
118
+
119
+            Map<String, Object> result = new LinkedHashMap<>();
120
+            result.put("categories", categories);
121
+            return HttpResult.success(result);
122
+
123
+        } catch (Exception e) {
124
+            log.error("获取总体问题分布失败", e);
125
+            return HttpResult.error("获取总体问题分布失败:" + e.getMessage());
126
+        }
127
+    }
128
+
129
+    // =====================================================================
130
+    // 接口2:问题分布对比(雷达图,仅站长)
131
+    // =====================================================================
132
+
133
+    /**
134
+     * 问题分布对比雷达图(仅站长可见,各大队在各大类的错题数对比)
135
+     *
136
+     * @param timeType  year/month/custom,默认 month
137
+     * @param startDate 自定义开始日期
138
+     * @param endDate   自定义结束日期
139
+     */
140
+    @GetMapping("/radar")
141
+    public HttpResult<Map<String, Object>> getRadar(
142
+            @RequestParam(required = false, defaultValue = "month") String timeType,
143
+            @RequestParam(required = false) String startDate,
144
+            @RequestParam(required = false) String endDate) {
145
+
146
+        try {
147
+            RoleInfo role = getRoleInfo();
148
+            if (!role.isStation) {
149
+                Map<String, Object> noShow = new HashMap<>();
150
+                noShow.put("show", false);
151
+                return HttpResult.success(noShow);
152
+            }
153
+
154
+            LocalDate[] range = resolveDateRange(timeType, startDate, endDate);
155
+
156
+            // 查询所有大类,作为雷达图的维度
157
+            BaseCheckCategory level1Query = new BaseCheckCategory();
158
+            level1Query.setLevel(1);
159
+            List<BaseCheckCategory> level1Categories = baseCheckCategoryService.selectBaseCheckCategoryList(level1Query);
160
+            level1Categories.sort(Comparator.comparingInt(c -> c.getOrderNum() != null ? c.getOrderNum() : 99));
161
+
162
+            List<String> indicators = level1Categories.stream()
163
+                    .map(BaseCheckCategory::getName)
164
+                    .collect(Collectors.toList());
165
+            List<Long> indicatorIds = level1Categories.stream()
166
+                    .map(BaseCheckCategory::getId)
167
+                    .collect(Collectors.toList());
168
+
169
+            // 获取站下各大队(直接下级)
170
+            List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(role.userDeptId);
171
+            List<SysDept> brigadeDepts = subDepts == null ? Collections.emptyList() :
172
+                    subDepts.stream()
173
+                            .filter(d -> role.userDeptId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
174
+                            .collect(Collectors.toList());
175
+
176
+            // 为每个大队构建一条雷达折线
177
+            List<Map<String, Object>> series = new ArrayList<>();
178
+            for (SysDept brigade : brigadeDepts) {
179
+                // 查该大队下所有主管的错误明细(通过dtDepartmentId匹配主管子部门)
180
+                List<DailyTaskDetail> details = getErrorDetailsBySubDepts(brigade.getDeptId(), range[0], range[1]);
181
+                Map<String, BaseCheckCategory> qcIdToLevel1 = buildQcIdToLevel1Map(details);
182
+
183
+                Map<Long, Long> countMap = details.stream()
184
+                        .filter(d -> qcIdToLevel1.get(d.getDtdModuleId()) != null)
185
+                        .collect(Collectors.groupingBy(
186
+                                d -> qcIdToLevel1.get(d.getDtdModuleId()).getId(),
187
+                                Collectors.counting()));
188
+
189
+                List<Long> data = indicatorIds.stream()
190
+                        .map(id -> countMap.getOrDefault(id, 0L))
191
+                        .collect(Collectors.toList());
192
+
193
+                Map<String, Object> seriesItem = new LinkedHashMap<>();
194
+                seriesItem.put("name", brigade.getDeptName());
195
+                seriesItem.put("deptId", brigade.getDeptId());
196
+                seriesItem.put("data", data);
197
+                series.add(seriesItem);
198
+            }
199
+
200
+            Map<String, Object> result = new LinkedHashMap<>();
201
+            result.put("show", true);
202
+            result.put("indicators", indicators);
203
+            result.put("series", series);
204
+            return HttpResult.success(result);
205
+
206
+        } catch (Exception e) {
207
+            log.error("获取问题分布对比失败", e);
208
+            return HttpResult.error("获取问题分布对比失败:" + e.getMessage());
209
+        }
210
+    }
211
+
212
+    // =====================================================================
213
+    // 接口3:大类详情(站长/大队长/主管)
214
+    // =====================================================================
215
+
216
+    /**
217
+     * 大类详情 - 点击饼图某一块后展示该大类下的小类分布及趋势
218
+     * <p>
219
+     * 站长:各大队小类对比表 + 各大队按日趋势
220
+     * 大队长:各主管小类对比表 + 各主管按日趋势
221
+     * 主管:本主管各小类甜甜圈分布 + 本主管按日趋势
222
+     * 班组长及以下:show=false
223
+     *
224
+     * @param categoryId 大类ID(base_check_category.id,level=1)
225
+     * @param param      dateRangeQueryType=YEAR|QUARTER|MONTH,year,quarter,month
226
+     */
227
+    @GetMapping("/category-detail")
228
+    public HttpResult<Map<String, Object>> getCategoryDetail(
229
+            @RequestParam Long categoryId,
230
+            SysAnalysisReportParamDto param) {
231
+
232
+        try {
233
+            RoleInfo role = getRoleInfo();
234
+
235
+            if (!role.isStation && !role.isBrigade && !role.isKezhang) {
236
+                Map<String, Object> noShow = new HashMap<>();
237
+                noShow.put("show", false);
238
+                return HttpResult.success(noShow);
239
+            }
240
+
241
+            LocalDate[] range = resolveDateRange(param);
242
+
243
+            BaseCheckCategory level1Cat = baseCheckCategoryService.selectBaseCheckCategoryById(categoryId);
244
+            if (level1Cat == null) {
245
+                return HttpResult.error("未找到该分类");
246
+            }
247
+
248
+            BaseCheckCategory level2Query = new BaseCheckCategory();
249
+            level2Query.setParentId(categoryId);
250
+            level2Query.setLevel(2);
251
+            List<BaseCheckCategory> subCategories = baseCheckCategoryService.selectBaseCheckCategoryList(level2Query);
252
+            subCategories.sort(Comparator.comparingInt(c -> c.getOrderNum() != null ? c.getOrderNum() : 99));
253
+
254
+            List<String> subCatNames = subCategories.stream()
255
+                    .map(BaseCheckCategory::getName).collect(Collectors.toList());
256
+            Map<String, String> subCatNameToQcId = buildSubCatNameToQcIdMap(subCatNames);
257
+            Map<String, String> qcIdToSubCatName = new HashMap<>();
258
+            subCatNameToQcId.forEach((name, qcId) -> { if (qcId != null) qcIdToSubCatName.put(qcId, name); });
259
+
260
+            Map<String, Object> result = new LinkedHashMap<>();
261
+            result.put("show", true);
262
+            result.put("categoryId", categoryId);
263
+            result.put("categoryName", level1Cat.getName());
264
+
265
+            boolean byMonth = "YEAR".equals(param != null ? param.getDateRangeQueryType() : null);
266
+            List<String> xAxis = buildXAxis(range[0], range[1], byMonth);
267
+            SimpleDateFormat sdf = new SimpleDateFormat(byMonth ? "yyyy-MM" : "yyyy-MM-dd");
268
+
269
+            if (role.isStation) {
270
+                // ---- 站长视角:各大队对比 ----
271
+                List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(role.userDeptId);
272
+                List<SysDept> brigadeDepts = subDepts == null ? Collections.emptyList() :
273
+                        subDepts.stream()
274
+                                .filter(d -> role.userDeptId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
275
+                                .collect(Collectors.toList());
276
+
277
+                // 查各大队错误明细(按qcId过滤)
278
+                Map<Long, List<DailyTaskDetail>> detailsByBrigade = new LinkedHashMap<>();
279
+                for (SysDept brigade : brigadeDepts) {
280
+                    List<DailyTaskDetail> details = getErrorDetailsBySubDeptsAndQcIds(
281
+                            brigade.getDeptId(), new HashSet<>(qcIdToSubCatName.keySet()), range[0], range[1]);
282
+                    detailsByBrigade.put(brigade.getDeptId(), details);
283
+                }
284
+
285
+                // 表格数据:行=小类,列=各大队
286
+                List<Map<String, Object>> tableRows = new ArrayList<>();
287
+                for (String subCatName : subCatNames) {
288
+                    String qcId = subCatNameToQcId.get(subCatName);
289
+                    Map<String, Object> row = new LinkedHashMap<>();
290
+                    row.put("subCategory", subCatName);
291
+                    List<Long> deptCounts = new ArrayList<>();
292
+                    for (SysDept brigade : brigadeDepts) {
293
+                        List<DailyTaskDetail> details = detailsByBrigade.get(brigade.getDeptId());
294
+                        long cnt = details == null ? 0 : details.stream()
295
+                                .filter(d -> qcId != null && qcId.equals(d.getDtdModuleId()))
296
+                                .count();
297
+                        deptCounts.add(cnt);
298
+                    }
299
+                    row.put("counts", deptCounts);
300
+                    tableRows.add(row);
301
+                }
302
+
303
+                List<String> deptNames = brigadeDepts.stream()
304
+                        .map(SysDept::getDeptName).collect(Collectors.toList());
305
+
306
+                Map<String, Object> table = new LinkedHashMap<>();
307
+                table.put("depts", deptNames);
308
+                table.put("rows", tableRows);
309
+                result.put("table", table);
310
+
311
+                // 趋势数据(各大队按日/月错题数)
312
+                List<Map<String, Object>> trendSeries = new ArrayList<>();
313
+                for (SysDept brigade : brigadeDepts) {
314
+                    List<DailyTaskDetail> details = detailsByBrigade.get(brigade.getDeptId());
315
+                    List<Integer> data = buildTrendData(xAxis, details, sdf);
316
+                    Map<String, Object> s = new LinkedHashMap<>();
317
+                    s.put("name", brigade.getDeptName());
318
+                    s.put("deptId", brigade.getDeptId());
319
+                    s.put("data", data);
320
+                    trendSeries.add(s);
321
+                }
322
+                Map<String, Object> trend = new LinkedHashMap<>();
323
+                trend.put("xAxis", xAxis);
324
+                trend.put("series", trendSeries);
325
+                result.put("trend", trend);
326
+
327
+            } else if (role.isBrigade) {
328
+                // ---- 大队长视角:各主管对比 ----
329
+                List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(role.userDeptId);
330
+                List<SysDept> managerDepts = subDepts == null ? Collections.emptyList() :
331
+                        subDepts.stream()
332
+                                .filter(d -> role.userDeptId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
333
+                                .collect(Collectors.toList());
334
+
335
+                // 查各主管错误明细(按qcId过滤)
336
+                Map<Long, List<DailyTaskDetail>> detailsByManager = new LinkedHashMap<>();
337
+                for (SysDept manager : managerDepts) {
338
+                    List<DailyTaskDetail> details = getErrorDetailsByDepartmentIdAndQcIds(
339
+                            manager.getDeptId(), new HashSet<>(qcIdToSubCatName.keySet()), range[0], range[1]);
340
+                    detailsByManager.put(manager.getDeptId(), details);
341
+                }
342
+
343
+                // 表格数据:行=小类,列=各主管
344
+                List<Map<String, Object>> tableRows = new ArrayList<>();
345
+                for (String subCatName : subCatNames) {
346
+                    String qcId = subCatNameToQcId.get(subCatName);
347
+                    Map<String, Object> row = new LinkedHashMap<>();
348
+                    row.put("subCategory", subCatName);
349
+                    List<Long> deptCounts = new ArrayList<>();
350
+                    for (SysDept manager : managerDepts) {
351
+                        List<DailyTaskDetail> details = detailsByManager.get(manager.getDeptId());
352
+                        long cnt = details == null ? 0 : details.stream()
353
+                                .filter(d -> qcId != null && qcId.equals(d.getDtdModuleId()))
354
+                                .count();
355
+                        deptCounts.add(cnt);
356
+                    }
357
+                    row.put("counts", deptCounts);
358
+                    tableRows.add(row);
359
+                }
360
+
361
+                List<String> deptNames = managerDepts.stream()
362
+                        .map(SysDept::getDeptName).collect(Collectors.toList());
363
+
364
+                Map<String, Object> table = new LinkedHashMap<>();
365
+                table.put("depts", deptNames);
366
+                table.put("rows", tableRows);
367
+                result.put("table", table);
368
+
369
+                // 趋势数据(各主管按日/月错题数)
370
+                List<Map<String, Object>> trendSeries = new ArrayList<>();
371
+                for (SysDept manager : managerDepts) {
372
+                    List<DailyTaskDetail> details = detailsByManager.get(manager.getDeptId());
373
+                    List<Integer> data = buildTrendData(xAxis, details, sdf);
374
+                    Map<String, Object> s = new LinkedHashMap<>();
375
+                    s.put("name", manager.getDeptName());
376
+                    s.put("deptId", manager.getDeptId());
377
+                    s.put("data", data);
378
+                    trendSeries.add(s);
379
+                }
380
+                Map<String, Object> trend = new LinkedHashMap<>();
381
+                trend.put("xAxis", xAxis);
382
+                trend.put("series", trendSeries);
383
+                result.put("trend", trend);
384
+
385
+            } else {
386
+                // ---- 主管视角:甜甜圈 + 趋势 ----
387
+                List<DailyTaskDetail> details = getErrorDetailsByDepartmentIdAndQcIds(
388
+                        role.userDeptId, new HashSet<>(qcIdToSubCatName.keySet()), range[0], range[1]);
389
+
390
+                Map<String, Long> qcIdCountMap = details.stream()
391
+                        .collect(Collectors.groupingBy(
392
+                                d -> d.getDtdModuleId() != null ? d.getDtdModuleId() : "",
393
+                                Collectors.counting()));
394
+
395
+                List<Map<String, Object>> donut = new ArrayList<>();
396
+                for (String subCatName : subCatNames) {
397
+                    String qcId = subCatNameToQcId.get(subCatName);
398
+                    Map<String, Object> item = new LinkedHashMap<>();
399
+                    item.put("name", subCatName);
400
+                    item.put("count", qcId != null ? qcIdCountMap.getOrDefault(qcId, 0L) : 0L);
401
+                    donut.add(item);
402
+                }
403
+                result.put("donut", donut);
404
+
405
+                String deptName = getDeptName(role.userDeptId);
406
+                List<Integer> trendData = buildTrendData(xAxis, details, sdf);
407
+                Map<String, Object> seriesItem = new LinkedHashMap<>();
408
+                seriesItem.put("name", deptName);
409
+                seriesItem.put("deptId", role.userDeptId);
410
+                seriesItem.put("data", trendData);
411
+
412
+                Map<String, Object> trend = new LinkedHashMap<>();
413
+                trend.put("xAxis", xAxis);
414
+                trend.put("series", Collections.singletonList(seriesItem));
415
+                result.put("trend", trend);
416
+            }
417
+
418
+            return HttpResult.success(result);
419
+
420
+        } catch (Exception e) {
421
+            log.error("获取大类详情失败 categoryId={}", categoryId, e);
422
+            return HttpResult.error("获取大类详情失败:" + e.getMessage());
423
+        }
424
+    }
425
+
426
+    // =====================================================================
427
+    // 私有工具方法
428
+    // =====================================================================
429
+
430
+    /**
431
+     * 获取当前用户角色信息
432
+     * 美兰角色优先级:站长 > 大队长(经理/行政) > 主管 > 班组长 > 安检员
433
+     */
434
+    private RoleInfo getRoleInfo() {
435
+        LoginUser loginUser = SecurityUtils.getLoginUser();
436
+        List<String> roleKeys = loginUser.getUser().getRoles().stream()
437
+                .map(SysRole::getRoleKey).collect(Collectors.toList());
438
+        Long userDeptId = loginUser.getDeptId();
439
+        Long userId = loginUser.getUserId();
440
+
441
+        boolean isStation = userId == 1L
442
+                || roleKeys.contains(RoleTypeEnum.admin.getCode())
443
+                || roleKeys.contains(RoleTypeEnum.test.getCode())
444
+                || roleKeys.contains(RoleTypeEnum.zhijianke.getCode());
445
+        boolean isBrigade = !isStation && (roleKeys.contains(RoleTypeEnum.jingli.getCode())
446
+                || roleKeys.contains(RoleTypeEnum.xingzheng.getCode()));
447
+        boolean isKezhang = !isStation && !isBrigade && roleKeys.contains(RoleTypeEnum.kezhang.getCode());
448
+
449
+        RoleInfo info = new RoleInfo();
450
+        info.isStation = isStation;
451
+        info.isBrigade = isBrigade;
452
+        info.isKezhang = isKezhang;
453
+        info.userDeptId = userDeptId;
454
+        return info;
455
+    }
456
+
457
+    private static class RoleInfo {
458
+        boolean isStation;
459
+        boolean isBrigade;
460
+        boolean isKezhang;
461
+        Long userDeptId;
462
+    }
463
+
464
+    /**
465
+     * 根据角色获取错误答题明细
466
+     */
467
+    private List<DailyTaskDetail> getErrorDetails(RoleInfo role, LocalDate start, LocalDate end) {
468
+        if (role.isStation) {
469
+            // 站长:站下所有子部门的任务(dtDeptId匹配)
470
+            List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(role.userDeptId);
471
+            Set<Long> allDeptIds = new HashSet<>();
472
+            allDeptIds.add(role.userDeptId);
473
+            if (subDepts != null) {
474
+                subDepts.forEach(d -> allDeptIds.add(d.getDeptId()));
475
+            }
476
+            return getErrorDetailsByDeptIds(allDeptIds, start, end);
477
+        } else if (role.isBrigade) {
478
+            // 大队长:本大队下所有子部门的任务(dtDeptId匹配)
479
+            List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(role.userDeptId);
480
+            Set<Long> allDeptIds = new HashSet<>();
481
+            allDeptIds.add(role.userDeptId);
482
+            if (subDepts != null) {
483
+                subDepts.forEach(d -> allDeptIds.add(d.getDeptId()));
484
+            }
485
+            return getErrorDetailsByDeptIds(allDeptIds, start, end);
486
+        } else if (role.isKezhang) {
487
+            // 主管:本主管室(dtDepartmentId匹配)
488
+            return getErrorDetailsByDepartmentId(role.userDeptId, start, end);
489
+        } else {
490
+            // 班组长:本班组(dtDeptId匹配)
491
+            return getErrorDetailsByDeptId(role.userDeptId, start, end);
492
+        }
493
+    }
494
+
495
+    /**
496
+     * 按 dt_dept_id 集合查询错误明细
497
+     */
498
+    private List<DailyTaskDetail> getErrorDetailsByDeptIds(Set<Long> deptIds, LocalDate start, LocalDate end) {
499
+        if (deptIds == null || deptIds.isEmpty()) return Collections.emptyList();
500
+        LambdaQueryWrapper<DailyTask> taskWrapper = new LambdaQueryWrapper<>();
501
+        taskWrapper.in(DailyTask::getDtDeptId, deptIds);
502
+        taskWrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
503
+        taskWrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
504
+        taskWrapper.eq(DailyTask::getDelStatus, 0);
505
+        List<DailyTask> tasks = dailyTaskMapper.selectList(taskWrapper);
506
+        return getErrorDetailsFromTasks(tasks);
507
+    }
508
+
509
+    /**
510
+     * 按 dt_department_id(主管级别)查询错误明细
511
+     */
512
+    private List<DailyTaskDetail> getErrorDetailsByDepartmentId(Long departmentId, LocalDate start, LocalDate end) {
513
+        LambdaQueryWrapper<DailyTask> taskWrapper = new LambdaQueryWrapper<>();
514
+        taskWrapper.eq(DailyTask::getDtDepartmentId, departmentId);
515
+        taskWrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
516
+        taskWrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
517
+        taskWrapper.eq(DailyTask::getDelStatus, 0);
518
+        List<DailyTask> tasks = dailyTaskMapper.selectList(taskWrapper);
519
+        return getErrorDetailsFromTasks(tasks);
520
+    }
521
+
522
+    /**
523
+     * 按 dt_department_id + 指定 qcId 集合查询错误明细
524
+     */
525
+    private List<DailyTaskDetail> getErrorDetailsByDepartmentIdAndQcIds(
526
+            Long departmentId, Set<String> qcIds, LocalDate start, LocalDate end) {
527
+        List<DailyTaskDetail> all = getErrorDetailsByDepartmentId(departmentId, start, end);
528
+        if (qcIds == null || qcIds.isEmpty()) return all;
529
+        return all.stream()
530
+                .filter(d -> qcIds.contains(d.getDtdModuleId()))
531
+                .collect(Collectors.toList());
532
+    }
533
+
534
+    /**
535
+     * 按 dt_dept_id(班组级别)查询错误明细
536
+     */
537
+    private List<DailyTaskDetail> getErrorDetailsByDeptId(Long deptId, LocalDate start, LocalDate end) {
538
+        LambdaQueryWrapper<DailyTask> taskWrapper = new LambdaQueryWrapper<>();
539
+        taskWrapper.eq(DailyTask::getDtDeptId, deptId);
540
+        taskWrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
541
+        taskWrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
542
+        taskWrapper.eq(DailyTask::getDelStatus, 0);
543
+        List<DailyTask> tasks = dailyTaskMapper.selectList(taskWrapper);
544
+        return getErrorDetailsFromTasks(tasks);
545
+    }
546
+
547
+    /**
548
+     * 按上级部门ID(大队)查询其下所有主管的错误明细
549
+     * 通过查询大队的直接下级主管,再按 dtDepartmentId 匹配
550
+     */
551
+    private List<DailyTaskDetail> getErrorDetailsBySubDepts(Long parentDeptId, LocalDate start, LocalDate end) {
552
+        List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(parentDeptId);
553
+        if (subDepts == null || subDepts.isEmpty()) return Collections.emptyList();
554
+        Set<Long> managerIds = subDepts.stream()
555
+                .filter(d -> parentDeptId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
556
+                .map(SysDept::getDeptId)
557
+                .collect(Collectors.toSet());
558
+        if (managerIds.isEmpty()) return Collections.emptyList();
559
+
560
+        LambdaQueryWrapper<DailyTask> taskWrapper = new LambdaQueryWrapper<>();
561
+        taskWrapper.in(DailyTask::getDtDepartmentId, managerIds);
562
+        taskWrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
563
+        taskWrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
564
+        taskWrapper.eq(DailyTask::getDelStatus, 0);
565
+        List<DailyTask> tasks = dailyTaskMapper.selectList(taskWrapper);
566
+        return getErrorDetailsFromTasks(tasks);
567
+    }
568
+
569
+    /**
570
+     * 按上级部门ID(大队)查询其下所有主管的错误明细,并按 qcId 过滤
571
+     */
572
+    private List<DailyTaskDetail> getErrorDetailsBySubDeptsAndQcIds(
573
+            Long parentDeptId, Set<String> qcIds, LocalDate start, LocalDate end) {
574
+        List<DailyTaskDetail> all = getErrorDetailsBySubDepts(parentDeptId, start, end);
575
+        if (qcIds == null || qcIds.isEmpty()) return all;
576
+        return all.stream()
577
+                .filter(d -> qcIds.contains(d.getDtdModuleId()))
578
+                .collect(Collectors.toList());
579
+    }
580
+
581
+    /**
582
+     * 从任务列表中查询错误答题明细(dtdIsCorrect=false)
583
+     */
584
+    private List<DailyTaskDetail> getErrorDetailsFromTasks(List<DailyTask> tasks) {
585
+        if (tasks == null || tasks.isEmpty()) return Collections.emptyList();
586
+        Set<String> taskIds = tasks.stream().map(DailyTask::getDtId).collect(Collectors.toSet());
587
+
588
+        LambdaQueryWrapper<DailyTaskDetail> detailWrapper = new LambdaQueryWrapper<>();
589
+        detailWrapper.in(DailyTaskDetail::getDtdTaskId, taskIds);
590
+        detailWrapper.eq(DailyTaskDetail::getDtdIsCorrect, false);
591
+        detailWrapper.eq(DailyTaskDetail::getDelStatus, 0);
592
+        return dailyTaskDetailMapper.selectList(detailWrapper);
593
+    }
594
+
595
+    /**
596
+     * 构建 qcId -> level1 BaseCheckCategory 的映射(带缓存,避免重复查询)
597
+     */
598
+    private Map<String, BaseCheckCategory> buildQcIdToLevel1Map(List<DailyTaskDetail> details) {
599
+        Map<String, BaseCheckCategory> result = new HashMap<>();
600
+        Map<String, BaseCheckCategory> qcNameToLevel1Cache = new HashMap<>();
601
+
602
+        for (DailyTaskDetail detail : details) {
603
+            String qcId = detail.getDtdModuleId();
604
+            if (qcId == null || qcId.trim().isEmpty() || result.containsKey(qcId)) continue;
605
+
606
+            LambdaQueryWrapper<QuesCat> catWrapper = new LambdaQueryWrapper<>();
607
+            catWrapper.eq(QuesCat::getQcId, qcId);
608
+            catWrapper.eq(QuesCat::getDelStatus, 0);
609
+            QuesCat quesCat = quesCatService.getOne(catWrapper);
610
+
611
+            if (quesCat == null) {
612
+                result.put(qcId, null);
613
+                continue;
614
+            }
615
+
616
+            String qcName = quesCat.getQcName();
617
+            if (qcNameToLevel1Cache.containsKey(qcName)) {
618
+                result.put(qcId, qcNameToLevel1Cache.get(qcName));
619
+            } else {
620
+                BaseCheckCategory level1 = baseCheckCategoryService.selectLevel1ByLevel2Name(qcName);
621
+                qcNameToLevel1Cache.put(qcName, level1);
622
+                result.put(qcId, level1);
623
+            }
624
+        }
625
+        return result;
626
+    }
627
+
628
+    /**
629
+     * 构建小类名称 -> qcId 的映射
630
+     */
631
+    private Map<String, String> buildSubCatNameToQcIdMap(List<String> subCatNames) {
632
+        Map<String, String> result = new HashMap<>();
633
+        for (String name : subCatNames) {
634
+            LambdaQueryWrapper<QuesCat> wrapper = new LambdaQueryWrapper<>();
635
+            wrapper.eq(QuesCat::getQcName, name);
636
+            wrapper.eq(QuesCat::getDelStatus, 0);
637
+            wrapper.last("LIMIT 1");
638
+            QuesCat cat = quesCatService.getOne(wrapper);
639
+            result.put(name, cat != null ? cat.getQcId() : null);
640
+        }
641
+        return result;
642
+    }
643
+
644
+    /**
645
+     * 构建折线趋势数据,与 xAxis 对齐
646
+     */
647
+    private List<Integer> buildTrendData(List<String> xAxis, List<DailyTaskDetail> details, SimpleDateFormat sdf) {
648
+        if (details == null || details.isEmpty()) {
649
+            return xAxis.stream().map(x -> 0).collect(Collectors.toList());
650
+        }
651
+        Set<String> taskIds = details.stream().map(DailyTaskDetail::getDtdTaskId).collect(Collectors.toSet());
652
+        LambdaQueryWrapper<DailyTask> taskWrapper = new LambdaQueryWrapper<>();
653
+        taskWrapper.in(DailyTask::getDtId, taskIds);
654
+        taskWrapper.eq(DailyTask::getDelStatus, 0);
655
+        List<DailyTask> tasks = dailyTaskMapper.selectList(taskWrapper);
656
+        Map<String, String> taskIdToDate = tasks.stream()
657
+                .filter(t -> t.getDtBusinessDate() != null)
658
+                .collect(Collectors.toMap(DailyTask::getDtId, t -> sdf.format(t.getDtBusinessDate()), (a, b) -> a));
659
+
660
+        Map<String, Long> countByDate = details.stream()
661
+                .filter(d -> taskIdToDate.containsKey(d.getDtdTaskId()))
662
+                .collect(Collectors.groupingBy(d -> taskIdToDate.get(d.getDtdTaskId()), Collectors.counting()));
663
+
664
+        return xAxis.stream()
665
+                .map(label -> countByDate.getOrDefault(label, 0L).intValue())
666
+                .collect(Collectors.toList());
667
+    }
668
+
669
+    /**
670
+     * 根据 timeType 计算起止日期(供 overview/radar 使用)
671
+     */
672
+    private LocalDate[] resolveDateRange(String timeType, String startDate, String endDate) {
673
+        LocalDate today = LocalDate.now();
674
+        LocalDate start;
675
+        LocalDate end = today;
676
+        switch (timeType == null ? "month" : timeType) {
677
+            case "year":
678
+                start = today.withDayOfYear(1);
679
+                end = today.withMonth(12).withDayOfMonth(31);
680
+                break;
681
+            case "custom":
682
+                start = (startDate != null) ? LocalDate.parse(startDate) : today.withDayOfMonth(1);
683
+                end = (endDate != null) ? LocalDate.parse(endDate) : today;
684
+                break;
685
+            default: // month
686
+                start = today.withDayOfMonth(1);
687
+                break;
688
+        }
689
+        return new LocalDate[]{start, end};
690
+    }
691
+
692
+    /**
693
+     * 根据 SysAnalysisReportParamDto 计算起止日期(供 category-detail 使用)
694
+     * YEAR → 全年;QUARTER → 指定季度;MONTH → 指定月份;默认 → 当月
695
+     */
696
+    private LocalDate[] resolveDateRange(SysAnalysisReportParamDto param) {
697
+        LocalDate today = LocalDate.now();
698
+        int year = (param != null && param.getYear() != null) ? param.getYear() : today.getYear();
699
+        String type = (param != null && param.getDateRangeQueryType() != null) ? param.getDateRangeQueryType() : "MONTH";
700
+        com.sundot.airport.common.core.domain.SysAnalysisReportDateRangeDto range;
701
+        switch (type) {
702
+            case "YEAR":
703
+                range = DateRangeQueryUtils.getYearRange(year, null);
704
+                break;
705
+            case "QUARTER":
706
+                int quarter = (param.getQuarter() != null) ? param.getQuarter() : 1;
707
+                range = DateRangeQueryUtils.getQuarterRange(year, quarter, null);
708
+                break;
709
+            default: // MONTH
710
+                int month = (param != null && param.getMonth() != null) ? param.getMonth() : today.getMonthValue();
711
+                range = DateRangeQueryUtils.getMonthRange(year, month, null);
712
+                break;
713
+        }
714
+        return new LocalDate[]{
715
+                ((java.sql.Date) range.getStartDate()).toLocalDate(),
716
+                ((java.sql.Date) range.getEndDate()).toLocalDate()
717
+        };
718
+    }
719
+
720
+    /**
721
+     * 生成 X 轴标签列表
722
+     */
723
+    private List<String> buildXAxis(LocalDate start, LocalDate end, boolean byMonth) {
724
+        List<String> xAxis = new ArrayList<>();
725
+        if (byMonth) {
726
+            LocalDate cursor = start.withDayOfMonth(1);
727
+            LocalDate endMonth = end.withDayOfMonth(1);
728
+            while (!cursor.isAfter(endMonth)) {
729
+                xAxis.add(cursor.format(DateTimeFormatter.ofPattern("yyyy-MM")));
730
+                cursor = cursor.plusMonths(1);
731
+            }
732
+        } else {
733
+            LocalDate cursor = start;
734
+            while (!cursor.isAfter(end)) {
735
+                xAxis.add(cursor.toString());
736
+                cursor = cursor.plusDays(1);
737
+            }
738
+        }
739
+        return xAxis;
740
+    }
741
+
742
+    /**
743
+     * 获取部门名称
744
+     */
745
+    private String getDeptName(Long deptId) {
746
+        try {
747
+            LoginUser loginUser = SecurityUtils.getLoginUser();
748
+            if (loginUser.getUser().getDept() != null) {
749
+                return loginUser.getUser().getDept().getDeptName();
750
+            }
751
+        } catch (Exception ignored) {
752
+        }
753
+        return "本主管";
754
+    }
755
+}

+ 542 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/DailyExamWrongAnalysisPcController.java

@@ -0,0 +1,542 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.sundot.airport.common.core.domain.entity.SysDept;
5
+import com.sundot.airport.common.core.domain.entity.SysRole;
6
+import com.sundot.airport.common.core.domain.model.LoginUser;
7
+import com.sundot.airport.common.enums.RoleTypeEnum;
8
+import com.sundot.airport.common.utils.SecurityUtils;
9
+import com.sundot.airport.exam.common.HttpResult;
10
+import com.sundot.airport.exam.common.service.QuesCatService;
11
+import com.sundot.airport.exam.domain.DailyTask;
12
+import com.sundot.airport.exam.domain.DailyTaskDetail;
13
+import com.sundot.airport.exam.domain.QuesCat;
14
+import com.sundot.airport.exam.mapper.DailyTaskDetailMapper;
15
+import com.sundot.airport.exam.mapper.DailyTaskMapper;
16
+import com.sundot.airport.system.domain.BaseCheckCategory;
17
+import com.sundot.airport.system.mapper.SysDeptMapper;
18
+import com.sundot.airport.system.service.IBaseCheckCategoryService;
19
+import org.slf4j.Logger;
20
+import org.slf4j.LoggerFactory;
21
+import org.springframework.beans.factory.annotation.Autowired;
22
+import org.springframework.web.bind.annotation.GetMapping;
23
+import org.springframework.web.bind.annotation.RequestMapping;
24
+import org.springframework.web.bind.annotation.RequestParam;
25
+import org.springframework.web.bind.annotation.RestController;
26
+
27
+import java.sql.Date;
28
+import java.time.LocalDate;
29
+import java.util.*;
30
+import java.util.stream.Collectors;
31
+
32
+/**
33
+ * 抽问抽答错题分析 PC管理端接口(美兰4级架构)
34
+ * 美兰组织架构:STATION(安检站)> BRIGADE(大队)> MANAGER(主管)> 班组
35
+ *
36
+ * 接口1: GET /wrong-analysis/pc-overview  总体问题分布(饼图),支持 scopeType/scopeId
37
+ * 接口2: GET /wrong-analysis/pc-radar     问题分布对比(雷达图),支持 scopeType/scopeId
38
+ *
39
+ * scopeType 说明:
40
+ *   STATION  - 全站(scopeId=安检站deptId,可选)
41
+ *   BRIGADE  - 大队(scopeId=大队deptId)
42
+ *   MANAGER  - 主管(scopeId=主管deptId)
43
+ *   USER     - 个人(scopeId=userId)
44
+ *   不传     - 按登录用户角色自动判断(与 app 端 /wrong-analysis 相同逻辑)
45
+ *
46
+ * pc-radar scopeType 对应 series 规则:
47
+ *   STATION → 各大队各一条 series
48
+ *   BRIGADE → 该大队下各主管各一条 series
49
+ *   MANAGER → 单条主管 series
50
+ *   USER    → 单条用户 series
51
+ *   不传    → 站长:各大队 series;其他角色:show=false
52
+ */
53
+@RestController
54
+@RequestMapping("/v1/cs/app/daily-exam/wrong-analysis")
55
+public class DailyExamWrongAnalysisPcController {
56
+
57
+    private static final Logger log = LoggerFactory.getLogger(DailyExamWrongAnalysisPcController.class);
58
+
59
+    @Autowired
60
+    private DailyTaskMapper dailyTaskMapper;
61
+
62
+    @Autowired
63
+    private DailyTaskDetailMapper dailyTaskDetailMapper;
64
+
65
+    @Autowired
66
+    private SysDeptMapper sysDeptMapper;
67
+
68
+    @Autowired
69
+    private QuesCatService quesCatService;
70
+
71
+    @Autowired
72
+    private IBaseCheckCategoryService baseCheckCategoryService;
73
+
74
+    // =====================================================================
75
+    // 接口1:总体问题分布(饼图)PC端
76
+    // =====================================================================
77
+
78
+    /**
79
+     * 总体问题分布(按大类统计错题数)
80
+     *
81
+     * @param scopeType 统计范围:STATION/BRIGADE/MANAGER/USER,不传则按角色自动判断
82
+     * @param scopeId   范围ID
83
+     */
84
+    @GetMapping("/pc-overview")
85
+    public HttpResult<Map<String, Object>> getPcOverview(
86
+            @RequestParam(required = false, defaultValue = "MONTH") String dateRangeQueryType,
87
+            @RequestParam(required = false) Integer year,
88
+            @RequestParam(required = false) Integer quarter,
89
+            @RequestParam(required = false) Integer month,
90
+            @RequestParam(required = false) String scopeType,
91
+            @RequestParam(required = false) Long scopeId) {
92
+
93
+        try {
94
+            LocalDate[] range = resolveDateRange(dateRangeQueryType, year, quarter, month);
95
+            List<DailyTaskDetail> errorDetails = getErrorDetailsByScope(scopeType, scopeId, range[0], range[1]);
96
+
97
+            Map<String, BaseCheckCategory> qcIdToLevel1 = buildQcIdToLevel1Map(errorDetails);
98
+
99
+            Map<Long, long[]> countByCategory = new LinkedHashMap<>();
100
+            Map<Long, String> categoryNames = new LinkedHashMap<>();
101
+
102
+            for (DailyTaskDetail detail : errorDetails) {
103
+                BaseCheckCategory cat = qcIdToLevel1.get(detail.getDtdModuleId());
104
+                if (cat != null) {
105
+                    countByCategory.merge(cat.getId(),
106
+                            new long[]{1, cat.getOrderNum() != null ? cat.getOrderNum() : 99},
107
+                            (a, b) -> new long[]{a[0] + 1, a[1]});
108
+                    categoryNames.put(cat.getId(), cat.getName());
109
+                }
110
+            }
111
+
112
+            List<Map<String, Object>> categories = countByCategory.entrySet().stream()
113
+                    .sorted(Comparator.comparingLong(e -> e.getValue()[1]))
114
+                    .map(e -> {
115
+                        Map<String, Object> item = new LinkedHashMap<>();
116
+                        item.put("id", e.getKey());
117
+                        item.put("name", categoryNames.get(e.getKey()));
118
+                        item.put("count", e.getValue()[0]);
119
+                        return item;
120
+                    })
121
+                    .collect(Collectors.toList());
122
+
123
+            Map<String, Object> result = new LinkedHashMap<>();
124
+            result.put("categories", categories);
125
+            return HttpResult.success(result);
126
+
127
+        } catch (Exception e) {
128
+            log.error("获取总体问题分布失败(PC)", e);
129
+            return HttpResult.error("获取总体问题分布失败:" + e.getMessage());
130
+        }
131
+    }
132
+
133
+    // =====================================================================
134
+    // 接口2:问题分布对比(雷达图)PC端
135
+    // =====================================================================
136
+
137
+    /**
138
+     * 问题分布对比雷达图
139
+     *
140
+     * @param scopeType 统计范围:STATION/BRIGADE/MANAGER/USER,不传则按角色自动判断
141
+     * @param scopeId   范围ID
142
+     */
143
+    @GetMapping("/pc-radar")
144
+    public HttpResult<Map<String, Object>> getPcRadar(
145
+            @RequestParam(required = false, defaultValue = "MONTH") String dateRangeQueryType,
146
+            @RequestParam(required = false) Integer year,
147
+            @RequestParam(required = false) Integer quarter,
148
+            @RequestParam(required = false) Integer month,
149
+            @RequestParam(required = false) String scopeType,
150
+            @RequestParam(required = false) Long scopeId) {
151
+
152
+        try {
153
+            LocalDate[] range = resolveDateRange(dateRangeQueryType, year, quarter, month);
154
+
155
+            // 查询所有大类作为雷达维度
156
+            BaseCheckCategory level1Query = new BaseCheckCategory();
157
+            level1Query.setLevel(1);
158
+            List<BaseCheckCategory> level1Categories = baseCheckCategoryService.selectBaseCheckCategoryList(level1Query);
159
+            level1Categories.sort(Comparator.comparingInt(c -> c.getOrderNum() != null ? c.getOrderNum() : 99));
160
+
161
+            List<String> indicators = level1Categories.stream()
162
+                    .map(BaseCheckCategory::getName).collect(Collectors.toList());
163
+            List<Long> indicatorIds = level1Categories.stream()
164
+                    .map(BaseCheckCategory::getId).collect(Collectors.toList());
165
+
166
+            List<Map<String, Object>> series;
167
+
168
+            if ("BRIGADE".equals(scopeType) && scopeId != null) {
169
+                // 大队下各主管各一条 series
170
+                series = buildRadarSeriesByManagersInBrigade(scopeId, range, indicatorIds);
171
+
172
+            } else if ("MANAGER".equals(scopeType) && scopeId != null) {
173
+                // 单条主管 series
174
+                series = buildRadarSeriesForSingleManager(scopeId, range, indicatorIds);
175
+
176
+            } else if ("USER".equals(scopeType) && scopeId != null) {
177
+                // 单条用户 series
178
+                series = buildRadarSeriesForSingleUser(scopeId, range, indicatorIds);
179
+
180
+            } else {
181
+                // STATION 或 role-based:各大队各一条 series
182
+                Long stationDeptId = getStationDeptId(scopeType, scopeId);
183
+                if (stationDeptId == null) {
184
+                    Map<String, Object> noShow = new HashMap<>();
185
+                    noShow.put("show", false);
186
+                    return HttpResult.success(noShow);
187
+                }
188
+                series = buildRadarSeriesByBrigadesInStation(stationDeptId, range, indicatorIds);
189
+            }
190
+
191
+            Map<String, Object> result = new LinkedHashMap<>();
192
+            result.put("show", true);
193
+            result.put("indicators", indicators);
194
+            result.put("series", series);
195
+            return HttpResult.success(result);
196
+
197
+        } catch (Exception e) {
198
+            log.error("获取问题分布对比失败(PC)", e);
199
+            return HttpResult.error("获取问题分布对比失败:" + e.getMessage());
200
+        }
201
+    }
202
+
203
+    // =====================================================================
204
+    // 私有工具方法
205
+    // =====================================================================
206
+
207
+    /**
208
+     * 根据 scopeType/scopeId 获取错误明细
209
+     */
210
+    private List<DailyTaskDetail> getErrorDetailsByScope(String scopeType, Long scopeId,
211
+                                                          LocalDate start, LocalDate end) {
212
+        if ("MANAGER".equals(scopeType) && scopeId != null) {
213
+            return getErrorDetailsByDepartmentId(scopeId, start, end);
214
+        } else if ("USER".equals(scopeType) && scopeId != null) {
215
+            return getErrorDetailsByUserId(scopeId, start, end);
216
+        } else if ("BRIGADE".equals(scopeType) && scopeId != null) {
217
+            return getErrorDetailsBySubDepts(scopeId, start, end);
218
+        } else {
219
+            // STATION 或 role-based
220
+            Long deptId = getStationDeptId(scopeType, scopeId);
221
+            if (deptId == null) {
222
+                // 非站长,按角色判断
223
+                RoleInfo role = getRoleInfo();
224
+                if (role.isBrigade) {
225
+                    return getErrorDetailsBySubDepts(role.userDeptId, start, end);
226
+                } else if (role.isKezhang) {
227
+                    return getErrorDetailsByDepartmentId(role.userDeptId, start, end);
228
+                } else {
229
+                    return getErrorDetailsByDeptId(role.userDeptId, start, end);
230
+                }
231
+            }
232
+            List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(deptId);
233
+            Set<Long> allDeptIds = new HashSet<>();
234
+            allDeptIds.add(deptId);
235
+            if (subDepts != null) subDepts.forEach(d -> allDeptIds.add(d.getDeptId()));
236
+            return getErrorDetailsByDeptIds(allDeptIds, start, end);
237
+        }
238
+    }
239
+
240
+    /**
241
+     * 获取站级 deptId:
242
+     * - scopeType=STATION → 取 scopeId(若有)或当前用户 deptId(需是站长角色)
243
+     * - scopeType=null → 若当前用户是站长则取其 deptId,否则返回 null
244
+     */
245
+    private Long getStationDeptId(String scopeType, Long scopeId) {
246
+        if ("STATION".equals(scopeType)) {
247
+            if (scopeId != null) return scopeId;
248
+            return SecurityUtils.getLoginUser().getDeptId();
249
+        }
250
+        RoleInfo role = getRoleInfo();
251
+        if (role.isStation) return role.userDeptId;
252
+        return null;
253
+    }
254
+
255
+    /**
256
+     * 雷达图:站下各大队各一条 series
257
+     */
258
+    private List<Map<String, Object>> buildRadarSeriesByBrigadesInStation(
259
+            Long stationDeptId, LocalDate[] range, List<Long> indicatorIds) {
260
+
261
+        List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(stationDeptId);
262
+        List<SysDept> brigadeDepts = subDepts == null ? Collections.emptyList() :
263
+                subDepts.stream()
264
+                        .filter(d -> stationDeptId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
265
+                        .collect(Collectors.toList());
266
+
267
+        List<Map<String, Object>> series = new ArrayList<>();
268
+        for (SysDept brigade : brigadeDepts) {
269
+            List<DailyTaskDetail> details = getErrorDetailsBySubDepts(brigade.getDeptId(), range[0], range[1]);
270
+            series.add(buildRadarSeriesItem(brigade.getDeptName(), brigade.getDeptId(), details, indicatorIds));
271
+        }
272
+        return series;
273
+    }
274
+
275
+    /**
276
+     * 雷达图:大队下各主管各一条 series
277
+     */
278
+    private List<Map<String, Object>> buildRadarSeriesByManagersInBrigade(
279
+            Long brigadeId, LocalDate[] range, List<Long> indicatorIds) {
280
+
281
+        List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(brigadeId);
282
+        List<SysDept> managerDepts = subDepts == null ? Collections.emptyList() :
283
+                subDepts.stream()
284
+                        .filter(d -> brigadeId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
285
+                        .collect(Collectors.toList());
286
+
287
+        List<Map<String, Object>> series = new ArrayList<>();
288
+        for (SysDept manager : managerDepts) {
289
+            List<DailyTaskDetail> details = getErrorDetailsByDepartmentId(manager.getDeptId(), range[0], range[1]);
290
+            series.add(buildRadarSeriesItem(manager.getDeptName(), manager.getDeptId(), details, indicatorIds));
291
+        }
292
+        return series;
293
+    }
294
+
295
+    /**
296
+     * 雷达图:单条主管 series
297
+     */
298
+    private List<Map<String, Object>> buildRadarSeriesForSingleManager(
299
+            Long managerId, LocalDate[] range, List<Long> indicatorIds) {
300
+
301
+        List<DailyTaskDetail> details = getErrorDetailsByDepartmentId(managerId, range[0], range[1]);
302
+        String name = getManagerName(managerId);
303
+        return Collections.singletonList(buildRadarSeriesItem(name, managerId, details, indicatorIds));
304
+    }
305
+
306
+    /**
307
+     * 雷达图:单条用户 series
308
+     */
309
+    private List<Map<String, Object>> buildRadarSeriesForSingleUser(
310
+            Long userId, LocalDate[] range, List<Long> indicatorIds) {
311
+
312
+        List<DailyTaskDetail> details = getErrorDetailsByUserId(userId, range[0], range[1]);
313
+        String name = getUserName(userId, details);
314
+        Map<String, Object> item = buildRadarSeriesItemWithUserId(name, userId, details, indicatorIds);
315
+        return Collections.singletonList(item);
316
+    }
317
+
318
+    /**
319
+     * 构建雷达图单条 series(deptId)
320
+     */
321
+    private Map<String, Object> buildRadarSeriesItem(String name, Long deptId,
322
+                                                      List<DailyTaskDetail> details,
323
+                                                      List<Long> indicatorIds) {
324
+        Map<String, BaseCheckCategory> qcIdToLevel1 = buildQcIdToLevel1Map(details);
325
+        Map<Long, Long> countMap = details.stream()
326
+                .filter(d -> qcIdToLevel1.get(d.getDtdModuleId()) != null)
327
+                .collect(Collectors.groupingBy(
328
+                        d -> qcIdToLevel1.get(d.getDtdModuleId()).getId(),
329
+                        Collectors.counting()));
330
+
331
+        List<Long> data = indicatorIds.stream()
332
+                .map(id -> countMap.getOrDefault(id, 0L))
333
+                .collect(Collectors.toList());
334
+
335
+        Map<String, Object> s = new LinkedHashMap<>();
336
+        s.put("name", name);
337
+        s.put("deptId", deptId);
338
+        s.put("data", data);
339
+        return s;
340
+    }
341
+
342
+    /**
343
+     * 构建雷达图单条 series(userId)
344
+     */
345
+    private Map<String, Object> buildRadarSeriesItemWithUserId(String name, Long userId,
346
+                                                                List<DailyTaskDetail> details,
347
+                                                                List<Long> indicatorIds) {
348
+        Map<String, BaseCheckCategory> qcIdToLevel1 = buildQcIdToLevel1Map(details);
349
+        Map<Long, Long> countMap = details.stream()
350
+                .filter(d -> qcIdToLevel1.get(d.getDtdModuleId()) != null)
351
+                .collect(Collectors.groupingBy(
352
+                        d -> qcIdToLevel1.get(d.getDtdModuleId()).getId(),
353
+                        Collectors.counting()));
354
+
355
+        List<Long> data = indicatorIds.stream()
356
+                .map(id -> countMap.getOrDefault(id, 0L))
357
+                .collect(Collectors.toList());
358
+
359
+        Map<String, Object> s = new LinkedHashMap<>();
360
+        s.put("name", name);
361
+        s.put("userId", userId);
362
+        s.put("data", data);
363
+        return s;
364
+    }
365
+
366
+    private RoleInfo getRoleInfo() {
367
+        LoginUser loginUser = SecurityUtils.getLoginUser();
368
+        List<String> roleKeys = loginUser.getUser().getRoles().stream()
369
+                .map(SysRole::getRoleKey).collect(Collectors.toList());
370
+        Long userDeptId = loginUser.getDeptId();
371
+        Long userId = loginUser.getUserId();
372
+
373
+        boolean isStation = userId == 1L
374
+                || roleKeys.contains(RoleTypeEnum.admin.getCode())
375
+                || roleKeys.contains(RoleTypeEnum.test.getCode())
376
+                || roleKeys.contains(RoleTypeEnum.zhijianke.getCode());
377
+        boolean isBrigade = !isStation && (roleKeys.contains(RoleTypeEnum.jingli.getCode())
378
+                || roleKeys.contains(RoleTypeEnum.xingzheng.getCode()));
379
+        boolean isKezhang = !isStation && !isBrigade && roleKeys.contains(RoleTypeEnum.kezhang.getCode());
380
+
381
+        RoleInfo info = new RoleInfo();
382
+        info.isStation = isStation;
383
+        info.isBrigade = isBrigade;
384
+        info.isKezhang = isKezhang;
385
+        info.userDeptId = userDeptId;
386
+        return info;
387
+    }
388
+
389
+    private static class RoleInfo {
390
+        boolean isStation;
391
+        boolean isBrigade;
392
+        boolean isKezhang;
393
+        Long userDeptId;
394
+    }
395
+
396
+    private List<DailyTaskDetail> getErrorDetailsByDeptIds(Set<Long> deptIds, LocalDate start, LocalDate end) {
397
+        if (deptIds == null || deptIds.isEmpty()) return Collections.emptyList();
398
+        LambdaQueryWrapper<DailyTask> taskWrapper = new LambdaQueryWrapper<>();
399
+        taskWrapper.in(DailyTask::getDtDeptId, deptIds);
400
+        taskWrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
401
+        taskWrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
402
+        taskWrapper.eq(DailyTask::getDelStatus, 0);
403
+        List<DailyTask> tasks = dailyTaskMapper.selectList(taskWrapper);
404
+        return getErrorDetailsFromTasks(tasks);
405
+    }
406
+
407
+    private List<DailyTaskDetail> getErrorDetailsByDepartmentId(Long departmentId, LocalDate start, LocalDate end) {
408
+        LambdaQueryWrapper<DailyTask> taskWrapper = new LambdaQueryWrapper<>();
409
+        taskWrapper.eq(DailyTask::getDtDepartmentId, departmentId);
410
+        taskWrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
411
+        taskWrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
412
+        taskWrapper.eq(DailyTask::getDelStatus, 0);
413
+        List<DailyTask> tasks = dailyTaskMapper.selectList(taskWrapper);
414
+        return getErrorDetailsFromTasks(tasks);
415
+    }
416
+
417
+    private List<DailyTaskDetail> getErrorDetailsByDeptId(Long deptId, LocalDate start, LocalDate end) {
418
+        LambdaQueryWrapper<DailyTask> taskWrapper = new LambdaQueryWrapper<>();
419
+        taskWrapper.eq(DailyTask::getDtDeptId, deptId);
420
+        taskWrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
421
+        taskWrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
422
+        taskWrapper.eq(DailyTask::getDelStatus, 0);
423
+        List<DailyTask> tasks = dailyTaskMapper.selectList(taskWrapper);
424
+        return getErrorDetailsFromTasks(tasks);
425
+    }
426
+
427
+    private List<DailyTaskDetail> getErrorDetailsByUserId(Long userId, LocalDate start, LocalDate end) {
428
+        LambdaQueryWrapper<DailyTask> taskWrapper = new LambdaQueryWrapper<>();
429
+        taskWrapper.eq(DailyTask::getDtUserId, userId);
430
+        taskWrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
431
+        taskWrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
432
+        taskWrapper.eq(DailyTask::getDelStatus, 0);
433
+        List<DailyTask> tasks = dailyTaskMapper.selectList(taskWrapper);
434
+        return getErrorDetailsFromTasks(tasks);
435
+    }
436
+
437
+    /**
438
+     * 查询上级部门(大队)下所有直接子部门(主管)的错误明细
439
+     */
440
+    private List<DailyTaskDetail> getErrorDetailsBySubDepts(Long parentDeptId, LocalDate start, LocalDate end) {
441
+        List<SysDept> subDepts = sysDeptMapper.selectChildrenDeptById(parentDeptId);
442
+        if (subDepts == null || subDepts.isEmpty()) return Collections.emptyList();
443
+        Set<Long> managerIds = subDepts.stream()
444
+                .filter(d -> parentDeptId.equals(d.getParentId()) && "0".equals(d.getDelFlag()))
445
+                .map(SysDept::getDeptId)
446
+                .collect(Collectors.toSet());
447
+        if (managerIds.isEmpty()) return Collections.emptyList();
448
+        LambdaQueryWrapper<DailyTask> taskWrapper = new LambdaQueryWrapper<>();
449
+        taskWrapper.in(DailyTask::getDtDepartmentId, managerIds);
450
+        taskWrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(start));
451
+        taskWrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(end));
452
+        taskWrapper.eq(DailyTask::getDelStatus, 0);
453
+        List<DailyTask> tasks = dailyTaskMapper.selectList(taskWrapper);
454
+        return getErrorDetailsFromTasks(tasks);
455
+    }
456
+
457
+    private List<DailyTaskDetail> getErrorDetailsFromTasks(List<DailyTask> tasks) {
458
+        if (tasks == null || tasks.isEmpty()) return Collections.emptyList();
459
+        Set<String> taskIds = tasks.stream().map(DailyTask::getDtId).collect(Collectors.toSet());
460
+        LambdaQueryWrapper<DailyTaskDetail> detailWrapper = new LambdaQueryWrapper<>();
461
+        detailWrapper.in(DailyTaskDetail::getDtdTaskId, taskIds);
462
+        detailWrapper.eq(DailyTaskDetail::getDtdIsCorrect, false);
463
+        detailWrapper.eq(DailyTaskDetail::getDelStatus, 0);
464
+        return dailyTaskDetailMapper.selectList(detailWrapper);
465
+    }
466
+
467
+    private Map<String, BaseCheckCategory> buildQcIdToLevel1Map(List<DailyTaskDetail> details) {
468
+        Map<String, BaseCheckCategory> result = new HashMap<>();
469
+        Map<String, BaseCheckCategory> qcNameToLevel1Cache = new HashMap<>();
470
+
471
+        for (DailyTaskDetail detail : details) {
472
+            String qcId = detail.getDtdModuleId();
473
+            if (qcId == null || qcId.trim().isEmpty() || result.containsKey(qcId)) continue;
474
+
475
+            LambdaQueryWrapper<QuesCat> catWrapper = new LambdaQueryWrapper<>();
476
+            catWrapper.eq(QuesCat::getQcId, qcId);
477
+            catWrapper.eq(QuesCat::getDelStatus, 0);
478
+            QuesCat quesCat = quesCatService.getOne(catWrapper);
479
+
480
+            if (quesCat == null) {
481
+                result.put(qcId, null);
482
+                continue;
483
+            }
484
+
485
+            String qcName = quesCat.getQcName();
486
+            if (qcNameToLevel1Cache.containsKey(qcName)) {
487
+                result.put(qcId, qcNameToLevel1Cache.get(qcName));
488
+            } else {
489
+                BaseCheckCategory level1 = baseCheckCategoryService.selectLevel1ByLevel2Name(qcName);
490
+                qcNameToLevel1Cache.put(qcName, level1);
491
+                result.put(qcId, level1);
492
+            }
493
+        }
494
+        return result;
495
+    }
496
+
497
+    private LocalDate[] resolveDateRange(String dateRangeQueryType, Integer year, Integer quarter, Integer month) {
498
+        LocalDate today = LocalDate.now();
499
+        int y = (year != null) ? year : today.getYear();
500
+        String type = (dateRangeQueryType != null) ? dateRangeQueryType : "MONTH";
501
+        LocalDate start, end;
502
+        switch (type) {
503
+            case "YEAR":
504
+                start = LocalDate.of(y, 1, 1);
505
+                end = LocalDate.of(y, 12, 31);
506
+                break;
507
+            case "QUARTER":
508
+                int q = (quarter != null) ? quarter : 1;
509
+                start = LocalDate.of(y, (q - 1) * 3 + 1, 1);
510
+                end = start.plusMonths(3).minusDays(1);
511
+                break;
512
+            case "MONTH":
513
+            default:
514
+                int m = (month != null) ? month : today.getMonthValue();
515
+                start = LocalDate.of(y, m, 1);
516
+                end = start.withDayOfMonth(start.lengthOfMonth());
517
+                break;
518
+        }
519
+        return new LocalDate[]{start, end};
520
+    }
521
+
522
+    private String getManagerName(Long managerId) {
523
+        try {
524
+            SysDept dept = sysDeptMapper.selectDeptById(managerId);
525
+            if (dept != null) return dept.getDeptName();
526
+        } catch (Exception ignored) {
527
+        }
528
+        return "主管" + managerId;
529
+    }
530
+
531
+    private String getUserName(Long userId, List<DailyTaskDetail> details) {
532
+        if (details.isEmpty()) return "用户" + userId;
533
+        Set<String> taskIds = details.stream().map(DailyTaskDetail::getDtdTaskId).collect(Collectors.toSet());
534
+        LambdaQueryWrapper<DailyTask> w = new LambdaQueryWrapper<>();
535
+        w.in(DailyTask::getDtId, taskIds);
536
+        w.eq(DailyTask::getDelStatus, 0);
537
+        w.last("LIMIT 1");
538
+        DailyTask task = dailyTaskMapper.selectOne(w);
539
+        if (task != null && task.getDtUserName() != null) return task.getDtUserName();
540
+        return "用户" + userId;
541
+    }
542
+}

+ 109 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/EighteenController.java

@@ -0,0 +1,109 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import com.sundot.airport.exam.common.ClientTypeConstant;
4
+import com.sundot.airport.exam.common.HttpResult;
5
+import com.sundot.airport.exam.common.PageData;
6
+import com.sundot.airport.exam.logic.eighteen.model.params.*;
7
+import com.sundot.airport.exam.logic.eighteen.model.result.*;
8
+import com.sundot.airport.exam.logic.eighteen.model.result.subresult.ItemIn18ExamRecordListResult;
9
+import com.sundot.airport.exam.logic.eighteen.service.EighteenService;
10
+import com.sundot.airport.exam.logic.eighteen.service.ExamOnlineService;
11
+import org.springframework.validation.annotation.Validated;
12
+import org.springframework.web.bind.annotation.*;
13
+
14
+import javax.annotation.Resource;
15
+
16
+/**
17
+ * <b>功能名:</b>EighteenController<br>
18
+ * <b>说明:</b> 十八大接口控制层 <br>
19
+ * <b>著作权:</b> Copyright (C) 2021 HUIFANEDU  CORPORATION<br>
20
+ * <b>修改履历:
21
+ *
22
+ * @author 2021-06-04 huifan
23
+ */
24
+@RestController
25
+@RequestMapping("/v1/cs/app/eighteen")
26
+public class EighteenController {
27
+
28
+    @Resource
29
+    private EighteenService eighteenService;
30
+    @Resource
31
+    private ExamOnlineService examOnlineService;
32
+
33
+
34
+
35
+    /**
36
+     * <b>方法名: </b> getExam <br>
37
+     * <b>说明: </b> 查询考试基本信息 <br>
38
+     *
39
+     * @param param Get18ExamIntroParams 查询考试基本信息参数类
40
+     * @return HttpResult<Get18ExamIntroResult>
41
+     * <b>修改履历: </b>
42
+     * @author 2021/7/5 Mokou
43
+     */
44
+    @PostMapping("/get_exam_intro")
45
+    public HttpResult<Get18ExamIntroResult> getExamIntro(@Validated @RequestBody Get18ExamIntroParams param) {
46
+        return eighteenService.getExamIntro(param);
47
+    }
48
+
49
+    /**
50
+     * <b>方法名: </b> applyExam <br>
51
+     * <b>说明: </b> 开始考试 <br>
52
+     *
53
+     * @param token java.lang.String token信息
54
+     * @param param com.sundot.airport.logic.eighteen.model.params.ExamRecAddParam 在线测试用户获取考试基本信息和试题请求参数
55
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.sundot.airport.logic.eighteen.model.result.Check18VideoFinishResult><br>
56
+     * <b>修改履历: </b>
57
+     * @author 2021/7/5 liangxiao
58
+     */
59
+    @PostMapping("/get_exam")
60
+
61
+    public HttpResult<ExamGetResult> applyExam(@RequestHeader(value="Authorization", required=true) String token,
62
+                                               @Validated @RequestBody ExamRecAddParam param) {
63
+        // 十八大对应的院长端
64
+        param.setClientType(ClientTypeConstant.CLIENT_TYPE_KG_APP);
65
+        return examOnlineService.applyExam(token, param);
66
+    }
67
+
68
+    /**
69
+     * <b>方法名: </b> getExamRecordList <br>
70
+     * <b>说明: </b> 查询测试记录列表 <br>
71
+     *
72
+     * @param param Get18ExamRecordListParams 查询测试记录列表参数类
73
+     * @return HttpResult<PageData < ItemIn18ExamRecordListResult>>
74
+     * <b>修改履历: </b>
75
+     * @author 2021/7/5 Mokou
76
+     */
77
+    @PostMapping("/get_exam_record_list")
78
+    public HttpResult<PageData<ItemIn18ExamRecordListResult>> getExamRecordList(@Validated @RequestBody Get18ExamRecordListParams param) {
79
+        return eighteenService.getExamRecordList(param);
80
+    }
81
+
82
+    /**
83
+     * <b>方法名: </b> getExamRecord <br>
84
+     * <b>说明: </b> 查询测试记录详情 <br>
85
+     *
86
+     * @param param Get18ExamRecordParams 查询测试记录详情参数类
87
+     * @return HttpResult<ItemIn18ExamRecordResult>
88
+     * <b>修改履历: </b>
89
+     * @author 2021/7/5 Mokou
90
+     */
91
+    @PostMapping("/get_exam_record")
92
+    public HttpResult<ItemIn18ExamRecordResult> getExamRecord(@Validated @RequestBody Get18ExamRecordParams param) {
93
+        return eighteenService.getExamRecord(param);
94
+    }
95
+
96
+    /**
97
+     * <b>方法名: </b> submitExam <br>
98
+     * <b>说明: </b> 提交考试 <br>
99
+     *
100
+     * @param param SubmitExamParam 提交考试参数类
101
+     * @return HttpResult<SubmitExamResult>
102
+     * <b>修改履历: </b>
103
+     * @author 2021/7/6 Mokou
104
+     */
105
+    @PostMapping("/submit_exam")
106
+    public HttpResult<SubmitExamResult> submitExam(@Validated @RequestBody SubmitExamParam param) {
107
+        return eighteenService.submitExam(param);
108
+    }
109
+}

+ 139 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/ExamController.java

@@ -0,0 +1,139 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import com.sundot.airport.exam.common.HttpResult;
4
+import com.sundot.airport.exam.common.PageData;
5
+import com.sundot.airport.exam.logic.manage.model.params.*;
6
+import com.sundot.airport.exam.logic.manage.model.result.*;
7
+import com.sundot.airport.exam.logic.manage.service.ExamBizService;
8
+import org.springframework.web.bind.annotation.*;
9
+
10
+import javax.annotation.Resource;
11
+import javax.validation.Valid;
12
+
13
+/**
14
+ * <b>功能名:</b>ExamController<br>
15
+ * <b>说明:</b> 考试管理表管理 225200 - 225299 <br>
16
+ * <b>著作权:</b> Copyright (C) 2021 HUIFANEDU  CORPORATION<br>
17
+ * <b>修改履历:
18
+ *
19
+ * @author 2021-06-23 liangxiao
20
+ */
21
+@RestController
22
+@RequestMapping("/v1/cs/manage/exam")
23
+public class ExamController extends BaseController {
24
+
25
+    @Resource
26
+    private ExamBizService examBizService;
27
+
28
+    /**
29
+     * <b>方法名: </b> page <br>
30
+     * <b>说明: </b> 考试管理表分页列表 <br>
31
+     *
32
+     * @param param com.sundot.airport.model.params.ExamPageParam 考试管理表分页列表请求参数
33
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.huifanedu.kernel.model.pager.PageData < com.sundot.airport.model.result.ExamPageResult>>
34
+     * <b>修改履历: </b>
35
+     * @author 2021-06-23 liangxiao
36
+     */
37
+    @PostMapping("/list")
38
+    public HttpResult<PageData<ExamPageResult>> page(@RequestBody ExamPageParam param) {
39
+        return examBizService.pageList(param);
40
+    }
41
+
42
+    /**
43
+     * <b>方法名: </b> get <br>
44
+     * <b>说明: </b> 考试管理表详情 <br>
45
+     *
46
+     * @param exId java.lang.String 考试业务id
47
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.sundot.airport.model.result.ExamGetResult>
48
+     * <b>修改履历: </b>
49
+     * @author 2021-06-23 liangxiao
50
+     */
51
+    @GetMapping("")
52
+    public HttpResult<ExamGetResult> get(@RequestParam("exId") String exId) {
53
+        HttpResult<ExamGetResult> res = examBizService.get(exId);
54
+        if (res.getModel().getExExamTime() != null && res.getModel().getExExamTime() > 0) {
55
+            // 数据库中保存的时间为秒 前端需要分钟
56
+            res.getModel().setExExamTime(res.getModel().getExExamTime() / 60);
57
+        }
58
+        return res;
59
+    }
60
+
61
+    /**
62
+     * <b>方法名: </b> add <br>
63
+     * <b>说明: </b> 考试管理表添加 <br>
64
+     *
65
+     * @param param com.sundot.airport.model.params.ExamAddParam 考试管理表添加请求参数
66
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.sundot.airport.model.result.ExamAddResult>
67
+     * <b>修改履历: </b>
68
+     * @author 2021-06-23 liangxiao
69
+     */
70
+    @PostMapping("")
71
+    public HttpResult<ExamAddResult> add(@Valid @RequestBody ExamAddParam param) {
72
+        if (param.getExExamTime() > 0) {
73
+            // 前端传入分钟 数据库中为秒
74
+            param.setExExamTime(param.getExExamTime() * 60);
75
+        }
76
+        return examBizService.add(param);
77
+    }
78
+
79
+    /**
80
+     * <b>方法名: </b> delete <br>
81
+     * <b>说明: </b> 考试管理表删除 <br>
82
+     *
83
+     * @param param com.sundot.airport.model.params.ExamDelParam 考试管理表删除请求参数
84
+     * @return com.huifanedu.kernel.model.response.HttpResult<java.lang.Boolean>
85
+     * <b>修改履历: </b>
86
+     * @author 2021-06-23 liangxiao
87
+     */
88
+    @DeleteMapping("")
89
+    public HttpResult<Boolean> del(@Valid @RequestBody ExamDelParam param) {
90
+        return examBizService.delete(param);
91
+    }
92
+
93
+    /**
94
+     * <b>方法名: </b> update <br>
95
+     * <b>说明: </b> 考试管理表修改 <br>
96
+     *
97
+     * @param param com.sundot.airport.model.params.ExamUpdParam 考试管理表修改请求参数
98
+     * @return com.huifanedu.kernel.model.response.HttpResult<java.lang.Boolean>
99
+     * <b>修改履历: </b>
100
+     * @author 2021-06-23 liangxiao
101
+     */
102
+    @PutMapping("")
103
+    public HttpResult<Boolean> update(@Valid @RequestBody ExamUpdParam param) {
104
+        if (param.getExExamTime() > 0) {
105
+            // 前端传入分钟 数据库为秒
106
+            param.setExExamTime(param.getExExamTime() * 60);
107
+        }
108
+        return examBizService.update(param);
109
+    }
110
+
111
+    /**
112
+     * <b>方法名: </b> recordList <br>
113
+     * <b>说明: </b> 考试批阅列表 <br>
114
+     *
115
+     * @param param com.sundot.airport.logic.manage.model.params.ExamRecordPageParam 考试记录查询的参数封装
116
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.huifanedu.kernel.model.pager.PageData < com.sundot.airport.logic.manage.model.result.ExamRecordPageResult>><br>
117
+     * <b>修改履历: </b>
118
+     * @author 2021/7/6 liangxiao
119
+     */
120
+    @PostMapping("/record_list")
121
+    public HttpResult<PageData<ExamRecordPageResult>> recordList(@Valid @RequestBody ExamRecordPageParam param) {
122
+        return examBizService.recordList(param);
123
+    }
124
+
125
+    /**
126
+     * <b>方法名: </b> recordDetail <br>
127
+     * <b>说明: </b> 考试记录详情 <br>
128
+     *
129
+     * @param erId java.lang.String 测试记录业务id
130
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.huifanedu.kernel.model.pager.PageData < com.sundot.airport.logic.manage.model.result.ExamRecordDetailResult>><br>
131
+     * <b>修改履历: </b>
132
+     * @author 2021/7/6 liangxiao
133
+     */
134
+    @GetMapping("/record_detail")
135
+    public HttpResult<ExamRecordDetailResult> recordDetail(@RequestParam("erId") String erId) {
136
+        return examBizService.recordDetail(erId);
137
+    }
138
+
139
+}

+ 104 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/QuesCatController.java

@@ -0,0 +1,104 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import com.sundot.airport.exam.common.HttpResult;
4
+import com.sundot.airport.exam.common.PageData;
5
+import com.sundot.airport.exam.logic.manage.model.params.QuesCatAddParam;
6
+import com.sundot.airport.exam.logic.manage.model.params.QuesCatDelParam;
7
+import com.sundot.airport.exam.logic.manage.model.params.QuesCatPageParam;
8
+import com.sundot.airport.exam.logic.manage.model.params.QuesCatUpdParam;
9
+import com.sundot.airport.exam.logic.manage.model.result.QuesCatAddResult;
10
+import com.sundot.airport.exam.logic.manage.model.result.QuesCatGetResult;
11
+import com.sundot.airport.exam.logic.manage.model.result.QuesCatPageResult;
12
+import com.sundot.airport.exam.logic.manage.service.QuesCatBizService;
13
+import org.springframework.web.bind.annotation.*;
14
+
15
+import javax.annotation.Resource;
16
+import javax.validation.Valid;
17
+
18
+/**
19
+ * <b>功能名:</b>QuesCatController<br>
20
+ * <b>说明:</b> 题目分类表管理 <br>
21
+ * <b>著作权:</b> Copyright (C) 2021 HUIFANEDU  CORPORATION<br>
22
+ * <b>修改履历:
23
+ *
24
+ * @author 2021-06-17 liangxiao
25
+ */
26
+@RestController
27
+@RequestMapping("/v1/cs/manage/ques_cat")
28
+public class QuesCatController extends BaseController {
29
+
30
+    @Resource
31
+    private QuesCatBizService quesCatBizService;
32
+
33
+    /**
34
+     * <b>方法名: </b> page <br>
35
+     * <b>说明: </b> 题目分类表分页列表 <br>
36
+     *
37
+     * @param param com.sundot.airport.model.params.QuesCatPageParam 题目分类表分页列表请求参数
38
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.huifanedu.kernel.model.pager.PageData < com.sundot.airport.model.result.QuesCatPageResult>>
39
+     * <b>修改履历: </b>
40
+     * @author 2021-06-17 liangxiao
41
+     */
42
+    @PostMapping("/list")
43
+    public HttpResult<PageData<QuesCatPageResult>> page(@RequestBody QuesCatPageParam param) {
44
+        return quesCatBizService.pageList(param);
45
+    }
46
+
47
+    /**
48
+     * <b>方法名: </b> get <br>
49
+     * <b>说明: </b> 题目分类表详情 <br>
50
+     *
51
+     * @param qcId java.lang.String 题目分类的业务id
52
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.sundot.airport.model.result.QuesCatGetResult>
53
+     * <b>修改履历: </b>
54
+     * @author 2021-06-17 liangxiao
55
+     */
56
+    @GetMapping("")
57
+    public HttpResult<QuesCatGetResult> get(@Valid @RequestParam("qcId") String qcId) {
58
+        return quesCatBizService.get(qcId);
59
+    }
60
+
61
+    /**
62
+     * <b>方法名: </b> add <br>
63
+     * <b>说明: </b> 题目分类表添加 <br>
64
+     *
65
+     * @param param com.sundot.airport.model.params.QuesCatDelParam 题目分类表添加请求参数
66
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.sundot.airport.logic.manage.model.result.QuesCatAddResult><br>
67
+     * <b>修改履历: </b>
68
+     * @author 2021/6/18 liangxiao
69
+     */
70
+    @PostMapping("")
71
+    public HttpResult<QuesCatAddResult> add(@Valid @RequestBody QuesCatAddParam param) {
72
+        return quesCatBizService.add(param);
73
+    }
74
+
75
+    /**
76
+     * <b>方法名: </b> delete <br>
77
+     * <b>说明: </b> 题目分类表删除 <br>
78
+     *
79
+     * @param param com.sundot.airport.model.params.QuesCatDelParam 题目分类表删除请求参数
80
+     * @return com.huifanedu.kernel.model.response.HttpResult<java.lang.Boolean>
81
+     * <b>修改履历: </b>
82
+     * @author 2021-06-17 liangxiao
83
+     */
84
+    @DeleteMapping("")
85
+    public HttpResult<Boolean> del(@Valid @RequestBody QuesCatDelParam param) {
86
+        return quesCatBizService.delete(param);
87
+    }
88
+
89
+    /**
90
+     * <b>方法名: </b> update <br>
91
+     * <b>说明: </b> 题目分类表修改 <br>
92
+     *
93
+     * @param param com.sundot.airport.model.params.QuesCatUpdParam 题目分类表修改请求参数
94
+     * @return com.huifanedu.kernel.model.response.HttpResult<java.lang.Boolean>
95
+     * <b>修改履历: </b>
96
+     * @author 2021-06-17 liangxiao
97
+     */
98
+    @PutMapping("")
99
+    public HttpResult<Boolean> update(@Valid @RequestBody QuesCatUpdParam param) {
100
+        return quesCatBizService.update(param);
101
+    }
102
+
103
+
104
+}

+ 143 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/QuesController.java

@@ -0,0 +1,143 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import cn.hutool.core.collection.CollectionUtil;
4
+import com.sundot.airport.attendance.dto.AttendancePostRecordImportVO;
5
+import com.sundot.airport.common.annotation.Log;
6
+import com.sundot.airport.common.core.domain.AjaxResult;
7
+import com.sundot.airport.common.enums.BusinessType;
8
+import com.sundot.airport.common.exception.ServiceException;
9
+import com.sundot.airport.common.utils.poi.ExcelUtil;
10
+import com.sundot.airport.exam.common.HttpResult;
11
+import com.sundot.airport.exam.common.PageData;
12
+import com.sundot.airport.exam.dto.QuestionImportDTO;
13
+import com.sundot.airport.exam.logic.manage.model.params.*;
14
+import com.sundot.airport.exam.logic.manage.model.result.QuesAddResult;
15
+import com.sundot.airport.exam.logic.manage.model.result.QuesGetResult;
16
+import com.sundot.airport.exam.logic.manage.model.result.QuesPageResult;
17
+import com.sundot.airport.exam.logic.manage.service.QuesBizService;
18
+import org.springframework.web.bind.annotation.*;
19
+import org.springframework.web.multipart.MultipartFile;
20
+
21
+import javax.annotation.Resource;
22
+import javax.servlet.http.HttpServletResponse;
23
+import javax.validation.Valid;
24
+import java.util.List;
25
+
26
+/**
27
+ * <b>功能名:</b>QuesController<br>
28
+ * <b>说明:</b> 题目操作管理 <br>
29
+ * <b>著作权:</b> Copyright (C) 2021 HUIFANEDU  CORPORATION<br>
30
+ * <b>修改履历:
31
+ *
32
+ * @author 2021-06-18 liangxiao
33
+ */
34
+@RestController
35
+@RequestMapping("/v1/cs/manage/ques")
36
+public class QuesController extends BaseController {
37
+
38
+    @Resource
39
+    private QuesBizService quesBizService;
40
+
41
+    /**
42
+     * <b>方法名: </b> page <br>
43
+     * <b>说明: </b> 题目分页列表 <br>
44
+     *
45
+     * @param param com.sundot.airport.model.params.QuesPageParam 题目分页列表请求参数
46
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.huifanedu.kernel.model.pager.PageData < com.sundot.airport.model.result.QuesPageResult>>
47
+     * <b>修改履历: </b>
48
+     * @author 2021-06-18 liangxiao
49
+     */
50
+    @PostMapping("/list")
51
+    public HttpResult<PageData<QuesPageResult>> page(@RequestBody QuesPageParam param) {
52
+        return quesBizService.pageList(param);
53
+    }
54
+
55
+    /**
56
+     * <b>方法名: </b> get <br>
57
+     * <b>说明: </b> 题目详情 <br>
58
+     *
59
+     * @param quId java.lang.String 题目业务id
60
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.sundot.airport.model.result.QuesGetResult>
61
+     * <b>修改履历: </b>
62
+     * @author 2021-06-18 liangxiao
63
+     */
64
+    @GetMapping("")
65
+    public HttpResult<QuesGetResult> get(@RequestParam("quId") String quId) {
66
+        return quesBizService.get(quId);
67
+    }
68
+
69
+    /**
70
+     * <b>方法名: </b> add <br>
71
+     * <b>说明: </b> 题目添加 <br>
72
+     *
73
+     * @param param com.sundot.airport.model.params.QuesAddParam 新增题目添加请求参数
74
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.sundot.airport.model.result.QuesAddResult>
75
+     * <b>修改履历: </b>
76
+     * @author 2021-06-18 liangxiao
77
+     */
78
+    @PostMapping("")
79
+    public HttpResult<QuesAddResult> add(@Valid @RequestBody QuesAddParam param) {
80
+        return quesBizService.add(param);
81
+    }
82
+
83
+    /**
84
+     * <b>方法名: </b> delete <br>
85
+     * <b>说明: </b> 题目删除 <br>
86
+     *
87
+     * @param param com.sundot.airport.model.params.QuesDelParam 题目删除请求参数
88
+     * @return com.huifanedu.kernel.model.response.HttpResult<java.lang.Boolean>
89
+     * <b>修改履历: </b>
90
+     * @author 2021-06-18 liangxiao
91
+     */
92
+    @DeleteMapping("")
93
+    public HttpResult<Boolean> del(@Valid @RequestBody QuesDelParam param) {
94
+        return quesBizService.delete(param);
95
+    }
96
+
97
+    /**
98
+     * <b>方法名: </b> update <br>
99
+     * <b>说明: </b> 题目修改 <br>
100
+     *
101
+     * @param param com.sundot.airport.model.params.QuesUpdParam QuesUpdParam
102
+     * @return com.huifanedu.kernel.model.response.HttpResult<java.lang.Boolean>
103
+     * <b>修改履历: </b>
104
+     * @author 2021-06-18 liangxiao
105
+     */
106
+    @PutMapping("")
107
+    public HttpResult<Boolean> update(@Valid @RequestBody QuesUpdParam param) {
108
+        return quesBizService.update(param);
109
+    }
110
+
111
+    /**
112
+     * <b>方法名: </b> update <br>
113
+     * <b>说明: </b> 题目分类转移 <br>
114
+     *
115
+     * @param param com.sundot.airport.model.params.QuesUpdParam 题目修改分类请求参数
116
+     * @return com.huifanedu.kernel.model.response.HttpResult<java.lang.Boolean>
117
+     * <b>修改履历: </b>
118
+     * @author 2021-06-18 liangxiao
119
+     */
120
+    @PutMapping("/cat")
121
+    public HttpResult<Boolean> batchUpdCat(@Valid @RequestBody QuesUpdCatParam param) {
122
+        return quesBizService.batchCatUpdate(param);
123
+    }
124
+
125
+    @PostMapping("/importTemplate")
126
+    public void importTemplate(HttpServletResponse response) {
127
+        ExcelUtil<QuestionImportDTO> util = new ExcelUtil<QuestionImportDTO>(QuestionImportDTO.class);
128
+        util.importTemplateExcel(response, "题目操作");
129
+    }
130
+
131
+    @Log(title = "题目操作管理", businessType = BusinessType.IMPORT)
132
+    @PostMapping("/importData")
133
+    public AjaxResult importData(MultipartFile file) throws Exception {
134
+        ExcelUtil<QuestionImportDTO> util = new ExcelUtil<>(QuestionImportDTO.class);
135
+        List<QuestionImportDTO> list = util.importExcel(file.getInputStream());
136
+        if (CollectionUtil.isEmpty(list)) {
137
+            throw new ServiceException("导入用户数据不能为空");
138
+        }
139
+        quesBizService.importData(list);
140
+        return AjaxResult.success();
141
+    }
142
+
143
+}

+ 43 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/QuizAccuracyAnalysisController.java

@@ -0,0 +1,43 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import com.sundot.airport.common.core.domain.AjaxResult;
4
+import com.sundot.airport.common.dto.QuizAccuracyAnalysisResultDto;
5
+import com.sundot.airport.exam.service.IAccuracyStatisticsService;
6
+import org.springframework.beans.factory.annotation.Autowired;
7
+import org.springframework.format.annotation.DateTimeFormat;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.RequestMapping;
10
+import org.springframework.web.bind.annotation.RequestParam;
11
+import org.springframework.web.bind.annotation.RestController;
12
+
13
+import java.text.SimpleDateFormat;
14
+import java.util.Date;
15
+
16
+/**
17
+ * 抽问抽答正确率分析接口
18
+ */
19
+@RestController
20
+@RequestMapping("/exam/quiz/accuracy-analysis")
21
+public class QuizAccuracyAnalysisController {
22
+
23
+    @Autowired
24
+    private IAccuracyStatisticsService accuracyStatisticsService;
25
+
26
+    /**
27
+     * 获取抽问抽答正确率分析
28
+     * 返回全站平均正确率、各题目分类正确率,并标注最高/薄弱分类
29
+     *
30
+     * @param startDate 开始日期(yyyy-MM-dd)
31
+     * @param endDate   结束日期(yyyy-MM-dd)
32
+     * @return 正确率分析结果
33
+     */
34
+    @GetMapping
35
+    public AjaxResult getQuizAccuracyAnalysis(
36
+            @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
37
+            @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
38
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
39
+        QuizAccuracyAnalysisResultDto result = accuracyStatisticsService.getQuizAccuracyAnalysis(
40
+                sdf.format(startDate), sdf.format(endDate));
41
+        return AjaxResult.success(result);
42
+    }
43
+}

+ 109 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/TestPaperCatController.java

@@ -0,0 +1,109 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import com.sundot.airport.exam.common.HttpResult;
4
+import com.sundot.airport.exam.common.PageData;
5
+import com.sundot.airport.exam.logic.manage.model.params.TestPaperCatAddParam;
6
+import com.sundot.airport.exam.logic.manage.model.params.TestPaperCatDelParam;
7
+import com.sundot.airport.exam.logic.manage.model.params.TestPaperCatPageParam;
8
+import com.sundot.airport.exam.logic.manage.model.params.TestPaperCatUpdParam;
9
+import com.sundot.airport.exam.logic.manage.model.result.TestPaperCatAddResult;
10
+import com.sundot.airport.exam.logic.manage.model.result.TestPaperCatGetResult;
11
+import com.sundot.airport.exam.logic.manage.model.result.TestPaperCatPageResult;
12
+import com.sundot.airport.exam.logic.manage.service.TestPaperCatBizService;
13
+import org.springframework.web.bind.annotation.*;
14
+
15
+import javax.annotation.Resource;
16
+import javax.validation.Valid;
17
+
18
+/**
19
+ * <b>功能名:</b>TestPaperCatController<br>
20
+ * <b>说明:</b> 试卷分类表管理 <br>
21
+ * <b>著作权:</b> Copyright (C) 2021 HUIFANEDU  CORPORATION<br>
22
+ * <b>修改履历:
23
+ *
24
+ * @author 2021-06-18 liangxiao
25
+ */
26
+@RestController
27
+@RequestMapping("/v1/cs/manage/test_paper_cat")
28
+public class TestPaperCatController extends BaseController {
29
+
30
+    @Resource
31
+    private TestPaperCatBizService testPaperCatBizService;
32
+
33
+    /**
34
+     * <b>方法名: </b> page <br>
35
+     * <b>说明: </b> 试卷分类表分页列表 <br>
36
+     *
37
+     * @param param com.sundot.airport.model.params.TestPaperCatPageParam 试卷分类表分页列表请求参数
38
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.huifanedu.kernel.model.pager.PageData < com.sundot.airport.model.result.TestPaperCatPageResult>>
39
+     * <b>修改履历: </b>
40
+     * @author 2021-06-18 liangxiao
41
+     */
42
+    @PostMapping("/list")
43
+//    @Log(key = 225101, value = "试卷分类表分页列表", mobile = SystemConstant.YGHD_LIANG_XIAO)
44
+    public HttpResult<PageData<TestPaperCatPageResult>> page(@RequestBody TestPaperCatPageParam param) {
45
+        return testPaperCatBizService.pageList(param);
46
+    }
47
+
48
+    /**
49
+     * <b>方法名: </b> get <br>
50
+     * <b>说明: </b> 试卷分类表详情 <br>
51
+     *
52
+     * @param tpcId java.lang.String 试卷分类的业务id
53
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.sundot.airport.model.result.TestPaperCatGetResult>
54
+     * <b>修改履历: </b>
55
+     * @author 2021-06-18 liangxiao
56
+     */
57
+    @GetMapping("")
58
+//    @Log(key = 225102, value = "试卷分类表详情", mobile = SystemConstant.YGHD_LIANG_XIAO)
59
+    public HttpResult<TestPaperCatGetResult> get(@Valid @RequestParam("tpcId") String tpcId) {
60
+        return testPaperCatBizService.get(tpcId);
61
+    }
62
+
63
+    /**
64
+     * <b>方法名: </b> add <br>
65
+     * <b>说明: </b> 试卷分类表添加 <br>
66
+     *
67
+     * @param param com.sundot.airport.model.params.TestPaperCatAddParam 试卷分类表添加请求参数
68
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.sundot.airport.model.result.TestPaperCatAddResult>
69
+     * <b>修改履历: </b>
70
+     * @author 2021-06-18 liangxiao
71
+     */
72
+    @PostMapping("")
73
+//    @Log(key = 225103, value = "试卷分类表添加", mobile = SystemConstant.YGHD_LIANG_XIAO)
74
+    public HttpResult<TestPaperCatAddResult> add(@Valid @RequestBody TestPaperCatAddParam param) {
75
+        return testPaperCatBizService.add(param);
76
+    }
77
+
78
+    /**
79
+     * <b>方法名: </b> delete <br>
80
+     * <b>说明: </b> 试卷分类表删除 <br>
81
+     *
82
+     * @param param com.sundot.airport.model.params.TestPaperCatDelParam 试卷分类表删除请求参数
83
+     * @return com.huifanedu.kernel.model.response.HttpResult<java.lang.Boolean>
84
+     * <b>修改履历: </b>
85
+     * @author 2021-06-18 liangxiao
86
+     */
87
+    @DeleteMapping("")
88
+//    @Log(key = 225104, value = "试卷分类表删除", mobile = SystemConstant.YGHD_LIANG_XIAO)
89
+    public HttpResult<Boolean> del(@Valid @RequestBody TestPaperCatDelParam param) {
90
+        return testPaperCatBizService.delete(param);
91
+    }
92
+
93
+    /**
94
+     * <b>方法名: </b> update <br>
95
+     * <b>说明: </b> 试卷分类表修改 <br>
96
+     *
97
+     * @param param com.sundot.airport.model.params.TestPaperCatUpdParam 试卷分类表修改请求参数
98
+     * @return com.huifanedu.kernel.model.response.HttpResult<java.lang.Boolean>
99
+     * <b>修改履历: </b>
100
+     * @author 2021-06-18 liangxiao
101
+     */
102
+    @PutMapping("")
103
+//    @Log(key = 225105, value = "试卷分类表修改", mobile = SystemConstant.YGHD_LIANG_XIAO)
104
+    public HttpResult<Boolean> update(@Valid @RequestBody TestPaperCatUpdParam param) {
105
+        return testPaperCatBizService.update(param);
106
+    }
107
+
108
+
109
+}

+ 135 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/exam/TestPaperController.java

@@ -0,0 +1,135 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import com.sundot.airport.exam.common.HttpResult;
4
+import com.sundot.airport.exam.common.PageData;
5
+import com.sundot.airport.exam.logic.manage.model.params.*;
6
+import com.sundot.airport.exam.logic.manage.model.result.TestPaperAddResult;
7
+import com.sundot.airport.exam.logic.manage.model.result.TestPaperGetResult;
8
+import com.sundot.airport.exam.logic.manage.model.result.TestPaperPageResult;
9
+import com.sundot.airport.exam.logic.manage.service.TestPaperBizService;
10
+import org.springframework.web.bind.annotation.*;
11
+
12
+import javax.annotation.Resource;
13
+import javax.validation.Valid;
14
+
15
+/**
16
+ * <b>功能名:</b>TestPaperController<br>
17
+ * <b>说明:</b> 试卷表管理 <br>
18
+ * <b>著作权:</b> Copyright (C) 2021 HUIFANEDU  CORPORATION<br>
19
+ * <b>修改履历:
20
+ *
21
+ * @author 2021-06-22 liangxiao
22
+ */
23
+@RestController
24
+@RequestMapping("/v1/cs/manage/test_paper")
25
+public class TestPaperController extends BaseController {
26
+
27
+    @Resource
28
+    private TestPaperBizService testPaperBizService;
29
+
30
+    /**
31
+     * <b>方法名: </b> page <br>
32
+     * <b>说明: </b> 试卷表分页列表 <br>
33
+     *
34
+     * @param param com.sundot.airport.model.params.TestPaperPageParam 试卷表分页列表请求参数
35
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.huifanedu.kernel.model.pager.PageData < com.sundot.airport.model.result.TestPaperPageResult>>
36
+     * <b>修改履历: </b>
37
+     * @author 2021-06-22 liangxiao
38
+     */
39
+    @PostMapping("/list")
40
+//    @Log(key = 225151, value = "试卷表分页列表", mobile = SystemConstant.YGHD_LIANG_XIAO)
41
+    public HttpResult<PageData<TestPaperPageResult>> page(@RequestBody TestPaperPageParam param) {
42
+        return testPaperBizService.pageList(param);
43
+    }
44
+
45
+    /**
46
+     * <b>方法名: </b> get <br>
47
+     * <b>说明: </b> 试卷表详情 <br>
48
+     *
49
+     * @param tpId java.lang.String 试卷业务id
50
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.sundot.airport.model.result.TestPaperGetResult>
51
+     * <b>修改履历: </b>
52
+     * @author 2021-06-22 liangxiao
53
+     */
54
+    @GetMapping("")
55
+//    @Log(key = 225152, value = "试卷表详情", mobile = SystemConstant.YGHD_LIANG_XIAO)
56
+    public HttpResult<TestPaperGetResult> get(@RequestParam("tpId") String tpId) {
57
+        return testPaperBizService.get(tpId);
58
+    }
59
+
60
+    /**
61
+     * <b>方法名: </b> add <br>
62
+     * <b>说明: </b> 试卷表添加 <br>
63
+     *
64
+     * @param param com.sundot.airport.model.params.TestPaperAddParam 试卷表添加请求参数
65
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.sundot.airport.model.result.TestPaperAddResult>
66
+     * <b>修改履历: </b>
67
+     * @author 2021-06-22 liangxiao
68
+     */
69
+    @PostMapping("")
70
+//    @Log(key = 225153, value = "试卷表添加", mobile = SystemConstant.YGHD_LIANG_XIAO)
71
+    public HttpResult<TestPaperAddResult> add(@Valid @RequestBody TestPaperAddParam param) {
72
+        return testPaperBizService.add(param);
73
+    }
74
+
75
+    /**
76
+     * <b>方法名: </b> delete <br>
77
+     * <b>说明: </b> 试卷表删除 <br>
78
+     *
79
+     * @param param com.sundot.airport.model.params.TestPaperDelParam 试卷表删除请求参数
80
+     * @return com.huifanedu.kernel.model.response.HttpResult<java.lang.Boolean>
81
+     * <b>修改履历: </b>
82
+     * @author 2021-06-22 liangxiao
83
+     */
84
+    @DeleteMapping("")
85
+//    @Log(key = 225154, value = "试卷表删除", mobile = SystemConstant.YGHD_LIANG_XIAO)
86
+    public HttpResult<Boolean> del(@Valid @RequestBody TestPaperDelParam param) {
87
+        return testPaperBizService.delete(param);
88
+    }
89
+
90
+    /**
91
+     * <b>方法名: </b> update <br>
92
+     * <b>说明: </b> 试卷表修改 <br>
93
+     *
94
+     * @param param com.sundot.airport.model.params.TestPaperUpdParam 试卷表修改请求参数
95
+     * @return com.huifanedu.kernel.model.response.HttpResult<java.lang.Boolean>
96
+     * <b>修改履历: </b>
97
+     * @author 2021-06-22 liangxiao
98
+     */
99
+    @PutMapping("")
100
+//    @Log(key = 225155, value = "试卷表修改", mobile = SystemConstant.YGHD_LIANG_XIAO)
101
+    public HttpResult<Boolean> update(@Valid @RequestBody TestPaperUpdParam param) {
102
+        return testPaperBizService.update(param);
103
+    }
104
+
105
+
106
+    /**
107
+     * <b>方法名: </b> update <br>
108
+     * <b>说明: </b> 试卷分类转移 <br>
109
+     *
110
+     * @param param com.sundot.airport.model.params.TestPaperUpdCatParam 试卷修改分类请求参数
111
+     * @return com.huifanedu.kernel.model.response.HttpResult<java.lang.Boolean>
112
+     * <b>修改履历: </b>
113
+     * @author 2021-06-18 liangxiao
114
+     */
115
+    @PutMapping("/cat")
116
+    //@Log(key = 225156, value = "试卷分类转移", mobile = SystemConstant.YGHD_LIANG_XIAO)
117
+    public HttpResult<Boolean> batchUpdCat(@Valid @RequestBody TestPaperUpdCatParam param) {
118
+        return testPaperBizService.batchCatUpdate(param);
119
+    }
120
+
121
+    /**
122
+     * <b>方法名: </b> copy <br>
123
+     * <b>说明: </b> 试卷复制 <br>
124
+     *
125
+     * @param param com.sundot.airport.model.params.TestPaperAddParam 试卷表添加请求参数
126
+     * @return com.huifanedu.kernel.model.response.HttpResult<com.sundot.airport.model.result.TestPaperAddResult>
127
+     * <b>修改履历: </b>
128
+     * @author 2021-06-22 liangxiao
129
+     */
130
+    @PostMapping("/copy")
131
+    //@Log(key = 225157, value = "试卷复制", mobile = SystemConstant.YGHD_LIANG_XIAO)
132
+    public HttpResult<Boolean> copy(@Valid @RequestBody TestPaperCopyParam param) {
133
+        return testPaperBizService.copy(param);
134
+    }
135
+}

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

@@ -0,0 +1,138 @@
1
+package com.sundot.airport.web.controller.exam;
2
+
3
+import com.sundot.airport.common.core.controller.BaseController;
4
+import com.sundot.airport.common.core.domain.AjaxResult;
5
+import com.sundot.airport.common.core.page.TableDataInfo;
6
+import com.sundot.airport.common.dto.WeakModuleDetailDTO;
7
+import com.sundot.airport.exam.domain.WeakModuleRule;
8
+import com.sundot.airport.exam.dto.WeakModuleRuleDTO;
9
+import com.sundot.airport.exam.service.IWeakModuleRuleService;
10
+import com.sundot.airport.exam.service.IWeakModuleStatisticsService;
11
+import io.swagger.annotations.Api;
12
+import io.swagger.annotations.ApiOperation;
13
+import io.swagger.annotations.ApiParam;
14
+import org.springframework.beans.factory.annotation.Autowired;
15
+import org.springframework.security.access.prepost.PreAuthorize;
16
+import org.springframework.validation.annotation.Validated;
17
+import org.springframework.web.bind.annotation.*;
18
+
19
+import java.util.List;
20
+
21
+/**
22
+ * <b>功能名:</b>WeakModuleRuleController<br>
23
+ * <b>说明:</b> 薄弱项出题规则管理Controller <br>
24
+ * <b>著作权:</b> Copyright (C) 2025 SUNDOT CORPORATION<br>
25
+ * <b>修改履历:
26
+ *
27
+ * @author Simon Lin
28
+ */
29
+@Api(tags = "薄弱项出题规则管理")
30
+@RestController
31
+@RequestMapping("/exam/weakModuleRule")
32
+public class WeakModuleRuleController extends BaseController {
33
+
34
+    @Autowired
35
+    private IWeakModuleRuleService weakModuleRuleService;
36
+
37
+    @Autowired
38
+    private IWeakModuleStatisticsService weakModuleStatisticsService;
39
+
40
+    /**
41
+     * 查询规则列表
42
+     */
43
+    @ApiOperation("查询规则列表")
44
+    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:list') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
45
+    @GetMapping("/list")
46
+    public TableDataInfo list() {
47
+        startPage();
48
+        List<WeakModuleRule> list = weakModuleRuleService.list();
49
+        return getDataTable(list);
50
+    }
51
+
52
+    /**
53
+     * 获取规则详情
54
+     */
55
+    @ApiOperation("获取规则详情")
56
+    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:query') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
57
+    @GetMapping("/{id}")
58
+    public AjaxResult getInfo(@PathVariable Long id) {
59
+        return success(weakModuleRuleService.getById(id));
60
+    }
61
+
62
+    /**
63
+     * 新增规则
64
+     */
65
+    @ApiOperation("新增规则")
66
+    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:add') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
67
+    @PostMapping
68
+    public AjaxResult add(@Validated @RequestBody WeakModuleRuleDTO dto) {
69
+        return toAjax(weakModuleRuleService.insert(dto));
70
+    }
71
+
72
+    /**
73
+     * 修改规则
74
+     */
75
+    @ApiOperation("修改规则")
76
+    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:edit') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
77
+    @PutMapping
78
+    public AjaxResult edit(@Validated @RequestBody WeakModuleRuleDTO dto) {
79
+        return toAjax(weakModuleRuleService.update(dto));
80
+    }
81
+
82
+    /**
83
+     * 删除规则
84
+     */
85
+    @ApiOperation("删除规则")
86
+    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:remove') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
87
+    @DeleteMapping("/{id}")
88
+    public AjaxResult remove(@PathVariable Long id) {
89
+        return toAjax(weakModuleRuleService.deleteById(id));
90
+    }
91
+
92
+    /**
93
+     * 启用规则
94
+     */
95
+    @ApiOperation("启用规则(同时只能有一个启用)")
96
+    @PreAuthorize("@ss.hasPermi('exam:weakModuleRule:edit') or @ss.hasRole('test') or @ss.hasRole('zhijianke')")
97
+    @PutMapping("/enable/{id}")
98
+    public AjaxResult enable(@PathVariable Long id) {
99
+        return toAjax(weakModuleRuleService.enable(id));
100
+    }
101
+
102
+    /**
103
+     * 获取当前启用的规则
104
+     */
105
+    @ApiOperation("获取当前启用的规则")
106
+    @GetMapping("/active")
107
+    public AjaxResult getActive() {
108
+        WeakModuleRule activeRule = weakModuleRuleService.getActiveRule();
109
+        if (activeRule == null) {
110
+            return warn("当前没有启用的规则");
111
+        }
112
+        return success(activeRule);
113
+    }
114
+
115
+    /**
116
+     * 获取用户的TOP3薄弱模块详细信息(用于验证算法)
117
+     */
118
+    @ApiOperation("获取用户的TOP3薄弱模块详细信息")
119
+    @GetMapping("/weakModules/{userId}")
120
+    public AjaxResult getWeakModulesDetail(
121
+            @ApiParam(value = "用户ID", required = true) @PathVariable Long userId) {
122
+        List<WeakModuleDetailDTO> details = weakModuleStatisticsService.getTop3WeakModulesDetail(userId);
123
+        return success(details);
124
+    }
125
+
126
+    /**
127
+     * 获取用户的TOP3薄弱模块详细信息及出题计划(用于验证出题算法)
128
+     */
129
+    @ApiOperation("获取用户的TOP3薄弱模块详细信息及出题计划")
130
+    @GetMapping("/weakModules/{userId}/allocation")
131
+    public AjaxResult getWeakModulesDetailWithAllocation(
132
+            @ApiParam(value = "用户ID", required = true) @PathVariable Long userId,
133
+            @ApiParam(value = "题目数量", required = true) @RequestParam Integer questionCount) {
134
+        List<WeakModuleDetailDTO> details = weakModuleStatisticsService
135
+                .getTop3WeakModulesDetailWithAllocation(userId, questionCount);
136
+        return success(details);
137
+    }
138
+}

+ 554 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/home/AttendanceIndexStatsController.java

@@ -0,0 +1,554 @@
1
+package com.sundot.airport.web.controller.home;
2
+
3
+import cn.hutool.core.collection.CollUtil;
4
+import cn.hutool.core.date.DateUtil;
5
+import cn.hutool.core.util.ObjectUtil;
6
+import cn.hutool.core.util.StrUtil;
7
+import com.sundot.airport.attendance.domain.AttendanceCheckRecord;
8
+import com.sundot.airport.attendance.domain.AttendancePostRecord;
9
+import com.sundot.airport.attendance.domain.AttendanceTeamUserRecord;
10
+import com.sundot.airport.attendance.enums.CheckInTypeEnum;
11
+import com.sundot.airport.attendance.service.IAttendanceCheckRecordService;
12
+import com.sundot.airport.attendance.service.IAttendancePostRecordService;
13
+import com.sundot.airport.attendance.service.IAttendanceTeamUserRecordService;
14
+import com.sundot.airport.check.service.ICheckLargeScreenService;
15
+import com.sundot.airport.common.core.controller.BaseController;
16
+import com.sundot.airport.common.core.domain.AjaxResult;
17
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
18
+import com.sundot.airport.common.core.domain.entity.SysDept;
19
+import com.sundot.airport.common.core.domain.entity.SysUser;
20
+import com.sundot.airport.common.core.domain.model.LoginUser;
21
+import com.sundot.airport.common.dto.AttendanceStatsResult;
22
+import com.sundot.airport.common.dto.BrigadeLeaderStats;
23
+import com.sundot.airport.common.dto.ChannelOpenStats;
24
+import com.sundot.airport.common.dto.SectionLeaderStats;
25
+import com.sundot.airport.common.dto.SecurityCheckStats;
26
+import com.sundot.airport.common.dto.StationLeaderStats;
27
+import com.sundot.airport.common.dto.TeamLeaderStats;
28
+import com.sundot.airport.common.enums.DeptType;
29
+import com.sundot.airport.common.enums.RoleTypeEnum;
30
+import com.sundot.airport.common.enums.SourceTypeEnum;
31
+import com.sundot.airport.common.exception.ServiceException;
32
+import com.sundot.airport.common.utils.SecurityUtils;
33
+import com.sundot.airport.common.utils.StringUtils;
34
+import com.sundot.airport.item.domain.ItemLargeScreenTotalSomeDto;
35
+import com.sundot.airport.item.domain.SimpleDutySchedule;
36
+import com.sundot.airport.item.mapper.SimpleDutyScheduleMapper;
37
+import com.sundot.airport.item.service.ItemLargeScreenService;
38
+import com.sundot.airport.system.domain.vo.PositionInfoVO;
39
+import com.sundot.airport.system.service.IBasePositionService;
40
+import com.sundot.airport.system.service.ISysDeptService;
41
+import com.sundot.airport.system.service.ISysUserService;
42
+import org.springframework.beans.factory.annotation.Autowired;
43
+import org.springframework.web.bind.annotation.GetMapping;
44
+import org.springframework.web.bind.annotation.RequestHeader;
45
+import org.springframework.web.bind.annotation.RequestMapping;
46
+import org.springframework.web.bind.annotation.RestController;
47
+
48
+import java.math.BigDecimal;
49
+import java.util.ArrayList;
50
+import java.util.Arrays;
51
+import java.util.Calendar;
52
+import java.util.Collections;
53
+import java.util.Date;
54
+import java.util.List;
55
+import java.util.Objects;
56
+import java.util.stream.Collectors;
57
+
58
+/**
59
+ * 考勤统计控制器
60
+ * 实现不同角色的考勤统计数据查询
61
+ *
62
+ * @author wangxx
63
+ */
64
+@RestController
65
+@RequestMapping("/attendance/stats")
66
+public class AttendanceIndexStatsController extends BaseController {
67
+
68
+    @Autowired
69
+    private IAttendancePostRecordService attendancePostRecordService;
70
+
71
+    @Autowired
72
+    private IAttendanceTeamUserRecordService attendanceTeamUserRecordService;
73
+
74
+
75
+    @Autowired
76
+    private ISysUserService sysUserService;
77
+
78
+    @Autowired
79
+    private ItemLargeScreenService itemLargeScreenService;
80
+
81
+    @Autowired
82
+    private IAttendanceCheckRecordService attendanceCheckRecordService;
83
+
84
+    @Autowired
85
+    private IBasePositionService basePositionService;
86
+
87
+    @Autowired
88
+    private ICheckLargeScreenService checkLargeScreenService;
89
+
90
+    @Autowired
91
+    private SimpleDutyScheduleMapper simpleDutyScheduleMapper;
92
+
93
+    @Autowired
94
+    private ISysDeptService sysDeptService;
95
+
96
+    /**
97
+     * 获取考勤统计数据
98
+     * 根据当前登录用户的角色返回不同的考勤统计数据
99
+     */
100
+    @GetMapping("/getAttendanceStats")
101
+    public AjaxResult getAttendanceStats(@RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
102
+        LoginUser loginUser = getLoginUser();
103
+        SysUser currentUser = loginUser.getUser();
104
+        Long userId = currentUser.getUserId();
105
+        Long deptId = currentUser.getDeptId();
106
+
107
+        // 获取用户角色列表
108
+        List<String> roleKeys = currentUser.getRoles().stream()
109
+                .map(role -> role.getRoleKey())
110
+                .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
111
+
112
+        AttendanceStatsResult result = new AttendanceStatsResult();
113
+
114
+        // 根据角色返回不同的统计数据
115
+        if (StrUtil.equals(SourceTypeEnum.mobile.getCode(), source)) {
116
+            if (roleKeys.contains(RoleTypeEnum.SecurityCheck.getCode())) {
117
+                // 安检员角色:查出勤班组、出勤通道、出勤时间
118
+                result.setSecurityCheckStats(getSecurityCheckStats(userId, deptId));
119
+            } else if (roleKeys.contains(RoleTypeEnum.banzuzhang.getCode())) {
120
+                // 班组长角色:查出勤班组、出勤通道、出勤时间
121
+                result.setTeamLeaderStats(getTeamLeaderStats(userId, deptId));
122
+            } else if (roleKeys.contains(RoleTypeEnum.kezhang.getCode())) {
123
+                // 科长角色:查在岗班组、在岗人员、出勤时间、今日查获上报
124
+                result.setSectionLeaderStats(getSectionLeaderStats(userId, deptId));
125
+            } else if (roleKeys.contains(RoleTypeEnum.jingli.getCode()) || roleKeys.contains(RoleTypeEnum.xingzheng.getCode())) {
126
+                // 经理和大队行政角色:查在岗班组、在岗人员、通道开放、今日查获上报
127
+                result.setBrigadeLeaderStats(getBrigadeLeaderStats(userId, deptId));
128
+            } else if (roleKeys.contains(RoleTypeEnum.test.getCode()) || roleKeys.contains(RoleTypeEnum.zhijianke.getCode()) || roleKeys.contains(RoleTypeEnum.admin.getCode())) {
129
+                // 站长和质检科角色:查在岗班组、在岗人员、通道开放、今日查获上报
130
+                result.setStationLeaderStats(getStationLeaderStats(userId, deptId));
131
+            } else {
132
+                throw new ServiceException("当前用户角色无法获取考勤统计数据");
133
+            }
134
+        } else {
135
+            List<SysDept> sysDeptList = sysDeptService.selectAllDept(SecurityUtils.getLoginUser().getDeptId());
136
+            Collections.reverse(sysDeptList);
137
+            SysDept stationDept = sysDeptList.stream().filter(x -> StrUtil.equals(DeptType.STATION.getCode(), x.getDeptType())).findFirst().orElse(null);
138
+            if (ObjectUtil.isNull(stationDept)) {
139
+                throw new ServiceException("未查询到站级部门信息");
140
+            }
141
+            // 站长和质检科角色:查在岗班组、在岗人员、通道开放、今日查获上报
142
+            result.setStationLeaderStats(getStationLeaderStats(userId, stationDept.getDeptId()));
143
+        }
144
+
145
+        return AjaxResult.success(result);
146
+    }
147
+
148
+    /**
149
+     * 安检员考勤统计数据
150
+     * 出勤班组:查询哪个班组长在勤务板块将该安检员维护到自己班组,则显示班组长所在组织架构的班组名称
151
+     * 出勤通道:进入该界面的时候班组长将该安检员带上哪个通道(仅显示最后一个层级,例如A02通道)
152
+     * 出勤时间:安检员打卡时间,若未打卡需要提示未打卡
153
+     */
154
+    private SecurityCheckStats getSecurityCheckStats(Long userId, Long deptId) {
155
+        SecurityCheckStats stats = new SecurityCheckStats();
156
+
157
+        // 获取当前日期
158
+        Date today = DateUtil.beginOfDay(new Date());
159
+
160
+        // 获取该安检员今日的班组维护记录
161
+        AttendanceTeamUserRecord teamUserRecord = attendanceTeamUserRecordService.selectAttendanceTeamUserRecordByUserId(userId, null, "1");
162
+
163
+        if (teamUserRecord != null) {
164
+            stats.setAttendanceTeamName(teamUserRecord.getAttendanceTeamName());
165
+        } else {
166
+            stats.setAttendanceTeamName("未分配班组");
167
+        }
168
+
169
+        // 获取安检员的打卡记录(上岗时间)
170
+        List<AttendancePostRecord> checkInRecords = attendancePostRecordService.selectTodayRecordsByUserId(null, userId, null, "1");
171
+        if (CollUtil.isNotEmpty(checkInRecords)) {
172
+            // 获取最后一条上岗记录
173
+            AttendancePostRecord lastRecord = checkInRecords.get(checkInRecords.size() - 1);
174
+            // 获取通道信息(从通道名称中提取最后一个层级)
175
+            if (StrUtil.isNotBlank(lastRecord.getChannelName())) {
176
+                String channelName = extractChannelName(lastRecord.getChannelName());
177
+                stats.setAttendanceChannel(channelName);
178
+            } else {
179
+                stats.setAttendanceChannel("未分配通道");
180
+            }
181
+        } else {
182
+            stats.setAttendanceChannel("未分配通道");
183
+        }
184
+
185
+        // 获取安检员的打卡记录
186
+        AttendanceCheckRecord attendanceCheckRecord = new AttendanceCheckRecord();
187
+        attendanceCheckRecord.setUserId(userId);
188
+        List<AttendanceCheckRecord> attendanceCheckRecords = attendanceCheckRecordService.selectAttendanceCheckRecordList(attendanceCheckRecord);
189
+        if (CollUtil.isNotEmpty(attendanceCheckRecords)) {
190
+            AttendanceCheckRecord lastRecord = attendanceCheckRecords.get(attendanceCheckRecords.size() - 1);
191
+            if (StrUtil.equals(CheckInTypeEnum.CLOCK_IN.getValue(), lastRecord.getCheckInType())) {
192
+                stats.setAttendanceTime(lastRecord.getCheckInTime());
193
+                stats.setAttendanceTimeTips("已打卡");
194
+            } else {
195
+                stats.setAttendanceTime(null);
196
+                stats.setAttendanceTimeTips("未打卡");
197
+            }
198
+        } else {
199
+            stats.setAttendanceTime(null);
200
+            stats.setAttendanceTimeTips("未打卡");
201
+        }
202
+        return stats;
203
+    }
204
+
205
+    /**
206
+     * 班组长考勤统计数据
207
+     * 出勤班组:查询该班组长在勤务板块将该安检员维护到自己班组,则显示自己所在组织架构的班组名称
208
+     * 出勤通道:进入该界面的时候班组长将该安检员带上哪个通道(仅显示最后一个层级,例如A02通道)
209
+     * 出勤时间:班组长打卡时间,若未打卡需要提示未打卡
210
+     */
211
+    private TeamLeaderStats getTeamLeaderStats(Long userId, Long deptId) {
212
+        TeamLeaderStats stats = new TeamLeaderStats();
213
+        Date today = DateUtil.beginOfDay(new Date());
214
+
215
+        // 获取班组长所在班组信息
216
+        AttendanceTeamUserRecord teamUserRecord = attendanceTeamUserRecordService.selectAttendanceTeamUserRecordByUserId(userId, null, "1");
217
+
218
+        if (teamUserRecord != null) {
219
+            stats.setAttendanceTeamName(teamUserRecord.getAttendanceTeamName());
220
+        } else {
221
+            stats.setAttendanceTeamName("未分配班组");
222
+        }
223
+        // 获取通道信息(从通道名称中提取最后一个层级)
224
+        List<AttendancePostRecord> checkInRecords = attendancePostRecordService.selectTodayRecordsByUserId(null, userId, null, "1");
225
+        if (CollUtil.isNotEmpty(checkInRecords)) {
226
+            // 获取最后一条上岗记录
227
+            AttendancePostRecord lastRecord = checkInRecords.get(checkInRecords.size() - 1);
228
+            if (StrUtil.isNotBlank(lastRecord.getChannelName())) {
229
+                String channelName = extractChannelName(lastRecord.getChannelName());
230
+                stats.setAttendanceChannel(channelName);
231
+            } else {
232
+                stats.setAttendanceChannel("未分配通道");
233
+            }
234
+        } else {
235
+            stats.setAttendanceChannel("未分配通道");
236
+        }
237
+
238
+        // 获取班组长的打卡记录
239
+        AttendanceCheckRecord attendanceCheckRecord = new AttendanceCheckRecord();
240
+        attendanceCheckRecord.setUserId(userId);
241
+        List<AttendanceCheckRecord> attendanceCheckRecords = attendanceCheckRecordService.selectAttendanceCheckRecordList(attendanceCheckRecord);
242
+        if (CollUtil.isNotEmpty(attendanceCheckRecords)) {
243
+            AttendanceCheckRecord lastRecord = attendanceCheckRecords.get(attendanceCheckRecords.size() - 1);
244
+            if (StrUtil.equals(CheckInTypeEnum.CLOCK_IN.getValue(), lastRecord.getCheckInType())) {
245
+                stats.setAttendanceTime(lastRecord.getCheckInTime());
246
+                stats.setAttendanceTimeTips("已打卡");
247
+            } else {
248
+                stats.setAttendanceTime(null);
249
+                stats.setAttendanceTimeTips("未打卡");
250
+            }
251
+        } else {
252
+            stats.setAttendanceTime(null);
253
+            stats.setAttendanceTimeTips("未打卡");
254
+        }
255
+
256
+        return stats;
257
+    }
258
+
259
+    /**
260
+     * 科长考勤统计数据
261
+     * 在岗班组:进入界面时有多少班组长打了上岗卡/多少班组长维护人员区域
262
+     * 在岗人员:进入该界面的时候多少人被带上通道/多少人的信息被班组长进行了维护
263
+     * 出勤时间:科长打卡时间,若未打卡需要提示未打卡
264
+     * 今日查获上报:今日安检员和班组长进行上报的数量,不需要完成审核
265
+     */
266
+    private SectionLeaderStats getSectionLeaderStats(Long userId, Long deptId) {
267
+        SectionLeaderStats stats = new SectionLeaderStats();
268
+
269
+        // 获取当天开始和结束时间
270
+        Date[] dayRange = getDayStartAndEnd();
271
+        Date startOfDay = dayRange[0];
272
+        Date endOfDay = dayRange[1];
273
+
274
+        AttendanceCheckRecord attendanceCheckRecord = new AttendanceCheckRecord();
275
+        attendanceCheckRecord.setUserId(userId);
276
+        List<AttendanceCheckRecord> attendanceCheckRecords = attendanceCheckRecordService.selectAttendanceCheckRecordList(attendanceCheckRecord);
277
+        if (CollUtil.isNotEmpty(attendanceCheckRecords)) {
278
+            AttendanceCheckRecord lastRecord = attendanceCheckRecords.get(attendanceCheckRecords.size() - 1);
279
+            if (StrUtil.equals(CheckInTypeEnum.CLOCK_IN.getValue(), lastRecord.getCheckInType())) {
280
+                stats.setAttendanceTime(lastRecord.getCheckInTime());
281
+                stats.setAttendanceTimeTips("已打卡");
282
+            } else {
283
+                stats.setAttendanceTime(null);
284
+                stats.setAttendanceTimeTips("未打卡");
285
+            }
286
+        } else {
287
+            stats.setAttendanceTime(null);
288
+            stats.setAttendanceTimeTips("未打卡");
289
+        }
290
+        // 获取在岗班组数量(打了上岗卡的班组长数量/维护了人员区域的班组长数量)
291
+        String onDutyTeamCount = getOnDutyTeamCount(deptId, startOfDay, endOfDay);
292
+        stats.setOnDutyTeamCount(onDutyTeamCount);
293
+
294
+        // 获取在岗人员数量(被带上通道的人员数量或被班组长维护的人员数量)
295
+        String onDutyPersonnelCount = getOnDutyPersonnelCount(deptId, startOfDay, endOfDay);
296
+        stats.setOnDutyPersonnelCount(onDutyPersonnelCount);
297
+
298
+        // 获取今日查获上报数量(安检员和班组长上报的数量,不需要完成审核)
299
+        BigDecimal todaySeizureReportCount = getTodaySeizureReportCount(deptId, startOfDay, endOfDay, "3");
300
+        stats.setTodaySeizureReportCount(todaySeizureReportCount);
301
+
302
+        // 获取今日巡检问题数
303
+        Integer todayCheckCorrectionCount = getTodayCheckCorrectionCount();
304
+        stats.setTodayCheckCorrectionCount(todayCheckCorrectionCount);
305
+
306
+        return stats;
307
+    }
308
+
309
+    /**
310
+     * 站长/质检科考勤统计数据
311
+     * 在岗班组:进入界面时有多少班组长打了上岗卡/多少班组长维护人员区域
312
+     * 在岗人员:进入该界面的时候多少人被带上通道/多少人的信息被班组长进行了维护
313
+     * 通道开放:开放通道/总通道
314
+     * 今日查获上报:今日安检员和班组长进行上报的数量,不需要完成审核
315
+     */
316
+    private StationLeaderStats getStationLeaderStats(Long userId, Long deptId) {
317
+        StationLeaderStats stats = new StationLeaderStats();
318
+
319
+        // 获取当天开始和结束时间
320
+        Date[] dayRange = getDayStartAndEnd();
321
+        Date startOfDay = dayRange[0];
322
+        Date endOfDay = dayRange[1];
323
+
324
+        // 获取在岗班组数量(打了上岗卡的班组长数量/维护了人员区域的班组长数量)
325
+        String onDutyTeamCount = getOnDutyTeamCount(deptId, startOfDay, endOfDay);
326
+        stats.setOnDutyTeamCount(onDutyTeamCount);
327
+
328
+        // 获取在岗人员数量(被带上通道的人员数量/被班组长维护的人员数量)
329
+        String onDutyPersonnelCount = getOnDutyPersonnelCount(deptId, startOfDay, endOfDay);
330
+        stats.setOnDutyPersonnelCount(onDutyPersonnelCount);
331
+
332
+        // 获取通道开放情况:开放通道/总通道
333
+        ChannelOpenStats channelStats = getChannelOpenStats(startOfDay, endOfDay);
334
+        stats.setChannelOpenStats(channelStats);
335
+
336
+        // 获取今日查获上报数量(安检员和班组长上报的数量,不需要完成审核)
337
+        BigDecimal todaySeizureReportCount = getTodaySeizureReportCount(deptId, startOfDay, endOfDay, "1");
338
+        stats.setTodaySeizureReportCount(todaySeizureReportCount);
339
+
340
+        // 获取今日巡检问题数
341
+        Integer todayCheckCorrectionCount = getTodayCheckCorrectionCount();
342
+        stats.setTodayCheckCorrectionCount(todayCheckCorrectionCount);
343
+
344
+        SimpleDutySchedule simpleDutySchedule = simpleDutyScheduleMapper.selectSimpleDutyScheduleByDate(new Date());
345
+        if (Objects.nonNull(simpleDutySchedule)) {
346
+            stats.setDutyDeptName(simpleDutySchedule.getDepartmentName());
347
+        }
348
+
349
+        return stats;
350
+    }
351
+
352
+    /**
353
+     * 获取当天开始和结束时间
354
+     */
355
+    private Date[] getDayStartAndEnd() {
356
+        Calendar calendar = Calendar.getInstance();
357
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
358
+        calendar.set(Calendar.MINUTE, 0);
359
+        calendar.set(Calendar.SECOND, 0);
360
+        calendar.set(Calendar.MILLISECOND, 0);
361
+        Date startOfDay = calendar.getTime();
362
+
363
+        calendar.set(Calendar.HOUR_OF_DAY, 23);
364
+        calendar.set(Calendar.MINUTE, 59);
365
+        calendar.set(Calendar.SECOND, 59);
366
+        calendar.set(Calendar.MILLISECOND, 999);
367
+        Date endOfDay = calendar.getTime();
368
+
369
+        return new Date[]{startOfDay, endOfDay};
370
+    }
371
+
372
+    /**
373
+     * 上岗卡的班组长数量/维护了人员区域的班组长数量
374
+     */
375
+    private String getOnDutyTeamCount(Long deptId, Date startOfDay, Date endOfDay) {
376
+        // 查询该部门下的班组长上岗记录(在指定时间范围内的记录)
377
+        // 先获取该部门下的所有班组长
378
+        List<SysUser> teamLeaderUsers = sysUserService.selectUserListByRoleKeyAndDeptId(Collections.singletonList(RoleTypeEnum.banzuzhang.getCode()), deptId);
379
+
380
+        if (CollUtil.isEmpty(teamLeaderUsers)) {
381
+            return "0/0";
382
+        }
383
+
384
+        int onDutyCount = 0;
385
+        // 统计在指定时间段内有上岗记录的班组长数量
386
+        for (SysUser teamLeader : teamLeaderUsers) {
387
+            List<AttendancePostRecord> attendancePostRecords = attendancePostRecordService.selectTeamOnList(new Date(), teamLeader.getDeptId());
388
+            if (CollUtil.isNotEmpty(attendancePostRecords)) {
389
+                onDutyCount++;
390
+            }
391
+        }
392
+
393
+        List<Long> teamList = teamLeaderUsers.stream().map(SysUser::getDeptId).distinct().collect(Collectors.toList());
394
+        // 统计该部门下维护了人员的班组数量
395
+        AttendanceTeamUserRecord queryRecord = new AttendanceTeamUserRecord();
396
+        queryRecord.setCheckInType("1");
397
+        List<AttendanceTeamUserRecord> teamUserRecords = attendanceTeamUserRecordService.selectAttendanceTeamUserRecordList(queryRecord, false);
398
+        List<Long> distinctTeamIds = teamUserRecords.stream().filter(team -> teamList.contains(team.getAttendanceTeamId())).map(AttendanceTeamUserRecord::getAttendanceTeamId).distinct().collect(Collectors.toList());
399
+
400
+
401
+        //打了上岗卡的班组长数量/维护了人员区域的班组长数量
402
+        return onDutyCount + "/" + distinctTeamIds.size();
403
+    }
404
+
405
+    /**
406
+     * 获取在岗人员数量(在岗人/被维护的人)
407
+     */
408
+    private String getOnDutyPersonnelCount(Long deptId, Date startOfDay, Date endOfDay) {
409
+        // 查询该部门下的人员上岗记录(在指定时间范围内的记录)
410
+        // 获取该部门下的所有安检员和班组长
411
+        List<SysUser> personnelUsers = sysUserService.selectUserListByRoleKeyAndDeptId(
412
+                Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()),
413
+                deptId
414
+        );
415
+
416
+        if (CollUtil.isEmpty(personnelUsers)) {
417
+            return "0/0";
418
+        }
419
+
420
+        List<Long> personnelList = personnelUsers.stream().filter(item -> Objects.nonNull(item.getUserId())).map(item -> item.getUserId()).collect(Collectors.toList());
421
+
422
+
423
+        int onDutyCount = 0;
424
+        // 统计在指定时间段内有上岗记录的人员数量
425
+        for (Long personnel : personnelList) {
426
+            List<AttendancePostRecord> records = attendancePostRecordService.selectTodayRecordsByUserId(null, personnel, null, "1");
427
+            if (CollUtil.isNotEmpty(records)) {
428
+                onDutyCount++;
429
+            }
430
+        }
431
+
432
+        // 统计该部门下被维护到班组的人员数量
433
+        AttendanceTeamUserRecord queryRecord = new AttendanceTeamUserRecord();
434
+        queryRecord.setCheckInType("1");
435
+        List<AttendanceTeamUserRecord> teamUserRecords = attendanceTeamUserRecordService.selectAttendanceTeamUserRecordList(queryRecord, false);
436
+        List<Long> teamUserIds = teamUserRecords.stream().filter(team -> personnelList.contains(team.getUserId())).map(team -> team.getUserId()).collect(Collectors.toList());
437
+
438
+        // 返回两个统计中较大的值(被带上通道的人员数量或被班组长维护的人员数量)
439
+        return onDutyCount + "/" + teamUserIds.size();
440
+    }
441
+
442
+    /**
443
+     * 获取今日查获上报数量
444
+     */
445
+    private BigDecimal getTodaySeizureReportCount(Long deptId, Date startOfDay, Date endOfDay, String type) {
446
+        // 查询今日的查获上报记录数量(安检员和班组长上报的数量,不需要完成审核)
447
+        BaseLargeScreenQueryParamDto record = new BaseLargeScreenQueryParamDto();
448
+        switch (type) {
449
+            case "1":
450
+                record.setInspectStationId(deptId);
451
+                break;
452
+            case "2":
453
+                record.setInspectBrigadeId(deptId);
454
+                break;
455
+            case "3":
456
+                record.setInspectDepartmentId(deptId);
457
+                break;
458
+            default:
459
+                break;
460
+        }
461
+        record.setStartDate(startOfDay);
462
+        record.setEndDate(endOfDay);
463
+        // 不限制process_status,即包含所有状态的记录(不需要完成审核)
464
+        ItemLargeScreenTotalSomeDto result = itemLargeScreenService.getPcTotalSome(record);
465
+        return ObjectUtil.isNotEmpty(result) ? result.getTotal() : BigDecimal.ZERO;
466
+    }
467
+
468
+    /**
469
+     * 获取通道开放统计
470
+     */
471
+    private ChannelOpenStats getChannelOpenStats(Date startOfDay, Date endOfDay) {
472
+        ChannelOpenStats stats = new ChannelOpenStats();
473
+
474
+        // 查询所有通道
475
+        List<PositionInfoVO> positionInfoVOS = basePositionService.selectChannelsUnderArea(new ArrayList<>());
476
+        // 获取所有通道的开放状态
477
+        AttendancePostRecord queryPostRecord = new AttendancePostRecord();
478
+        queryPostRecord.setCheckOutTimeType(1);
479
+        List<AttendancePostRecord> postRecords = attendancePostRecordService.selectAttendancePostRecordList(queryPostRecord);
480
+        List<String> collect = postRecords.stream().filter(item -> StringUtils.isNotEmpty(item.getChannelName())).map(item -> extractChannelName(item.getChannelName())).distinct().collect(Collectors.toList());
481
+
482
+        stats.setOpenChannels(collect.size());
483
+        stats.setTotalChannels(positionInfoVOS.size());
484
+        return stats;
485
+    }
486
+
487
+    /**
488
+     * 从通道名称中提取最后一个层级(例如从"安检A区/A02通道"中提取"A02通道")
489
+     */
490
+    private String extractChannelName(String fullChannelName) {
491
+        if (StrUtil.isBlank(fullChannelName)) {
492
+            return "";
493
+        }
494
+
495
+        // 按斜杠分割,取最后一部分
496
+        String[] parts = fullChannelName.split("/");
497
+        if (parts.length > 0) {
498
+            return parts[parts.length - 1].trim();
499
+        }
500
+
501
+        return fullChannelName.trim();
502
+    }
503
+
504
+    /**
505
+     * 获取今日巡检问题数
506
+     */
507
+    private Integer getTodayCheckCorrectionCount() {
508
+        BaseLargeScreenQueryParamDto queryParam = new BaseLargeScreenQueryParamDto();
509
+        return checkLargeScreenService.checkCorrectionCount(queryParam);
510
+    }
511
+
512
+    /**
513
+     * 经理和大队行政考勤统计数据
514
+     * 在岗班组:进入界面时有多少班组长打了上岗卡/多少班组长维护人员区域
515
+     * 在岗人员:进入该界面的时候多少人被带上通道/多少人的信息被班组长进行了维护
516
+     * 通道开放:开放通道/总通道
517
+     * 今日查获上报:今日安检员和班组长进行上报的数量,不需要完成审核
518
+     */
519
+    private BrigadeLeaderStats getBrigadeLeaderStats(Long userId, Long deptId) {
520
+        BrigadeLeaderStats stats = new BrigadeLeaderStats();
521
+
522
+        // 获取当天开始和结束时间
523
+        Date[] dayRange = getDayStartAndEnd();
524
+        Date startOfDay = dayRange[0];
525
+        Date endOfDay = dayRange[1];
526
+
527
+        // 获取在岗班组数量(打了上岗卡的班组长数量/维护了人员区域的班组长数量)
528
+        String onDutyTeamCount = getOnDutyTeamCount(deptId, startOfDay, endOfDay);
529
+        stats.setOnDutyTeamCount(onDutyTeamCount);
530
+
531
+        // 获取在岗人员数量(被带上通道的人员数量/被班组长维护的人员数量)
532
+        String onDutyPersonnelCount = getOnDutyPersonnelCount(deptId, startOfDay, endOfDay);
533
+        stats.setOnDutyPersonnelCount(onDutyPersonnelCount);
534
+
535
+        // 获取通道开放情况:开放通道/总通道
536
+        ChannelOpenStats channelStats = getChannelOpenStats(startOfDay, endOfDay);
537
+        stats.setChannelOpenStats(channelStats);
538
+
539
+        // 获取今日查获上报数量(安检员和班组长上报的数量,不需要完成审核)
540
+        BigDecimal todaySeizureReportCount = getTodaySeizureReportCount(deptId, startOfDay, endOfDay, "2");
541
+        stats.setTodaySeizureReportCount(todaySeizureReportCount);
542
+
543
+        // 获取今日巡检问题数
544
+        Integer todayCheckCorrectionCount = getTodayCheckCorrectionCount();
545
+        stats.setTodayCheckCorrectionCount(todayCheckCorrectionCount);
546
+
547
+        SimpleDutySchedule simpleDutySchedule = simpleDutyScheduleMapper.selectSimpleDutyScheduleByDate(new Date());
548
+        if (Objects.nonNull(simpleDutySchedule)) {
549
+            stats.setDutyDeptName(simpleDutySchedule.getDepartmentName());
550
+        }
551
+
552
+        return stats;
553
+    }
554
+}

+ 41 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/home/SeizureRankingController.java

@@ -0,0 +1,41 @@
1
+package com.sundot.airport.web.controller.home;
2
+
3
+import com.sundot.airport.common.core.controller.BaseController;
4
+import com.sundot.airport.common.core.domain.AjaxResult;
5
+import com.sundot.airport.item.domain.dto.RankingQueryParamDto;
6
+import com.sundot.airport.item.domain.dto.SeizureRankingDto;
7
+import com.sundot.airport.item.service.SeizureRankingService;
8
+import io.swagger.annotations.Api;
9
+import io.swagger.annotations.ApiOperation;
10
+import org.springframework.beans.factory.annotation.Autowired;
11
+import org.springframework.stereotype.Controller;
12
+import org.springframework.web.bind.annotation.PostMapping;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.ResponseBody;
16
+
17
+import java.util.List;
18
+
19
+/**
20
+ * 查获排名控制器
21
+ */
22
+@Api(tags = "查获排名")
23
+@Controller
24
+@RequestMapping("/item/seizure/ranking")
25
+public class SeizureRankingController extends BaseController {
26
+
27
+    @Autowired
28
+    private SeizureRankingService seizureRankingService;
29
+
30
+    /**
31
+     * 根据角色获取查获排名
32
+     */
33
+    @ApiOperation("根据角色获取查获排名")
34
+    @PostMapping("/getRankingByRole")
35
+    @ResponseBody
36
+    public AjaxResult getRankingByRole(@RequestBody RankingQueryParamDto param) {
37
+        List<SeizureRankingDto> result = seizureRankingService.getRankingByRole(param);
38
+        return AjaxResult.success(result);
39
+    }
40
+
41
+}

+ 63 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/home/SeizureReportController.java

@@ -0,0 +1,63 @@
1
+package com.sundot.airport.web.controller.home;
2
+
3
+import com.sundot.airport.common.core.domain.AjaxResult;
4
+import com.sundot.airport.common.core.domain.model.LoginUser;
5
+import com.sundot.airport.common.utils.SecurityUtils;
6
+import com.sundot.airport.item.domain.home.RoleBasedSeizureReportDTO;
7
+import com.sundot.airport.item.service.SeizureReportService;
8
+import org.springframework.beans.factory.annotation.Autowired;
9
+import org.springframework.format.annotation.DateTimeFormat;
10
+import org.springframework.web.bind.annotation.GetMapping;
11
+import org.springframework.web.bind.annotation.RequestHeader;
12
+import org.springframework.web.bind.annotation.RequestMapping;
13
+import org.springframework.web.bind.annotation.RequestParam;
14
+import org.springframework.web.bind.annotation.RestController;
15
+
16
+import java.util.Date;
17
+
18
+/**
19
+ * 查获上报 控制器
20
+ */
21
+@RestController
22
+@RequestMapping("/system/check/seizureReport")
23
+public class SeizureReportController {
24
+
25
+    @Autowired
26
+    private SeizureReportService seizureReportService;
27
+
28
+    /**
29
+     * 获取查获上报数据
30
+     *
31
+     * @param startDate  开始时间
32
+     * @param endDate    结束时间
33
+     * @param timeRange  时间范围, 可选值: week: 周, month: 月, quarter: 季度, half_year: 半年, year: 年
34
+     * @param dataSource individual=个人数据,team=班组数据(默认)
35
+     */
36
+    @GetMapping("/data")
37
+    public AjaxResult getSeizureReportData(
38
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
39
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate,
40
+            @RequestParam(required = false, defaultValue = "year") String timeRange,
41
+            @RequestParam(required = false, defaultValue = "team") String dataSource,
42
+            @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source
43
+    ) {
44
+        try {
45
+            // 获取当前登录用户信息
46
+            LoginUser loginUser = SecurityUtils.getLoginUser();
47
+            if (loginUser == null) {
48
+                return AjaxResult.error("用户未登录");
49
+            }
50
+
51
+            Long userId = loginUser.getUserId();
52
+            Long deptId = loginUser.getUser().getDeptId();
53
+
54
+            // 调用服务获取按角色分类的数据
55
+            RoleBasedSeizureReportDTO roleBasedDto = seizureReportService.getRoleBasedSeizureReportData(userId, deptId, startDate, endDate, timeRange, dataSource, source);
56
+
57
+            return AjaxResult.success(roleBasedDto);
58
+        } catch (Exception e) {
59
+            return AjaxResult.error("获取查获上报数据失败: " + e.getMessage());
60
+        }
61
+    }
62
+
63
+}

+ 227 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/item/FailedMatchItemController.java

@@ -0,0 +1,227 @@
1
+package com.sundot.airport.web.controller.item;
2
+
3
+import java.util.List;
4
+import java.util.Map;
5
+import javax.servlet.http.HttpServletResponse;
6
+
7
+import com.github.pagehelper.PageInfo;
8
+import com.sundot.airport.system.domain.SysPost;
9
+import com.sundot.airport.system.domain.BasePosition;
10
+import com.sundot.airport.system.service.ISysPostService;
11
+import com.sundot.airport.system.service.IBasePositionService;
12
+import org.springframework.security.access.prepost.PreAuthorize;
13
+import org.springframework.beans.factory.annotation.Autowired;
14
+import org.springframework.web.bind.annotation.*;
15
+import com.sundot.airport.common.annotation.Log;
16
+import com.sundot.airport.common.core.controller.BaseController;
17
+import com.sundot.airport.common.core.domain.AjaxResult;
18
+import com.sundot.airport.common.enums.BusinessType;
19
+import com.sundot.airport.item.domain.FailedMatchItem;
20
+import com.sundot.airport.item.service.IFailedMatchItemService;
21
+import com.sundot.airport.common.utils.poi.ExcelUtil;
22
+import com.sundot.airport.common.core.page.TableDataInfo;
23
+
24
+/**
25
+ * 匹配失败物品Controller
26
+ *
27
+ * @author airport
28
+ * @date 2025-11-21
29
+ */
30
+@RestController
31
+@RequestMapping("/item/failedMatch")
32
+public class FailedMatchItemController extends BaseController {
33
+    @Autowired
34
+    private IFailedMatchItemService failedMatchItemService;
35
+
36
+    @Autowired
37
+    private ISysPostService sysPostService;
38
+
39
+    @Autowired
40
+    private IBasePositionService basePositionService;
41
+
42
+    /**
43
+     * 查询当前用户的匹配失败物品列表(分页)
44
+     * 只查询未编辑的记录(app_edited=0或null)
45
+     */
46
+    @GetMapping("/myList")
47
+    public TableDataInfo getMyList(FailedMatchItem failedMatchItem) {
48
+        // 获取当前登录用户ID
49
+        Long currentUserId = getUserId();
50
+
51
+        // 设置查询条件为当前用户ID
52
+        failedMatchItem.setUserId(currentUserId);
53
+
54
+        // 只查询未编辑的记录(app_edited=0或null)
55
+        failedMatchItem.setAppEdited(false);
56
+
57
+        // 开启分页
58
+        startPage();
59
+
60
+        // 查询列表(按 update_time 降序排列)
61
+        List<FailedMatchItem> list = failedMatchItemService.selectFailedMatchItemList(failedMatchItem);
62
+
63
+        return getDataTable(list);
64
+    }
65
+
66
+    /**
67
+     * 查询匹配失败物品列表(管理员)
68
+     */
69
+    @PreAuthorize("@ss.hasPermi('item:failedMatch:list')")
70
+    @GetMapping("/list")
71
+    public TableDataInfo list(FailedMatchItem failedMatchItem) {
72
+        startPage();
73
+        List<FailedMatchItem> list = failedMatchItemService.selectFailedMatchItemList(failedMatchItem);
74
+        return getDataTable(list);
75
+    }
76
+
77
+    /**
78
+     * 导出匹配失败物品列表
79
+     */
80
+    @PreAuthorize("@ss.hasPermi('item:failedMatch:export')")
81
+    @Log(title = "匹配失败物品", businessType = BusinessType.EXPORT)
82
+    @PostMapping("/export")
83
+    public void export(HttpServletResponse response, FailedMatchItem failedMatchItem) {
84
+        List<FailedMatchItem> list = failedMatchItemService.selectFailedMatchItemList(failedMatchItem);
85
+        ExcelUtil<FailedMatchItem> util = new ExcelUtil<FailedMatchItem>(FailedMatchItem.class);
86
+        util.exportExcel(response, list, "匹配失败物品数据");
87
+    }
88
+
89
+    /**
90
+     * 获取匹配失败物品详细信息
91
+     */
92
+    @GetMapping(value = "/{id}")
93
+    public AjaxResult getInfo(@PathVariable("id") Integer id) {
94
+        return success(failedMatchItemService.selectFailedMatchItemById(id));
95
+    }
96
+
97
+    /**
98
+     * 新增匹配失败物品
99
+     */
100
+    @PreAuthorize("@ss.hasPermi('item:failedMatch:add')")
101
+    @Log(title = "匹配失败物品", businessType = BusinessType.INSERT)
102
+    @PostMapping
103
+    public AjaxResult add(@RequestBody FailedMatchItem failedMatchItem) {
104
+        return toAjax(failedMatchItemService.insertFailedMatchItem(failedMatchItem));
105
+    }
106
+
107
+    /**
108
+     * 修改匹配失败物品
109
+     */
110
+    @PreAuthorize("@ss.hasPermi('item:failedMatch:edit')")
111
+    @Log(title = "匹配失败物品", businessType = BusinessType.UPDATE)
112
+    @PutMapping
113
+    public AjaxResult edit(@RequestBody FailedMatchItem failedMatchItem) {
114
+        return toAjax(failedMatchItemService.updateFailedMatchItem(failedMatchItem));
115
+    }
116
+
117
+    /**
118
+     * 标记匹配失败物品为已编辑(App端提交后调用)
119
+     */
120
+    @Log(title = "标记匹配失败物品为已编辑", businessType = BusinessType.UPDATE)
121
+    @PutMapping("/markEdited/{id}")
122
+    public AjaxResult markEdited(@PathVariable Integer id, @RequestBody Map<String, Object> params) {
123
+        // 创建更新对象
124
+        FailedMatchItem failedMatchItem = new FailedMatchItem();
125
+        failedMatchItem.setId(id);
126
+
127
+        // 设置基本字段
128
+        if (params.containsKey("name") && params.get("name") != null) {
129
+            failedMatchItem.setName(params.get("name").toString());
130
+        }
131
+        if (params.containsKey("categoryCode") && params.get("categoryCode") != null) {
132
+            failedMatchItem.setCategoryCode(params.get("categoryCode").toString());
133
+        }
134
+        if (params.containsKey("categoryName") && params.get("categoryName") != null) {
135
+            failedMatchItem.setCategoryName(params.get("categoryName").toString());
136
+        }
137
+        if (params.containsKey("extractedQuantity") && params.get("extractedQuantity") != null) {
138
+            failedMatchItem.setExtractedQuantity(params.get("extractedQuantity").toString());
139
+        }
140
+        if (params.containsKey("extractedLocation") && params.get("extractedLocation") != null) {
141
+            failedMatchItem.setExtractedLocation(params.get("extractedLocation").toString());
142
+        }
143
+        if (params.containsKey("teamId") && params.get("teamId") != null) {
144
+            failedMatchItem.setTeamId(Long.valueOf(params.get("teamId").toString()));
145
+        }
146
+        if (params.containsKey("teamName") && params.get("teamName") != null) {
147
+            failedMatchItem.setTeamName(params.get("teamName").toString());
148
+        }
149
+        if (params.containsKey("checkPointId") && params.get("checkPointId") != null) {
150
+            failedMatchItem.setCheckPointId(Integer.valueOf(params.get("checkPointId").toString()));
151
+        }
152
+        if (params.containsKey("checkPointName") && params.get("checkPointName") != null) {
153
+            failedMatchItem.setCheckPointName(params.get("checkPointName").toString());
154
+        }
155
+        if (params.containsKey("isActiveConcealment") && params.get("isActiveConcealment") != null) {
156
+            failedMatchItem.setIsActiveConcealment(Integer.valueOf(params.get("isActiveConcealment").toString()));
157
+        }
158
+
159
+        // 处理 postCode:通过 postCode 查询 sys_post 表获取 position_id 和 position_name
160
+        if (params.containsKey("postCode") && params.get("postCode") != null) {
161
+            String postCode = params.get("postCode").toString();
162
+            SysPost post = new SysPost();
163
+            post.setPostCode(postCode);
164
+            List<SysPost> postList = sysPostService.selectPostList(post);
165
+            if (postList != null && !postList.isEmpty()) {
166
+                SysPost foundPost = postList.get(0);
167
+                failedMatchItem.setPositionId(foundPost.getPostId());
168
+                failedMatchItem.setPositionName(foundPost.getPostName());
169
+            }
170
+        }
171
+
172
+        // 处理查获位置三层结构:航站楼、区域、通道
173
+        // terminalId, regionalId, channelId 前端传的是 base_position 的 code 字段
174
+        // 需要查询 base_position 表获取对应的 id 和 name
175
+        if (params.containsKey("terminalId") && params.get("terminalId") != null && !params.get("terminalId").toString().isEmpty()) {
176
+            String terminalCode = params.get("terminalId").toString();
177
+            BasePosition terminalQuery = new BasePosition();
178
+            terminalQuery.setCode(terminalCode);
179
+            List<BasePosition> terminalList = basePositionService.selectBasePositionList(terminalQuery);
180
+            if (terminalList != null && !terminalList.isEmpty()) {
181
+                BasePosition terminal = terminalList.get(0);
182
+                failedMatchItem.setTerminalId(terminal.getId());
183
+                failedMatchItem.setTerminalName(terminal.getName());
184
+            }
185
+        }
186
+
187
+        if (params.containsKey("regionalId") && params.get("regionalId") != null && !params.get("regionalId").toString().isEmpty()) {
188
+            String regionalCode = params.get("regionalId").toString();
189
+            BasePosition regionalQuery = new BasePosition();
190
+            regionalQuery.setCode(regionalCode);
191
+            List<BasePosition> regionalList = basePositionService.selectBasePositionList(regionalQuery);
192
+            if (regionalList != null && !regionalList.isEmpty()) {
193
+                BasePosition regional = regionalList.get(0);
194
+                failedMatchItem.setRegionalId(regional.getId());
195
+                failedMatchItem.setRegionalName(regional.getName());
196
+            }
197
+        }
198
+
199
+        if (params.containsKey("channelId") && params.get("channelId") != null && !params.get("channelId").toString().isEmpty()) {
200
+            String channelCode = params.get("channelId").toString();
201
+            BasePosition channelQuery = new BasePosition();
202
+            channelQuery.setCode(channelCode);
203
+            List<BasePosition> channelList = basePositionService.selectBasePositionList(channelQuery);
204
+            if (channelList != null && !channelList.isEmpty()) {
205
+                BasePosition channel = channelList.get(0);
206
+                failedMatchItem.setChannelId(channel.getId());
207
+                failedMatchItem.setChannelName(channel.getName());
208
+            }
209
+        }
210
+
211
+        // 标记为已编辑
212
+        failedMatchItem.setAppEdited(true);
213
+
214
+        // 更新记录
215
+        return toAjax(failedMatchItemService.updateFailedMatchItem(failedMatchItem));
216
+    }
217
+
218
+    /**
219
+     * 删除匹配失败物品
220
+     */
221
+//    @PreAuthorize("@ss.hasPermi('item:failedMatch:remove')")
222
+    @Log(title = "匹配失败物品", businessType = BusinessType.DELETE)
223
+    @DeleteMapping("/{ids}")
224
+    public AjaxResult remove(@PathVariable Integer[] ids) {
225
+        return toAjax(failedMatchItemService.deleteFailedMatchItemByIds(ids));
226
+    }
227
+}

+ 161 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/item/ItemLargeScreenController.java

@@ -0,0 +1,161 @@
1
+package com.sundot.airport.web.controller.item;
2
+
3
+import com.sundot.airport.common.core.controller.BaseController;
4
+import com.sundot.airport.common.core.domain.AjaxResult;
5
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
6
+import com.sundot.airport.item.domain.*;
7
+import com.sundot.airport.item.service.ItemLargeScreenService;
8
+import org.springframework.beans.factory.annotation.Autowired;
9
+import org.springframework.security.access.prepost.PreAuthorize;
10
+import org.springframework.web.bind.annotation.*;
11
+
12
+import java.util.List;
13
+
14
+/**
15
+ * 查获大屏Controller
16
+ *
17
+ * @author ruoyi
18
+ * @date 2025-07-25
19
+ */
20
+@RestController
21
+@RequestMapping("/item/largeScreen")
22
+public class ItemLargeScreenController extends BaseController {
23
+
24
+    @Autowired
25
+    private ItemLargeScreenService itemLargeScreenService;
26
+
27
+    /**
28
+     * 移交公安情况
29
+     */
30
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
31
+    @GetMapping("/police")
32
+    public AjaxResult police(BaseLargeScreenQueryParamDto dto) {
33
+        List<ItemLargeScreenInfoDto> result = itemLargeScreenService.police(dto);
34
+        return success(result);
35
+    }
36
+
37
+    /**
38
+     * 故意隐匿情况
39
+     */
40
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
41
+    @GetMapping("/conceal")
42
+    public AjaxResult conceal(BaseLargeScreenQueryParamDto dto) {
43
+        List<ItemLargeScreenInfoDto> result = itemLargeScreenService.conceal(dto);
44
+        return success(result);
45
+    }
46
+
47
+    /**
48
+     * 查获类别分布
49
+     */
50
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
51
+    @GetMapping("/category")
52
+    public AjaxResult category(BaseLargeScreenQueryParamDto dto) {
53
+        List<ItemLargeScreenCommonDto> result = itemLargeScreenService.category(dto);
54
+        return success(result);
55
+    }
56
+
57
+    /**
58
+     * 查获岗位分布
59
+     */
60
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
61
+    @GetMapping("/post")
62
+    public AjaxResult post(BaseLargeScreenQueryParamDto dto) {
63
+        List<ItemLargeScreenCommonDto> result = itemLargeScreenService.post(dto);
64
+        return success(result);
65
+    }
66
+
67
+    /**
68
+     * 查获总数量+移交公安数量+故意隐匿数量
69
+     */
70
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
71
+    @GetMapping("/getTotalSome")
72
+    public AjaxResult getTotalSome(BaseLargeScreenQueryParamDto dto) {
73
+        ItemLargeScreenTotalSomeDto result = itemLargeScreenService.getTotalSome(dto);
74
+        return success(result);
75
+    }
76
+
77
+    /**
78
+     * app端查获总数量+移交公安数量+故意隐匿数量
79
+     */
80
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
81
+    @GetMapping("/getAppTotalSome")
82
+    public AjaxResult getAppTotalSome(BaseLargeScreenQueryParamDto dto) {
83
+        ItemLargeScreenTotalSomeDto result = itemLargeScreenService.getAppTotalSome(dto);
84
+        return success(result);
85
+    }
86
+
87
+    /**
88
+     * PC端-查获总数+移交公安+隐匿携带(不需要完成审核)
89
+     */
90
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
91
+    @GetMapping("/getPcTotalSome")
92
+    public AjaxResult getPcTotalSome(BaseLargeScreenQueryParamDto dto) {
93
+        ItemLargeScreenTotalSomeDto result = itemLargeScreenService.getPcTotalSome(dto);
94
+        return success(result);
95
+    }
96
+
97
+    /**
98
+     * 查获排名
99
+     */
100
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
101
+    @GetMapping("/rank")
102
+    public AjaxResult rank(BaseLargeScreenQueryParamDto dto, int type) {
103
+        List<ItemLargeScreenCommonDto> result = itemLargeScreenService.rank(dto, type);
104
+        return success(result);
105
+    }
106
+
107
+    /**
108
+     * 查获时段分布
109
+     */
110
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
111
+    @GetMapping("/timeSpan")
112
+    public AjaxResult timeSpan(BaseLargeScreenQueryParamDto dto) {
113
+        List<ItemLargeScreenTimeSpanDto> result = itemLargeScreenService.timeSpan(dto);
114
+        return success(result);
115
+    }
116
+
117
+    /**
118
+     * 查获时段分布
119
+     */
120
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
121
+    @GetMapping("/appTimeSpan")
122
+    public AjaxResult appTimeSpan(BaseLargeScreenQueryParamDto dto) {
123
+        List<ItemLargeScreenTimeSpanDto> result = itemLargeScreenService.appTimeSpan(dto);
124
+        return success(result);
125
+    }
126
+
127
+    /**
128
+     * 查获位置分布
129
+     */
130
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
131
+    @GetMapping("/position")
132
+    public AjaxResult position(BaseLargeScreenQueryParamDto dto) {
133
+        List<ItemLargeScreenPositionDto> result = itemLargeScreenService.position(dto);
134
+        return success(result);
135
+    }
136
+
137
+    /**
138
+     * app端 查获位置分布
139
+     *
140
+     * @param dto 大屏查询参数
141
+     * @return 查获位置分布
142
+     */
143
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
144
+    @GetMapping("/appPosition")
145
+    public AjaxResult appPosition(BaseLargeScreenQueryParamDto dto) {
146
+        List<ItemLargeScreenPositionAppDto> result = itemLargeScreenService.appPosition(dto);
147
+        return success(result);
148
+    }
149
+
150
+    /**
151
+     * 查获通道分布
152
+     */
153
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
154
+    @GetMapping("/channel")
155
+    public AjaxResult channel(BaseLargeScreenQueryParamDto dto) {
156
+        List<ItemLargeScreenChannelDto> result = itemLargeScreenService.channel(dto);
157
+        return success(result);
158
+    }
159
+
160
+
161
+}

+ 280 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/item/ItemSeizureItemsController.java

@@ -0,0 +1,280 @@
1
+package com.sundot.airport.web.controller.item;
2
+
3
+import java.util.Comparator;
4
+import java.util.List;
5
+import java.util.stream.Collectors;
6
+import javax.servlet.http.HttpServletResponse;
7
+
8
+import com.sundot.airport.common.core.domain.entity.SysDept;
9
+import com.sundot.airport.common.enums.DeptTypeEnum;
10
+import com.sundot.airport.common.enums.PostTypeEnum;
11
+import com.sundot.airport.item.domain.*;
12
+import com.sundot.airport.system.domain.SysPost;
13
+import com.sundot.airport.system.service.ISysDeptService;
14
+import com.sundot.airport.system.service.ISysPostService;
15
+import org.apache.commons.lang3.ObjectUtils;
16
+import org.springframework.security.access.prepost.PreAuthorize;
17
+import org.springframework.beans.factory.annotation.Autowired;
18
+import org.springframework.util.CollectionUtils;
19
+import org.springframework.web.bind.annotation.GetMapping;
20
+import org.springframework.web.bind.annotation.PostMapping;
21
+import org.springframework.web.bind.annotation.PutMapping;
22
+import org.springframework.web.bind.annotation.DeleteMapping;
23
+import org.springframework.web.bind.annotation.PathVariable;
24
+import org.springframework.web.bind.annotation.RequestBody;
25
+import org.springframework.web.bind.annotation.RequestMapping;
26
+import org.springframework.web.bind.annotation.RestController;
27
+import com.sundot.airport.common.annotation.Log;
28
+import com.sundot.airport.common.core.controller.BaseController;
29
+import com.sundot.airport.common.core.domain.AjaxResult;
30
+import com.sundot.airport.common.enums.BusinessType;
31
+import com.sundot.airport.item.service.IItemSeizureItemsService;
32
+import com.sundot.airport.common.utils.poi.ExcelUtil;
33
+import com.sundot.airport.common.core.page.TableDataInfo;
34
+
35
+/**
36
+ * 查获物品明细Controller
37
+ *
38
+ * @author ruoyi
39
+ * @date 2025-07-10
40
+ */
41
+@RestController
42
+@RequestMapping("/item/items")
43
+public class ItemSeizureItemsController extends BaseController {
44
+    @Autowired
45
+    private ISysPostService sysPostService;
46
+    @Autowired
47
+    private ISysDeptService sysDeptService;
48
+    @Autowired
49
+    private IItemSeizureItemsService itemSeizureItemsService;
50
+
51
+    /**
52
+     * 查询查获物品明细列表
53
+     */
54
+    @PreAuthorize("@ss.hasPermi('item:items:list')")
55
+    @GetMapping("/list")
56
+    public TableDataInfo list(ItemSeizureItems itemSeizureItems) {
57
+        startPage();
58
+        List<ItemSeizureItems> list = itemSeizureItemsService.selectItemSeizureItemsList(itemSeizureItems);
59
+        return getDataTable(list);
60
+    }
61
+
62
+    /**
63
+     * 导出查获物品明细列表
64
+     */
65
+    @PreAuthorize("@ss.hasPermi('item:items:export')")
66
+    @Log(title = "查获物品明细", businessType = BusinessType.EXPORT)
67
+    @PostMapping("/export")
68
+    public void export(HttpServletResponse response, ItemSeizureItems itemSeizureItems) {
69
+        List<ItemSeizureItems> list = itemSeizureItemsService.selectItemSeizureItemsList(itemSeizureItems);
70
+        ExcelUtil<ItemSeizureItems> util = new ExcelUtil<ItemSeizureItems>(ItemSeizureItems.class);
71
+        util.exportExcel(response, list, "查获物品明细数据");
72
+    }
73
+
74
+    /**
75
+     * 获取查获物品明细详细信息
76
+     */
77
+    @PreAuthorize("@ss.hasPermi('item:items:query')")
78
+    @GetMapping(value = "/{id}")
79
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
80
+        return success(itemSeizureItemsService.selectItemSeizureItemsById(id));
81
+    }
82
+
83
+    /**
84
+     * 新增查获物品明细
85
+     */
86
+    @PreAuthorize("@ss.hasPermi('item:items:add')")
87
+    @Log(title = "查获物品明细", businessType = BusinessType.INSERT)
88
+    @PostMapping
89
+    public AjaxResult add(@RequestBody ItemSeizureItems itemSeizureItems) {
90
+        itemSeizureItems.setCreateBy(getUsername());
91
+        return toAjax(itemSeizureItemsService.insertItemSeizureItems(itemSeizureItems));
92
+    }
93
+
94
+    /**
95
+     * 修改查获物品明细
96
+     */
97
+    @PreAuthorize("@ss.hasPermi('item:items:edit')")
98
+    @Log(title = "查获物品明细", businessType = BusinessType.UPDATE)
99
+    @PutMapping
100
+    public AjaxResult edit(@RequestBody ItemSeizureItems itemSeizureItems) {
101
+        itemSeizureItems.setUpdateBy(getUsername());
102
+        return toAjax(itemSeizureItemsService.updateItemSeizureItems(itemSeizureItems));
103
+    }
104
+
105
+    /**
106
+     * 删除查获物品明细
107
+     */
108
+    @PreAuthorize("@ss.hasPermi('item:items:remove')")
109
+    @Log(title = "查获物品明细", businessType = BusinessType.DELETE)
110
+    @DeleteMapping("/{ids}")
111
+    public AjaxResult remove(@PathVariable Long[] ids) {
112
+        return toAjax(itemSeizureItemsService.deleteItemSeizureItemsByIds(ids));
113
+    }
114
+
115
+    /**
116
+     * 根据记录ID查询查获物品明细列表
117
+     */
118
+    @PreAuthorize("@ss.hasPermi('item:items:list')")
119
+    @GetMapping("/listByRecorId")
120
+    public AjaxResult listByRecorId(Long recordId) {
121
+        return success(itemSeizureItemsService.listByRecorId(recordId));
122
+    }
123
+
124
+    /**
125
+     * 根据筛选条件查询查获物品明细列表
126
+     */
127
+    @PreAuthorize("@ss.hasPermi('item:items:list')")
128
+    @GetMapping("/selectByConditions")
129
+    public TableDataInfo selectByConditions(ItemSeizureParam itemSeizureParam) {
130
+        if (itemSeizureParam.getRecordType() == 1) {
131
+            itemSeizureParam.setInspectUserId(getUserId());
132
+        } else {
133
+            itemSeizureParam.setInspectUserId(null);
134
+            List<Long> userPostList = sysPostService.selectPostListByUserId(getUserId());
135
+            SysDept sysDept = sysDeptService.selectDeptById(getDeptId());
136
+            if (CollectionUtils.isEmpty(userPostList) || ObjectUtils.isEmpty(sysDept)) {
137
+                itemSeizureParam.setInspectUserId(getUserId());
138
+            } else {
139
+                if (sysDept.getDeptType().equals(DeptTypeEnum.STATION.getCode())) {
140
+                    itemSeizureParam.setInspectStationId(sysDept.getDeptId());
141
+                } else if (sysDept.getDeptType().equals(DeptTypeEnum.BRIGADE.getCode())) {
142
+                    itemSeizureParam.setInspectBrigadeId(sysDept.getDeptId());
143
+                } else if (sysDept.getDeptType().equals(DeptTypeEnum.MANAGER.getCode())) {
144
+                    itemSeizureParam.setInspectDepartmentId(sysDept.getDeptId());
145
+                } else if (sysDept.getDeptType().equals(DeptTypeEnum.TEAMS.getCode())) {
146
+                    itemSeizureParam.setInspectTeamId(sysDept.getDeptId());
147
+                } else {
148
+                    itemSeizureParam.setInspectUserId(getUserId());
149
+                }
150
+//                List<SysPost> sysPostList = sysPostService.selectPostAll();
151
+//                sysPostList = sysPostList.stream().filter(sysPost -> userPostList.contains(sysPost.getPostId())).collect(Collectors.toList());
152
+//                PostTypeEnum typeEnum = sysPostList.stream()
153
+//                        .map(item -> {
154
+//                            String postCode = item.getPostCode();
155
+//                            return postCode != null ? PostTypeEnum.getByCode(postCode) : null;
156
+//                        })
157
+//                        .filter(java.util.Objects::nonNull)
158
+//                        .min(Comparator.comparingInt(PostTypeEnum::getLevel))
159
+//                        .orElse(null);
160
+//                if (typeEnum == null || typeEnum == PostTypeEnum.security_inspector) {
161
+//                    itemSeizureParam.setInspectUserId(getUserId());
162
+//                } else {
163
+//                    if (sysDept.getDeptType().equals(DeptTypeEnum.STATION.getCode())) {
164
+//                        itemSeizureParam.setInspectStationId(sysDept.getDeptId());
165
+//                    } else if (sysDept.getDeptType().equals(DeptTypeEnum.DEPARTMENT.getCode())) {
166
+//                        itemSeizureParam.setInspectDepartmentId(sysDept.getDeptId());
167
+//                    } else if (sysDept.getDeptType().equals(DeptTypeEnum.TEAMS.getCode())) {
168
+//                        itemSeizureParam.setInspectTeamId(sysDept.getDeptId());
169
+//                    } else {
170
+//                        itemSeizureParam.setInspectUserId(getUserId());
171
+//                    }
172
+//                }
173
+            }
174
+        }
175
+        startPage();
176
+        List<ItemSeizureDto> list = itemSeizureItemsService.selectByConditions(itemSeizureParam);
177
+        return getDataTable(list);
178
+    }
179
+
180
+    /**
181
+     * 根据筛选条件查询分类数量
182
+     */
183
+    @PreAuthorize("@ss.hasPermi('item:items:list')")
184
+    @GetMapping("/selectGroupCount")
185
+    public AjaxResult selectGroupCount(ItemSeizureParam itemSeizureParam) {
186
+        if (itemSeizureParam.getRecordType() == 1) {
187
+            itemSeizureParam.setInspectUserId(getUserId());
188
+        } else {
189
+            itemSeizureParam.setInspectUserId(null);
190
+            List<Long> userPostList = sysPostService.selectPostListByUserId(getUserId());
191
+            SysDept sysDept = sysDeptService.selectDeptById(getDeptId());
192
+            if (CollectionUtils.isEmpty(userPostList) || ObjectUtils.isEmpty(sysDept)) {
193
+                itemSeizureParam.setInspectUserId(getUserId());
194
+            } else {
195
+                List<SysPost> sysPostList = sysPostService.selectPostAll();
196
+                sysPostList = sysPostList.stream().filter(sysPost -> userPostList.contains(sysPost.getPostId())).collect(Collectors.toList());
197
+                PostTypeEnum typeEnum = sysPostList.stream()
198
+                        .map(item -> {
199
+                            String postCode = item.getPostCode();
200
+                            return postCode != null ? PostTypeEnum.getByCode(postCode) : null;
201
+                        })
202
+                        .filter(java.util.Objects::nonNull)
203
+                        .min(Comparator.comparingInt(PostTypeEnum::getLevel))
204
+                        .orElse(null);
205
+                if (typeEnum == null || typeEnum == PostTypeEnum.security_inspector) {
206
+                    itemSeizureParam.setInspectUserId(getUserId());
207
+                } else {
208
+                    if (sysDept.getDeptType().equals(DeptTypeEnum.STATION.getCode())) {
209
+                        itemSeizureParam.setInspectStationId(sysDept.getDeptId());
210
+                    } else if (sysDept.getDeptType().equals(DeptTypeEnum.BRIGADE.getCode())) {
211
+                        itemSeizureParam.setInspectBrigadeId(sysDept.getDeptId());
212
+                    } else if (sysDept.getDeptType().equals(DeptTypeEnum.MANAGER.getCode())) {
213
+                        itemSeizureParam.setInspectDepartmentId(sysDept.getDeptId());
214
+                    } else if (sysDept.getDeptType().equals(DeptTypeEnum.TEAMS.getCode())) {
215
+                        itemSeizureParam.setInspectTeamId(sysDept.getDeptId());
216
+                    } else {
217
+                        itemSeizureParam.setInspectUserId(getUserId());
218
+                    }
219
+                }
220
+            }
221
+        }
222
+        List<ItemSeizureCount> list = itemSeizureItemsService.selectGroupCount(itemSeizureParam);
223
+        return success(list);
224
+    }
225
+
226
+    /**
227
+     * 根据筛选条件查询数量
228
+     */
229
+    @PreAuthorize("@ss.hasPermi('item:items:list')")
230
+    @GetMapping("/selectCount")
231
+    public AjaxResult selectCount(ItemSeizureParam itemSeizureParam) {
232
+        if (itemSeizureParam.getRecordType() == 1) {
233
+            itemSeizureParam.setInspectUserId(getUserId());
234
+        } else {
235
+            itemSeizureParam.setInspectUserId(null);
236
+            List<Long> userPostList = sysPostService.selectPostListByUserId(getUserId());
237
+            SysDept sysDept = sysDeptService.selectDeptById(getDeptId());
238
+            if (CollectionUtils.isEmpty(userPostList) || ObjectUtils.isEmpty(sysDept)) {
239
+                itemSeizureParam.setInspectUserId(getUserId());
240
+            } else {
241
+                if (sysDept.getDeptType().equals(DeptTypeEnum.STATION.getCode())) {
242
+                    itemSeizureParam.setInspectStationId(sysDept.getDeptId());
243
+                } else if (sysDept.getDeptType().equals(DeptTypeEnum.BRIGADE.getCode())) {
244
+                    itemSeizureParam.setInspectBrigadeId(sysDept.getDeptId());
245
+                } else if (sysDept.getDeptType().equals(DeptTypeEnum.MANAGER.getCode())) {
246
+                    itemSeizureParam.setInspectDepartmentId(sysDept.getDeptId());
247
+                } else if (sysDept.getDeptType().equals(DeptTypeEnum.TEAMS.getCode())) {
248
+                    itemSeizureParam.setInspectTeamId(sysDept.getDeptId());
249
+                } else {
250
+                    itemSeizureParam.setInspectUserId(getUserId());
251
+                }
252
+//                List<SysPost> sysPostList = sysPostService.selectPostAll();
253
+//                sysPostList = sysPostList.stream().filter(sysPost -> userPostList.contains(sysPost.getPostId())).collect(Collectors.toList());
254
+//                PostTypeEnum typeEnum = sysPostList.stream()
255
+//                        .map(item -> {
256
+//                            String postCode = item.getPostCode();
257
+//                            return postCode != null ? PostTypeEnum.getByCode(postCode) : null;
258
+//                        })
259
+//                        .filter(java.util.Objects::nonNull)
260
+//                        .min(Comparator.comparingInt(PostTypeEnum::getLevel))
261
+//                        .orElse(null);
262
+//                if (typeEnum == null || typeEnum == PostTypeEnum.security_inspector) {
263
+//                    itemSeizureParam.setInspectUserId(getUserId());
264
+//                } else {
265
+//                    if (sysDept.getDeptType().equals(DeptTypeEnum.STATION.getCode())) {
266
+//                        itemSeizureParam.setInspectStationId(sysDept.getDeptId());
267
+//                    } else if (sysDept.getDeptType().equals(DeptTypeEnumDeptTypeEnumDeptTypeEnumDeptTypeEnum)) {
268
+//                        itemSeizureParam.setInspectDepartmentId(sysDept.getDeptId());
269
+//                    } else if (sysDept.getDeptType().equals(DeptTypeEnum.TEAMS.getCode())) {
270
+//                        itemSeizureParam.setInspectTeamId(sysDept.getDeptId());
271
+//                    } else {
272
+//                        itemSeizureParam.setInspectUserId(getUserId());
273
+//                    }
274
+//                }
275
+            }
276
+        }
277
+        ItemSeizureTotal result = itemSeizureItemsService.selectCount(itemSeizureParam);
278
+        return success(result);
279
+    }
280
+}

+ 270 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/item/ItemSeizureRecordController.java

@@ -0,0 +1,270 @@
1
+package com.sundot.airport.web.controller.item;
2
+
3
+import java.util.ArrayList;
4
+import java.util.List;
5
+import javax.servlet.http.HttpServletResponse;
6
+
7
+import cn.hutool.core.util.ObjectUtil;
8
+import com.sundot.airport.common.dto.UserInfo;
9
+import com.sundot.airport.common.exception.ServiceException;
10
+import com.sundot.airport.common.utils.DictUtils;
11
+import com.sundot.airport.common.utils.StringUtils;
12
+import com.sundot.airport.web.core.utils.DataPermissionUtils;
13
+import com.sundot.airport.item.domain.ItemSeizureItems;
14
+import com.sundot.airport.item.domain.ItemSeizureRecordDTO;
15
+import com.sundot.airport.item.domain.vo.ItemSeizureRecordExportVO;
16
+import com.sundot.airport.system.service.ISysDeptService;
17
+import com.sundot.airport.system.service.ISysPostService;
18
+import com.sundot.airport.web.core.cache.UserCache;
19
+import com.sundot.airport.common.core.domain.DataPermissionResult;
20
+import org.springframework.security.access.prepost.PreAuthorize;
21
+import org.springframework.beans.factory.annotation.Autowired;
22
+import org.springframework.web.bind.annotation.GetMapping;
23
+import org.springframework.web.bind.annotation.PostMapping;
24
+import org.springframework.web.bind.annotation.PutMapping;
25
+import org.springframework.web.bind.annotation.DeleteMapping;
26
+import org.springframework.web.bind.annotation.PathVariable;
27
+import org.springframework.web.bind.annotation.RequestBody;
28
+import org.springframework.web.bind.annotation.RequestMapping;
29
+import org.springframework.web.bind.annotation.RestController;
30
+import com.sundot.airport.common.annotation.Log;
31
+import com.sundot.airport.common.core.controller.BaseController;
32
+import com.sundot.airport.common.core.domain.AjaxResult;
33
+import com.sundot.airport.common.enums.BusinessType;
34
+import com.sundot.airport.item.domain.ItemSeizureRecord;
35
+import com.sundot.airport.item.service.IItemSeizureRecordService;
36
+import com.sundot.airport.common.utils.poi.ExcelUtil;
37
+import com.sundot.airport.common.core.page.TableDataInfo;
38
+
39
+/**
40
+ * 查获记录Controller
41
+ *
42
+ * @author ruoyi
43
+ * @date 2025-07-08
44
+ */
45
+@RestController
46
+@RequestMapping("/item/record")
47
+public class ItemSeizureRecordController extends BaseController {
48
+    @Autowired
49
+    private UserCache userCache;
50
+    @Autowired
51
+    private ISysPostService sysPostService;
52
+    @Autowired
53
+    private ISysDeptService sysDeptService;
54
+    @Autowired
55
+    private IItemSeizureRecordService itemSeizureRecordService;
56
+
57
+    /**
58
+     * 查询查获记录列表
59
+     */
60
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
61
+    @GetMapping("/list")
62
+    public TableDataInfo list(ItemSeizureRecordDTO itemSeizureRecord) {
63
+        // 应用数据权限过滤
64
+        DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
65
+        switch (dataPermission.getPermissionType()) {
66
+            case SELF:
67
+                itemSeizureRecord.setInspectUserId(dataPermission.getValue());
68
+                break;
69
+            case STATION:
70
+                itemSeizureRecord.setInspectStationId(dataPermission.getValue());
71
+                break;
72
+            case BRIGADE:
73
+                itemSeizureRecord.setInspectBrigadeId(dataPermission.getValue());
74
+                break;
75
+            case DEPARTMENT:
76
+                itemSeizureRecord.setInspectDepartmentId(dataPermission.getValue());
77
+                break;
78
+            case TEAM:
79
+                itemSeizureRecord.setInspectTeamId(dataPermission.getValue());
80
+                break;
81
+            case ALL:
82
+            default:
83
+                // 不设置过滤条件,查看所有数据
84
+                break;
85
+        }
86
+
87
+        startPage();
88
+        List<ItemSeizureRecord> list = itemSeizureRecordService.selectItemSeizureRecordList(itemSeizureRecord);
89
+        return getDataTable(list);
90
+    }
91
+
92
+    /**
93
+     * 导出查获记录列表
94
+     */
95
+    @PreAuthorize("@ss.hasPermi('item:record:export')")
96
+    @Log(title = "查获记录", businessType = BusinessType.EXPORT)
97
+    @PostMapping("/export")
98
+    public void export(HttpServletResponse response, ItemSeizureRecordDTO itemSeizureRecord) {
99
+        // 应用数据权限过滤
100
+        DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
101
+        switch (dataPermission.getPermissionType()) {
102
+            case SELF:
103
+                itemSeizureRecord.setInspectUserId(dataPermission.getValue());
104
+                break;
105
+            case STATION:
106
+                itemSeizureRecord.setInspectStationId(dataPermission.getValue());
107
+                break;
108
+            case BRIGADE:
109
+                itemSeizureRecord.setInspectBrigadeId(dataPermission.getValue());
110
+                break;
111
+            case DEPARTMENT:
112
+                itemSeizureRecord.setInspectDepartmentId(dataPermission.getValue());
113
+                break;
114
+            case TEAM:
115
+                itemSeizureRecord.setInspectTeamId(dataPermission.getValue());
116
+                break;
117
+            case ALL:
118
+            default:
119
+                // 不设置过滤条件,查看所有数据
120
+                break;
121
+        }
122
+
123
+        List<ItemSeizureRecord> list = itemSeizureRecordService.selectItemSeizureRecordListForExport(itemSeizureRecord);
124
+        exportData(response, list);
125
+    }
126
+
127
+    /**
128
+     * 导出查获记录数据
129
+     *
130
+     * @param response 响应对象
131
+     * @param list     查获记录列表
132
+     */
133
+    private void exportData(HttpServletResponse response, List<ItemSeizureRecord> list) {
134
+        List<ItemSeizureRecordExportVO> exportList = new ArrayList<>();
135
+
136
+        // 直接使用已查询到的记录,避免再次查询数据库
137
+        for (ItemSeizureRecord record : list) {
138
+            if (record != null) {
139
+                // 处理没有物品的记录
140
+                if (record.getItemSeizureItemsList() == null || record.getItemSeizureItemsList().isEmpty()) {
141
+                    ItemSeizureRecordExportVO exportVO = new ItemSeizureRecordExportVO();
142
+                    // 基本信息
143
+                    exportVO.setInspectUserName(record.getInspectUserName());
144
+                    exportVO.setSeizureTime(record.getSeizureTime());
145
+                    StringBuilder sb = new StringBuilder();
146
+                    if (StringUtils.isNotEmpty(record.getTerminlName())) {
147
+                        sb.append(record.getTerminlName()).append("/");
148
+                    }
149
+                    if (StringUtils.isNotEmpty(record.getRegionalName())) {
150
+                        sb.append(record.getRegionalName()).append("/");
151
+                    }
152
+                    if (StringUtils.isNotEmpty(record.getChannelName())) {
153
+                        sb.append(record.getChannelName());
154
+                    }
155
+                    exportVO.setChannelName(sb.toString());
156
+                    exportVO.setCheckMethodDesc(record.getCheckMethodDesc());
157
+                    exportVO.setInspectTeamName(record.getInspectTeamName());
158
+                    exportVO.setAttendanceTeamName(record.getAttendanceTeamName());
159
+                    exportVO.setPowerOnInstruction(record.getPowerOnInstruction());
160
+                    exportVO.setXrayOperator(record.getXrayOperatorName());
161
+                    exportVO.setProcessStatus(record.getProcessStatus());
162
+
163
+                    // 物品信息为空
164
+                    exportVO.setCategoryNameTwo("");
165
+                    exportVO.setQuantity(null);
166
+                    exportVO.setCheckPositionNameTwo("");
167
+                    exportVO.setHandlingMethod("");
168
+                    exportVO.setIsActiveConcealmentDesc("");
169
+                    exportVO.setCommonContraband(null);
170
+                    exportVO.setContrabandDesc("");
171
+                    exportList.add(exportVO);
172
+                } else {
173
+                    // 处理有物品的记录
174
+                    for (ItemSeizureItems item : record.getItemSeizureItemsList()) {
175
+                        ItemSeizureRecordExportVO exportVO = new ItemSeizureRecordExportVO();
176
+                        // 基本信息
177
+                        exportVO.setInspectUserName(record.getInspectUserName());
178
+                        exportVO.setSeizureTime(record.getSeizureTime());
179
+                        StringBuilder sb = new StringBuilder();
180
+                        if (StringUtils.isNotEmpty(record.getTerminlName())) {
181
+                            sb.append(record.getTerminlName()).append("/");
182
+                        }
183
+                        if (StringUtils.isNotEmpty(record.getRegionalName())) {
184
+                            sb.append(record.getRegionalName()).append("/");
185
+                        }
186
+                        if (StringUtils.isNotEmpty(record.getChannelName())) {
187
+                            sb.append(record.getChannelName());
188
+                        }
189
+                        exportVO.setChannelName(sb.toString());
190
+                        exportVO.setCheckMethodDesc(record.getCheckMethodDesc());
191
+                        exportVO.setInspectTeamName(record.getInspectTeamName());
192
+                        exportVO.setAttendanceTeamName(record.getAttendanceTeamName());
193
+                        exportVO.setPowerOnInstruction(record.getPowerOnInstruction());
194
+                        exportVO.setXrayOperator(record.getXrayOperatorName());
195
+                        exportVO.setProcessStatus(record.getProcessStatus());
196
+
197
+                        // 物品信息
198
+                        exportVO.setCategoryNameTwo(item.getCategoryNameOne() + "/" + item.getCategoryNameTwo());
199
+                        exportVO.setQuantity(item.getQuantity());
200
+                        exportVO.setCheckPositionNameTwo(item.getCheckPositionNameOne() + "/" + item.getCheckPositionNameTwo());
201
+                        //处理方式查字典
202
+                        String handlingMethodLabel = DictUtils.getDictLabel("item_handling_method", item.getHandlingMethod());
203
+                        exportVO.setHandlingMethod(StringUtils.isNotEmpty(handlingMethodLabel) ? handlingMethodLabel : item.getHandlingMethod());
204
+                        exportVO.setIsActiveConcealmentDesc("1".equals(String.valueOf(item.getIsActiveConcealment())) ? "是" : "否");
205
+                        exportVO.setCommonContraband(item.getCommonContraband());
206
+                        exportVO.setContrabandDesc(item.getContrabandDesc());
207
+                        exportList.add(exportVO);
208
+                    }
209
+                }
210
+            }
211
+        }
212
+
213
+        ExcelUtil<ItemSeizureRecordExportVO> util = new ExcelUtil<ItemSeizureRecordExportVO>(ItemSeizureRecordExportVO.class);
214
+        util.exportExcel(response, exportList, "查获记录数据");
215
+    }
216
+
217
+    /**
218
+     * 获取查获记录详细信息
219
+     */
220
+    @PreAuthorize("@ss.hasPermi('item:record:query')")
221
+    @GetMapping(value = "/{id}")
222
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
223
+        return success(itemSeizureRecordService.selectItemSeizureRecordById(id));
224
+    }
225
+
226
+    /**
227
+     * 新增查获记录
228
+     */
229
+    @PreAuthorize("@ss.hasPermi('item:record:add')")
230
+    @Log(title = "查获记录", businessType = BusinessType.INSERT)
231
+    @PostMapping("/add")
232
+    public AjaxResult add(@RequestBody ItemSeizureRecord itemSeizureRecord) {
233
+        itemSeizureRecord.setCreateBy(getUsername());
234
+        UserInfo userInfo = userCache.getUserInfo(itemSeizureRecord.getInspectUserId());
235
+        if (ObjectUtil.isNull(userInfo) || ObjectUtil.isNull(userInfo.getStationId()) || ObjectUtil.isNull(userInfo.getDepartmentId()) || ObjectUtil.isNull(userInfo.getTeamsId())) {
236
+            throw new ServiceException("操作失败,检查人员非班组成员");
237
+        }
238
+        itemSeizureRecord.setInspectStationId(userInfo.getStationId());
239
+        itemSeizureRecord.setInspectStationName(userInfo.getStationName());
240
+        itemSeizureRecord.setInspectBrigadeId(userInfo.getBrigadeId());
241
+        itemSeizureRecord.setInspectBrigadeName(userInfo.getBrigadeName());
242
+        itemSeizureRecord.setInspectDepartmentId(userInfo.getDepartmentId());
243
+        itemSeizureRecord.setInspectDepartmentName(userInfo.getDepartmentName());
244
+        itemSeizureRecord.setInspectTeamId(userInfo.getTeamsId());
245
+        itemSeizureRecord.setInspectTeamName(userInfo.getTeamsName());
246
+        itemSeizureRecord.getItemSeizureItemsList().forEach(itemSeizureItems -> itemSeizureItems.setCreateBy(getUsername()));
247
+        return success(itemSeizureRecordService.insertItemSeizureRecord(itemSeizureRecord));
248
+    }
249
+
250
+    /**
251
+     * 修改查获记录
252
+     */
253
+    @PreAuthorize("@ss.hasPermi('item:record:edit')")
254
+    @Log(title = "查获记录", businessType = BusinessType.UPDATE)
255
+    @PutMapping
256
+    public AjaxResult edit(@RequestBody ItemSeizureRecord itemSeizureRecord) {
257
+        itemSeizureRecord.setUpdateBy(getUsername());
258
+        return toAjax(itemSeizureRecordService.updateItemSeizureRecord(itemSeizureRecord));
259
+    }
260
+
261
+    /**
262
+     * 删除查获记录
263
+     */
264
+    @PreAuthorize("@ss.hasPermi('item:record:remove')")
265
+    @Log(title = "查获记录", businessType = BusinessType.DELETE)
266
+    @DeleteMapping("/{ids}")
267
+    public AjaxResult remove(@PathVariable Long[] ids) {
268
+        return toAjax(itemSeizureRecordService.deleteItemSeizureRecordByIds(ids));
269
+    }
270
+}

File diff suppressed because it is too large
+ 1711 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/item/PerformanceDimensionController.java


+ 556 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/item/PerformanceIndicatorsController.java

@@ -0,0 +1,556 @@
1
+package com.sundot.airport.web.controller.item;
2
+
3
+import cn.hutool.core.collection.CollectionUtil;
4
+import cn.hutool.core.date.DateUtil;
5
+import com.sundot.airport.common.core.controller.BaseController;
6
+import com.sundot.airport.common.core.domain.AjaxResult;
7
+import com.sundot.airport.common.core.domain.entity.SysUser;
8
+import com.sundot.airport.common.dto.*;
9
+import com.sundot.airport.common.enums.RoleTypeEnum;
10
+import com.sundot.airport.common.utils.DeptUtils;
11
+import com.sundot.airport.common.utils.GeometricMeanUtils;
12
+import com.sundot.airport.common.utils.poi.ExcelUtil;
13
+import com.sundot.airport.system.domain.SysConfig;
14
+import com.sundot.airport.system.mapper.SysUserMapper;
15
+import com.sundot.airport.system.service.ISysConfigService;
16
+import com.sundot.airport.system.service.ISysDeptService;
17
+import io.swagger.annotations.Api;
18
+import lombok.extern.slf4j.Slf4j;
19
+import org.springframework.beans.factory.annotation.Autowired;
20
+import org.springframework.web.bind.annotation.*;
21
+
22
+import javax.servlet.http.HttpServletResponse;
23
+import java.math.BigDecimal;
24
+import java.util.*;
25
+import java.util.stream.Collectors;
26
+
27
+/**
28
+ * 绩效指标管理 Controller
29
+ *
30
+ * @author sundot
31
+ * @date 2026-03-16
32
+ */
33
+@Slf4j
34
+@RestController
35
+@RequestMapping("/item/indicators")
36
+@Api(tags = "绩效指标管理")
37
+public class PerformanceIndicatorsController extends BaseController {
38
+
39
+    @Autowired
40
+    private ISysConfigService configService;
41
+
42
+    @Autowired
43
+    private PerformanceMetricsController performanceMetricsController;
44
+
45
+    @Autowired
46
+    private SysUserMapper userMapper;
47
+
48
+    @Autowired
49
+    private ISysDeptService sysDeptService;
50
+
51
+
52
+    // 绩效矩阵参数的配置键名
53
+    private static final String CONFIG_KEY_PARAM_A = "item.performance.matrix.param_a";
54
+    private static final String CONFIG_KEY_PARAM_B = "item.performance.matrix.param_b";
55
+    private static final String CONFIG_KEY_PARAM_C = "item.performance.matrix.param_c";
56
+
57
+
58
+    /**
59
+     * 保存绩效矩阵
60
+     * 保存前端传入的绩效矩阵参数 a、b、c
61
+     * 默认值 a=3, b=5, c=2
62
+     *
63
+     * @param a
64
+     * @param b
65
+     * @param c
66
+     * @return 保存结果
67
+     */
68
+    @GetMapping("/matrix/add")
69
+    public AjaxResult performanceMatrix(
70
+            @RequestParam(value = "a", defaultValue = "3") BigDecimal a,
71
+            @RequestParam(value = "b", defaultValue = "5") BigDecimal b,
72
+            @RequestParam(value = "c", defaultValue = "2") BigDecimal c) {
73
+
74
+        try {
75
+            // 保存到 sys_config 表
76
+            saveConfig(CONFIG_KEY_PARAM_A, "绩效矩阵参数 a", a.toString());
77
+            saveConfig(CONFIG_KEY_PARAM_B, "绩效矩阵参数 b", b.toString());
78
+            saveConfig(CONFIG_KEY_PARAM_C, "绩效矩阵参数 c", c.toString());
79
+
80
+            // 同步更新到 GeometricMeanUtils
81
+            GeometricMeanUtils.setParams(a.doubleValue(), b.doubleValue(), c.doubleValue());
82
+
83
+            return success("保存成功");
84
+        } catch (Exception e) {
85
+            log.error("保存绩效矩阵参数失败:{}", e.getMessage());
86
+            return error("保存失败:" + e.getMessage());
87
+        }
88
+    }
89
+
90
+    /**
91
+     * 获取绩效矩阵参数配置
92
+     *
93
+     * @return 绩效矩阵参数配置
94
+     */
95
+    @GetMapping("/matrix/config")
96
+    public AjaxResult getMatrixConfig() {
97
+        try {
98
+            String paramA = configService.selectConfigByKey(CONFIG_KEY_PARAM_A);
99
+            String paramB = configService.selectConfigByKey(CONFIG_KEY_PARAM_B);
100
+            String paramC = configService.selectConfigByKey(CONFIG_KEY_PARAM_C);
101
+
102
+            // 如果不存在,返回默认值
103
+            if (paramA == null || paramA.isEmpty()) paramA = "3";
104
+            if (paramB == null || paramB.isEmpty()) paramB = "5";
105
+            if (paramC == null || paramC.isEmpty()) paramC = "2";
106
+
107
+            Map<String, Object> result = new HashMap<>();
108
+            result.put("paramA", new BigDecimal(paramA));
109
+            result.put("paramB", new BigDecimal(paramB));
110
+            result.put("paramC", new BigDecimal(paramC));
111
+
112
+            return success(result);
113
+        } catch (Exception e) {
114
+            log.error("获取绩效矩阵参数失败:{}", e.getMessage());
115
+            return error("获取失败:" + e.getMessage());
116
+        }
117
+    }
118
+
119
+    /**
120
+     * 绩效查询
121
+     * 调用现有的 /metrics 接口获取绩效数据
122
+     *
123
+     * @param param 查询参数
124
+     * @return 绩效数据列表
125
+     */
126
+    @GetMapping("/query")
127
+    public AjaxResult queryPerformance(PerformanceMetricsParamDto param) {
128
+        try {
129
+            // 设置默认值
130
+            if (param.getDimension() == null) {
131
+                param.setDimension(4); // 默认查询大队
132
+            }
133
+            if (param.getSortOrder() == null) {
134
+                param.setSortOrder(2); // 默认降序
135
+            }
136
+
137
+            return performanceMetricsController.getPerformanceMetrics(param);
138
+        } catch (Exception e) {
139
+            log.error("绩效查询失败:{}", e.getMessage());
140
+            return AjaxResult.error("查询失败:" + e.getMessage());
141
+        }
142
+    }
143
+
144
+
145
+    /**
146
+     * 导出绩效数据
147
+     * 根据维度不同导出不同的内容
148
+     *
149
+     * @param response HTTP 响应
150
+     * @param param    查询参数
151
+     */
152
+    @GetMapping("/export")
153
+    public void exportPerformance(HttpServletResponse response, PerformanceMetricsParamDto param) {
154
+        // 设置默认值
155
+        if (param.getDimension() == null) {
156
+            param.setDimension(4); // 默认查询大队
157
+        }
158
+        if (param.getSortOrder() == null) {
159
+            param.setSortOrder(2); // 默认降序
160
+        }
161
+
162
+        // 查询绩效数据
163
+        AjaxResult result = performanceMetricsController.getPerformanceMetrics(param);
164
+        List<PerformanceMetricsResultDto> dataList = (List<PerformanceMetricsResultDto>) result.get("data");
165
+
166
+        String fileName = "绩效数据";
167
+
168
+        // 根据维度转换数据
169
+        if (param.getDimension() == 1) {
170
+            // 人员维度 - 导出个人绩效(包含姓名、班组名、科室名、大队名)
171
+            List<PerformanceMetricsPersonalExportDto> exportList = convertToPersonalExportList(dataList, param.getStartTime(), param.getEndTime());
172
+            ExcelUtil<PerformanceMetricsPersonalExportDto> util = new ExcelUtil<>(PerformanceMetricsPersonalExportDto.class);
173
+            util.exportExcel(response, exportList, fileName);
174
+        } else if (param.getDimension() == 2) {
175
+            // 班组维度 - 导出班组绩效(包含班组名、科室名、大队名)
176
+            List<PerformanceMetricsTeamExportDto> exportList = convertToTeamExportList(dataList, param.getStartTime(), param.getEndTime());
177
+            ExcelUtil<PerformanceMetricsTeamExportDto> util = new ExcelUtil<>(PerformanceMetricsTeamExportDto.class);
178
+            util.exportExcel(response, exportList, fileName);
179
+        } else if (param.getDimension() == 3) {
180
+            // 科室维度 - 导出科室绩效(包含科室名、大队名)
181
+            List<PerformanceMetricsDeptExportDto> exportList = convertToDeptExportList(dataList, param.getStartTime(), param.getEndTime());
182
+            ExcelUtil<PerformanceMetricsDeptExportDto> util = new ExcelUtil<>(PerformanceMetricsDeptExportDto.class);
183
+            util.exportExcel(response, exportList, fileName);
184
+        } else if (param.getDimension() == 4) {
185
+            // 大队维度 - 导出大队绩效(仅大队名)
186
+            List<PerformanceMetricsBrigadeExportDto> exportList = convertToBrigadeExportList(dataList, param.getStartTime(), param.getEndTime());
187
+            ExcelUtil<PerformanceMetricsBrigadeExportDto> util = new ExcelUtil<>(PerformanceMetricsBrigadeExportDto.class);
188
+            util.exportExcel(response, exportList, fileName);
189
+        }
190
+    }
191
+
192
+    /**
193
+     * 班组组建
194
+     *
195
+     * @param param 组建参数
196
+     * @return 组建结果
197
+     */
198
+    @PostMapping("/team/build")
199
+    public AjaxResult teamBuild(@RequestBody TeamBuildParamDto param) {
200
+        try {
201
+            // 参数校验
202
+            if (param.getTeamCount() == null || param.getTeamCount() < 1 || param.getTeamCount() > 10) {
203
+                return error("重点班组数量必须在1-10之间");
204
+            }
205
+            if (param.getTargetSize() == null || param.getTargetSize() < 2) {
206
+                return error("目标班组人数至少为2人");
207
+            }
208
+            if (param.getLeaderCondition() == null || param.getLeaderCondition() < 1 || param.getLeaderCondition() > 3) {
209
+                return error("组建班组长条件必须为1-综合能力、2-管理能力、3-业务能力");
210
+            }
211
+
212
+            // 查询个人维度的绩效数据(包含班组长和安检员)
213
+            PerformanceMetricsParamDto metricsParam = new PerformanceMetricsParamDto();
214
+            metricsParam.setDimension(1); // 个人维度
215
+            metricsParam.setStartTime(DateUtil.parse(param.getStartTime(), "yyyy-MM-dd"));
216
+            metricsParam.setEndTime(DateUtil.parse(param.getEndTime(), "yyyy-MM-dd"));
217
+            metricsParam.setSortField("totalScore");
218
+            metricsParam.setSortOrder(2); // 降序
219
+
220
+            AjaxResult result = performanceMetricsController.getPerformanceMetrics(metricsParam);
221
+            List<PerformanceMetricsResultDto> personalMetrics = (List<PerformanceMetricsResultDto>) result.get("data");
222
+
223
+            if (personalMetrics == null || personalMetrics.isEmpty()) {
224
+                return error("未查询到绩效数据");
225
+            }
226
+
227
+            // 获取所有班组长和安检员的用户信息
228
+            Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
229
+            if (topSiteId == null) {
230
+                return error("无法找到有效的站点信息");
231
+            }
232
+
233
+            List<SysUser> allUsers = userMapper.selectUserByDeptId(topSiteId);
234
+            Map<Long, SysUser> userMap = allUsers.stream()
235
+                    .filter(user -> user.getRoles() != null)
236
+                    .filter(user -> user.getRoles().stream()
237
+                            .anyMatch(role -> RoleTypeEnum.banzuzhang.getCode().equals(role.getRoleKey())
238
+                                    || RoleTypeEnum.SecurityCheck.getCode().equals(role.getRoleKey())))
239
+                    .collect(Collectors.toMap(SysUser::getUserId, user -> user, (u1, u2) -> u1));
240
+
241
+            // 根据组建条件选择班组长
242
+            List<TeamBuildResultDto> teamResults = new ArrayList<>();
243
+            Set<Long> selectedUserIds = new HashSet<>(); // 记录已选中的用户
244
+
245
+            // 1. 选择班组长
246
+            List<TeamMemberDto> leaders = selectLeaders(param, personalMetrics, userMap);
247
+            if (leaders.size() < param.getTeamCount()) {
248
+                return error("班组长人数不足,无法组建" + param.getTeamCount() + "个重点班组");
249
+            }
250
+
251
+            // 2. 选择成员并分配
252
+            List<TeamMemberDto> members = selectMembers(param, personalMetrics, userMap, selectedUserIds);
253
+
254
+            // 3. 组建班组
255
+            int memberCount = param.getTargetSize() - 1; // 每个班组的成员数 = 目标人数 - 1个班组长
256
+            int totalMembersNeeded = param.getTeamCount() * memberCount; // 总共需要的成员数
257
+
258
+            if (members.size() < totalMembersNeeded) {
259
+                return error("成员人数不足,需要" + totalMembersNeeded + "人,但只有" + members.size() + "人");
260
+            }
261
+
262
+            // 4. 分配成员到各个班组
263
+            for (int i = 0; i < param.getTeamCount(); i++) {
264
+                TeamBuildResultDto teamResult = new TeamBuildResultDto();
265
+                teamResult.setTeamNo(i + 1);
266
+
267
+                List<TeamMemberDto> teamMembers = new ArrayList<>();
268
+                // 添加班组长
269
+                teamMembers.add(leaders.get(i));
270
+                selectedUserIds.add(leaders.get(i).getUserId());
271
+
272
+                // 随机分配成员
273
+                for (int j = 0; j < memberCount; j++) {
274
+                    int randomIndex = new Random().nextInt(members.size());
275
+                    TeamMemberDto member = members.remove(randomIndex);
276
+                    teamMembers.add(member);
277
+                    selectedUserIds.add(member.getUserId());
278
+                }
279
+
280
+                teamResult.setMembers(teamMembers);
281
+                teamResults.add(teamResult);
282
+            }
283
+
284
+            return success(teamResults);
285
+        } catch (Exception e) {
286
+            log.error("班组组建失败:{}", e.getMessage());
287
+            return error("班组组建失败:" + e.getMessage());
288
+        }
289
+    }
290
+
291
+    /**
292
+     * 根据条件选择班组长
293
+     */
294
+    private List<TeamMemberDto> selectLeaders(TeamBuildParamDto param,
295
+                                              List<PerformanceMetricsResultDto> personalMetrics,
296
+                                              Map<Long, SysUser> userMap) {
297
+        List<TeamMemberDto> leaders = new ArrayList<>();
298
+
299
+        // 筛选出班组长角色的用户绩效数据
300
+        List<PerformanceMetricsResultDto> leaderMetrics = personalMetrics.stream()
301
+                .filter(metric -> {
302
+                    SysUser user = userMap.get(metric.getId());
303
+                    return user != null && user.getRoles().stream()
304
+                            .anyMatch(role -> RoleTypeEnum.banzuzhang.getCode().equals(role.getRoleKey()));
305
+                })
306
+                .collect(Collectors.toList());
307
+
308
+        Integer leaderCondition = param.getLeaderCondition();
309
+        if (leaderCondition == 1) {
310
+            // 综合能力:个人总分和班组总分之和除以2
311
+            // 获取班组绩效数据
312
+            PerformanceMetricsParamDto teamParam = new PerformanceMetricsParamDto();
313
+            teamParam.setDimension(2);
314
+            teamParam.setStartTime(DateUtil.parse(param.getStartTime(), "yyyy-MM-dd"));
315
+            teamParam.setEndTime(DateUtil.parse(param.getEndTime(), "yyyy-MM-dd"));
316
+            teamParam.setSortField("totalScore");
317
+            teamParam.setSortOrder(2);
318
+
319
+            AjaxResult teamResult = performanceMetricsController.getPerformanceMetrics(teamParam);
320
+            List<PerformanceMetricsResultDto> teamMetrics = (List<PerformanceMetricsResultDto>) teamResult.get("data");
321
+            Map<Long, BigDecimal> teamScoreMap = teamMetrics.stream()
322
+                    .collect(Collectors.toMap(PerformanceMetricsResultDto::getId, PerformanceMetricsResultDto::getTotalScore, (s1, s2) -> s1));
323
+
324
+            // 计算综合得分
325
+            for (PerformanceMetricsResultDto metric : leaderMetrics) {
326
+                SysUser user = userMap.get(metric.getId());
327
+                if (user != null && user.getDeptId() != null) {
328
+                    BigDecimal teamScore = teamScoreMap.getOrDefault(user.getDeptId(), BigDecimal.ZERO);
329
+                    BigDecimal comprehensiveScore = metric.getTotalScore()
330
+                            .add(teamScore)
331
+                            .divide(new BigDecimal("2"), 2, BigDecimal.ROUND_HALF_UP);
332
+
333
+                    TeamMemberDto leader = new TeamMemberDto();
334
+                    leader.setUserId(user.getUserId());
335
+                    leader.setName(user.getNickName());
336
+                    leader.setUserName(user.getUserName());
337
+                    leader.setRole("班组长");
338
+                    leader.setScore(comprehensiveScore);
339
+                    leaders.add(leader);
340
+                }
341
+            }
342
+
343
+            // 按综合得分降序排序
344
+            leaders.sort((a, b) -> b.getScore().compareTo(a.getScore()));
345
+
346
+        } else if (leaderCondition == 2) {
347
+            // 管理能力:使用班组维度的绩效总分
348
+            PerformanceMetricsParamDto teamParam = new PerformanceMetricsParamDto();
349
+            teamParam.setDimension(2);
350
+            teamParam.setStartTime(DateUtil.parse(param.getStartTime(), "yyyy-MM-dd"));
351
+            teamParam.setEndTime(DateUtil.parse(param.getEndTime(), "yyyy-MM-dd"));
352
+            teamParam.setSortField("totalScore");
353
+            teamParam.setSortOrder(2);
354
+
355
+            AjaxResult teamResult = performanceMetricsController.getPerformanceMetrics(teamParam);
356
+            List<PerformanceMetricsResultDto> teamMetrics = (List<PerformanceMetricsResultDto>) teamResult.get("data");
357
+            Map<Long, BigDecimal> teamScoreMap = teamMetrics.stream()
358
+                    .collect(Collectors.toMap(PerformanceMetricsResultDto::getId, PerformanceMetricsResultDto::getTotalScore, (s1, s2) -> s1));
359
+
360
+            // 获取班组长所在的班组得分
361
+            for (PerformanceMetricsResultDto metric : leaderMetrics) {
362
+                SysUser user = userMap.get(metric.getId());
363
+                if (user != null && user.getDeptId() != null) {
364
+                    BigDecimal teamScore = teamScoreMap.getOrDefault(user.getDeptId(), BigDecimal.ZERO);
365
+
366
+                    TeamMemberDto leader = new TeamMemberDto();
367
+                    leader.setUserId(user.getUserId());
368
+                    leader.setName(user.getNickName());
369
+                    leader.setUserName(user.getUserName());
370
+                    leader.setRole("班组长");
371
+                    leader.setScore(teamScore);
372
+                    leaders.add(leader);
373
+                }
374
+            }
375
+
376
+            // 按班组得分降序排序
377
+            leaders.sort((a, b) -> b.getScore().compareTo(a.getScore()));
378
+
379
+        } else if (leaderCondition == 3) {
380
+            // 业务能力:使用个人的绩效总分
381
+            for (PerformanceMetricsResultDto metric : leaderMetrics) {
382
+                SysUser user = userMap.get(metric.getId());
383
+                if (user != null) {
384
+                    TeamMemberDto leader = new TeamMemberDto();
385
+                    leader.setUserId(user.getUserId());
386
+                    leader.setName(user.getNickName());
387
+                    leader.setUserName(user.getUserName());
388
+                    leader.setRole("班组长");
389
+                    leader.setScore(metric.getTotalScore());
390
+                    leaders.add(leader);
391
+                }
392
+            }
393
+
394
+            // 按个人总分降序排序
395
+            leaders.sort((a, b) -> b.getScore().compareTo(a.getScore()));
396
+        }
397
+
398
+        // 取前 teamCount 个班组长
399
+        int teamCount = param.getTeamCount();
400
+        return leaders.subList(0, Math.min(teamCount, leaders.size()));
401
+    }
402
+
403
+    /**
404
+     * 选择成员(安检员角色)
405
+     */
406
+    private List<TeamMemberDto> selectMembers(TeamBuildParamDto param,
407
+                                              List<PerformanceMetricsResultDto> personalMetrics,
408
+                                              Map<Long, SysUser> userMap,
409
+                                              Set<Long> selectedUserIds) {
410
+        List<TeamMemberDto> members = new ArrayList<>();
411
+
412
+        // 筛选出安检员角色的用户绩效数据
413
+        List<PerformanceMetricsResultDto> memberMetrics = personalMetrics.stream()
414
+                .filter(metric -> {
415
+                    SysUser user = userMap.get(metric.getId());
416
+                    return user != null && user.getRoles().stream()
417
+                            .anyMatch(role -> RoleTypeEnum.SecurityCheck.getCode().equals(role.getRoleKey()));
418
+                })
419
+                .collect(Collectors.toList());
420
+
421
+        // 转换为成员DTO
422
+        for (PerformanceMetricsResultDto metric : memberMetrics) {
423
+            if (selectedUserIds.contains(metric.getId())) {
424
+                continue; // 跳过已选中的用户
425
+            }
426
+
427
+            SysUser user = userMap.get(metric.getId());
428
+            if (user != null) {
429
+                TeamMemberDto member = new TeamMemberDto();
430
+                member.setUserId(user.getUserId());
431
+                member.setName(user.getNickName());
432
+                member.setUserName(user.getUserName());
433
+                member.setRole("安检员");
434
+                member.setScore(metric.getTotalScore());
435
+                members.add(member);
436
+            }
437
+        }
438
+
439
+        // 按个人总分降序排序
440
+        members.sort((a, b) -> b.getScore().compareTo(a.getScore()));
441
+
442
+        return members;
443
+    }
444
+
445
+
446
+    /**
447
+     * 转换为人员导出 DTO 列表
448
+     */
449
+    private List<PerformanceMetricsPersonalExportDto> convertToPersonalExportList(List<PerformanceMetricsResultDto> dataList, Date startTime, Date endTime) {
450
+        return dataList.stream()
451
+                .map(item -> {
452
+                    PerformanceMetricsPersonalExportDto dto = new PerformanceMetricsPersonalExportDto();
453
+                    dto.setName(item.getName());
454
+                    dto.setClassName(item.getClassName());
455
+                    dto.setDeptName(item.getDeptName());
456
+                    dto.setBrigadeName(item.getBrigadeName());
457
+                    dto.setSeizureEfficiency(item.getSeizureEfficiency());
458
+                    dto.setInspectionPassRate(item.getInspectionPassRate());
459
+                    dto.setTrainingScore(item.getTrainingScore());
460
+                    dto.setTotalScore(item.getTotalScore());
461
+                    dto.setStartTime(DateUtil.format(startTime, "yyyy-MM-dd"));
462
+                    dto.setEndTime(DateUtil.format(endTime, "yyyy-MM-dd"));
463
+                    return dto;
464
+                })
465
+                .collect(Collectors.toList());
466
+    }
467
+
468
+    /**
469
+     * 转换为班组导出 DTO 列表
470
+     */
471
+    private List<PerformanceMetricsTeamExportDto> convertToTeamExportList(List<PerformanceMetricsResultDto> dataList, Date startTime, Date endTime) {
472
+        return dataList.stream()
473
+                .map(item -> {
474
+                    PerformanceMetricsTeamExportDto dto = new PerformanceMetricsTeamExportDto();
475
+                    dto.setName(item.getName());
476
+                    dto.setDeptName(item.getDeptName());
477
+                    dto.setBrigadeName(item.getBrigadeName());
478
+                    dto.setSeizureEfficiency(item.getSeizureEfficiency());
479
+                    dto.setInspectionPassRate(item.getInspectionPassRate());
480
+                    dto.setTrainingScore(item.getTrainingScore());
481
+                    dto.setTotalScore(item.getTotalScore());
482
+                    dto.setStartTime(DateUtil.format(startTime, "yyyy-MM-dd"));
483
+                    dto.setEndTime(DateUtil.format(endTime, "yyyy-MM-dd"));
484
+                    return dto;
485
+                })
486
+                .collect(Collectors.toList());
487
+    }
488
+
489
+    /**
490
+     * 转换为科室导出 DTO 列表
491
+     */
492
+    private List<PerformanceMetricsDeptExportDto> convertToDeptExportList(List<PerformanceMetricsResultDto> dataList, Date startTime, Date endTime) {
493
+        return dataList.stream()
494
+                .map(item -> {
495
+                    PerformanceMetricsDeptExportDto dto = new PerformanceMetricsDeptExportDto();
496
+                    dto.setName(item.getName());
497
+                    dto.setBrigadeName(item.getBrigadeName());
498
+                    dto.setSeizureEfficiency(item.getSeizureEfficiency());
499
+                    dto.setInspectionPassRate(item.getInspectionPassRate());
500
+                    dto.setTrainingScore(item.getTrainingScore());
501
+                    dto.setTotalScore(item.getTotalScore());
502
+                    dto.setStartTime(DateUtil.format(startTime, "yyyy-MM-dd"));
503
+                    dto.setEndTime(DateUtil.format(endTime, "yyyy-MM-dd"));
504
+                    return dto;
505
+                })
506
+                .collect(Collectors.toList());
507
+    }
508
+
509
+    /**
510
+     * 转换为大队导出 DTO 列表
511
+     */
512
+    private List<PerformanceMetricsBrigadeExportDto> convertToBrigadeExportList(List<PerformanceMetricsResultDto> dataList, Date startTime, Date endTime) {
513
+        return dataList.stream()
514
+                .map(item -> {
515
+                    PerformanceMetricsBrigadeExportDto dto = new PerformanceMetricsBrigadeExportDto();
516
+                    dto.setName(item.getName());
517
+                    dto.setSeizureEfficiency(item.getSeizureEfficiency());
518
+                    dto.setInspectionPassRate(item.getInspectionPassRate());
519
+                    dto.setTrainingScore(item.getTrainingScore());
520
+                    dto.setTotalScore(item.getTotalScore());
521
+                    dto.setStartTime(DateUtil.format(startTime, "yyyy-MM-dd"));
522
+                    dto.setEndTime(DateUtil.format(endTime, "yyyy-MM-dd"));
523
+                    return dto;
524
+                })
525
+                .collect(Collectors.toList());
526
+    }
527
+
528
+
529
+    /**
530
+     * 保存配置到 sys_config 表
531
+     */
532
+    private void saveConfig(String configKey, String configName, String configValue) {
533
+        try {
534
+            SysConfig queryConfig = new SysConfig();
535
+            queryConfig.setConfigKey(configKey);
536
+            List<SysConfig> configList = configService.selectConfigList(queryConfig);
537
+            if (CollectionUtil.isNotEmpty(configList)) {
538
+                SysConfig config = configList.get(0);
539
+                config.setConfigValue(configValue);
540
+                configService.updateConfig(config);
541
+            } else {
542
+                // 新增配置
543
+                SysConfig config = new SysConfig();
544
+                config.setConfigName(configName);
545
+                config.setConfigKey(configKey);
546
+                config.setConfigValue(configValue);
547
+                config.setConfigType("N"); // 非系统内置
548
+                configService.insertConfig(config);
549
+            }
550
+        } catch (Exception e) {
551
+            log.error("保存配置失败:{}", e.getMessage());
552
+            throw e;
553
+        }
554
+    }
555
+
556
+}

+ 940 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/item/PerformanceMetricsController.java

@@ -0,0 +1,940 @@
1
+package com.sundot.airport.web.controller.item;
2
+
3
+import cn.hutool.core.collection.CollectionUtil;
4
+import com.sundot.airport.check.domain.CheckEfficiencyDto;
5
+import com.sundot.airport.check.domain.CheckEfficiencyRankDto;
6
+import com.sundot.airport.check.service.ICheckEfficiencyService;
7
+import com.sundot.airport.common.core.domain.AjaxResult;
8
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
9
+import com.sundot.airport.common.core.domain.GroupedBoxPlotDataDto;
10
+import com.sundot.airport.common.core.domain.entity.SysDept;
11
+import com.sundot.airport.common.core.domain.entity.SysUser;
12
+import com.sundot.airport.common.dto.PerformanceMetricsParamDto;
13
+import com.sundot.airport.common.dto.PerformanceMetricsResultDto;
14
+import com.sundot.airport.common.enums.DeptTypeEnum;
15
+import com.sundot.airport.common.enums.RoleTypeEnum;
16
+import com.sundot.airport.common.exception.ServiceException;
17
+import com.sundot.airport.common.utils.DeptUtils;
18
+import com.sundot.airport.common.utils.GeometricMeanUtils;
19
+import com.sundot.airport.common.utils.StringUtils;
20
+import com.sundot.airport.exam.service.IAccuracyStatisticsService;
21
+import com.sundot.airport.item.domain.SeizureEfficiencyDto;
22
+import com.sundot.airport.item.domain.SeizureEfficiencyRankDto;
23
+import com.sundot.airport.item.domain.dto.ConcealmentPositionTop1DTO;
24
+import com.sundot.airport.item.domain.dto.ProhibitedItemsTop3DTO;
25
+import com.sundot.airport.item.mapper.ItemLargeScreenMapper;
26
+import com.sundot.airport.item.service.SeizureDistributionService;
27
+import com.sundot.airport.item.service.SeizureEfficiencyService;
28
+import com.sundot.airport.system.mapper.SysDeptMapper;
29
+import com.sundot.airport.system.mapper.SysUserMapper;
30
+import com.sundot.airport.system.service.ISysDeptService;
31
+import lombok.extern.slf4j.Slf4j;
32
+import org.springframework.beans.factory.annotation.Autowired;
33
+import org.springframework.format.annotation.DateTimeFormat;
34
+import org.springframework.web.bind.annotation.*;
35
+
36
+import java.math.BigDecimal;
37
+import java.math.RoundingMode;
38
+import java.text.SimpleDateFormat;
39
+import java.util.*;
40
+import java.util.stream.Collectors;
41
+
42
+import static com.sundot.airport.common.core.domain.AjaxResult.success;
43
+import static com.sundot.airport.common.utils.SecurityUtils.getDeptId;
44
+
45
+/**
46
+ * 绩效指标Controller
47
+ *
48
+ * @author ruoyi
49
+ * @date 2025-07-25
50
+ */
51
+@Slf4j
52
+@RestController
53
+@RequestMapping("/item/performance")
54
+public class PerformanceMetricsController {
55
+
56
+
57
+    @Autowired
58
+    private SeizureEfficiencyService seizureEfficiencyService;
59
+
60
+    @Autowired
61
+    private ICheckEfficiencyService checkEfficiencyService;
62
+
63
+
64
+    @Autowired
65
+    private SeizureDistributionService seizureDistributionService;
66
+
67
+    @Autowired
68
+    private ISysDeptService sysDeptService;
69
+
70
+
71
+    @Autowired
72
+    private SysUserMapper userMapper;
73
+
74
+    @Autowired
75
+    private SysDeptMapper deptMapper;
76
+
77
+    @Autowired
78
+    private IAccuracyStatisticsService accuracyStatisticsService;
79
+
80
+    @Autowired
81
+    private ItemLargeScreenMapper itemLargeScreenMapper;
82
+
83
+    /**
84
+     * 查询查获效率统计
85
+     */
86
+    @GetMapping("/list")
87
+    public AjaxResult list(BaseLargeScreenQueryParamDto dto) {
88
+        SeizureEfficiencyDto result = seizureEfficiencyService.getSeizureEfficiency(dto, null);
89
+        return success(result);
90
+    }
91
+
92
+    /**
93
+     * 获取查获数量分布直方图数据
94
+     *
95
+     * @return 直方图数据列表,每个元素包含:组下限、组上限、该组内安检员人数
96
+     */
97
+    @GetMapping("/histogram")
98
+    public AjaxResult getSeizureHistogramData(BaseLargeScreenQueryParamDto dto) {
99
+        // 获取当前用户所在站点ID
100
+        Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
101
+        if (topSiteId == null) {
102
+            return AjaxResult.error("无法找到有效的站点信息");
103
+        }
104
+        dto.setInspectStationId(topSiteId);
105
+        // 计算查获数量分布
106
+        List<SeizureDistributionService.SeizureHistogramData> result =
107
+                seizureDistributionService.calculateSeizureDistribution(dto);
108
+
109
+        return success(result);
110
+    }
111
+
112
+
113
+    /**
114
+     * 获取查获数量箱线图数据
115
+     *
116
+     * @return 箱线图数据,包含最小观察值、Q1、中位数、Q3、最大观察值、离群点等信息
117
+     */
118
+    @GetMapping("/boxplot")
119
+    public AjaxResult getSeizureBoxPlotData(BaseLargeScreenQueryParamDto dto) {
120
+        // 获取当前用户所在站点ID
121
+        Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
122
+        if (topSiteId == null) {
123
+            return AjaxResult.error("无法找到有效的站点信息");
124
+        }
125
+        dto.setInspectStationId(topSiteId);
126
+        // 计算查获数量箱线图数据
127
+        List<GroupedBoxPlotDataDto> result = seizureDistributionService.calculateBoxPlotData(dto);
128
+
129
+        return success(result);
130
+    }
131
+
132
+    /**
133
+     * 查询全站查获违禁品 TOP3
134
+     *
135
+     * @param startDate 查询开始时间
136
+     *                  endDate 查询结束时间
137
+     * @return 违禁品 TOP3 数据,包含物品名称、查获数量、占比
138
+     */
139
+    @GetMapping("/prohibited-top3")
140
+    public AjaxResult getProhibitedItemsTop3(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
141
+                                             @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
142
+        try {
143
+            BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
144
+            dto.setStartDate(startDate);
145
+            dto.setEndDate(endDate);
146
+            // 获取当前用户所在站点 ID
147
+            Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
148
+            if (topSiteId == null) {
149
+                return AjaxResult.error("无法找到有效的站点信息");
150
+            }
151
+            dto.setInspectStationId(topSiteId);
152
+
153
+            // 查询违禁品 TOP3
154
+            List<ProhibitedItemsTop3DTO> top3List = itemLargeScreenMapper.selectProhibitedItemsTop3(dto);
155
+
156
+            // 计算总数和占比
157
+            BigDecimal totalQuantity = itemLargeScreenMapper.getTotalSum(dto);
158
+            for (int i = 0; i < top3List.size(); i++) {
159
+                ProhibitedItemsTop3DTO item = top3List.get(i);
160
+                item.setRank(i + 1);
161
+
162
+                // 计算占比
163
+                if (totalQuantity.compareTo(BigDecimal.ZERO) > 0) {
164
+                    BigDecimal percentage = item.getQuantity()
165
+                            .multiply(new BigDecimal("100"))
166
+                            .divide(totalQuantity, 2, RoundingMode.HALF_UP);
167
+                    item.setPercentage(percentage);
168
+                } else {
169
+                    item.setPercentage(BigDecimal.ZERO);
170
+                }
171
+            }
172
+
173
+            return success(top3List);
174
+        } catch (Exception e) {
175
+            log.error("查询全站查获违禁品 TOP3 失败:{}", e.getMessage());
176
+            return AjaxResult.error("查询失败:" + e.getMessage());
177
+        }
178
+    }
179
+
180
+    /**
181
+     * 查询隐匿重点部位 Top1
182
+     *
183
+     * @param startDate 查询开始时间
184
+     *                  endDate 查询结束时间
185
+     * @return 隐匿重点部位 Top1 数据
186
+     */
187
+    @GetMapping("/concealment-position-top1")
188
+    public AjaxResult getConcealmentPositionTop1(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
189
+                                                 @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
190
+        try {
191
+            BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
192
+            dto.setStartDate(startDate);
193
+            dto.setEndDate(endDate);
194
+            // 获取当前用户所在站点 ID
195
+            Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
196
+            if (topSiteId == null) {
197
+                return AjaxResult.error("无法找到有效的站点信息");
198
+            }
199
+            dto.setInspectStationId(topSiteId);
200
+
201
+            // 查询隐匿重点部位 Top1
202
+            List<ConcealmentPositionTop1DTO> top1List = itemLargeScreenMapper.selectConcealmentPositionTop1(dto);
203
+
204
+            if (top1List == null || top1List.isEmpty()) {
205
+                return success(new ArrayList<>());
206
+            }
207
+
208
+            return success(top1List);
209
+        } catch (Exception e) {
210
+            log.error("查询隐匿重点部位 Top1 失败:{}", e.getMessage());
211
+            return AjaxResult.error("查询失败:" + e.getMessage());
212
+        }
213
+    }
214
+
215
+    /**
216
+     * 绩效指标列表
217
+     *
218
+     * @param param 查询参数
219
+     * @return 组织/姓名、查获效率、巡检合格率、培训得分、总分
220
+     */
221
+    @PostMapping("/metrics")
222
+    public AjaxResult getPerformanceMetrics(@RequestBody PerformanceMetricsParamDto param) {
223
+        try {
224
+            // 参数校验和默认值设置
225
+            validateAndSetDefaults(param);
226
+
227
+            List<PerformanceMetricsResultDto> result = calculatePerformanceMetrics(param);
228
+
229
+            // 填充科室和班级信息
230
+            enrichDeptAndClassInfo(result, param);
231
+
232
+            sortAndRank(result, param);
233
+
234
+            return success(result);
235
+        } catch (Exception e) {
236
+            return AjaxResult.error("获取绩效指标失败: " + e.getMessage());
237
+        }
238
+    }
239
+
240
+
241
+    /**
242
+     * 填充科室和班级信息
243
+     */
244
+    private void enrichDeptAndClassInfo(List<PerformanceMetricsResultDto> result, PerformanceMetricsParamDto param) {
245
+        // 只处理人员维度和班组维度和科室维度
246
+        if (param.getDimension() == null || param.getDimension() == 4) {
247
+            return;
248
+        }
249
+
250
+
251
+        if (result == null || result.isEmpty()) {
252
+            return;
253
+        }
254
+
255
+        // 收集所有需要查询的 ID
256
+        Set<Long> ids = result.stream()
257
+                .map(PerformanceMetricsResultDto::getId)
258
+                .collect(Collectors.toSet());
259
+
260
+        // 预加载这些 ID 的信息
261
+        Map<Long, SysUser> userMap = new HashMap<>();
262
+        Map<Long, SysDept> deptMap = new HashMap<>();
263
+
264
+        if (param.getDimension() == 1) {
265
+            // 人员维度:加载用户和部门信息
266
+            for (Long id : ids) {
267
+                SysUser user = userMapper.selectUserById(id);
268
+                if (user != null) {
269
+                    userMap.put(id, user);
270
+                }
271
+            }
272
+            // 加载所有部门信息用于查找班组
273
+            List<SysDept> allDepts = sysDeptService.selectChildrenDeptById(DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId())));
274
+            for (SysDept dept : allDepts) {
275
+                deptMap.put(dept.getDeptId(), dept);
276
+            }
277
+        } else if (param.getDimension() == 2 || param.getDimension() == 3) {
278
+            // 班组维度:加载部门信息
279
+            List<SysDept> allDepts = sysDeptService.selectChildrenDeptById(DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId())));
280
+            for (SysDept dept : allDepts) {
281
+                deptMap.put(dept.getDeptId(), dept);
282
+            }
283
+        }
284
+
285
+        // 为所有时间点的数据填充科室和班级信息
286
+        for (PerformanceMetricsResultDto item : result) {
287
+            if (param.getDimension() == 1) {
288
+                // 人员维度
289
+                SysUser user = userMap.get(item.getId());
290
+                if (user != null) {
291
+                    Long deptId = user.getDeptId();
292
+                    if (deptId != null) {
293
+                        SysDept dept = deptMap.get(deptId);
294
+                        if (dept != null) {
295
+                            item.setClassName(dept.getDeptName());
296
+                            if (dept.getParentId() != null) {
297
+                                SysDept parentDept = deptMap.get(dept.getParentId());
298
+                                if (parentDept != null) {
299
+                                    item.setDeptName(parentDept.getDeptName());
300
+                                    if (parentDept.getParentId() != null) {
301
+                                        SysDept brigadeDept = deptMap.get(parentDept.getParentId());
302
+                                        if (brigadeDept != null) {
303
+                                            item.setBrigadeName(brigadeDept.getDeptName());
304
+                                        }
305
+                                    }
306
+                                }
307
+                            }
308
+                        }
309
+                    }
310
+                }
311
+            } else if (param.getDimension() == 2) {
312
+                // 班组维度
313
+                SysDept dept = deptMap.get(item.getId());
314
+                if (dept != null) {
315
+                    if (dept.getParentId() != null) {
316
+                        SysDept parentDept = deptMap.get(dept.getParentId());
317
+                        if (parentDept != null) {
318
+                            item.setDeptName(parentDept.getDeptName());
319
+                            if (parentDept.getParentId() != null) {
320
+                                SysDept brigadeDept = deptMap.get(parentDept.getParentId());
321
+                                if (brigadeDept != null) {
322
+                                    item.setBrigadeName(brigadeDept.getDeptName());
323
+                                }
324
+                            }
325
+                        }
326
+                    }
327
+                }
328
+            } else if (param.getDimension() == 3) {
329
+                // 科室维度
330
+                SysDept dept = deptMap.get(item.getId());
331
+                if (dept != null) {
332
+                    if (dept.getParentId() != null) {
333
+                        SysDept parentDept = deptMap.get(dept.getParentId());
334
+                        if (parentDept != null) {
335
+                            item.setBrigadeName(parentDept.getDeptName());
336
+                        }
337
+                    }
338
+                }
339
+            }
340
+        }
341
+    }
342
+
343
+    /**
344
+     * 参数校验和设置默认值
345
+     */
346
+    private void validateAndSetDefaults(PerformanceMetricsParamDto param) {
347
+        if (param.getDimension() == null) {
348
+            param.setDimension(1); // 默认个人维度
349
+        }
350
+        if (param.getSortOrder() == null) {
351
+            param.setSortOrder(2); // 默认降序
352
+        }
353
+    }
354
+
355
+    /**
356
+     * 计算绩效指标
357
+     */
358
+    private List<PerformanceMetricsResultDto> calculatePerformanceMetrics(PerformanceMetricsParamDto param) {
359
+        List<PerformanceMetricsResultDto> result = new ArrayList<>();
360
+
361
+        if (param.getDimension() == 1) {
362
+            // 个人维度
363
+            result = calculatePersonalMetrics(param);
364
+        } else if (param.getDimension() == 2) {
365
+            // 班组维度
366
+            result = calculateTeamMetrics(param);
367
+        } else if (param.getDimension() == 3) {
368
+            // 科室维度
369
+            result = calculateDepartmentMetrics(param);
370
+        } else if (param.getDimension() == 4) {
371
+            // 大队维度
372
+            result = calculateBrigadeMetrics(param);
373
+        }
374
+
375
+        return result;
376
+    }
377
+
378
+    /**
379
+     * 计算查获效率
380
+     *
381
+     * @param param
382
+     * @return
383
+     */
384
+    private List<SeizureEfficiencyRankDto> calculateSeizureEfficiency(PerformanceMetricsParamDto param) {
385
+        List<SeizureEfficiencyRankDto> rankList = new ArrayList<>();
386
+
387
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
388
+        dto.setStartDate(param.getStartTime());
389
+        dto.setEndDate(param.getEndTime());
390
+        SeizureEfficiencyDto result = seizureEfficiencyService.getSeizureEfficiency(dto, param.getDimension());
391
+        if (param.getDimension() == 1) {
392
+            // 个人维度
393
+            rankList = result.getIndividualRankList();
394
+        } else if (param.getDimension() == 2) {
395
+            // 班组维度
396
+            rankList = result.getTeamRankList();
397
+        } else if (param.getDimension() == 3) {
398
+            // 科室维度
399
+            rankList = result.getDepartmentRankList();
400
+        } else if (param.getDimension() == 4) {
401
+            // 大队维度
402
+            rankList = result.getBrigadeRankList();
403
+        }
404
+        return rankList;
405
+    }
406
+
407
+
408
+    /**
409
+     * 计算个人绩效指标
410
+     */
411
+    private List<PerformanceMetricsResultDto> calculatePersonalMetrics(PerformanceMetricsParamDto param) {
412
+        List<PerformanceMetricsResultDto> result = new ArrayList<>();
413
+
414
+        // 获取当前用户所在站点ID
415
+        Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
416
+        if (topSiteId == null) {
417
+            throw new ServiceException("无法找到有效的站点信息");
418
+        }
419
+
420
+        // 根据大队 ID 筛选
421
+        if (param.getBrigadeId() != null) {
422
+            topSiteId = param.getBrigadeId();
423
+        }
424
+
425
+        // 根据科室 ID 筛选
426
+        if (param.getDeptId() != null) {
427
+            topSiteId = param.getDeptId();
428
+        }
429
+
430
+        // 根据班组 ID 筛选
431
+        if (param.getTeamId() != null) {
432
+            topSiteId = param.getTeamId();
433
+        }
434
+
435
+        List<SysUser> sysUserTempList = userMapper.selectUserByDeptId(topSiteId);
436
+        List<SysUser> users = sysUserTempList.stream()
437
+                .filter(item -> item.getRoles().stream()
438
+                        .anyMatch(sysRole -> RoleTypeEnum.banzuzhang.getCode().equals(sysRole.getRoleKey())
439
+                                || RoleTypeEnum.SecurityCheck.getCode().equals(sysRole.getRoleKey())))
440
+                .collect(Collectors.toList());
441
+
442
+        if (users.isEmpty()) return result;
443
+
444
+        // 根据姓名模糊筛选
445
+        if (StringUtils.isNotEmpty(param.getUserName())) {
446
+            users = users.stream()
447
+                    .filter(user -> user.getNickName() != null
448
+                            && user.getNickName().contains(param.getUserName()))
449
+                    .collect(Collectors.toList());
450
+        }
451
+
452
+        if (users.isEmpty()) return result;
453
+
454
+        //查获效率
455
+        List<SeizureEfficiencyRankDto> rankList = calculateSeizureEfficiency(param);
456
+        //巡检合格率
457
+        List<CheckEfficiencyRankDto> checkRankList = calculateCheckEfficiency(param);
458
+
459
+        // 批量查询抽问抽答正确率
460
+        List<Long> userIds = users.stream().map(SysUser::getUserId).collect(Collectors.toList());
461
+        String startDate = formatDate(param.getStartTime());
462
+        String endDate = formatDate(param.getEndTime());
463
+        Map<Long, BigDecimal> accuracyMap = accuracyStatisticsService.getUsersAccuracyMap(userIds, startDate, endDate);
464
+
465
+        // 批量查询指标数据
466
+        List<MetricData> metricDataList = new ArrayList<>();
467
+        for (SysUser user : users) {
468
+            try {
469
+                MetricData data = new MetricData();
470
+                data.id = user.getUserId();
471
+                data.name = user.getNickName();
472
+                data.seizureEfficiency = queryPersonalSeizureEfficiency(user.getUserId(), rankList);
473
+                data.inspectionPassRate = queryPersonalInspectionRate(user.getUserId(), checkRankList);
474
+                data.trainingScore = queryPersonalTrainingScore(user.getUserId(), accuracyMap);
475
+                metricDataList.add(data);
476
+            } catch (Exception e) {
477
+                log.error("查询用户{}指标失败:{}", user.getUserName(), e.getMessage());
478
+            }
479
+        }
480
+
481
+        // 集体处理
482
+        return normalizeAndCreateResult(metricDataList);
483
+    }
484
+
485
+    /**
486
+     * 计算班组绩效指标
487
+     */
488
+    private List<PerformanceMetricsResultDto> calculateTeamMetrics(PerformanceMetricsParamDto param) {
489
+        List<PerformanceMetricsResultDto> result = new ArrayList<>();
490
+        // 查询所有班组
491
+        Long topSiteId = DeptUtils.getTopSiteId(deptMapper.selectDeptById(getDeptId()));
492
+
493
+        // 根据大队 ID 筛选
494
+        if (param.getBrigadeId() != null) {
495
+            topSiteId = param.getBrigadeId();
496
+        }
497
+
498
+        // 根据科室 ID 筛选
499
+        if (param.getDeptId() != null) {
500
+            topSiteId = param.getDeptId();
501
+        }
502
+
503
+        List<SysDept> teams = sysDeptService.selectChildrenDeptById(topSiteId).stream()
504
+                .filter(dept -> DeptTypeEnum.TEAMS.getCode().equals(dept.getDeptType()))
505
+                .collect(Collectors.toList());
506
+
507
+        // 根据班组 ID 筛选
508
+        if (param.getTeamId() != null && CollectionUtil.isNotEmpty(teams)) {
509
+            teams = teams.stream()
510
+                    .filter(item -> param.getTeamId().equals(item.getDeptId()))
511
+                    .collect(Collectors.toList());
512
+        }
513
+
514
+        if (teams.isEmpty()) return result;
515
+
516
+        //查获效率
517
+        List<SeizureEfficiencyRankDto> rankList = calculateSeizureEfficiency(param);
518
+        //巡检合格率
519
+        List<CheckEfficiencyRankDto> checkRankList = calculateCheckEfficiency(param);
520
+
521
+        // 批量查询抽问抽答正确率
522
+        List<Long> teamIds = teams.stream().map(SysDept::getDeptId).collect(Collectors.toList());
523
+        String startDate = formatDate(param.getStartTime());
524
+        String endDate = formatDate(param.getEndTime());
525
+        Map<Long, BigDecimal> accuracyMap = accuracyStatisticsService.getTeamsAccuracyMap(teamIds, startDate, endDate);
526
+
527
+        // 批量查询指标数据
528
+        List<MetricData> metricDataList = new ArrayList<>();
529
+        for (SysDept team : teams) {
530
+            try {
531
+                MetricData data = new MetricData();
532
+                data.id = team.getDeptId();
533
+                data.name = team.getDeptName();
534
+                data.seizureEfficiency = queryTeamSeizureEfficiency(team.getDeptId(), rankList);
535
+                data.inspectionPassRate = queryTeamInspectionRate(team.getDeptId(), checkRankList);
536
+                data.trainingScore = queryTeamTrainingScore(team.getDeptId(), accuracyMap);
537
+                metricDataList.add(data);
538
+            } catch (Exception e) {
539
+                log.error("查询班组{}指标失败:{}", team.getDeptName(), e.getMessage());
540
+            }
541
+        }
542
+
543
+        // 集体处理
544
+        return normalizeAndCreateResult(metricDataList);
545
+    }
546
+
547
+    /**
548
+     * 计算科室绩效指标
549
+     */
550
+    private List<PerformanceMetricsResultDto> calculateDepartmentMetrics(PerformanceMetricsParamDto param) {
551
+        List<PerformanceMetricsResultDto> result = new ArrayList<>();
552
+
553
+        // 查询所有科室
554
+        Long topSiteId = DeptUtils.getTopSiteId(deptMapper.selectDeptById(getDeptId()));
555
+
556
+        // 根据大队 ID 筛选
557
+        if (param.getBrigadeId() != null) {
558
+            topSiteId = param.getBrigadeId();
559
+        }
560
+
561
+        List<SysDept> departments = sysDeptService.selectChildrenDeptById(topSiteId).stream()
562
+                .filter(dept -> DeptTypeEnum.MANAGER.getCode().equals(dept.getDeptType()))
563
+                .collect(Collectors.toList());
564
+
565
+        // 根据科室 ID 筛选
566
+        if (param.getDeptId() != null && CollectionUtil.isNotEmpty(departments)) {
567
+            departments = departments.stream()
568
+                    .filter(item -> param.getDeptId().equals(item.getDeptId()))
569
+                    .collect(Collectors.toList());
570
+        }
571
+
572
+        if (departments.isEmpty()) return result;
573
+
574
+        //查获效率
575
+        List<SeizureEfficiencyRankDto> rankList = calculateSeizureEfficiency(param);
576
+        //巡检合格率
577
+        List<CheckEfficiencyRankDto> checkRankList = calculateCheckEfficiency(param);
578
+
579
+        // 批量查询抽问抽答正确率
580
+        List<Long> deptIds = departments.stream().map(SysDept::getDeptId).collect(Collectors.toList());
581
+        String startDate = formatDate(param.getStartTime());
582
+        String endDate = formatDate(param.getEndTime());
583
+        Map<Long, BigDecimal> accuracyMap = accuracyStatisticsService.getDeptsAccuracyMap(deptIds, startDate, endDate);
584
+
585
+        // 批量查询指标数据
586
+        List<MetricData> metricDataList = new ArrayList<>();
587
+        for (SysDept dept : departments) {
588
+            try {
589
+                MetricData data = new MetricData();
590
+                data.id = dept.getDeptId();
591
+                data.name = dept.getDeptName();
592
+                data.seizureEfficiency = queryDepartmentSeizureEfficiency(dept.getDeptId(), rankList);
593
+                data.inspectionPassRate = queryDepartmentInspectionRate(dept.getDeptId(), checkRankList);
594
+                data.trainingScore = queryDepartmentTrainingScore(dept.getDeptId(), accuracyMap);
595
+                metricDataList.add(data);
596
+            } catch (Exception e) {
597
+                log.error("查询科室{}指标失败:{}", dept.getDeptName(), e.getMessage());
598
+            }
599
+        }
600
+
601
+        // 集体处理
602
+        return normalizeAndCreateResult(metricDataList);
603
+    }
604
+
605
+    /**
606
+     * 计算大队绩效指标
607
+     */
608
+    private List<PerformanceMetricsResultDto> calculateBrigadeMetrics(PerformanceMetricsParamDto param) {
609
+        List<PerformanceMetricsResultDto> result = new ArrayList<>();
610
+
611
+        // 查询所有科室
612
+        Long topSiteId = DeptUtils.getTopSiteId(deptMapper.selectDeptById(getDeptId()));
613
+        List<SysDept> brigades = sysDeptService.selectChildrenDeptById(topSiteId).stream()
614
+                .filter(dept -> DeptTypeEnum.BRIGADE.getCode().equals(dept.getDeptType()))
615
+                .collect(Collectors.toList());
616
+
617
+        // 根据大队 ID 筛选
618
+        if (param.getBrigadeId() != null && CollectionUtil.isNotEmpty(brigades)) {
619
+            brigades = brigades.stream()
620
+                    .filter(item -> param.getBrigadeId().equals(item.getDeptId()))
621
+                    .collect(Collectors.toList());
622
+        }
623
+
624
+        if (brigades.isEmpty()) return result;
625
+
626
+        //查获效率
627
+        List<SeizureEfficiencyRankDto> rankList = calculateSeizureEfficiency(param);
628
+        //巡检合格率
629
+        List<CheckEfficiencyRankDto> checkRankList = calculateCheckEfficiency(param);
630
+
631
+        // 批量查询抽问抽答正确率
632
+        List<Long> deptIds = brigades.stream().map(SysDept::getDeptId).collect(Collectors.toList());
633
+        String startDate = formatDate(param.getStartTime());
634
+        String endDate = formatDate(param.getEndTime());
635
+        Map<Long, BigDecimal> accuracyMap = accuracyStatisticsService.getDeptsAccuracyMap(deptIds, startDate, endDate);
636
+
637
+        // 批量查询指标数据
638
+        List<MetricData> metricDataList = new ArrayList<>();
639
+        for (SysDept dept : brigades) {
640
+            try {
641
+                MetricData data = new MetricData();
642
+                data.id = dept.getDeptId();
643
+                data.name = dept.getDeptName();
644
+                data.seizureEfficiency = queryDepartmentSeizureEfficiency(dept.getDeptId(), rankList);
645
+                data.inspectionPassRate = queryDepartmentInspectionRate(dept.getDeptId(), checkRankList);
646
+                data.trainingScore = queryDepartmentTrainingScore(dept.getDeptId(), accuracyMap);
647
+                metricDataList.add(data);
648
+            } catch (Exception e) {
649
+                log.error("查询科室{}指标失败:{}", dept.getDeptName(), e.getMessage());
650
+            }
651
+        }
652
+
653
+        // 集体处理
654
+        return normalizeAndCreateResult(metricDataList);
655
+    }
656
+
657
+    /**
658
+     * 集体处理
659
+     */
660
+    private List<PerformanceMetricsResultDto> normalizeAndCreateResult(List<MetricData> dataList) {
661
+        List<PerformanceMetricsResultDto> result = new ArrayList<>();
662
+
663
+        if (dataList.isEmpty()) return result;
664
+
665
+        // 标准化各项指标
666
+        List<NormalizedData> normalizedList = new ArrayList<>();
667
+        for (MetricData data : dataList) {
668
+            NormalizedData normalized = new NormalizedData();
669
+            normalized.id = data.id;
670
+            normalized.name = data.name;
671
+            normalized.seizureEfficiency = data.seizureEfficiency.multiply(new BigDecimal(100));
672
+            normalized.inspectionPassRate = data.inspectionPassRate.multiply(new BigDecimal(100));
673
+            normalized.trainingScore = data.trainingScore.multiply(new BigDecimal(100));
674
+            // 总分 = 查获效率 + 巡检合格率 + 抽问抽答正确率
675
+            normalized.totalScore = normalizeValue(data.seizureEfficiency, "w1")
676
+                    .add(normalizeValue(data.inspectionPassRate, "w2"))
677
+                    .add(normalizeValue(data.trainingScore, "w3"));
678
+            normalizedList.add(normalized);
679
+        }
680
+
681
+        // 转换为结果对象
682
+        for (NormalizedData data : normalizedList) {
683
+            PerformanceMetricsResultDto dto = new PerformanceMetricsResultDto();
684
+            dto.setId(data.id);
685
+            dto.setName(data.name);
686
+            dto.setSeizureEfficiency(data.seizureEfficiency);
687
+            dto.setInspectionPassRate(data.inspectionPassRate);
688
+            dto.setTrainingScore(data.trainingScore);
689
+            dto.setTotalScore(data.totalScore);
690
+            result.add(dto);
691
+        }
692
+
693
+        return result;
694
+    }
695
+
696
+    /**
697
+     * 标准化值处理
698
+     */
699
+    private BigDecimal normalizeValue(BigDecimal value, String w) {
700
+        BigDecimal bigDecimal = value != null ? value : BigDecimal.ZERO;
701
+        GeometricMeanUtils.CalculationResult calculationResult = GeometricMeanUtils.calculateWithWeights();
702
+        double d = 0;
703
+        if ("w1".equals(w)) {
704
+            d = calculationResult.getW1();
705
+        }
706
+        if ("w2".equals(w)) {
707
+            d = calculationResult.getW2();
708
+        }
709
+        if ("w3".equals(w)) {
710
+            d = calculationResult.getW3();
711
+        }
712
+        return bigDecimal.multiply(new BigDecimal(d)).setScale(4, RoundingMode.HALF_UP).multiply(new BigDecimal("100"));
713
+    }
714
+
715
+    /**
716
+     * 排序和设置排名
717
+     */
718
+    private void sortAndRank(List<PerformanceMetricsResultDto> result, PerformanceMetricsParamDto param) {
719
+        // 排序
720
+        String sortField = param.getSortField();
721
+        if (sortField == null) sortField = "totalScore";
722
+
723
+        Comparator<PerformanceMetricsResultDto> comparator;
724
+        switch (sortField) {
725
+            case "seizureEfficiency":
726
+                comparator = Comparator.comparing(PerformanceMetricsResultDto::getSeizureEfficiency,
727
+                        Comparator.nullsFirst(BigDecimal::compareTo));
728
+                break;
729
+            case "inspectionPassRate":
730
+                comparator = Comparator.comparing(PerformanceMetricsResultDto::getInspectionPassRate,
731
+                        Comparator.nullsFirst(BigDecimal::compareTo));
732
+                break;
733
+            case "trainingScore":
734
+                comparator = Comparator.comparing(PerformanceMetricsResultDto::getTrainingScore,
735
+                        Comparator.nullsFirst(BigDecimal::compareTo));
736
+                break;
737
+            default: // totalScore
738
+                comparator = Comparator.comparing(PerformanceMetricsResultDto::getTotalScore,
739
+                        Comparator.nullsFirst(BigDecimal::compareTo));
740
+                break;
741
+        }
742
+
743
+        if (Integer.valueOf(2).equals(param.getSortOrder())) {
744
+            comparator = comparator.reversed(); // 降序
745
+        }
746
+
747
+        result.sort(comparator);
748
+
749
+        // 设置排名
750
+        for (int i = 0; i < result.size(); i++) {
751
+            result.get(i).setRank(i + 1);
752
+        }
753
+    }
754
+
755
+    /**
756
+     * 实现个人查获效率查询
757
+     * 个人查获效率=(个人查获效率-个人最低查获效率)/(个人最高查获效率-个人最低查获效率)
758
+     */
759
+    private BigDecimal queryPersonalSeizureEfficiency(Long userId, List<SeizureEfficiencyRankDto> rankList) {
760
+        BigDecimal reduce = rankList.stream().filter(rank -> rank.getId().equals(userId)).map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).reduce(BigDecimal.ZERO, BigDecimal::add);
761
+        BigDecimal min = reduce.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : rankList.stream().map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
762
+        BigDecimal max = rankList.stream().map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
763
+        BigDecimal difference = max.subtract(min);
764
+        BigDecimal normalized = difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : reduce.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
765
+        normalized = new BigDecimal("0.8").add(new BigDecimal("0.2").multiply(normalized));
766
+        return normalized;
767
+    }
768
+
769
+    /**
770
+     * 实现个人巡检合格率查询
771
+     *
772
+     * @param userId
773
+     * @param rankList
774
+     * @return
775
+     */
776
+    private BigDecimal queryPersonalInspectionRate(Long userId, List<CheckEfficiencyRankDto> rankList) {
777
+        BigDecimal reduce = rankList.stream().filter(rank -> rank.getId().equals(userId)).map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).reduce(BigDecimal.ZERO, BigDecimal::add);
778
+        BigDecimal min = reduce.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : rankList.stream().map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
779
+        BigDecimal max = rankList.stream().map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
780
+        BigDecimal difference = max.subtract(min);
781
+        BigDecimal normalized = difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : reduce.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
782
+        normalized = new BigDecimal("0.8").add(new BigDecimal("0.2").multiply(normalized));
783
+        return normalized;
784
+    }
785
+
786
+    /**
787
+     * 实现个人抽问抽答正确率查询
788
+     * 归一化公式:(当前值 - min) / (max - min)
789
+     */
790
+    private BigDecimal queryPersonalTrainingScore(Long userId, Map<Long, BigDecimal> accuracyMap) {
791
+        return new BigDecimal("0.8").add(new BigDecimal("0.2").multiply(normalizeFromMap(userId, accuracyMap)));
792
+    }
793
+
794
+    /**
795
+     * 实现班组查获效率查询
796
+     * 组织查获效率=(组织查获效率-组织最低查获效率)/(组织最高查获效率-组织最低查获效率)
797
+     */
798
+    private BigDecimal queryTeamSeizureEfficiency(Long teamId, List<SeizureEfficiencyRankDto> rankList) {
799
+        BigDecimal reduce = rankList.stream().filter(rank -> rank.getId().equals(teamId)).map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).reduce(BigDecimal.ZERO, BigDecimal::add);
800
+        BigDecimal min = reduce.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : rankList.stream().map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
801
+        BigDecimal max = rankList.stream().map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
802
+        BigDecimal difference = max.subtract(min);
803
+        BigDecimal normalized = difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : reduce.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
804
+        normalized = new BigDecimal("0.8").add(new BigDecimal("0.2").multiply(normalized));
805
+        return normalized;
806
+    }
807
+
808
+    /**
809
+     * 实现班组巡检合格率查询
810
+     */
811
+    private BigDecimal queryTeamInspectionRate(Long teamId, List<CheckEfficiencyRankDto> rankList) {
812
+        BigDecimal reduce = rankList.stream().filter(rank -> rank.getId().equals(teamId)).map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).reduce(BigDecimal.ZERO, BigDecimal::add);
813
+        BigDecimal min = reduce.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : rankList.stream().map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
814
+        BigDecimal max = rankList.stream().map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
815
+        BigDecimal difference = max.subtract(min);
816
+        BigDecimal normalized = difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : reduce.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
817
+        normalized = new BigDecimal("0.8").add(new BigDecimal("0.2").multiply(normalized));
818
+        return normalized;
819
+    }
820
+
821
+    /**
822
+     * 实现班组抽问抽答正确率查询
823
+     * 归一化公式:(当前值 - min) / (max - min)
824
+     */
825
+    private BigDecimal queryTeamTrainingScore(Long teamId, Map<Long, BigDecimal> accuracyMap) {
826
+        return new BigDecimal("0.8").add(new BigDecimal("0.2").multiply(normalizeFromMap(teamId, accuracyMap)));
827
+    }
828
+
829
+    /**
830
+     * 实现科室查获效率查询
831
+     * 组织查获效率=(组织查获效率-组织最低查获效率)/(组织最高查获效率-组织最低查获效率)
832
+     */
833
+    private BigDecimal queryDepartmentSeizureEfficiency(Long deptId, List<SeizureEfficiencyRankDto> rankList) {
834
+        BigDecimal reduce = rankList.stream().filter(rank -> rank.getId().equals(deptId)).map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).reduce(BigDecimal.ZERO, BigDecimal::add);
835
+        BigDecimal min = reduce.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : rankList.stream().map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
836
+        BigDecimal max = rankList.stream().map(rank -> rank.getEfficiency() == null ? BigDecimal.ZERO : rank.getEfficiency()).max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
837
+        BigDecimal difference = max.subtract(min);
838
+        BigDecimal normalized = difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : reduce.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
839
+        normalized = new BigDecimal("0.8").add(new BigDecimal("0.2").multiply(normalized));
840
+        return normalized;
841
+    }
842
+
843
+    /**
844
+     * 实现科室巡检合格率查询
845
+     */
846
+    private BigDecimal queryDepartmentInspectionRate(Long deptId, List<CheckEfficiencyRankDto> rankList) {
847
+        BigDecimal reduce = rankList.stream().filter(rank -> rank.getId().equals(deptId)).map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).reduce(BigDecimal.ZERO, BigDecimal::add);
848
+        BigDecimal min = reduce.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : rankList.stream().map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
849
+        BigDecimal max = rankList.stream().map(rank -> rank.getPassRate() == null ? BigDecimal.ZERO : rank.getPassRate()).max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
850
+        BigDecimal difference = max.subtract(min);
851
+        BigDecimal normalized = difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : reduce.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
852
+        normalized = new BigDecimal("0.8").add(new BigDecimal("0.2").multiply(normalized));
853
+        return normalized;
854
+    }
855
+
856
+    /**
857
+     * 实现科室抽问抽答正确率查询
858
+     * 归一化公式:(当前值 - min) / (max - min)
859
+     */
860
+    private BigDecimal queryDepartmentTrainingScore(Long deptId, Map<Long, BigDecimal> accuracyMap) {
861
+        return new BigDecimal("0.8").add(new BigDecimal("0.2").multiply(normalizeFromMap(deptId, accuracyMap)));
862
+    }
863
+
864
+    /**
865
+     * 从正确率Map中归一化指定ID的值
866
+     * 归一化公式:(当前值 - min) / (max - min),与查获效率的归一化逻辑一致
867
+     */
868
+    private BigDecimal normalizeFromMap(Long id, Map<Long, BigDecimal> accuracyMap) {
869
+        if (accuracyMap == null || accuracyMap.isEmpty()) {
870
+            return BigDecimal.ZERO;
871
+        }
872
+        BigDecimal value = accuracyMap.getOrDefault(id, BigDecimal.ZERO);
873
+        BigDecimal min = accuracyMap.values().stream().min(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
874
+        BigDecimal max = accuracyMap.values().stream().max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
875
+        BigDecimal difference = max.subtract(min);
876
+        return difference.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : value.subtract(min).divide(difference, 2, RoundingMode.HALF_UP);
877
+    }
878
+
879
+    /**
880
+     * Date转yyyy-MM-dd字符串
881
+     */
882
+    private String formatDate(Date date) {
883
+        if (date == null) {
884
+            return null;
885
+        }
886
+        return new SimpleDateFormat("yyyy-MM-dd").format(date);
887
+    }
888
+
889
+    /**
890
+     * 指标数据内部类
891
+     */
892
+    private static class MetricData {
893
+        Long id;
894
+        String name;
895
+        BigDecimal seizureEfficiency;
896
+        BigDecimal inspectionPassRate;
897
+        BigDecimal trainingScore;
898
+    }
899
+
900
+    /**
901
+     * 标准化数据内部类
902
+     */
903
+    private static class NormalizedData {
904
+        Long id;
905
+        String name;
906
+        BigDecimal seizureEfficiency;
907
+        BigDecimal inspectionPassRate;
908
+        BigDecimal trainingScore;
909
+        BigDecimal totalScore;
910
+    }
911
+
912
+    /**
913
+     * 计算巡检合格率
914
+     *
915
+     * @param param
916
+     * @return
917
+     */
918
+    private List<CheckEfficiencyRankDto> calculateCheckEfficiency(PerformanceMetricsParamDto param) {
919
+        List<CheckEfficiencyRankDto> rankList = new ArrayList<>();
920
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
921
+        dto.setStartDate(param.getStartTime());
922
+        dto.setEndDate(param.getEndTime());
923
+        CheckEfficiencyDto result = checkEfficiencyService.getCheckEfficiency(dto, param.getDimension());
924
+        if (param.getDimension() == 1) {
925
+            // 个人维度
926
+            rankList = result.getIndividualRankList();
927
+        } else if (param.getDimension() == 2) {
928
+            // 班组维度
929
+            rankList = result.getTeamRankList();
930
+        } else if (param.getDimension() == 3) {
931
+            // 科室维度
932
+            rankList = result.getDepartmentRankList();
933
+        } else if (param.getDimension() == 3) {
934
+            // 大队维度
935
+            rankList = result.getBrigadeRankList();
936
+        }
937
+        return rankList;
938
+    }
939
+
940
+}

+ 73 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/item/SeizureMessagePushController.java

@@ -0,0 +1,73 @@
1
+package com.sundot.airport.web.controller.item;
2
+
3
+import com.sundot.airport.common.core.controller.BaseController;
4
+import com.sundot.airport.common.core.domain.AjaxResult;
5
+import com.sundot.airport.common.core.domain.model.LoginUser;
6
+import com.sundot.airport.common.utils.DateUtils;
7
+import com.sundot.airport.common.utils.SecurityUtils;
8
+import com.sundot.airport.item.domain.push.SeizureMessagePushDTO;
9
+import com.sundot.airport.item.service.ISeizureMessagePushService;
10
+import io.swagger.annotations.Api;
11
+import io.swagger.annotations.ApiOperation;
12
+import lombok.extern.slf4j.Slf4j;
13
+import org.springframework.beans.factory.annotation.Autowired;
14
+import org.springframework.web.bind.annotation.GetMapping;
15
+import org.springframework.web.bind.annotation.RequestMapping;
16
+import org.springframework.web.bind.annotation.RequestParam;
17
+import org.springframework.web.bind.annotation.RestController;
18
+
19
+import java.util.Date;
20
+
21
+/**
22
+ * 查获消息推送 Controller
23
+ *
24
+ * @author wangxx
25
+ * @date 2026-03-31
26
+ */
27
+@Slf4j
28
+@RestController
29
+@RequestMapping("/item/seizure/push")
30
+@Api(tags = "查获消息推送")
31
+public class SeizureMessagePushController extends BaseController {
32
+
33
+    @Autowired
34
+    private ISeizureMessagePushService seizureMessagePushService;
35
+
36
+    /**
37
+     * 获取查获消息推送数据
38
+     *
39
+     * @param startDate 开始日期(可选,格式:yyyy-MM-dd)
40
+     * @param endDate   结束日期(可选,格式:yyyy-MM-dd)
41
+     * @return 查获消息推送数据
42
+     */
43
+    @ApiOperation("获取查获消息推送数据")
44
+    @GetMapping("/message")
45
+    public AjaxResult getMessagePushData(@RequestParam(required = false) String startDate,
46
+                                         @RequestParam(required = false) String endDate) {
47
+        try {
48
+            LoginUser loginUser = SecurityUtils.getLoginUser();
49
+            Long userId = loginUser.getUserId();
50
+            Long deptId = loginUser.getDeptId();
51
+
52
+            Date startDateTime;
53
+            Date endDateTime;
54
+
55
+            if (startDate == null || endDate == null) {
56
+                // 默认近 15 天(不包含今天)
57
+                endDateTime = DateUtils.addDays(new Date(), -1);
58
+                startDateTime = DateUtils.addDays(endDateTime, -14);
59
+            } else {
60
+                startDateTime = DateUtils.parseDate(startDate);
61
+                endDateTime = DateUtils.parseDate(endDate);
62
+            }
63
+
64
+            SeizureMessagePushDTO result = seizureMessagePushService.getSeizureMessagePushData(
65
+                    userId, deptId, startDateTime, endDateTime);
66
+
67
+            return AjaxResult.success(result);
68
+        } catch (Exception e) {
69
+            log.error("获取查获消息推送数据失败,error: ", e);
70
+            return AjaxResult.error("获取查获消息推送数据失败:" + e.getMessage());
71
+        }
72
+    }
73
+}

+ 115 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/monitor/CacheController.java

@@ -0,0 +1,115 @@
1
+package com.sundot.airport.web.controller.monitor;
2
+
3
+import java.util.ArrayList;
4
+import java.util.Collection;
5
+import java.util.HashMap;
6
+import java.util.List;
7
+import java.util.Map;
8
+import java.util.Properties;
9
+import java.util.Set;
10
+import java.util.TreeSet;
11
+
12
+import org.springframework.beans.factory.annotation.Autowired;
13
+import org.springframework.data.redis.core.RedisCallback;
14
+import org.springframework.data.redis.core.RedisTemplate;
15
+import org.springframework.security.access.prepost.PreAuthorize;
16
+import org.springframework.web.bind.annotation.DeleteMapping;
17
+import org.springframework.web.bind.annotation.GetMapping;
18
+import org.springframework.web.bind.annotation.PathVariable;
19
+import org.springframework.web.bind.annotation.RequestMapping;
20
+import org.springframework.web.bind.annotation.RestController;
21
+import com.sundot.airport.common.constant.CacheConstants;
22
+import com.sundot.airport.common.core.domain.AjaxResult;
23
+import com.sundot.airport.common.utils.StringUtils;
24
+import com.sundot.airport.system.domain.SysCache;
25
+
26
+/**
27
+ * 缓存监控
28
+ *
29
+ * @author ruoyi
30
+ */
31
+@RestController
32
+@RequestMapping("/monitor/cache")
33
+public class CacheController {
34
+    @Autowired
35
+    private RedisTemplate<String, String> redisTemplate;
36
+
37
+    private final static List<SysCache> caches = new ArrayList<SysCache>();
38
+
39
+    {
40
+        caches.add(new SysCache(CacheConstants.LOGIN_TOKEN_KEY, "用户信息"));
41
+        caches.add(new SysCache(CacheConstants.SYS_CONFIG_KEY, "配置信息"));
42
+        caches.add(new SysCache(CacheConstants.SYS_DICT_KEY, "数据字典"));
43
+        caches.add(new SysCache(CacheConstants.CAPTCHA_CODE_KEY, "验证码"));
44
+        caches.add(new SysCache(CacheConstants.REPEAT_SUBMIT_KEY, "防重提交"));
45
+        caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理"));
46
+        caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
47
+    }
48
+
49
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
50
+    @GetMapping()
51
+    public AjaxResult getInfo() throws Exception {
52
+        Properties info = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info());
53
+        Properties commandStats = (Properties) redisTemplate.execute((RedisCallback<Object>) connection -> connection.info("commandstats"));
54
+        Object dbSize = redisTemplate.execute((RedisCallback<Object>) connection -> connection.dbSize());
55
+
56
+        Map<String, Object> result = new HashMap<>(3);
57
+        result.put("info", info);
58
+        result.put("dbSize", dbSize);
59
+
60
+        List<Map<String, String>> pieList = new ArrayList<>();
61
+        commandStats.stringPropertyNames().forEach(key -> {
62
+            Map<String, String> data = new HashMap<>(2);
63
+            String property = commandStats.getProperty(key);
64
+            data.put("name", StringUtils.removeStart(key, "cmdstat_"));
65
+            data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
66
+            pieList.add(data);
67
+        });
68
+        result.put("commandStats", pieList);
69
+        return AjaxResult.success(result);
70
+    }
71
+
72
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
73
+    @GetMapping("/getNames")
74
+    public AjaxResult cache() {
75
+        return AjaxResult.success(caches);
76
+    }
77
+
78
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
79
+    @GetMapping("/getKeys/{cacheName}")
80
+    public AjaxResult getCacheKeys(@PathVariable String cacheName) {
81
+        Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
82
+        return AjaxResult.success(new TreeSet<>(cacheKeys));
83
+    }
84
+
85
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
86
+    @GetMapping("/getValue/{cacheName}/{cacheKey}")
87
+    public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey) {
88
+        String cacheValue = redisTemplate.opsForValue().get(cacheKey);
89
+        SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
90
+        return AjaxResult.success(sysCache);
91
+    }
92
+
93
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
94
+    @DeleteMapping("/clearCacheName/{cacheName}")
95
+    public AjaxResult clearCacheName(@PathVariable String cacheName) {
96
+        Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
97
+        redisTemplate.delete(cacheKeys);
98
+        return AjaxResult.success();
99
+    }
100
+
101
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
102
+    @DeleteMapping("/clearCacheKey/{cacheKey}")
103
+    public AjaxResult clearCacheKey(@PathVariable String cacheKey) {
104
+        redisTemplate.delete(cacheKey);
105
+        return AjaxResult.success();
106
+    }
107
+
108
+    @PreAuthorize("@ss.hasPermi('monitor:cache:list')")
109
+    @DeleteMapping("/clearCacheAll")
110
+    public AjaxResult clearCacheAll() {
111
+        Collection<String> cacheKeys = redisTemplate.keys("*");
112
+        redisTemplate.delete(cacheKeys);
113
+        return AjaxResult.success();
114
+    }
115
+}

+ 25 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/monitor/ServerController.java

@@ -0,0 +1,25 @@
1
+package com.sundot.airport.web.controller.monitor;
2
+
3
+import org.springframework.security.access.prepost.PreAuthorize;
4
+import org.springframework.web.bind.annotation.GetMapping;
5
+import org.springframework.web.bind.annotation.RequestMapping;
6
+import org.springframework.web.bind.annotation.RestController;
7
+import com.sundot.airport.common.core.domain.AjaxResult;
8
+import com.sundot.airport.framework.web.domain.Server;
9
+
10
+/**
11
+ * 服务器监控
12
+ *
13
+ * @author ruoyi
14
+ */
15
+@RestController
16
+@RequestMapping("/monitor/server")
17
+public class ServerController {
18
+    @PreAuthorize("@ss.hasPermi('monitor:server:list')")
19
+    @GetMapping()
20
+    public AjaxResult getInfo() throws Exception {
21
+        Server server = new Server();
22
+        server.copyTo();
23
+        return AjaxResult.success(server);
24
+    }
25
+}

+ 77 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/monitor/SysLogininforController.java

@@ -0,0 +1,77 @@
1
+package com.sundot.airport.web.controller.monitor;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.beans.factory.annotation.Autowired;
7
+import org.springframework.security.access.prepost.PreAuthorize;
8
+import org.springframework.web.bind.annotation.DeleteMapping;
9
+import org.springframework.web.bind.annotation.GetMapping;
10
+import org.springframework.web.bind.annotation.PathVariable;
11
+import org.springframework.web.bind.annotation.PostMapping;
12
+import org.springframework.web.bind.annotation.RequestMapping;
13
+import org.springframework.web.bind.annotation.RestController;
14
+import com.sundot.airport.common.annotation.Log;
15
+import com.sundot.airport.common.core.controller.BaseController;
16
+import com.sundot.airport.common.core.domain.AjaxResult;
17
+import com.sundot.airport.common.core.page.TableDataInfo;
18
+import com.sundot.airport.common.enums.BusinessType;
19
+import com.sundot.airport.common.utils.poi.ExcelUtil;
20
+import com.sundot.airport.framework.web.service.SysPasswordService;
21
+import com.sundot.airport.system.domain.SysLogininfor;
22
+import com.sundot.airport.system.service.ISysLogininforService;
23
+
24
+/**
25
+ * 系统访问记录
26
+ *
27
+ * @author ruoyi
28
+ */
29
+@RestController
30
+@RequestMapping("/monitor/logininfor")
31
+public class SysLogininforController extends BaseController {
32
+    @Autowired
33
+    private ISysLogininforService logininforService;
34
+
35
+    @Autowired
36
+    private SysPasswordService passwordService;
37
+
38
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:list')")
39
+    @GetMapping("/list")
40
+    public TableDataInfo list(SysLogininfor logininfor) {
41
+        startPage();
42
+        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
43
+        return getDataTable(list);
44
+    }
45
+
46
+    @Log(title = "登录日志", businessType = BusinessType.EXPORT)
47
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:export')")
48
+    @PostMapping("/export")
49
+    public void export(HttpServletResponse response, SysLogininfor logininfor) {
50
+        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
51
+        ExcelUtil<SysLogininfor> util = new ExcelUtil<SysLogininfor>(SysLogininfor.class);
52
+        util.exportExcel(response, list, "登录日志");
53
+    }
54
+
55
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
56
+    @Log(title = "登录日志", businessType = BusinessType.DELETE)
57
+    @DeleteMapping("/{infoIds}")
58
+    public AjaxResult remove(@PathVariable Long[] infoIds) {
59
+        return toAjax(logininforService.deleteLogininforByIds(infoIds));
60
+    }
61
+
62
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:remove')")
63
+    @Log(title = "登录日志", businessType = BusinessType.CLEAN)
64
+    @DeleteMapping("/clean")
65
+    public AjaxResult clean() {
66
+        logininforService.cleanLogininfor();
67
+        return success();
68
+    }
69
+
70
+    @PreAuthorize("@ss.hasPermi('monitor:logininfor:unlock')")
71
+    @Log(title = "账户解锁", businessType = BusinessType.OTHER)
72
+    @GetMapping("/unlock/{userName}")
73
+    public AjaxResult unlock(@PathVariable("userName") String userName) {
74
+        passwordService.clearLoginRecordCache(userName);
75
+        return success();
76
+    }
77
+}

+ 65 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/monitor/SysOperlogController.java

@@ -0,0 +1,65 @@
1
+package com.sundot.airport.web.controller.monitor;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.beans.factory.annotation.Autowired;
7
+import org.springframework.security.access.prepost.PreAuthorize;
8
+import org.springframework.web.bind.annotation.DeleteMapping;
9
+import org.springframework.web.bind.annotation.GetMapping;
10
+import org.springframework.web.bind.annotation.PathVariable;
11
+import org.springframework.web.bind.annotation.PostMapping;
12
+import org.springframework.web.bind.annotation.RequestMapping;
13
+import org.springframework.web.bind.annotation.RestController;
14
+import com.sundot.airport.common.annotation.Log;
15
+import com.sundot.airport.common.core.controller.BaseController;
16
+import com.sundot.airport.common.core.domain.AjaxResult;
17
+import com.sundot.airport.common.core.page.TableDataInfo;
18
+import com.sundot.airport.common.enums.BusinessType;
19
+import com.sundot.airport.common.utils.poi.ExcelUtil;
20
+import com.sundot.airport.system.domain.SysOperLog;
21
+import com.sundot.airport.system.service.ISysOperLogService;
22
+
23
+/**
24
+ * 操作日志记录
25
+ *
26
+ * @author ruoyi
27
+ */
28
+@RestController
29
+@RequestMapping("/monitor/operlog")
30
+public class SysOperlogController extends BaseController {
31
+    @Autowired
32
+    private ISysOperLogService operLogService;
33
+
34
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:list')")
35
+    @GetMapping("/list")
36
+    public TableDataInfo list(SysOperLog operLog) {
37
+        startPage();
38
+        List<SysOperLog> list = operLogService.selectOperLogList(operLog);
39
+        return getDataTable(list);
40
+    }
41
+
42
+    @Log(title = "操作日志", businessType = BusinessType.EXPORT)
43
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:export')")
44
+    @PostMapping("/export")
45
+    public void export(HttpServletResponse response, SysOperLog operLog) {
46
+        List<SysOperLog> list = operLogService.selectOperLogList(operLog);
47
+        ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
48
+        util.exportExcel(response, list, "操作日志");
49
+    }
50
+
51
+    @Log(title = "操作日志", businessType = BusinessType.DELETE)
52
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
53
+    @DeleteMapping("/{operIds}")
54
+    public AjaxResult remove(@PathVariable Long[] operIds) {
55
+        return toAjax(operLogService.deleteOperLogByIds(operIds));
56
+    }
57
+
58
+    @Log(title = "操作日志", businessType = BusinessType.CLEAN)
59
+    @PreAuthorize("@ss.hasPermi('monitor:operlog:remove')")
60
+    @DeleteMapping("/clean")
61
+    public AjaxResult clean() {
62
+        operLogService.cleanOperLog();
63
+        return success();
64
+    }
65
+}

+ 73 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/monitor/SysUserOnlineController.java

@@ -0,0 +1,73 @@
1
+package com.sundot.airport.web.controller.monitor;
2
+
3
+import java.util.ArrayList;
4
+import java.util.Collection;
5
+import java.util.Collections;
6
+import java.util.List;
7
+
8
+import org.springframework.beans.factory.annotation.Autowired;
9
+import org.springframework.security.access.prepost.PreAuthorize;
10
+import org.springframework.web.bind.annotation.DeleteMapping;
11
+import org.springframework.web.bind.annotation.GetMapping;
12
+import org.springframework.web.bind.annotation.PathVariable;
13
+import org.springframework.web.bind.annotation.RequestMapping;
14
+import org.springframework.web.bind.annotation.RestController;
15
+import com.sundot.airport.common.annotation.Log;
16
+import com.sundot.airport.common.constant.CacheConstants;
17
+import com.sundot.airport.common.core.controller.BaseController;
18
+import com.sundot.airport.common.core.domain.AjaxResult;
19
+import com.sundot.airport.common.core.domain.model.LoginUser;
20
+import com.sundot.airport.common.core.page.TableDataInfo;
21
+import com.sundot.airport.common.core.redis.RedisCache;
22
+import com.sundot.airport.common.enums.BusinessType;
23
+import com.sundot.airport.common.utils.StringUtils;
24
+import com.sundot.airport.system.domain.SysUserOnline;
25
+import com.sundot.airport.system.service.ISysUserOnlineService;
26
+
27
+/**
28
+ * 在线用户监控
29
+ *
30
+ * @author ruoyi
31
+ */
32
+@RestController
33
+@RequestMapping("/monitor/online")
34
+public class SysUserOnlineController extends BaseController {
35
+    @Autowired
36
+    private ISysUserOnlineService userOnlineService;
37
+
38
+    @Autowired
39
+    private RedisCache redisCache;
40
+
41
+    @PreAuthorize("@ss.hasPermi('monitor:online:list')")
42
+    @GetMapping("/list")
43
+    public TableDataInfo list(String ipaddr, String userName) {
44
+        Collection<String> keys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
45
+        List<SysUserOnline> userOnlineList = new ArrayList<SysUserOnline>();
46
+        for (String key : keys) {
47
+            LoginUser user = redisCache.getCacheObject(key);
48
+            if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName)) {
49
+                userOnlineList.add(userOnlineService.selectOnlineByInfo(ipaddr, userName, user));
50
+            } else if (StringUtils.isNotEmpty(ipaddr)) {
51
+                userOnlineList.add(userOnlineService.selectOnlineByIpaddr(ipaddr, user));
52
+            } else if (StringUtils.isNotEmpty(userName) && StringUtils.isNotNull(user.getUser())) {
53
+                userOnlineList.add(userOnlineService.selectOnlineByUserName(userName, user));
54
+            } else {
55
+                userOnlineList.add(userOnlineService.loginUserToUserOnline(user));
56
+            }
57
+        }
58
+        Collections.reverse(userOnlineList);
59
+        userOnlineList.removeAll(Collections.singleton(null));
60
+        return getDataTable(userOnlineList);
61
+    }
62
+
63
+    /**
64
+     * 强退用户
65
+     */
66
+    @PreAuthorize("@ss.hasPermi('monitor:online:forceLogout')")
67
+    @Log(title = "在线用户", businessType = BusinessType.FORCE)
68
+    @DeleteMapping("/{tokenId}")
69
+    public AjaxResult forceLogout(@PathVariable String tokenId) {
70
+        redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + tokenId);
71
+        return success();
72
+    }
73
+}

+ 209 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/portrait/AttendanceStatsController.java

@@ -0,0 +1,209 @@
1
+package com.sundot.airport.web.controller.portrait;
2
+
3
+import com.sundot.airport.attendance.domain.AttendancePostRecord;
4
+import com.sundot.airport.attendance.portrait.StationWorkStatsService;
5
+import com.sundot.airport.attendance.service.IAttendancePostRecordService;
6
+import com.sundot.airport.common.domain.portrait.IndicatorCalculateParams;
7
+import com.sundot.airport.common.domain.portrait.ModularIndicatorResult;
8
+import com.sundot.airport.common.enums.portrait.UserType;
9
+import com.sundot.airport.common.exception.ServiceException;
10
+import com.sundot.airport.common.service.portrait.ModuleIndicatorManager;
11
+import com.sundot.airport.common.utils.DateUtils;
12
+import com.sundot.airport.common.utils.StringUtils;
13
+import com.sundot.airport.system.domain.vo.PositionInfoVO;
14
+import com.sundot.airport.system.service.IBasePositionService;
15
+import com.sundot.airport.system.service.ISysConfigService;
16
+import org.springframework.beans.factory.annotation.Autowired;
17
+import org.springframework.cache.annotation.Cacheable;
18
+import org.springframework.web.bind.annotation.*;
19
+import springfox.documentation.spring.web.json.JacksonModuleRegistrar;
20
+
21
+import java.math.BigDecimal;
22
+import java.math.RoundingMode;
23
+import java.time.LocalDate;
24
+import java.time.ZoneId;
25
+import java.time.temporal.ChronoUnit;
26
+import java.util.*;
27
+import java.util.stream.Collectors;
28
+
29
+/**
30
+ * 考勤统计控制器
31
+ * 提供考勤指标计算的REST API接口
32
+ */
33
+@RestController
34
+@RequestMapping("/attendance/stats")
35
+public class AttendanceStatsController {
36
+
37
+    @Autowired
38
+    private ModuleIndicatorManager moduleIndicatorManager;
39
+
40
+    @Autowired
41
+    private StationWorkStatsService stationWorkStatsService;
42
+
43
+    @Autowired
44
+    private IAttendancePostRecordService attendancePostRecordService;
45
+
46
+    @Autowired
47
+    private IBasePositionService basePositionService;
48
+
49
+    @Autowired
50
+    private ISysConfigService configService;
51
+
52
+
53
+    /**
54
+     * 计算站级考勤工作统计
55
+     *
56
+     * @param params 指标计算参数
57
+     * @return 站级工作统计数据
58
+     */
59
+    @Cacheable(
60
+            value = "attendance_station",
61
+            keyGenerator = "statisticsKeyGenerator",
62
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
63
+    )
64
+    @GetMapping("/station")
65
+    public Object getStationWorkStats(IndicatorCalculateParams params) {
66
+        return stationWorkStatsService.calculateStationWorkStats(params);
67
+    }
68
+
69
+
70
+    /**
71
+     * 通道开放趋势图(折线图)
72
+     * 通道开放数、开放时长、通道开放平均线
73
+     */
74
+    @Cacheable(
75
+            value = "attendance_open_trend",
76
+            keyGenerator = "statisticsKeyGenerator",
77
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
78
+    )
79
+    @GetMapping("/channel/open/trend")
80
+    public Object getChannelOpenTrend(IndicatorCalculateParams params) {
81
+        // 处理默认时间范围(91天)
82
+        Date startDate = params.getStartTime();
83
+        Date endDate = params.getEndTime();
84
+
85
+        // 如果没有提供时间范围,默认为最近91天
86
+        if (startDate == null || endDate == null) {
87
+            endDate = new Date();
88
+            startDate = DateUtils.addDays(endDate, -90); // 91天包括今天
89
+        }
90
+
91
+        // 生成日期范围
92
+        List<String> dateRange = generateDateRange(startDate, endDate);
93
+        Map<String, Object> channelOpenTrend = new HashMap<>();
94
+        // 按日期分组处理数据
95
+        List<Map<String, Object>> trendData = new ArrayList<>();
96
+        try {
97
+            // 获取班次开始和结束时间
98
+//            String startTime = configService.selectConfigByKey("attendance.post.record.start.time");
99
+//            String endTime = configService.selectConfigByKey("attendance.post.record.end.time");
100
+//            Date shiftStartTime = DateUtils.parseDate(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, startDate) + " " + startTime);
101
+//            Date shiftEndTime = DateUtils.parseDate(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, DateUtils.addDays(endDate, 1)) + " " + endTime);
102
+            String startTime = "00:00:00";
103
+            String endTime = "23:59:59";
104
+            Date shiftStartTime = DateUtils.parseDate(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, startDate) + " " + startTime);
105
+            Date shiftEndTime = DateUtils.parseDate(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, endDate) + " " + endTime);
106
+
107
+            // 一次性查询整个时间范围内的所有在岗记录
108
+            AttendancePostRecord queryPostRecord = new AttendancePostRecord();
109
+            queryPostRecord.setCheckInTimeStart(shiftStartTime);
110
+            queryPostRecord.setCheckInTimeEnd(shiftEndTime);
111
+            if (Objects.nonNull(params.getDeptId())) {
112
+                queryPostRecord.setAttendanceBrigadeId(params.getDeptId());
113
+            }
114
+            List<AttendancePostRecord> postRecords = attendancePostRecordService.selectAttendancePostRecordList(queryPostRecord);
115
+
116
+
117
+            for (String date : dateRange) {
118
+                Map<String, Object> dataPoint = new HashMap<>();
119
+                dataPoint.put("date", date);
120
+
121
+                // 过滤出当前日期的数据
122
+                List<AttendancePostRecord> currentDateRecords = filterRecordsByDate(postRecords, date);
123
+
124
+                // 统计开放通道数
125
+                long openChannelCount = currentDateRecords.stream()
126
+                        .map(AttendancePostRecord::getChannelCode)
127
+                        .filter(Objects::nonNull)
128
+                        .count();
129
+
130
+                // 计算总开放时长(分钟),直接使用数据库中的工作时长字段
131
+                long totalOpenDuration = currentDateRecords.stream()
132
+                        .filter(item -> StringUtils.isNotEmpty(item.getChannelCode()))
133
+                        .mapToLong(record -> {
134
+                            Long workDuration = record.getWorkDuration();
135
+                            // 如果工作时长为null或小于等于0,则返回0
136
+                            return workDuration != null && workDuration > 0 ? workDuration : 0;
137
+                        })
138
+                        .sum();
139
+
140
+
141
+                //开放通道
142
+                dataPoint.put("openCount", openChannelCount);
143
+                //开放时长(小时)
144
+                dataPoint.put("openDuration", totalOpenDuration == 0 ? 0 : new BigDecimal(totalOpenDuration).divide(new BigDecimal(60), 2, RoundingMode.HALF_UP));
145
+                trendData.add(dataPoint);
146
+            }
147
+            BigDecimal divide = new BigDecimal(postRecords.size()).divide(new BigDecimal(dateRange.size()), 1, RoundingMode.HALF_UP);
148
+            channelOpenTrend.put("avg", divide);
149
+            channelOpenTrend.put("trend", trendData);
150
+            return channelOpenTrend;
151
+        } catch (Exception e) {
152
+            throw new ServiceException("通道开放趋势计算错误: " + e.getMessage());
153
+        }
154
+    }
155
+
156
+    /**
157
+     * 根据日期过滤记录
158
+     *
159
+     * @param records    所有记录
160
+     * @param targetDate 目标日期
161
+     * @return 过滤后的记录列表
162
+     */
163
+    private List<AttendancePostRecord> filterRecordsByDate(List<AttendancePostRecord> records, String targetDate) {
164
+        return records.stream()
165
+                .filter(record -> {
166
+                    Date attendanceDate = record.getAttendanceDate();
167
+                    // 检查签到时间是否在目标日期
168
+                    return attendanceDate != null && DateUtils.parseDateToStr("yyyy-MM-dd", attendanceDate).equals(targetDate);
169
+                })
170
+                .collect(Collectors.toList());
171
+    }
172
+
173
+    /**
174
+     * 获取所有通道信息
175
+     *
176
+     * @return 所有通道列表
177
+     */
178
+    private List<PositionInfoVO> getAllChannels() {
179
+        // 这里传入空列表表示获取所有通道
180
+        return basePositionService.selectChannelsUnderArea(new ArrayList<>());
181
+    }
182
+
183
+    /**
184
+     * 生成日期范围列表
185
+     *
186
+     * @param startDate 开始日期
187
+     * @param endDate   结束日期
188
+     * @return 日期范围列表
189
+     */
190
+    private List<String> generateDateRange(Date startDate, Date endDate) {
191
+        List<String> dateRange = new ArrayList<>();
192
+
193
+        // 将Date转换为LocalDate
194
+        LocalDate start = startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
195
+        LocalDate end = endDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
196
+
197
+        // 计算天数差
198
+        long daysBetween = ChronoUnit.DAYS.between(start, end);
199
+
200
+        // 生成日期范围(包括起始和结束日期)
201
+        for (int i = 0; i <= daysBetween; i++) {
202
+            LocalDate currentDate = start.plusDays(i);
203
+            dateRange.add(currentDate.toString());
204
+        }
205
+
206
+        return dateRange;
207
+    }
208
+
209
+}

+ 309 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/portrait/ItemUserRankingController.java

@@ -0,0 +1,309 @@
1
+package com.sundot.airport.web.controller.portrait;
2
+
3
+import cn.hutool.core.collection.CollectionUtil;
4
+import com.sundot.airport.common.core.domain.AjaxResult;
5
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
6
+import com.sundot.airport.common.core.domain.entity.SysDept;
7
+import com.sundot.airport.common.core.domain.entity.SysUser;
8
+import com.sundot.airport.common.domain.portrait.ApprovalDurationStats;
9
+import com.sundot.airport.common.domain.portrait.IndicatorCalculateParams;
10
+import com.sundot.airport.common.enums.DeptTypeEnum;
11
+import com.sundot.airport.common.enums.RoleTypeEnum;
12
+import com.sundot.airport.common.enums.portrait.UserType;
13
+import com.sundot.airport.common.utils.DateUtils;
14
+import com.sundot.airport.common.utils.SecurityUtils;
15
+import com.sundot.airport.item.domain.ItemLargeScreenCommonDto;
16
+import com.sundot.airport.item.domain.ItemLargeScreenDailyTrendDto;
17
+import com.sundot.airport.item.domain.ItemLargeScreenTimeSpanDto;
18
+import com.sundot.airport.item.service.UserRankingService;
19
+import com.sundot.airport.system.domain.approval.ApprovalInstance;
20
+import com.sundot.airport.system.mapper.SysDeptMapper;
21
+import com.sundot.airport.system.mapper.approval.ApprovalHistoryMapper;
22
+import com.sundot.airport.system.mapper.approval.ApprovalInstanceMapper;
23
+import com.sundot.airport.system.mapper.SysUserMapper;
24
+import org.springframework.beans.factory.annotation.Autowired;
25
+import org.springframework.cache.annotation.Cacheable;
26
+import org.springframework.security.access.prepost.PreAuthorize;
27
+import org.springframework.web.bind.annotation.*;
28
+
29
+import javax.annotation.PostConstruct;
30
+import java.math.BigDecimal;
31
+import java.text.ParseException;
32
+import java.time.LocalDate;
33
+import java.time.ZoneId;
34
+import java.time.temporal.ChronoUnit;
35
+import java.util.*;
36
+import java.util.concurrent.TimeUnit;
37
+import java.util.stream.Collectors;
38
+
39
+@RestController
40
+@RequestMapping("/item/user-ranking")
41
+public class ItemUserRankingController {
42
+
43
+    @Autowired
44
+    private UserRankingService userRankingService;
45
+
46
+    @Autowired
47
+    private ApprovalInstanceMapper approvalInstanceMapper;
48
+
49
+    @Autowired
50
+    private ApprovalHistoryMapper approvalHistoryMapper;
51
+
52
+    @Autowired
53
+    private SysUserMapper sysUserMapper;
54
+
55
+    @Autowired
56
+    private SysDeptMapper sysDeptMapper;
57
+
58
+    /**
59
+     * 获取用户在指定层级的详细排名信息
60
+     *
61
+     * @param dto   查询参数
62
+     * @param level 层级类型:station(全站), brigade(大队), department(科室), team(班组)
63
+     * @return 用户在该层级的详细排名信息
64
+     */
65
+    @Cacheable(
66
+            value = "item_ranking_detail",
67
+            keyGenerator = "statisticsKeyGenerator",
68
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
69
+    )
70
+    @PreAuthorize("@ss.hasPermi('item:record:list')")
71
+    @GetMapping("/ranking-detail")
72
+    public AjaxResult getUserRankingByLevel(BaseLargeScreenQueryParamDto dto,
73
+                                            @RequestParam("level") String level) {
74
+        ItemLargeScreenCommonDto ranking = userRankingService.getUserRankingByLevel(dto, level);
75
+        return AjaxResult.success(ranking);
76
+    }
77
+
78
+
79
+    /**
80
+     * 计算站级查获统计
81
+     *
82
+     * @param params 参数
83
+     * @return 站级查获统计
84
+     */
85
+    @Cacheable(
86
+            value = "item_station",
87
+            keyGenerator = "statisticsKeyGenerator",
88
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
89
+    )
90
+    @GetMapping("/station")
91
+    public AjaxResult getStationItemSeizure(IndicatorCalculateParams params) {
92
+        return AjaxResult.success(userRankingService.calculateStationItemSeizure(params));
93
+    }
94
+
95
+    /**
96
+     * 查获审批时长统计
97
+     *
98
+     * @param params 查询参数
99
+     * @return 平均审批时长 最长审批时长 最短审批时长
100
+     */
101
+    @Cacheable(
102
+            value = "item_duration",
103
+            keyGenerator = "statisticsKeyGenerator",
104
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
105
+    )
106
+    @GetMapping("/seizure-approval/duration")
107
+    public AjaxResult getSeizureApprovalDurationStats(IndicatorCalculateParams params) {
108
+        // 设置默认时间范围
109
+        java.util.Date endTime = Optional.ofNullable(params.getEndTime()).orElse(new java.util.Date());
110
+        java.util.Date startTime = Optional.ofNullable(params.getStartTime())
111
+                .orElse(java.util.Date.from(endTime.toInstant().minusSeconds(7862400L))); // 91天
112
+
113
+        // 如果传了部门ID,则先查询该部门及其子部门下的所有用户ID
114
+        List<Long> userIds = null;
115
+        if (params.getDeptId() != null) {
116
+            List<SysUser> sysUserList = sysUserMapper.selectUserListByRoleKeyAndDeptId(
117
+                    Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()),
118
+                    params.getDeptId());
119
+            userIds = sysUserList.stream().map(SysUser::getUserId).collect(Collectors.toList());
120
+        }
121
+
122
+        // 查询workflow_id为4或5的审批实例(查获审批流程)
123
+        List<ApprovalInstance> instances = approvalInstanceMapper.selectApprovalInstancesByWorkflowIdsAndTimeRangeAndUsers(
124
+                Arrays.asList(7L),
125
+                startTime,
126
+                endTime,
127
+                userIds);
128
+
129
+        if (instances.isEmpty()) {
130
+            ApprovalDurationStats result = new ApprovalDurationStats();
131
+            result.setAverageDuration(0L);
132
+            result.setMaxDuration(0L);
133
+            result.setMinDuration(0L);
134
+            return AjaxResult.success(result);
135
+        }
136
+
137
+        // 计算每个实例的审批时长(毫秒)
138
+        List<Long> durations = instances.stream()
139
+                .map(instance -> {
140
+                    Date submitTime = instance.getSubmitTime();
141
+                    Date completionTime = instance.getCompletionTime();
142
+                    if (submitTime != null && completionTime != null) {
143
+                        return completionTime.getTime() - submitTime.getTime();
144
+                    }
145
+                    return 0L;
146
+                })
147
+                .filter(duration -> duration > 0)
148
+                .collect(Collectors.toList());
149
+
150
+        if (durations.isEmpty()) {
151
+            ApprovalDurationStats result = new ApprovalDurationStats();
152
+            result.setAverageDuration(0L);
153
+            result.setMaxDuration(0L);
154
+            result.setMinDuration(0L);
155
+            return AjaxResult.success(result);
156
+        }
157
+
158
+        // 计算统计数据
159
+        long totalDuration = durations.stream().mapToLong(Long::longValue).sum();
160
+        long averageDuration = totalDuration / durations.size();
161
+        long maxDuration = durations.stream().mapToLong(Long::longValue).max().orElse(0L);
162
+        long minDuration = durations.stream().mapToLong(Long::longValue).min().orElse(0L);
163
+
164
+        ApprovalDurationStats result = new ApprovalDurationStats();
165
+        result.setAverageDuration(averageDuration);
166
+        result.setMaxDuration(maxDuration);
167
+        result.setMinDuration(minDuration);
168
+
169
+        // 转换为小时和分钟显示
170
+        result.setAverageDurationText(formatDuration(averageDuration));
171
+        result.setMaxDurationText(formatDuration(maxDuration));
172
+        result.setMinDurationText(formatDuration(minDuration));
173
+
174
+        return AjaxResult.success(result);
175
+    }
176
+
177
+    /**
178
+     * 格式化持续时间显示
179
+     *
180
+     * @param durationMillis 毫秒数
181
+     * @return 格式化后的字符串,如"1天2小时30分钟45秒"
182
+     */
183
+    private String formatDuration(long durationMillis) {
184
+        long seconds = durationMillis / 1000;
185
+        long minutes = seconds / 60;
186
+        long hours = minutes / 60;
187
+        long days = hours / 24;
188
+
189
+        seconds = seconds % 60;
190
+        minutes = minutes % 60;
191
+        hours = hours % 24;
192
+
193
+        StringBuilder sb = new StringBuilder();
194
+        if (days > 0) {
195
+            sb.append(days).append("天");
196
+        }
197
+        if (hours > 0) {
198
+            sb.append(hours).append("小时");
199
+        }
200
+        if (minutes > 0) {
201
+            sb.append(minutes).append("分钟");
202
+        }
203
+        if (seconds > 0 || sb.length() == 0) {
204
+            sb.append(seconds).append("秒");
205
+        }
206
+        return sb.toString();
207
+    }
208
+
209
+    /**
210
+     * 查获趋势图,获取有效查获趋势数据(默认近90天)
211
+     *
212
+     * @param params 查询参数(科室就传部门id,站就可以不传参数)
213
+     * @return 查获趋势数据
214
+     */
215
+    @Cacheable(
216
+            value = "item_seizure_trend",
217
+            keyGenerator = "statisticsKeyGenerator",
218
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
219
+    )
220
+    @GetMapping("/seizure-trend")
221
+    public AjaxResult getSeizureTrendData(IndicatorCalculateParams params) {
222
+        try {
223
+            // 设置默认时间范围为近91天
224
+            Date endTime = Optional.ofNullable(params.getEndTime()).orElse(new Date());
225
+            Date startTime = Optional.ofNullable(params.getStartTime())
226
+                    .orElse(Date.from(endTime.toInstant().minusSeconds(TimeUnit.DAYS.toSeconds(91))));
227
+
228
+            // 参数校验
229
+            if (startTime.after(endTime)) {
230
+                return AjaxResult.error("开始时间不能晚于结束时间");
231
+            }
232
+
233
+            params.setStartTime(startTime);
234
+            params.setEndTime(endTime);
235
+
236
+            // 创建查询参数对象
237
+            BaseLargeScreenQueryParamDto queryDto = new BaseLargeScreenQueryParamDto();
238
+            queryDto.setStartDate(startTime);
239
+            queryDto.setEndDate(endTime);
240
+
241
+            // 如果传了部门ID,则设置到查询参数中
242
+            if (params.getDeptId() != null) {
243
+                SysDept sysDept = sysDeptMapper.selectDeptById(params.getDeptId());
244
+                if (sysDept != null) {
245
+                    if (DeptTypeEnum.BRIGADE.getCode().equals(sysDept.getDeptType())) {
246
+                        queryDto.setInspectBrigadeId(params.getDeptId());
247
+                    } else if (DeptTypeEnum.MANAGER.getCode().equals(sysDept.getDeptType())) {
248
+                        queryDto.setInspectDepartmentId(params.getDeptId());
249
+                    } else if (DeptTypeEnum.TEAMS.getCode().equals(sysDept.getDeptType())) {
250
+                        queryDto.setInspectTeamId(params.getDeptId());
251
+                    }
252
+                }
253
+            }
254
+
255
+            // 调用service获取按天统计的查获趋势数据,没有的补0
256
+            List<ItemLargeScreenDailyTrendDto> trendData = userRankingService.getDailyTrendData(queryDto);
257
+
258
+            List<Date> dateRange = generateDateRange(startTime, endTime);
259
+            // 创建一个Map来快速查找已有数据
260
+            Map<Date, ItemLargeScreenDailyTrendDto> trendDataMap = trendData.stream()
261
+                    .collect(Collectors.toMap(ItemLargeScreenDailyTrendDto::getDate, dto -> dto));
262
+
263
+            // 构建完整的趋势数据列表,缺失日期补0
264
+            List<ItemLargeScreenDailyTrendDto> completeTrendData = new ArrayList<>();
265
+            for (Date date : dateRange) {
266
+                if (trendDataMap.containsKey(date)) {
267
+                    completeTrendData.add(trendDataMap.get(date));
268
+                } else {
269
+                    // 创建一个新的对象并设置日期和默认值
270
+                    ItemLargeScreenDailyTrendDto emptyDto = new ItemLargeScreenDailyTrendDto();
271
+                    emptyDto.setDate(date);
272
+                    emptyDto.setTotal(BigDecimal.ZERO); // 假设没有查获记录时值为0
273
+                    completeTrendData.add(emptyDto);
274
+                }
275
+            }
276
+
277
+            return AjaxResult.success(completeTrendData);
278
+        } catch (Exception e) {
279
+            return AjaxResult.error("获取趋势数据失败:" + e.getMessage());
280
+        }
281
+    }
282
+
283
+    /**
284
+     * 生成日期范围列表
285
+     *
286
+     * @param startDate 开始日期
287
+     * @param endDate   结束日期
288
+     * @return 日期范围列表
289
+     */
290
+    private List<Date> generateDateRange(Date startDate, Date endDate) throws ParseException {
291
+        List<Date> dateRange = new ArrayList<>();
292
+
293
+        // 使用UTC时区确保一致性
294
+        ZoneId zoneId = ZoneId.of("UTC");
295
+        LocalDate start = startDate.toInstant().atZone(zoneId).toLocalDate();
296
+        LocalDate end = endDate.toInstant().atZone(zoneId).toLocalDate();
297
+
298
+        // 计算天数差
299
+        long daysBetween = ChronoUnit.DAYS.between(start, end);
300
+
301
+        // 生成日期范围(包括起始和结束日期)
302
+        for (int i = 0; i <= daysBetween; i++) {
303
+            LocalDate currentDate = start.plusDays(i);
304
+            dateRange.add(DateUtils.parseDate(currentDate.toString(), "yyyy-MM-dd"));
305
+        }
306
+
307
+        return dateRange;
308
+    }
309
+}

+ 121 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/portrait/UserBasicPortraitController.java

@@ -0,0 +1,121 @@
1
+package com.sundot.airport.web.controller.portrait;
2
+
3
+import cn.hutool.core.collection.CollectionUtil;
4
+import cn.hutool.core.util.ObjectUtil;
5
+import com.sundot.airport.common.core.controller.BaseController;
6
+import com.sundot.airport.common.core.domain.AjaxResult;
7
+import com.sundot.airport.common.core.domain.entity.SysDept;
8
+import com.sundot.airport.common.core.domain.entity.SysUser;
9
+import com.sundot.airport.common.domain.portrait.IndicatorCalculateParams;
10
+import com.sundot.airport.common.domain.portrait.ModularIndicatorResult;
11
+import com.sundot.airport.common.dto.UserInfo;
12
+import com.sundot.airport.common.enums.DeptTypeEnum;
13
+import com.sundot.airport.common.enums.RoleTypeEnum;
14
+import com.sundot.airport.common.enums.portrait.UserType;
15
+import com.sundot.airport.system.mapper.SysUserMapper;
16
+import com.sundot.airport.system.service.ISysDeptService;
17
+import com.sundot.airport.system.service.portrait.UserPortraitService;
18
+import com.sundot.airport.web.core.cache.UserCache;
19
+import org.springframework.beans.factory.annotation.Autowired;
20
+import org.springframework.cache.annotation.Cacheable;
21
+import org.springframework.web.bind.annotation.*;
22
+
23
+import java.util.ArrayList;
24
+import java.util.Arrays;
25
+import java.util.List;
26
+import java.util.Optional;
27
+import java.util.stream.Collectors;
28
+
29
+/**
30
+ * 用户画像基本信息
31
+ */
32
+@RestController
33
+@RequestMapping("/user/basic/portrait")
34
+public class UserBasicPortraitController extends BaseController {
35
+
36
+    @Autowired
37
+    private UserPortraitService userPortraitService;
38
+
39
+    @Autowired
40
+    private UserCache userCache;
41
+
42
+    @Autowired
43
+    private SysUserMapper sysUserMapper;
44
+
45
+    @Autowired
46
+    private ISysDeptService sysDeptService;
47
+
48
+
49
+    /**
50
+     * 获取指定模块的指标值
51
+     *
52
+     * @param modules 指定的模块列表,用逗号分隔
53
+     * @return 指定模块的指标值集合
54
+     */
55
+    @Cacheable(
56
+            value = "base_module_info",
57
+            keyGenerator = "statisticsKeyGenerator",
58
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
59
+    )
60
+    @GetMapping("/module/info")
61
+    public AjaxResult moduleInfo(IndicatorCalculateParams params,
62
+                                 @RequestParam(required = false) String modules) {
63
+        ModularIndicatorResult result = new ModularIndicatorResult();
64
+        try {
65
+            // 将字符串转换为枚举
66
+            UserType userTypeEnum = UserType.fromString(params.getUserTypeStr());
67
+            params.setUserType(userTypeEnum);
68
+
69
+            // 设置默认时间范围
70
+            java.util.Date endTime = Optional.ofNullable(params.getEndTime()).orElse(new java.util.Date());
71
+            java.util.Date startTime = Optional.ofNullable(params.getStartTime())
72
+                    .orElse(java.util.Date.from(endTime.toInstant().minusSeconds(7862400L)));
73
+            params.setStartTime(startTime);
74
+            params.setEndTime(endTime);
75
+            if (!UserType.PERSONAL.equals(userTypeEnum)) {
76
+                if (ObjectUtil.isNotEmpty(params.getUserId()) && ObjectUtil.isEmpty(params.getDeptId())) {
77
+                    SysUser user = sysUserMapper.selectUserById(params.getUserId());
78
+                    if (ObjectUtil.isEmpty(user)) {
79
+                        return AjaxResult.success(result);
80
+                    }
81
+                    params.setDeptId(user.getDeptId());
82
+                }
83
+                // 根据这个人的部门id,获取这个人部门下的所有安检员和班组长
84
+                List<SysUser> deptUsers = new ArrayList<>();
85
+                if (params.getDeptId() != null) {
86
+                    deptUsers = sysUserMapper.selectUserListByRoleKeyAndDeptId(
87
+                            Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()),
88
+                            params.getDeptId());
89
+                    if (CollectionUtil.isEmpty(deptUsers)) {
90
+                        return AjaxResult.success(result);
91
+                    }
92
+                }
93
+                if (CollectionUtil.isEmpty(deptUsers)) {
94
+                    return AjaxResult.success(result);
95
+                }
96
+                List<Long> userIds = deptUsers.stream()
97
+                        .map(SysUser::getUserId)
98
+                        .collect(Collectors.toList());
99
+                if (CollectionUtil.isEmpty(userIds)) {
100
+                    return AjaxResult.success(result);
101
+                }
102
+                params.setUserIds(userIds);
103
+            }
104
+
105
+
106
+            if (modules != null && !modules.isEmpty()) {
107
+                // 解析模块列表
108
+                List<String> moduleList = Arrays.asList(modules.split(","));
109
+                result = userPortraitService.getIndicators(params, moduleList);
110
+            } else {
111
+                // 获取所有模块的指标数据
112
+                result = userPortraitService.getIndicators(params);
113
+            }
114
+
115
+            return AjaxResult.success(result);
116
+        } catch (Exception e) {
117
+            return AjaxResult.error("获取用户画像信息失败:" + e.getMessage());
118
+        }
119
+    }
120
+
121
+}

+ 416 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/quality/ItemCategoryStatsController.java

@@ -0,0 +1,416 @@
1
+package com.sundot.airport.web.controller.quality;
2
+
3
+import com.sundot.airport.common.core.controller.BaseController;
4
+import com.sundot.airport.common.core.domain.AjaxResult;
5
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
6
+import com.sundot.airport.common.statistics.TimeDimensionParams;
7
+import com.sundot.airport.common.utils.DeptUtils;
8
+import com.sundot.airport.item.domain.*;
9
+import com.sundot.airport.item.domain.ItemLargeScreenInfoDto;
10
+import com.sundot.airport.item.domain.ItemLargeScreenTimeSpanDto;
11
+import com.sundot.airport.item.domain.dto.ChannelRankingStatsDTO;
12
+import com.sundot.airport.item.domain.dto.ConcealmentPositionStatsDTO;
13
+import com.sundot.airport.item.domain.dto.ItemCategoryStatsDTO;
14
+import com.sundot.airport.item.domain.dto.PostCategoryStatsDTO;
15
+import com.sundot.airport.item.domain.home.SeizureReportDTO;
16
+import com.sundot.airport.item.service.IItemCategoryStatsService;
17
+import com.sundot.airport.item.service.ItemLargeScreenService;
18
+import com.sundot.airport.system.service.ISysDeptService;
19
+import io.swagger.annotations.Api;
20
+import io.swagger.annotations.ApiOperation;
21
+import lombok.extern.slf4j.Slf4j;
22
+import org.springframework.beans.factory.annotation.Autowired;
23
+import org.springframework.web.bind.annotation.GetMapping;
24
+import org.springframework.web.bind.annotation.RequestMapping;
25
+import org.springframework.web.bind.annotation.RestController;
26
+
27
+import java.util.ArrayList;
28
+import java.util.Calendar;
29
+import java.util.List;
30
+import java.util.stream.Collectors;
31
+
32
+/**
33
+ * 物品分类统计Controller
34
+ * 提供物品分类统计相关接口
35
+ *
36
+ * @author wangxx
37
+ * @date 2024-07-23
38
+ */
39
+@Slf4j
40
+@RestController
41
+@RequestMapping("/quality/item-category-stats")
42
+@Api(tags = "物品分类统计")
43
+public class ItemCategoryStatsController extends BaseController {
44
+
45
+    @Autowired
46
+    private ISysDeptService sysDeptService;
47
+
48
+
49
+    @Autowired
50
+    private IItemCategoryStatsService iItemCategoryStatsService;
51
+
52
+    @Autowired
53
+    private ItemLargeScreenService itemLargeScreenService;
54
+
55
+
56
+    /**
57
+     * 获取物品分类统计(全站整体数据)
58
+     * 支持按年、季度、月维度统计
59
+     */
60
+    @ApiOperation("获取物品分类统计")
61
+    @GetMapping("/category-stats")
62
+    public AjaxResult getItemCategoryStats(TimeDimensionParams params) {
63
+        try {
64
+            // 获取当前用户所在站点ID
65
+            Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
66
+            if (topSiteId == null) {
67
+                return AjaxResult.error("无法找到有效的站点信息");
68
+            }
69
+
70
+            ItemCategoryStatsDTO stationCategoryStats = iItemCategoryStatsService.getStationCategoryStats(params, topSiteId);
71
+
72
+            return AjaxResult.success("获取成功", stationCategoryStats);
73
+
74
+        } catch (Exception e) {
75
+            log.error("获取物品分类统计失败", e);
76
+            return AjaxResult.error("获取物品分类统计失败: " + e.getMessage());
77
+        }
78
+    }
79
+
80
+    /**
81
+     * 获取查获时段趋势图
82
+     */
83
+    @ApiOperation("获取查获时段趋势图")
84
+    @GetMapping("/seizure-time-trend")
85
+    public AjaxResult getSeizureTimeTrend(TimeDimensionParams params) {
86
+        try {
87
+            // 获取当前用户所在站点ID
88
+            Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
89
+            if (topSiteId == null) {
90
+                return AjaxResult.error("无法找到有效的站点信息");
91
+            }
92
+
93
+            // 计算时间范围
94
+            TimeDimensionParams timeParams = calculateTimeRange(params);
95
+            BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
96
+            dto.setStartDate(timeParams.getStartTime());
97
+            dto.setEndDate(timeParams.getEndTime());
98
+            dto.setInspectStationId(topSiteId);
99
+            // 设置大队、科室、班、人员筛选条件
100
+            dto.setInspectBrigadeId(params.getBrigadeId());
101
+            dto.setInspectDepartmentId(params.getDepartmentId());
102
+            dto.setInspectTeamId(params.getTeamId());
103
+            dto.setUserId(params.getUserId());
104
+            List<ItemLargeScreenTimeSpanDto> result = itemLargeScreenService.appTimeSpan(dto);
105
+            return AjaxResult.success("获取成功", result);
106
+
107
+        } catch (Exception e) {
108
+            log.error("获取查获时段趋势失败", e);
109
+            return AjaxResult.error("获取查获时段趋势失败: " + e.getMessage());
110
+        }
111
+    }
112
+
113
+    /**
114
+     * 获取隐匿夹带部位分布统计(饼状图数据)
115
+     * 支持按年、季度、月维度统计
116
+     */
117
+    @ApiOperation("获取隐匿夹带部位分布统计")
118
+    @GetMapping("/concealment-position-stats")
119
+    public AjaxResult getConcealmentPositionStats(TimeDimensionParams params) {
120
+        try {
121
+            // 获取当前用户所在站点ID
122
+            Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
123
+            if (topSiteId == null) {
124
+                return AjaxResult.error("无法找到有效的站点信息");
125
+            }
126
+
127
+            ConcealmentPositionStatsDTO stationConcealmentPositionStats =
128
+                    iItemCategoryStatsService.getStationConcealmentPositionStats(params, topSiteId);
129
+
130
+            return AjaxResult.success("获取成功", stationConcealmentPositionStats);
131
+
132
+        } catch (Exception e) {
133
+            log.error("获取隐匿夹带部位分布统计失败", e);
134
+            return AjaxResult.error("获取隐匿夹带部位分布统计失败: " + e.getMessage());
135
+        }
136
+    }
137
+
138
+    /**
139
+     * 获取岗位分类统计(饼状图数据)
140
+     * 支持按年、季度、月维度统计
141
+     */
142
+    @ApiOperation("获取岗位分类统计")
143
+    @GetMapping("/post-category-stats")
144
+    public AjaxResult getPostCategoryStats(TimeDimensionParams params) {
145
+        try {
146
+            // 获取当前用户所在站点ID
147
+            Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
148
+            if (topSiteId == null) {
149
+                return AjaxResult.error("无法找到有效的站点信息");
150
+            }
151
+
152
+            PostCategoryStatsDTO stationPostCategoryStats =
153
+                    iItemCategoryStatsService.getStationPostCategoryStats(params, topSiteId);
154
+
155
+            return AjaxResult.success("获取成功", stationPostCategoryStats);
156
+
157
+        } catch (Exception e) {
158
+            log.error("获取岗位分类统计失败", e);
159
+            return AjaxResult.error("获取岗位分类统计失败: " + e.getMessage());
160
+        }
161
+    }
162
+
163
+    /**
164
+     * 获取通道排名统计(表格数据)
165
+     * 支持按年、季度、月维度统计
166
+     */
167
+    @ApiOperation("获取通道排名统计")
168
+    @GetMapping("/channel-ranking-stats")
169
+    public AjaxResult getChannelRankingStats(TimeDimensionParams params) {
170
+        try {
171
+            // 获取当前用户所在站点ID
172
+            Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
173
+            if (topSiteId == null) {
174
+                return AjaxResult.error("无法找到有效的站点信息");
175
+            }
176
+
177
+            ChannelRankingStatsDTO stationChannelRankingStats =
178
+                    iItemCategoryStatsService.getStationChannelRankingStats(params, topSiteId);
179
+
180
+            return AjaxResult.success("获取成功", stationChannelRankingStats);
181
+
182
+        } catch (Exception e) {
183
+            log.error("获取通道排名统计失败", e);
184
+            return AjaxResult.error("获取通道排名统计失败: " + e.getMessage());
185
+        }
186
+    }
187
+
188
+
189
+    /**
190
+     * 各大队查获排名
191
+     */
192
+    @ApiOperation("获取各大队查获排名")
193
+    @GetMapping("/brigade-ranking")
194
+    public AjaxResult getBrigadeRanking(TimeDimensionParams params) {
195
+        List<SeizureReportDTO.BrigadeRankingItem> brigadeRankings = new ArrayList<>();
196
+        try {
197
+            // 获取当前用户所在站点ID
198
+            Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
199
+            if (topSiteId == null) {
200
+                return AjaxResult.error("无法找到有效的站点信息");
201
+            }
202
+            brigadeRankings = iItemCategoryStatsService.getBrigadeRanking(topSiteId, params);
203
+        } catch (Exception e) {
204
+            log.error("获取各科室查获排名失败", e);
205
+            return AjaxResult.error("获取各科室查获排名失败: " + e.getMessage());
206
+        }
207
+        return AjaxResult.success("获取成功", brigadeRankings);
208
+    }
209
+
210
+
211
+    /**
212
+     * 计算时间范围
213
+     */
214
+    private TimeDimensionParams calculateTimeRange(TimeDimensionParams params) {
215
+        TimeDimensionParams result = new TimeDimensionParams();
216
+
217
+        if (params.getYear() != null) {
218
+            // 按年、季度、月计算时间范围
219
+            int year = params.getYear();
220
+            int startMonth = 1;
221
+            int endMonth = 12;
222
+
223
+            if (params.getQuarter() != null) {
224
+                // 按季度
225
+                startMonth = (params.getQuarter() - 1) * 3 + 1;
226
+                endMonth = startMonth + 2;
227
+            } else if (params.getMonth() != null) {
228
+                // 按月
229
+                startMonth = params.getMonth();
230
+                endMonth = params.getMonth();
231
+            }
232
+
233
+            Calendar cal = Calendar.getInstance();
234
+            cal.set(year, startMonth - 1, 1, 0, 0, 0);
235
+            result.setStartTime(cal.getTime());
236
+
237
+            cal.set(year, endMonth - 1, cal.getActualMaximum(Calendar.DAY_OF_MONTH), 0, 0, 0);
238
+            result.setEndTime(cal.getTime());
239
+        } else {
240
+            // 使用传入的时间范围
241
+            result.setStartTime(params.getStartTime());
242
+            result.setEndTime(params.getEndTime());
243
+        }
244
+
245
+        return result;
246
+    }
247
+
248
+    /**
249
+     * 获取当前用户所在站点 ID
250
+     */
251
+    private Long getCurrentTopSiteId() {
252
+        return DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
253
+    }
254
+
255
+    /**
256
+     * 将 TimeDimensionParams 转换为 BaseLargeScreenQueryParamDto
257
+     */
258
+    private BaseLargeScreenQueryParamDto convertToQueryParamDto(TimeDimensionParams params) {
259
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
260
+
261
+        // 获取当前用户所在站点 ID
262
+        Long topSiteId = getCurrentTopSiteId();
263
+        if (topSiteId != null) {
264
+            dto.setInspectStationId(topSiteId);
265
+        }
266
+
267
+        // 计算时间范围
268
+        TimeDimensionParams timeParams = calculateTimeRange(params);
269
+        dto.setStartDate(timeParams.getStartTime());
270
+        dto.setEndDate(timeParams.getEndTime());
271
+
272
+        // 设置大队、科室、班、人员筛选条件
273
+        dto.setInspectBrigadeId(params.getBrigadeId());
274
+        dto.setInspectDepartmentId(params.getDepartmentId());
275
+        dto.setInspectTeamId(params.getTeamId());
276
+        dto.setUserId(params.getUserId());
277
+
278
+        return dto;
279
+    }
280
+
281
+    /**
282
+     * 移交公安数据
283
+     * 支持按年、季度、月维度统计
284
+     */
285
+    @ApiOperation("移交公安数据")
286
+    @GetMapping("/police-data")
287
+    public AjaxResult getPoliceData(TimeDimensionParams params) {
288
+        try {
289
+            BaseLargeScreenQueryParamDto dto = convertToQueryParamDto(params);
290
+            List<ItemLargeScreenInfoDto> result = itemLargeScreenService.police(dto);
291
+
292
+            List<ItemLargeScreenPoliceGroupDto> tableData = result.stream().map(item -> {
293
+                ItemLargeScreenPoliceGroupDto groupDto = new ItemLargeScreenPoliceGroupDto();
294
+                groupDto.setBrigadeName(item.getInspectBrigadeName());
295
+                groupDto.setDepartmentName(item.getInspectDepartmentName());
296
+                groupDto.setTeamName(item.getInspectTeamName());
297
+                groupDto.setUserName(item.getInspectUserName());
298
+                groupDto.setItemName(item.getCategoryNameTwo());
299
+                groupDto.setQuantity(item.getQuantity());
300
+
301
+                // 查获部位拼接
302
+                StringBuilder position = new StringBuilder();
303
+                if (item.getCheckPositionNameOne() != null && !item.getCheckPositionNameOne().isEmpty()) {
304
+                    position.append(item.getCheckPositionNameOne());
305
+                }
306
+                if (item.getCheckPositionNameTwo() != null && !item.getCheckPositionNameTwo().isEmpty()) {
307
+                    if (position.length() > 0) {
308
+                        position.append("/");
309
+                    }
310
+                    position.append(item.getCheckPositionNameTwo());
311
+                }
312
+                if (item.getCheckPositionSpecific() != null && !item.getCheckPositionSpecific().isEmpty()) {
313
+                    if (position.length() > 0) {
314
+                        position.append("/");
315
+                    }
316
+                    position.append(item.getCheckPositionSpecific());
317
+                }
318
+                groupDto.setPositionName(position.toString());
319
+                groupDto.setSeizureTime(item.getSeizureTime());
320
+                groupDto.setChannelName(item.getChannelName());
321
+                return groupDto;
322
+            }).collect(Collectors.toList());
323
+
324
+            return AjaxResult.success("获取成功", tableData);
325
+        } catch (Exception e) {
326
+            log.error("获取移交公安数据失败", e);
327
+            return AjaxResult.error("获取移交公安数据失败:" + e.getMessage());
328
+        }
329
+    }
330
+
331
+    /**
332
+     * 移交公安数据统计
333
+     * 支持按年、季度、月维度统计
334
+     */
335
+    @ApiOperation("移交公安数据统计")
336
+    @GetMapping("/police-stats")
337
+    public AjaxResult getPoliceStats(TimeDimensionParams params) {
338
+        try {
339
+            BaseLargeScreenQueryParamDto dto = convertToQueryParamDto(params);
340
+            ItemLargeScreenPoliceStatsDto result = itemLargeScreenService.getPoliceStats(dto);
341
+            return AjaxResult.success("获取成功", result);
342
+        } catch (Exception e) {
343
+            log.error("获取移交公安数据统计失败", e);
344
+            return AjaxResult.error("获取移交公安数据统计失败:" + e.getMessage());
345
+        }
346
+    }
347
+
348
+    /**
349
+     * X 光机漏检数据
350
+     * 支持按年、季度、月维度统计
351
+     */
352
+    @ApiOperation("X 光机漏检数据")
353
+    @GetMapping("/xray-miss-check")
354
+    public AjaxResult getXRayMissCheck(TimeDimensionParams params) {
355
+        try {
356
+            BaseLargeScreenQueryParamDto dto = convertToQueryParamDto(params);
357
+            List<ItemXRayMissCheckDto> result = itemLargeScreenService.xRayMissCheck(dto);
358
+            return AjaxResult.success("获取成功", result);
359
+        } catch (Exception e) {
360
+            log.error("获取 X 光机漏检数据失败", e);
361
+            return AjaxResult.error("获取 X 光机漏检数据失败:" + e.getMessage());
362
+        }
363
+    }
364
+
365
+    /**
366
+     * X 光机漏检人员统计 TOP3
367
+     * 支持按年、季度、月维度统计
368
+     */
369
+    @ApiOperation("X 光机漏检人员统计 TOP3")
370
+    @GetMapping("/xray-miss-check-top3")
371
+    public AjaxResult getXRayMissCheckTop3(TimeDimensionParams params) {
372
+        try {
373
+            BaseLargeScreenQueryParamDto dto = convertToQueryParamDto(params);
374
+            List<ItemXRayMissCheckUserStatsDto> result = itemLargeScreenService.xRayMissCheckUserStatsTop3(dto);
375
+            return AjaxResult.success("获取成功", result);
376
+        } catch (Exception e) {
377
+            log.error("获取 X 光机漏检人员统计 TOP3 失败", e);
378
+            return AjaxResult.error("获取 X 光机漏检人员统计 TOP3 失败:" + e.getMessage());
379
+        }
380
+    }
381
+
382
+    /**
383
+     * 异常查获数据
384
+     * 支持按年、季度、月维度统计
385
+     */
386
+    @ApiOperation("异常查获数据")
387
+    @GetMapping("/abnormal-seizure-data")
388
+    public AjaxResult getAbnormalSeizureData(TimeDimensionParams params) {
389
+        try {
390
+            BaseLargeScreenQueryParamDto dto = convertToQueryParamDto(params);
391
+            List<ItemAbnormalSeizureDto> result = itemLargeScreenService.getAbnormalSeizureData(dto);
392
+            return AjaxResult.success("获取成功", result);
393
+        } catch (Exception e) {
394
+            log.error("获取异常查获数据失败", e);
395
+            return AjaxResult.error("获取异常查获数据失败:" + e.getMessage());
396
+        }
397
+    }
398
+
399
+    /**
400
+     * 异常查获数据 TOP3(3 个高于 +3 个低于)
401
+     * 支持按年、季度、月维度统计
402
+     */
403
+    @ApiOperation("异常查获数据 TOP3")
404
+    @GetMapping("/abnormal-seizure-data-top3")
405
+    public AjaxResult getAbnormalSeizureDataTop3(TimeDimensionParams params) {
406
+        try {
407
+            BaseLargeScreenQueryParamDto dto = convertToQueryParamDto(params);
408
+            ItemAbnormalSeizureRankDto result = itemLargeScreenService.getAbnormalSeizureDataTop3(dto);
409
+            return AjaxResult.success("获取成功", result);
410
+        } catch (Exception e) {
411
+            log.error("获取异常查获数据 TOP3 失败", e);
412
+            return AjaxResult.error("获取异常查获数据 TOP3 失败:" + e.getMessage());
413
+        }
414
+    }
415
+
416
+}

+ 541 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/quality/QualificationLevelStatsController.java

@@ -0,0 +1,541 @@
1
+package com.sundot.airport.web.controller.quality;
2
+
3
+import com.sundot.airport.common.core.controller.BaseController;
4
+import com.sundot.airport.common.core.domain.AjaxResult;
5
+import com.sundot.airport.common.core.domain.entity.SysDept;
6
+import com.sundot.airport.common.core.domain.entity.SysUser;
7
+import com.sundot.airport.common.enums.DeptTypeEnum;
8
+import com.sundot.airport.common.utils.DeptUtils;
9
+import com.sundot.airport.item.domain.dto.QualificationLevelStatsDTO;
10
+import com.sundot.airport.system.domain.QualificationLevelConverter;
11
+import com.sundot.airport.system.domain.portrait.QualificationStats;
12
+import com.sundot.airport.system.mapper.portrait.QualificationLevelIndicatorMapper;
13
+import com.sundot.airport.system.service.ISysDeptService;
14
+import com.sundot.airport.system.service.ISysPostService;
15
+import com.sundot.airport.system.service.ISysUserService;
16
+import io.swagger.annotations.Api;
17
+import io.swagger.annotations.ApiOperation;
18
+import org.springframework.beans.factory.annotation.Autowired;
19
+import org.springframework.web.bind.annotation.GetMapping;
20
+import org.springframework.web.bind.annotation.RequestMapping;
21
+import org.springframework.web.bind.annotation.RequestParam;
22
+import org.springframework.web.bind.annotation.RestController;
23
+
24
+import java.math.BigDecimal;
25
+import java.math.RoundingMode;
26
+import java.util.ArrayList;
27
+import java.util.Arrays;
28
+import java.util.HashMap;
29
+import java.util.List;
30
+import java.util.Map;
31
+import java.util.Objects;
32
+import java.util.stream.Collectors;
33
+
34
+/**
35
+ * 资质等级统计控制器
36
+ * 提供资质等级统计的API接口
37
+ *
38
+ * @author wangxx
39
+ */
40
+@Api(tags = "资质等级统计")
41
+@RestController
42
+@RequestMapping("/statistics/qualification")
43
+public class QualificationLevelStatsController extends BaseController {
44
+
45
+    @Autowired
46
+    private QualificationLevelIndicatorMapper qualificationLevelMapper;
47
+
48
+    @Autowired
49
+    private ISysDeptService sysDeptService;
50
+
51
+    @Autowired
52
+    private ISysUserService sysUserService;
53
+
54
+    @Autowired
55
+    private ISysPostService sysPostService;
56
+
57
+
58
+    /**
59
+     * 固定的资质等级列表(初级、中级、高级)
60
+     */
61
+    private static final List<String> QUALIFICATION_LEVELS = Arrays.asList(
62
+            "初级", "中级", "高级"
63
+    );
64
+
65
+    /**
66
+     * 获取饼状图数据(资质等级分布)
67
+     * 支持按部门 ID、用户 ID 进行统计
68
+     */
69
+    @ApiOperation("获取饼状图数据")
70
+    @GetMapping("/pie-chart")
71
+    public AjaxResult getPieChartData(
72
+            @RequestParam(required = false) Long deptId) {
73
+
74
+        try {
75
+            List<Long> targetUserIds;
76
+
77
+            if (deptId != null) {
78
+                // 按部门(大队/科室/班组)统计
79
+                targetUserIds = getAllUserIdsUnderDept(deptId);
80
+            } else {
81
+                // 默认按全站统计
82
+                Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
83
+                if (topSiteId == null) {
84
+                    return AjaxResult.error("无法找到有效的站点信息");
85
+                }
86
+                targetUserIds = getAllUserIdsUnderSite(topSiteId);
87
+            }
88
+
89
+            if (targetUserIds.isEmpty()) {
90
+                return AjaxResult.success("获取成功", new QualificationLevelStatsDTO.PieChartData());
91
+            }
92
+
93
+            QualificationLevelStatsDTO.PieChartData pieChart = calculatePieChartData(targetUserIds);
94
+            return AjaxResult.success("获取成功", pieChart);
95
+
96
+        } catch (Exception e) {
97
+            logger.error("获取饼状图数据失败", e);
98
+            return AjaxResult.error("获取饼状图数据失败: " + e.getMessage());
99
+        }
100
+    }
101
+
102
+    /**
103
+     * 获取柱状图数据(资质等级分布)
104
+     * 支持按大队ID、科室ID、班组ID、用户ID 进行统计
105
+     */
106
+    @ApiOperation("获取柱状图数据")
107
+    @GetMapping("/bar-chart")
108
+    public AjaxResult getBarChartData(
109
+            @RequestParam(required = false) Long deptId,
110
+            @RequestParam(required = false) Long userId) {
111
+
112
+        try {
113
+            QualificationLevelStatsDTO.BarChartData barChart;
114
+
115
+            // 优先处理用户 ID 参数
116
+            if (userId != null) {
117
+                // 按个人统计,返回该用户的资质等级
118
+                barChart = calculateBarChartDataForUser(userId);
119
+            } else if (deptId != null) {
120
+                // 按部门 ID 统计,根据部门类型决定返回逻辑
121
+                barChart = calculateBarChartDataForDept(deptId);
122
+            } else {
123
+                // 未传参,默认统计全站下各科室的分布
124
+                Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
125
+                if (topSiteId == null) {
126
+                    return AjaxResult.error("无法找到有效的站点信息");
127
+                }
128
+                barChart = calculateBarChartDataOptimized(topSiteId);
129
+            }
130
+
131
+            return AjaxResult.success("获取成功", barChart);
132
+
133
+        } catch (Exception e) {
134
+            logger.error("获取柱状图数据失败", e);
135
+            return AjaxResult.error("获取柱状图数据失败: " + e.getMessage());
136
+        }
137
+    }
138
+
139
+    /**
140
+     * 计算饼状图数据(全站范围资质等级分布)
141
+     */
142
+    private QualificationLevelStatsDTO.PieChartData calculatePieChartData(List<Long> userIds) {
143
+        QualificationLevelStatsDTO.PieChartData pieChart = new QualificationLevelStatsDTO.PieChartData();
144
+
145
+        // 查询资质等级统计(暂不考虑时间参数,后续可根据需要扩展)
146
+        List<QualificationStats> statsList = qualificationLevelMapper.queryOrgQualificationLevelStats(userIds, null);
147
+
148
+        // 转换为分布数据
149
+        List<QualificationLevelStatsDTO.LevelDistribution> distributions = new ArrayList<>();
150
+        int totalPersons = 0;
151
+
152
+        // 统计实际存在的等级数据,按数据库存储的枚举值分组
153
+        Map<String, Integer> levelCountMap = statsList.stream()
154
+                .collect(Collectors.toMap(
155
+                        QualificationStats::getLevelName,
156
+                        QualificationStats::getCount,
157
+                        Integer::sum
158
+                ));
159
+
160
+        // 按(初级、中级、高级)分组统计
161
+        int juniorCount = levelCountMap.getOrDefault("LEVEL_FIVE", 0);  // 初级
162
+        int intermediateCount = levelCountMap.getOrDefault("LEVEL_FOUR", 0);  // 中级
163
+        int seniorCount = levelCountMap.getOrDefault("LEVEL_ONE", 0) +
164
+                levelCountMap.getOrDefault("LEVEL_TWO", 0) +
165
+                levelCountMap.getOrDefault("LEVEL_THREE", 0);  // 高级
166
+
167
+        totalPersons = userIds.size();
168
+
169
+        // 初级
170
+        QualificationLevelStatsDTO.LevelDistribution juniorDist = new QualificationLevelStatsDTO.LevelDistribution();
171
+        juniorDist.setLevelName("初级");
172
+        juniorDist.setCount(juniorCount);
173
+        juniorDist.setPercentage(totalPersons > 0 ?
174
+                new BigDecimal(juniorCount * 100.0 / totalPersons).setScale(2, RoundingMode.HALF_UP) :
175
+                BigDecimal.ZERO);
176
+        distributions.add(juniorDist);
177
+
178
+        // 中级
179
+        QualificationLevelStatsDTO.LevelDistribution intermediateDist = new QualificationLevelStatsDTO.LevelDistribution();
180
+        intermediateDist.setLevelName("中级");
181
+        intermediateDist.setCount(intermediateCount);
182
+        intermediateDist.setPercentage(totalPersons > 0 ?
183
+                new BigDecimal(intermediateCount * 100.0 / totalPersons).setScale(2, RoundingMode.HALF_UP) :
184
+                BigDecimal.ZERO);
185
+        distributions.add(intermediateDist);
186
+
187
+        // 高级
188
+        QualificationLevelStatsDTO.LevelDistribution seniorDist = new QualificationLevelStatsDTO.LevelDistribution();
189
+        seniorDist.setLevelName("高级");
190
+        seniorDist.setCount(seniorCount);
191
+        seniorDist.setPercentage(totalPersons > 0 ?
192
+                new BigDecimal(seniorCount * 100.0 / totalPersons).setScale(2, RoundingMode.HALF_UP) :
193
+                BigDecimal.ZERO);
194
+        distributions.add(seniorDist);
195
+
196
+        pieChart.setData(distributions);
197
+        pieChart.setTotalPersons(totalPersons);
198
+
199
+        return pieChart;
200
+    }
201
+
202
+    /**
203
+     * 计算柱状图数据
204
+     * - 站级/大队级/科级:返回下属组织的资质等级统计
205
+     * - 班组:返回班组内每个员工的资质等级
206
+     */
207
+    private QualificationLevelStatsDTO.BarChartData calculateBarChartDataForDept(Long deptId) {
208
+        QualificationLevelStatsDTO.BarChartData barChart = new QualificationLevelStatsDTO.BarChartData();
209
+
210
+        SysDept sysDept = sysDeptService.selectDeptById(deptId);
211
+        if (sysDept == null) {
212
+            barChart.setBrigades(new ArrayList<>());
213
+            return barChart;
214
+        }
215
+
216
+        List<SysDept> targetDepts = new ArrayList<>();
217
+
218
+        // 根据部门类型决定查询逻辑
219
+        if (DeptTypeEnum.TEAMS.getCode().equals(sysDept.getDeptType())) {
220
+            // 班组级别:查询该班组下的所有用户,返回每个人的资质等级
221
+            return calculateBarChartDataForTeam(deptId);
222
+        } else {
223
+            // 站级/大队级/科级:查询该部门下的所有子部门
224
+            List<SysDept> children = sysDeptService.selectChildrenDeptById(deptId);
225
+            if (DeptTypeEnum.MANAGER.getCode().equals(sysDept.getDeptType())) {
226
+                targetDepts.addAll(children.stream()
227
+                        .filter(dept -> DeptTypeEnum.TEAMS.getCode().equals(dept.getDeptType()))
228
+                        .collect(Collectors.toList()));
229
+            } else if (DeptTypeEnum.BRIGADE.getCode().equals(sysDept.getDeptType())) {
230
+                targetDepts.addAll(children.stream()
231
+                        .filter(dept -> DeptTypeEnum.MANAGER.getCode().equals(dept.getDeptType()))
232
+                        .collect(Collectors.toList()));
233
+            } else if (DeptTypeEnum.STATION.getCode().equals(sysDept.getDeptType())) {
234
+                targetDepts.addAll(children.stream()
235
+                        .filter(dept -> DeptTypeEnum.BRIGADE.getCode().equals(dept.getDeptType()))
236
+                        .collect(Collectors.toList()));
237
+
238
+            }
239
+            if (targetDepts.isEmpty()) {
240
+                barChart.setBrigades(new ArrayList<>());
241
+                return barChart;
242
+            }
243
+        }
244
+
245
+        // 批量获取所有子部门的用户 ID
246
+        Map<Long, List<Long>> deptUserIdsMap = batchGetUserIdsByBrigades(targetDepts);
247
+
248
+        // 批量查询所有子部门的资质统计数据
249
+        Map<Long, List<QualificationStats>> deptStatsMap = batchQueryQualificationStats(deptUserIdsMap);
250
+
251
+        // 构建部门数据
252
+        List<QualificationLevelStatsDTO.BrigadeData> deptDataList = targetDepts.stream()
253
+                .map(dept -> {
254
+                    QualificationLevelStatsDTO.BrigadeData deptData = new QualificationLevelStatsDTO.BrigadeData();
255
+                    deptData.setDeptId(dept.getDeptId());
256
+                    deptData.setDeptName(dept.getDeptName());
257
+
258
+                    List<QualificationStats> deptStats = deptStatsMap.get(dept.getDeptId());
259
+                    if (deptStats != null && !deptStats.isEmpty()) {
260
+                        List<QualificationLevelStatsDTO.LevelCount> levelCounts = convertToLevelCounts(deptStats);
261
+                        deptData.setLevelCounts(levelCounts);
262
+                    } else {
263
+                        // 部门无用户或无统计数据,初始化为空数据
264
+                        List<QualificationLevelStatsDTO.LevelCount> emptyCounts = QUALIFICATION_LEVELS.stream()
265
+                                .map(level -> {
266
+                                    QualificationLevelStatsDTO.LevelCount count = new QualificationLevelStatsDTO.LevelCount();
267
+                                    count.setLevelName(level);
268
+                                    count.setCount(0);
269
+                                    return count;
270
+                                })
271
+                                .collect(Collectors.toList());
272
+                        deptData.setLevelCounts(emptyCounts);
273
+                    }
274
+
275
+                    return deptData;
276
+                })
277
+                .collect(Collectors.toList());
278
+
279
+        barChart.setBrigades(deptDataList);
280
+        return barChart;
281
+    }
282
+
283
+    /**
284
+     * 计算班组的柱状图数据(返回班组内每个员工的资质等级)
285
+     */
286
+    private QualificationLevelStatsDTO.BarChartData calculateBarChartDataForTeam(Long teamId) {
287
+        QualificationLevelStatsDTO.BarChartData barChart = new QualificationLevelStatsDTO.BarChartData();
288
+
289
+        // 获取班组下的所有用户
290
+        List<Long> userIds = getAllUserIdsUnderDept(teamId);
291
+
292
+        if (userIds.isEmpty()) {
293
+            barChart.setBrigades(new ArrayList<>());
294
+            return barChart;
295
+        }
296
+
297
+        // 查询每个用户的资质等级
298
+        List<QualificationLevelStatsDTO.BrigadeData> userDataList = new ArrayList<>();
299
+        for (Long uid : userIds) {
300
+            SysUser user = sysUserService.selectUserById(uid);
301
+            if (user == null || "2".equals(user.getDelFlag()) || "1".equals(user.getStatus())) {
302
+                continue; // 跳过已删除或停用的用户
303
+            }
304
+
305
+            // 查询该用户的资质等级
306
+            String level = qualificationLevelMapper.queryPersonalQualificationLevel(uid);
307
+            String displayLevel = convertToDisplayLevel(level);
308
+
309
+            QualificationLevelStatsDTO.BrigadeData userData = new QualificationLevelStatsDTO.BrigadeData();
310
+            userData.setDeptId(uid); // 使用用户 ID 作为标识
311
+            userData.setDeptName(user.getNickName() != null ? user.getNickName() : user.getUserName());
312
+            userData.setQualificationLevel(displayLevel); // 直接设置资质等级
313
+
314
+            userDataList.add(userData);
315
+        }
316
+
317
+        barChart.setBrigades(userDataList);
318
+        return barChart;
319
+    }
320
+
321
+    /**
322
+     * 计算个人的柱状图数据(返回单个用户的资质等级)
323
+     */
324
+    private QualificationLevelStatsDTO.BarChartData calculateBarChartDataForUser(Long userId) {
325
+        QualificationLevelStatsDTO.BarChartData barChart = new QualificationLevelStatsDTO.BarChartData();
326
+
327
+        SysUser user = sysUserService.selectUserById(userId);
328
+        if (user == null || "2".equals(user.getDelFlag()) || "1".equals(user.getStatus())) {
329
+            barChart.setBrigades(new ArrayList<>());
330
+            return barChart;
331
+        }
332
+
333
+        // 查询该用户的资质等级
334
+        String level = qualificationLevelMapper.queryPersonalQualificationLevel(userId);
335
+        String displayLevel = convertToDisplayLevel(level);
336
+
337
+        // 构建用户数据
338
+        QualificationLevelStatsDTO.BrigadeData userData = new QualificationLevelStatsDTO.BrigadeData();
339
+        userData.setDeptId(userId);
340
+        userData.setDeptName(user.getNickName() != null ? user.getNickName() : user.getUserName());
341
+        userData.setQualificationLevel(displayLevel); // 直接设置资质等级
342
+        userData.setSysPostList(sysPostService.selectPostListsByUserId(userId));
343
+
344
+        List<QualificationLevelStatsDTO.BrigadeData> departments = new ArrayList<>();
345
+        departments.add(userData);
346
+        barChart.setBrigades(departments);
347
+
348
+        return barChart;
349
+    }
350
+
351
+    /**
352
+     * 将数据库枚举值转换为显示名称
353
+     */
354
+    private String convertToDisplayLevel(String dbValue) {
355
+        if (dbValue == null || dbValue.trim().isEmpty()) {
356
+            return null;
357
+        }
358
+        switch (dbValue.toUpperCase()) {
359
+            case "LEVEL_FIVE":
360
+                return "初级";
361
+            case "LEVEL_FOUR":
362
+                return "中级";
363
+            case "LEVEL_ONE":
364
+            case "LEVEL_TWO":
365
+            case "LEVEL_THREE":
366
+                return "高级";
367
+            default:
368
+                return dbValue;
369
+        }
370
+    }
371
+
372
+    /**
373
+     * 计算柱状图数据(各大队资质等级分布)
374
+     */
375
+    private QualificationLevelStatsDTO.BarChartData calculateBarChartDataOptimized(Long siteId) {
376
+        QualificationLevelStatsDTO.BarChartData barChart = new QualificationLevelStatsDTO.BarChartData();
377
+
378
+        // 获取站点下的所有大队
379
+        List<SysDept> brigades = getBrigadesUnderSite(siteId);
380
+        if (brigades.isEmpty()) {
381
+            barChart.setBrigades(new ArrayList<>());
382
+            return barChart;
383
+        }
384
+
385
+        // 批量获取所有大队的用户ID
386
+        Map<Long, List<Long>> deptUserIdsMap = batchGetUserIdsByBrigades(brigades);
387
+
388
+        // 批量查询所有大队的资质统计数据
389
+        Map<Long, List<QualificationStats>> deptStatsMap = batchQueryQualificationStats(deptUserIdsMap);
390
+
391
+        // 构建大队数据
392
+        List<QualificationLevelStatsDTO.BrigadeData> deptDataList = brigades.stream()
393
+                .map(dept -> {
394
+                    QualificationLevelStatsDTO.BrigadeData deptData = new QualificationLevelStatsDTO.BrigadeData();
395
+                    deptData.setDeptId(dept.getDeptId());
396
+                    deptData.setDeptName(dept.getDeptName());
397
+
398
+                    List<QualificationStats> deptStats = deptStatsMap.get(dept.getDeptId());
399
+                    if (deptStats != null && !deptStats.isEmpty()) {
400
+                        List<QualificationLevelStatsDTO.LevelCount> levelCounts = convertToLevelCounts(deptStats);
401
+                        deptData.setLevelCounts(levelCounts);
402
+                    } else {
403
+                        // 大队无用户或无统计数据,初始化为空数据
404
+                        List<QualificationLevelStatsDTO.LevelCount> emptyCounts = QUALIFICATION_LEVELS.stream()
405
+                                .map(level -> {
406
+                                    QualificationLevelStatsDTO.LevelCount count = new QualificationLevelStatsDTO.LevelCount();
407
+                                    count.setLevelName(level);
408
+                                    count.setCount(0);
409
+                                    return count;
410
+                                })
411
+                                .collect(Collectors.toList());
412
+                        deptData.setLevelCounts(emptyCounts);
413
+                    }
414
+
415
+                    return deptData;
416
+                })
417
+                .collect(Collectors.toList());
418
+
419
+        barChart.setBrigades(deptDataList);
420
+        return barChart;
421
+    }
422
+
423
+    /**
424
+     * 批量获取大队用户ID映射
425
+     */
426
+    private Map<Long, List<Long>> batchGetUserIdsByBrigades(List<SysDept> brigades) {
427
+        Map<Long, List<Long>> deptUserIdsMap = new HashMap<>();
428
+
429
+        for (SysDept dept : brigades) {
430
+            List<Long> deptUserIds = getAllUserIdsUnderDept(dept.getDeptId());
431
+            deptUserIdsMap.put(dept.getDeptId(), deptUserIds);
432
+        }
433
+
434
+        return deptUserIdsMap;
435
+    }
436
+
437
+    /**
438
+     * 批量查询资质统计数据
439
+     */
440
+    private Map<Long, List<QualificationStats>> batchQueryQualificationStats(Map<Long, List<Long>> deptUserIdsMap) {
441
+        Map<Long, List<QualificationStats>> deptStatsMap = new HashMap<>();
442
+
443
+        // 过滤掉没有用户的大队
444
+        Map<Long, List<Long>> validDeptUserIdsMap = deptUserIdsMap.entrySet().stream()
445
+                .filter(entry -> !entry.getValue().isEmpty())
446
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
447
+
448
+        if (validDeptUserIdsMap.isEmpty()) {
449
+            return deptStatsMap;
450
+        }
451
+
452
+        for (Map.Entry<Long, List<Long>> entry : validDeptUserIdsMap.entrySet()) {
453
+            Long deptId = entry.getKey();
454
+            List<Long> userIds = entry.getValue();
455
+            List<QualificationStats> stats = qualificationLevelMapper.queryOrgQualificationLevelStats(userIds, null);
456
+            deptStatsMap.put(deptId, stats);
457
+        }
458
+
459
+        return deptStatsMap;
460
+    }
461
+
462
+    /**
463
+     * 将资质统计转换为等级人数数据(初级、中级、高级)
464
+     */
465
+    private List<QualificationLevelStatsDTO.LevelCount> convertToLevelCounts(List<QualificationStats> statsList) {
466
+        // 按数据库存储的枚举值分组统计
467
+        Map<String, Integer> countMap = statsList.stream()
468
+                .collect(Collectors.toMap(QualificationStats::getLevelName, QualificationStats::getCount));
469
+
470
+        // 计算初级、中级、高级的人数
471
+        int juniorCount = countMap.getOrDefault("LEVEL_FIVE", 0);  // 初级
472
+        int intermediateCount = countMap.getOrDefault("LEVEL_FOUR", 0);  // 中级
473
+        int seniorCount = countMap.getOrDefault("LEVEL_ONE", 0) +
474
+                countMap.getOrDefault("LEVEL_TWO", 0) +
475
+                countMap.getOrDefault("LEVEL_THREE", 0);  // 高级
476
+
477
+        List<QualificationLevelStatsDTO.LevelCount> levelCounts = new ArrayList<>();
478
+
479
+        QualificationLevelStatsDTO.LevelCount junior = new QualificationLevelStatsDTO.LevelCount();
480
+        junior.setLevelName("初级");
481
+        junior.setCount(juniorCount);
482
+        levelCounts.add(junior);
483
+
484
+        QualificationLevelStatsDTO.LevelCount intermediate = new QualificationLevelStatsDTO.LevelCount();
485
+        intermediate.setLevelName("中级");
486
+        intermediate.setCount(intermediateCount);
487
+        levelCounts.add(intermediate);
488
+
489
+        QualificationLevelStatsDTO.LevelCount senior = new QualificationLevelStatsDTO.LevelCount();
490
+        senior.setLevelName("高级");
491
+        senior.setCount(seniorCount);
492
+        levelCounts.add(senior);
493
+
494
+        return levelCounts;
495
+    }
496
+
497
+
498
+    /**
499
+     * 获取站点下的所有用户ID
500
+     */
501
+    private List<Long> getAllUserIdsUnderSite(Long siteId) {
502
+        List<Long> userIds = new ArrayList<>();
503
+
504
+        // 获取站点本身及其下属部门
505
+        List<SysDept> allDepts = sysDeptService.selectChildrenDeptById(siteId);
506
+        allDepts.add(0, sysDeptService.selectDeptById(siteId)); // 添加站点本身
507
+
508
+        // 获取各部门下的用户
509
+        for (SysDept dept : allDepts) {
510
+            if (Objects.nonNull(dept)) {
511
+                List<Long> deptUserIds = sysUserService.selectUserByDeptId(dept.getDeptId()).stream()
512
+                        .map(SysUser::getUserId)
513
+                        .collect(Collectors.toList());
514
+                userIds.addAll(deptUserIds);
515
+            }
516
+        }
517
+
518
+        return userIds.stream().distinct().collect(Collectors.toList());
519
+    }
520
+
521
+    /**
522
+     * 获取站点下的所有大队
523
+     */
524
+    private List<SysDept> getBrigadesUnderSite(Long siteId) {
525
+        List<SysDept> children = sysDeptService.selectChildrenDeptById(siteId);
526
+        return children.stream()
527
+                .filter(dept -> DeptTypeEnum.BRIGADE.getCode().equals(dept.getDeptType()))
528
+                .collect(Collectors.toList());
529
+    }
530
+
531
+    /**
532
+     * 获取部门下的所有用户ID
533
+     */
534
+    private List<Long> getAllUserIdsUnderDept(Long deptId) {
535
+        List<SysUser> sysUserList = sysUserService.selectUserByDeptId(deptId);
536
+        return sysUserList.stream()
537
+                .map(SysUser::getUserId)
538
+                .collect(Collectors.toList());
539
+    }
540
+
541
+}

+ 642 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/quality/SimpleAttendancePersonController.java

@@ -0,0 +1,642 @@
1
+package com.sundot.airport.web.controller.quality;
2
+
3
+import com.sundot.airport.attendance.calculator.AttendancePersonCountCalculator;
4
+import com.sundot.airport.attendance.calculator.BrigadeAttendancePersonCountCalculator;
5
+import com.sundot.airport.attendance.dto.AttendanceTrendDataDTO;
6
+import com.sundot.airport.common.core.domain.entity.SysDept;
7
+import com.sundot.airport.attendance.dto.AttendancePersonStatsDTO;
8
+import com.sundot.airport.common.enums.DeptTypeEnum;
9
+import com.sundot.airport.common.statistics.*;
10
+import com.sundot.airport.common.utils.DeptUtils;
11
+import com.sundot.airport.common.utils.StringUtils;
12
+import com.sundot.airport.system.service.ISysDeptService;
13
+import io.swagger.annotations.Api;
14
+import io.swagger.annotations.ApiOperation;
15
+import org.springframework.beans.factory.annotation.Autowired;
16
+import org.springframework.web.bind.annotation.*;
17
+
18
+import java.math.BigDecimal;
19
+import java.util.*;
20
+import java.util.stream.Collectors;
21
+
22
+import static com.sundot.airport.common.utils.SecurityUtils.getDeptId;
23
+
24
+/**
25
+ * 出勤人次控制器
26
+ * 根据时间维度返回总体+大队的人次数据
27
+ */
28
+@Api(tags = "出勤人次")
29
+@RestController
30
+@RequestMapping("/quality/attendance")
31
+public class SimpleAttendancePersonController {
32
+
33
+    @Autowired
34
+    private AttendancePersonCountCalculator overallCalculator; // 全站计算器
35
+
36
+    @Autowired
37
+    private BrigadeAttendancePersonCountCalculator deptCalculator; // 大队计算器
38
+
39
+    @Autowired
40
+    private MultiModelIndicatorCalculator multiModelIndicatorCalculator;
41
+
42
+    @Autowired
43
+    private ISysDeptService deptService;
44
+
45
+
46
+    /**
47
+     * 出勤人次分析
48
+     * 支持按大队ID、科室ID、班组ID、用户ID 进行统计
49
+     */
50
+    @ApiOperation("出勤人次分析")
51
+    @PostMapping("/calculate")
52
+    public List<AttendancePersonStatsDTO> calculateStatistics(@RequestBody IndicatorCalculationParams params) {
53
+        List<AttendancePersonStatsDTO> results = new ArrayList<>();
54
+
55
+        if (params.getDeptId() != null || params.getUserId() != null) {
56
+            // 传入了部门 ID 或用户 ID,直接统计
57
+            results.add(createAttendancePersonStatsForParams(params));
58
+        } else {
59
+            // 未传入具体 ID,默认获取当前用户所在站点下的科室
60
+            Long topSiteId = DeptUtils.getTopSiteId(deptService.selectDeptById(getDeptId()));
61
+            List<SysDept> brigadeUnderSite = getBrigadesUnderSite(topSiteId);
62
+            brigadeUnderSite.add(deptService.selectDeptById(topSiteId));
63
+            brigadeUnderSite.forEach(dept -> {
64
+                results.add(createAttendancePersonStats(dept, params));
65
+            });
66
+        }
67
+
68
+        return results;
69
+    }
70
+
71
+    /**
72
+     * 出勤人次趋势数据
73
+     * 根据时间维度参数动态生成柱状图数据
74
+     * 支持月度(同比+环比)、季度(同比)、年度(同比)等多种展示方式
75
+     *
76
+     * @param params 时间维度参数(年、季度、月等)
77
+     * @return 趋势数据列表
78
+     */
79
+    @ApiOperation("出勤人次趋势数据")
80
+    @PostMapping("/trend-data")
81
+    public List<AttendanceTrendDataDTO> getTrendData(@RequestBody IndicatorCalculationParams params) {
82
+        // 根据传入参数决定统计策略
83
+        if (params.getDeptId() != null || params.getUserId() != null) {
84
+            // 传入了部门 ID 或用户 ID,返回简化版的趋势数据
85
+            return getSimpleTrendData(params);
86
+        }
87
+
88
+        // 默认按站点 + 三科模式统计
89
+        List<AttendanceTrendDataDTO> results = new ArrayList<>();
90
+
91
+        // 根据时间维度类型决定展示策略
92
+        if (params.getMonth() != null) {
93
+            // 月度:展示当前月、上月、去年同期(同比+环比)
94
+            results.addAll(generateMonthlyTrendData(params));
95
+        } else if (params.getQuarter() != null) {
96
+            // 季度:展示当前季度和去年同期
97
+            results.addAll(generateQuarterlyTrendData(params));
98
+        } else if (params.getYear() != null) {
99
+            // 年度:仅展示当前年和去年(仅同比)
100
+            results.addAll(generateYearlyTrendData(params));
101
+        } else {
102
+            // 默认按月度处理
103
+            results.addAll(generateMonthlyTrendData(params));
104
+        }
105
+
106
+        return results;
107
+    }
108
+
109
+    /**
110
+     * 趋势数据(用于指定部门/个人的场景)
111
+     * 仅返回单条数据序列
112
+     */
113
+    private List<AttendanceTrendDataDTO> getSimpleTrendData(IndicatorCalculationParams params) {
114
+        List<AttendanceTrendDataDTO> results = new ArrayList<>();
115
+
116
+        // 根据时间维度类型决定展示策略
117
+        if (params.getMonth() != null) {
118
+            // 月度:展示当前月、上月、去年同期
119
+            results.addAll(generateSimpleMonthlyTrendData(params));
120
+        } else if (params.getQuarter() != null) {
121
+            // 季度:展示当前季度和去年同期
122
+            results.addAll(generateSimpleQuarterlyTrendData(params));
123
+        } else if (params.getYear() != null) {
124
+            // 年度:仅展示当前年和去年
125
+            results.addAll(generateSimpleYearlyTrendData(params));
126
+        } else {
127
+            // 默认按月度处理
128
+            results.addAll(generateSimpleMonthlyTrendData(params));
129
+        }
130
+
131
+        return results;
132
+    }
133
+
134
+    /**
135
+     * 月度趋势数据
136
+     */
137
+    private List<AttendanceTrendDataDTO> generateSimpleMonthlyTrendData(IndicatorCalculationParams params) {
138
+        List<AttendanceTrendDataDTO> results = new ArrayList<>();
139
+
140
+        // 计算当前月数据
141
+        AttendancePersonStatsDTO currentData = createAttendancePersonStatsForParams(params);
142
+
143
+        // 计算上月数据
144
+        IndicatorCalculationParams lastMonthParams = createLastMonthParams(params);
145
+        AttendancePersonStatsDTO lastMonthData = createAttendancePersonStatsForParams(lastMonthParams);
146
+
147
+        // 计算去年同期数据
148
+        IndicatorCalculationParams lastYearParams = createLastYearParams(params);
149
+        AttendancePersonStatsDTO lastYearData = createAttendancePersonStatsForParams(lastYearParams);
150
+
151
+        // 构造趋势数据点
152
+        results.add(createSimpleTrendDataPoint(getLastYearMonthLabel(params), lastYearData));
153
+        results.add(createSimpleTrendDataPoint(getLastMonthLabel(params), lastMonthData));
154
+        results.add(createSimpleTrendDataPoint(getMonthLabel(params), currentData));
155
+
156
+        return results;
157
+    }
158
+
159
+    /**
160
+     * 季度趋势数据
161
+     */
162
+    private List<AttendanceTrendDataDTO> generateSimpleQuarterlyTrendData(IndicatorCalculationParams params) {
163
+        List<AttendanceTrendDataDTO> results = new ArrayList<>();
164
+
165
+        // 计算当前季度数据
166
+        AttendancePersonStatsDTO currentData = createAttendancePersonStatsForParams(params);
167
+
168
+        // 计算上季度数据
169
+        IndicatorCalculationParams lastQuarterParams = createLastQuarterParams(params);
170
+        AttendancePersonStatsDTO lastQuarterData = createAttendancePersonStatsForParams(lastQuarterParams);
171
+
172
+        // 计算去年同期季度数据
173
+        IndicatorCalculationParams lastYearParams = createLastYearParams(params);
174
+        AttendancePersonStatsDTO lastYearData = createAttendancePersonStatsForParams(lastYearParams);
175
+
176
+        // 构造趋势数据点
177
+        results.add(createSimpleTrendDataPoint(getLastYearQuarterLabel(params), lastYearData));
178
+        results.add(createSimpleTrendDataPoint(getLastQuarterLabel(params), lastQuarterData));
179
+        results.add(createSimpleTrendDataPoint(getQuarterLabel(params), currentData));
180
+
181
+        return results;
182
+    }
183
+
184
+    /**
185
+     * 年度趋势数据
186
+     */
187
+    private List<AttendanceTrendDataDTO> generateSimpleYearlyTrendData(IndicatorCalculationParams params) {
188
+        List<AttendanceTrendDataDTO> results = new ArrayList<>();
189
+
190
+        // 计算当前年数据
191
+        AttendancePersonStatsDTO currentData = createAttendancePersonStatsForParams(params);
192
+
193
+        // 计算去年数据
194
+        IndicatorCalculationParams lastYearParams = createLastYearParams(params);
195
+        AttendancePersonStatsDTO lastYearData = createAttendancePersonStatsForParams(lastYearParams);
196
+
197
+        // 构造趋势数据点
198
+        results.add(createSimpleTrendDataPoint((params.getYear() - 1) + "年", lastYearData));
199
+        results.add(createSimpleTrendDataPoint(params.getYear() + "年", currentData));
200
+
201
+        return results;
202
+    }
203
+
204
+    /**
205
+     * 趋势数据点
206
+     */
207
+    private AttendanceTrendDataDTO createSimpleTrendDataPoint(String timeLabel, AttendancePersonStatsDTO stats) {
208
+        AttendanceTrendDataDTO data = new AttendanceTrendDataDTO();
209
+        data.setTimeLabel(timeLabel);
210
+        // 将单个统计值设置为 overall 字段
211
+        data.setOverall(stats.getCurrentValue() != null ? stats.getCurrentValue() : BigDecimal.ZERO);
212
+        return data;
213
+    }
214
+
215
+    /**
216
+     * 生成月度趋势数据(同比+环比)
217
+     * 展示:当前月、上月、去年同期
218
+     */
219
+    private List<AttendanceTrendDataDTO> generateMonthlyTrendData(IndicatorCalculationParams params) {
220
+        List<AttendanceTrendDataDTO> results = new ArrayList<>();
221
+
222
+        // 获取当前用户所在站点ID
223
+        Long topSiteId = DeptUtils.getTopSiteId(deptService.selectDeptById(getDeptId()));
224
+        List<SysDept> stationAndThreeDepts = getBrigadesUnderSite(topSiteId);
225
+        stationAndThreeDepts.add(deptService.selectDeptById(topSiteId));
226
+
227
+
228
+        // 计算当前月数据
229
+        List<AttendancePersonStatsDTO> currentList = calculateDeptStats(stationAndThreeDepts, params);
230
+
231
+        // 计算上月数据
232
+        IndicatorCalculationParams lastMonthParams = createLastMonthParams(params);
233
+        List<AttendancePersonStatsDTO> lastMonthList = calculateDeptStats(stationAndThreeDepts, lastMonthParams);
234
+
235
+        // 计算去年同期数据
236
+        IndicatorCalculationParams lastYearParams = createLastYearParams(params);
237
+        List<AttendancePersonStatsDTO> lastYearList = calculateDeptStats(stationAndThreeDepts, lastYearParams);
238
+
239
+
240
+        // 构造柱状图数据
241
+        AttendanceTrendDataDTO currentData = createTrendDataPoint(getMonthLabel(params), currentList);
242
+        AttendanceTrendDataDTO lastMonthData = createTrendDataPoint(getLastMonthLabel(params), lastMonthList);
243
+        AttendanceTrendDataDTO lastYearData = createTrendDataPoint(getLastYearMonthLabel(params), lastYearList);
244
+
245
+        results.add(lastYearData);
246
+        results.add(lastMonthData);
247
+        results.add(currentData);
248
+
249
+        return results;
250
+    }
251
+
252
+    /**
253
+     * 生成季度趋势数据(同比+环比)
254
+     * 展示:当前季度、上季度、去年同期季度
255
+     */
256
+    private List<AttendanceTrendDataDTO> generateQuarterlyTrendData(IndicatorCalculationParams params) {
257
+        List<AttendanceTrendDataDTO> results = new ArrayList<>();
258
+        // 获取当前用户所在站点ID
259
+        Long topSiteId = DeptUtils.getTopSiteId(deptService.selectDeptById(getDeptId()));
260
+        List<SysDept> stationAndThreeDepts = getBrigadesUnderSite(topSiteId);
261
+        stationAndThreeDepts.add(deptService.selectDeptById(topSiteId));
262
+        // 计算当前季度数据
263
+        List<AttendancePersonStatsDTO> currentList = calculateDeptStats(stationAndThreeDepts, params);
264
+
265
+        // 计算上季度数据
266
+        IndicatorCalculationParams lastQuarterParams = createLastQuarterParams(params);
267
+        List<AttendancePersonStatsDTO> lastQuarterList = calculateDeptStats(stationAndThreeDepts, lastQuarterParams);
268
+
269
+        // 计算去年同期季度数据
270
+        IndicatorCalculationParams lastYearParams = createLastYearParams(params);
271
+        List<AttendancePersonStatsDTO> lastYearList = calculateDeptStats(stationAndThreeDepts, lastYearParams);
272
+
273
+        // 构造柱状图数据
274
+        AttendanceTrendDataDTO currentData = createTrendDataPoint(getQuarterLabel(params), currentList);
275
+        AttendanceTrendDataDTO lastQuarterData = createTrendDataPoint(getLastQuarterLabel(params), lastQuarterList);
276
+        AttendanceTrendDataDTO lastYearData = createTrendDataPoint(getLastYearQuarterLabel(params), lastYearList);
277
+
278
+        results.add(lastYearData);
279
+        results.add(lastQuarterData);
280
+        results.add(currentData);
281
+        return results;
282
+    }
283
+
284
+    /**
285
+     * 生成年度趋势数据(仅同比)
286
+     * 展示:当前年、去年
287
+     */
288
+    private List<AttendanceTrendDataDTO> generateYearlyTrendData(IndicatorCalculationParams params) {
289
+        List<AttendanceTrendDataDTO> results = new ArrayList<>();
290
+        // 获取当前用户所在站点ID
291
+        Long topSiteId = DeptUtils.getTopSiteId(deptService.selectDeptById(getDeptId()));
292
+        List<SysDept> stationAndThreeDepts = getBrigadesUnderSite(topSiteId);
293
+        stationAndThreeDepts.add(deptService.selectDeptById(topSiteId));
294
+        // 计算当前年数据
295
+        List<AttendancePersonStatsDTO> currentList = calculateDeptStats(stationAndThreeDepts, params);
296
+
297
+        // 计算去年数据
298
+        IndicatorCalculationParams lastYearParams = createLastYearParams(params);
299
+        List<AttendancePersonStatsDTO> lastYearList = calculateDeptStats(stationAndThreeDepts, lastYearParams);
300
+
301
+        // 构造柱状图数据
302
+        AttendanceTrendDataDTO currentData = createTrendDataPoint(params.getYear() + "年", currentList);
303
+        AttendanceTrendDataDTO lastYearData = createTrendDataPoint((params.getYear() - 1) + "年", lastYearList);
304
+        results.add(lastYearData);
305
+        results.add(currentData);
306
+
307
+
308
+        return results;
309
+    }
310
+
311
+    /**
312
+     * 计算部门统计数据
313
+     */
314
+    private List<AttendancePersonStatsDTO> calculateDeptStats(List<SysDept> depts, IndicatorCalculationParams params) {
315
+        List<AttendancePersonStatsDTO> results = new ArrayList<>();
316
+        depts.forEach(dept -> {
317
+            results.add(createAttendancePersonStats(dept, params));
318
+        });
319
+        return results;
320
+    }
321
+
322
+    /**
323
+     * 创建趋势数据点
324
+     */
325
+    private AttendanceTrendDataDTO createTrendDataPoint(String timeLabel, List<AttendancePersonStatsDTO> dataList) {
326
+        AttendanceTrendDataDTO data = new AttendanceTrendDataDTO();
327
+        data.setTimeLabel(timeLabel);
328
+        data.setOverall(getStationValue(dataList));
329
+        data.setData1(getDeptValue(dataList, "安检一大队"));
330
+        data.setData2(getDeptValue(dataList, "安检二大队"));
331
+        data.setData3(getDeptValue(dataList, "安检三大队"));
332
+        data.setData4(getDeptValue(dataList, "安检综合大队"));
333
+        return data;
334
+    }
335
+
336
+    /**
337
+     * 获取季度标签
338
+     */
339
+    private String getQuarterLabel(IndicatorCalculationParams params) {
340
+        if (params.getYear() != null && params.getQuarter() != null) {
341
+            return params.getYear() + "年第" + params.getQuarter() + "季度";
342
+        }
343
+        return "当前季度";
344
+    }
345
+
346
+    /**
347
+     * 获取上季度标签
348
+     */
349
+    private String getLastQuarterLabel(IndicatorCalculationParams params) {
350
+        if (params.getYear() != null && params.getQuarter() != null) {
351
+            int lastQuarter = params.getQuarter() - 1;
352
+            int year = params.getYear();
353
+            if (lastQuarter <= 0) {
354
+                lastQuarter = 4;
355
+                year = year - 1;
356
+            }
357
+            return year + "年第" + lastQuarter + "季度";
358
+        }
359
+        return "上季度";
360
+    }
361
+
362
+    /**
363
+     * 获取去年季度标签
364
+     */
365
+    private String getLastYearQuarterLabel(IndicatorCalculationParams params) {
366
+        if (params.getYear() != null && params.getQuarter() != null) {
367
+            return (params.getYear() - 1) + "年第" + params.getQuarter() + "季度";
368
+        }
369
+        return "去年同期季度";
370
+    }
371
+
372
+    /**
373
+     * 月度标签
374
+     */
375
+    private String getMonthLabel(IndicatorCalculationParams params) {
376
+        if (params.getYear() != null && params.getMonth() != null) {
377
+            return params.getYear() + "年" + params.getMonth() + "月";
378
+        }
379
+        return "当前月度";
380
+    }
381
+
382
+    /**
383
+     * 获取上月标签
384
+     */
385
+    private String getLastMonthLabel(IndicatorCalculationParams params) {
386
+        if (params.getYear() != null && params.getMonth() != null) {
387
+            int lastMonth = params.getMonth() - 1;
388
+            int year = params.getYear();
389
+            if (lastMonth <= 0) {
390
+                lastMonth = 12;
391
+                year = year - 1;
392
+            }
393
+            return year + "年" + lastMonth + "月";
394
+        }
395
+        return "上月";
396
+    }
397
+
398
+    /**
399
+     * 获取去年本月度标签
400
+     */
401
+    private String getLastYearMonthLabel(IndicatorCalculationParams params) {
402
+        if (params.getYear() != null && params.getMonth() != null) {
403
+            return (params.getYear() - 1) + "年" + params.getMonth() + "月";
404
+        }
405
+        return "去年同期月度";
406
+    }
407
+
408
+
409
+    /**
410
+     * 创建出勤人次统计对象(根据部门实体)
411
+     */
412
+    private AttendancePersonStatsDTO createAttendancePersonStats(SysDept sysDept, IndicatorCalculationParams params) {
413
+        AttendancePersonStatsDTO result = new AttendancePersonStatsDTO();
414
+        result.setDeptId(sysDept.getDeptId());
415
+        result.setDeptName(sysDept.getDeptName());
416
+        result.setDeptType(sysDept.getDeptType());
417
+        try {
418
+            IndicatorCalculationParams calcParams = new IndicatorCalculationParams();
419
+            calcParams.setDeptId(sysDept.getDeptId());
420
+            calcParams.setYear(params.getYear());
421
+            calcParams.setQuarter(params.getQuarter());
422
+            calcParams.setMonth(params.getMonth());
423
+            calcParams.setStartTime(params.getStartTime());
424
+            calcParams.setEndTime(params.getEndTime());
425
+            calcParams.setChainRatio(params.getChainRatio());
426
+            calcParams.setYearOnYear(params.getYearOnYear());
427
+
428
+            IndicatorCalculationResult currentResult;
429
+            if (sysDept.getDeptType().equals(DeptTypeEnum.STATION.getCode())) {
430
+                currentResult = multiModelIndicatorCalculator.calculateWithRatio(overallCalculator, calcParams);
431
+            } else {
432
+                currentResult = multiModelIndicatorCalculator.calculateWithRatio(deptCalculator, calcParams);
433
+            }
434
+
435
+            if ("SUCCESS".equals(currentResult.getStatus())) {
436
+                result.setCurrentValue(currentResult.getValue());
437
+                result.setBaselineValue(currentResult.getBaselineValue());
438
+
439
+                boolean showChainRatio = params.getChainRatio() != null && params.getChainRatio();
440
+                boolean showYearOnYear = params.getYearOnYear() != null && params.getYearOnYear();
441
+
442
+                if (params.getYear() != null && params.getQuarter() == null && params.getMonth() == null) {
443
+                    showChainRatio = false;
444
+                }
445
+
446
+                if (showYearOnYear && currentResult.getYearOnYearValue() != null) {
447
+                    result.setYearOnYearValue(currentResult.getYearOnYearValue());
448
+                    result.setYearOnYearDirection(getDirectionText(currentResult.getYearOnYearValue()));
449
+                    result.setYearOnYearTimeDimension((TimeDimensionParams) currentResult.getExtraInfo().get("yearOnYear"));
450
+                }
451
+
452
+                if (showChainRatio && currentResult.getChainRatioValue() != null) {
453
+                    result.setChainRatioValue(currentResult.getChainRatioValue());
454
+                    result.setChainRatioDirection(getDirectionText(currentResult.getChainRatioValue()));
455
+                    result.setChainTimeDimension((TimeDimensionParams) currentResult.getExtraInfo().get("chain"));
456
+                }
457
+            }
458
+        } catch (Exception e) {
459
+            throw new RuntimeException(e);
460
+        }
461
+
462
+        return result;
463
+    }
464
+
465
+    /**
466
+     * 创建出勤人次统计对象(根据参数直接计算)
467
+     * 支持按部门、班组、个人进行统计
468
+     */
469
+    private AttendancePersonStatsDTO createAttendancePersonStatsForParams(IndicatorCalculationParams params) {
470
+        AttendancePersonStatsDTO result = new AttendancePersonStatsDTO();
471
+
472
+        // 设置部门信息
473
+        if (params.getDeptId() != null) {
474
+            SysDept dept = deptService.selectDeptById(params.getDeptId());
475
+            if (dept != null) {
476
+                result.setDeptId(dept.getDeptId());
477
+                result.setDeptName(dept.getDeptName());
478
+                result.setDeptType(dept.getDeptType());
479
+            }
480
+        }
481
+
482
+        try {
483
+            IndicatorCalculationResult currentResult = multiModelIndicatorCalculator.calculateWithRatio(deptCalculator, params);
484
+
485
+            if ("SUCCESS".equals(currentResult.getStatus())) {
486
+                result.setCurrentValue(currentResult.getValue());
487
+                result.setBaselineValue(currentResult.getBaselineValue());
488
+
489
+                boolean showChainRatio = params.getChainRatio() != null && params.getChainRatio();
490
+                boolean showYearOnYear = params.getYearOnYear() != null && params.getYearOnYear();
491
+
492
+                if (params.getYear() != null && params.getQuarter() == null && params.getMonth() == null) {
493
+                    showChainRatio = false;
494
+                }
495
+
496
+                if (showYearOnYear && currentResult.getYearOnYearValue() != null) {
497
+                    result.setYearOnYearValue(currentResult.getYearOnYearValue());
498
+                    result.setYearOnYearDirection(getDirectionText(currentResult.getYearOnYearValue()));
499
+                    result.setYearOnYearTimeDimension((TimeDimensionParams) currentResult.getExtraInfo().get("yearOnYear"));
500
+                }
501
+
502
+                if (showChainRatio && currentResult.getChainRatioValue() != null) {
503
+                    result.setChainRatioValue(currentResult.getChainRatioValue());
504
+                    result.setChainRatioDirection(getDirectionText(currentResult.getChainRatioValue()));
505
+                    result.setChainTimeDimension((TimeDimensionParams) currentResult.getExtraInfo().get("chain"));
506
+                }
507
+            }
508
+        } catch (Exception e) {
509
+            throw new RuntimeException(e);
510
+        }
511
+
512
+        return result;
513
+    }
514
+
515
+    /**
516
+     * 创建上月参数
517
+     */
518
+    private IndicatorCalculationParams createLastMonthParams(IndicatorCalculationParams originalParams) {
519
+        IndicatorCalculationParams params = new IndicatorCalculationParams();
520
+        params.setYear(originalParams.getYear());
521
+        params.setQuarter(originalParams.getQuarter());
522
+        params.setMonth(originalParams.getMonth());
523
+        params.setStartTime(originalParams.getStartTime());
524
+        params.setEndTime(originalParams.getEndTime());
525
+        params.setUserId(originalParams.getUserId());
526
+        params.setDeptId(originalParams.getDeptId());
527
+        params.setChainRatio(false);
528
+        params.setYearOnYear(false);
529
+
530
+        // 计算上月
531
+        if (params.getMonth() != null) {
532
+            int lastMonth = params.getMonth() - 1;
533
+            int year = params.getYear();
534
+            if (lastMonth <= 0) {
535
+                lastMonth = 12;
536
+                year = year - 1;
537
+            }
538
+            params.setMonth(lastMonth);
539
+            params.setYear(year);
540
+        }
541
+
542
+        return params;
543
+    }
544
+
545
+    /**
546
+     * 创建上季度参数
547
+     */
548
+    private IndicatorCalculationParams createLastQuarterParams(IndicatorCalculationParams originalParams) {
549
+        IndicatorCalculationParams params = new IndicatorCalculationParams();
550
+        params.setYear(originalParams.getYear());
551
+        params.setQuarter(originalParams.getQuarter());
552
+        params.setMonth(originalParams.getMonth());
553
+        params.setStartTime(originalParams.getStartTime());
554
+        params.setEndTime(originalParams.getEndTime());
555
+        params.setUserId(originalParams.getUserId());
556
+        params.setDeptId(originalParams.getDeptId());
557
+        params.setChainRatio(false);
558
+        params.setYearOnYear(false);
559
+
560
+        // 计算上季度
561
+        if (params.getQuarter() != null) {
562
+            int lastQuarter = params.getQuarter() - 1;
563
+            int year = params.getYear();
564
+            if (lastQuarter <= 0) {
565
+                lastQuarter = 4;
566
+                year = year - 1;
567
+            }
568
+            params.setQuarter(lastQuarter);
569
+            params.setYear(year);
570
+        }
571
+
572
+        return params;
573
+    }
574
+
575
+
576
+    /**
577
+     * 创建去年同期参数
578
+     */
579
+    private IndicatorCalculationParams createLastYearParams(IndicatorCalculationParams originalParams) {
580
+        IndicatorCalculationParams params = new IndicatorCalculationParams();
581
+        params.setYear(originalParams.getYear());
582
+        params.setQuarter(originalParams.getQuarter());
583
+        params.setMonth(originalParams.getMonth());
584
+        params.setStartTime(originalParams.getStartTime());
585
+        params.setEndTime(originalParams.getEndTime());
586
+        params.setUserId(originalParams.getUserId());
587
+        params.setDeptId(originalParams.getDeptId());
588
+        params.setChainRatio(false);
589
+        params.setYearOnYear(false);
590
+
591
+        // 计算去年同期
592
+        if (params.getYear() != null) {
593
+            params.setYear(params.getYear() - 1);
594
+        }
595
+
596
+        return params;
597
+    }
598
+
599
+    /**
600
+     * 获取站点值
601
+     */
602
+    private BigDecimal getStationValue(List<AttendancePersonStatsDTO> list) {
603
+        return list.stream()
604
+                .filter(dto -> DeptTypeEnum.STATION.getCode().equals(dto.getDeptType()))
605
+                .findFirst()
606
+                .map(AttendancePersonStatsDTO::getCurrentValue)
607
+                .orElse(BigDecimal.ZERO);
608
+    }
609
+
610
+    /**
611
+     * 获取指定大队的值
612
+     */
613
+    private BigDecimal getDeptValue(List<AttendancePersonStatsDTO> list, String deptName) {
614
+        return list.stream()
615
+                .filter(dto -> DeptTypeEnum.BRIGADE.getCode().equals(dto.getDeptType())
616
+                        && deptName.equals(dto.getDeptName()))
617
+                .findFirst()
618
+                .map(AttendancePersonStatsDTO::getCurrentValue)
619
+                .orElse(BigDecimal.ZERO);
620
+    }
621
+
622
+    /**
623
+     * 获取方向文本:上升 或 下降
624
+     */
625
+    private String getDirectionText(String growthRate) {
626
+        if (StringUtils.isEmpty(growthRate)) return "";
627
+        if (growthRate.equals("--")) return "上升";
628
+        return new BigDecimal(growthRate).compareTo(BigDecimal.ZERO) >= 0 ? "上升" : "下降";
629
+    }
630
+
631
+
632
+    /**
633
+     * 获取站点下的所有大队
634
+     */
635
+    private List<SysDept> getBrigadesUnderSite(Long siteId) {
636
+        List<SysDept> children = deptService.selectChildrenDeptById(siteId);
637
+        return children.stream()
638
+                .filter(dept -> DeptTypeEnum.BRIGADE.getCode().equals(dept.getDeptType()))
639
+                .collect(Collectors.toList());
640
+    }
641
+
642
+}

+ 100 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseAttachmentController.java

@@ -0,0 +1,100 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.security.access.prepost.PreAuthorize;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.PostMapping;
10
+import org.springframework.web.bind.annotation.PutMapping;
11
+import org.springframework.web.bind.annotation.DeleteMapping;
12
+import org.springframework.web.bind.annotation.PathVariable;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.RestController;
16
+import com.sundot.airport.common.annotation.Log;
17
+import com.sundot.airport.common.core.controller.BaseController;
18
+import com.sundot.airport.common.core.domain.AjaxResult;
19
+import com.sundot.airport.common.enums.BusinessType;
20
+import com.sundot.airport.common.domain.BaseAttachment;
21
+import com.sundot.airport.common.service.IBaseAttachmentService;
22
+import com.sundot.airport.common.utils.poi.ExcelUtil;
23
+import com.sundot.airport.common.core.page.TableDataInfo;
24
+
25
+/**
26
+ * 附件Controller
27
+ *
28
+ * @author ruoyi
29
+ * @date 2025-07-14
30
+ */
31
+@RestController
32
+@RequestMapping("/system/attachment")
33
+public class BaseAttachmentController extends BaseController {
34
+    @Autowired
35
+    private IBaseAttachmentService baseAttachmentService;
36
+
37
+    /**
38
+     * 查询附件列表
39
+     */
40
+    @PreAuthorize("@ss.hasPermi('system:attachment:list')")
41
+    @GetMapping("/list")
42
+    public TableDataInfo list(BaseAttachment baseAttachment) {
43
+        startPage();
44
+        List<BaseAttachment> list = baseAttachmentService.selectBaseAttachmentList(baseAttachment);
45
+        return getDataTable(list);
46
+    }
47
+
48
+    /**
49
+     * 导出附件列表
50
+     */
51
+    @PreAuthorize("@ss.hasPermi('system:attachment:export')")
52
+    @Log(title = "附件", businessType = BusinessType.EXPORT)
53
+    @PostMapping("/export")
54
+    public void export(HttpServletResponse response, BaseAttachment baseAttachment) {
55
+        List<BaseAttachment> list = baseAttachmentService.selectBaseAttachmentList(baseAttachment);
56
+        ExcelUtil<BaseAttachment> util = new ExcelUtil<BaseAttachment>(BaseAttachment.class);
57
+        util.exportExcel(response, list, "附件数据");
58
+    }
59
+
60
+    /**
61
+     * 获取附件详细信息
62
+     */
63
+    @PreAuthorize("@ss.hasPermi('system:attachment:query')")
64
+    @GetMapping(value = "/{id}")
65
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
66
+        return success(baseAttachmentService.selectBaseAttachmentById(id));
67
+    }
68
+
69
+    /**
70
+     * 新增附件
71
+     */
72
+    @PreAuthorize("@ss.hasPermi('system:attachment:add')")
73
+    @Log(title = "附件", businessType = BusinessType.INSERT)
74
+    @PostMapping
75
+    public AjaxResult add(@RequestBody BaseAttachment baseAttachment) {
76
+        baseAttachment.setCreateBy(getUsername());
77
+        return toAjax(baseAttachmentService.insertBaseAttachment(baseAttachment));
78
+    }
79
+
80
+    /**
81
+     * 修改附件
82
+     */
83
+    @PreAuthorize("@ss.hasPermi('system:attachment:edit')")
84
+    @Log(title = "附件", businessType = BusinessType.UPDATE)
85
+    @PutMapping
86
+    public AjaxResult edit(@RequestBody BaseAttachment baseAttachment) {
87
+        baseAttachment.setUpdateBy(getUsername());
88
+        return toAjax(baseAttachmentService.updateBaseAttachment(baseAttachment));
89
+    }
90
+
91
+    /**
92
+     * 删除附件
93
+     */
94
+    @PreAuthorize("@ss.hasPermi('system:attachment:remove')")
95
+    @Log(title = "附件", businessType = BusinessType.DELETE)
96
+    @DeleteMapping("/{ids}")
97
+    public AjaxResult remove(@PathVariable Long[] ids) {
98
+        return toAjax(baseAttachmentService.deleteBaseAttachmentByIds(ids));
99
+    }
100
+}

+ 108 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseCheckCategoryController.java

@@ -0,0 +1,108 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.security.access.prepost.PreAuthorize;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.PostMapping;
10
+import org.springframework.web.bind.annotation.PutMapping;
11
+import org.springframework.web.bind.annotation.DeleteMapping;
12
+import org.springframework.web.bind.annotation.PathVariable;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.RestController;
16
+import com.sundot.airport.common.annotation.Log;
17
+import com.sundot.airport.common.core.controller.BaseController;
18
+import com.sundot.airport.common.core.domain.AjaxResult;
19
+import com.sundot.airport.common.enums.BusinessType;
20
+import com.sundot.airport.system.domain.BaseCheckCategory;
21
+import com.sundot.airport.system.service.IBaseCheckCategoryService;
22
+import com.sundot.airport.common.utils.poi.ExcelUtil;
23
+
24
+/**
25
+ * 检查项分类Controller
26
+ *
27
+ * @author ruoyi
28
+ * @date 2025-07-11
29
+ */
30
+@RestController
31
+@RequestMapping("/system/checkCategory")
32
+public class BaseCheckCategoryController extends BaseController {
33
+    @Autowired
34
+    private IBaseCheckCategoryService baseCheckCategoryService;
35
+
36
+    /**
37
+     * 查询检查项分类列表
38
+     */
39
+    @PreAuthorize("@ss.hasPermi('system:checkCategory:list')")
40
+    @GetMapping("/list")
41
+    public AjaxResult list(BaseCheckCategory baseCheckCategory) {
42
+        List<BaseCheckCategory> list = baseCheckCategoryService.selectBaseCheckCategoryList(baseCheckCategory);
43
+        return success(list);
44
+    }
45
+
46
+    /**
47
+     * 导出检查项分类列表
48
+     */
49
+    @PreAuthorize("@ss.hasPermi('system:checkCategory:export')")
50
+    @Log(title = "检查项分类", businessType = BusinessType.EXPORT)
51
+    @PostMapping("/export")
52
+    public void export(HttpServletResponse response, BaseCheckCategory baseCheckCategory) {
53
+        List<BaseCheckCategory> list = baseCheckCategoryService.selectBaseCheckCategoryList(baseCheckCategory);
54
+        ExcelUtil<BaseCheckCategory> util = new ExcelUtil<BaseCheckCategory>(BaseCheckCategory.class);
55
+        util.exportExcel(response, list, "检查项分类数据");
56
+    }
57
+
58
+    /**
59
+     * 获取检查项分类详细信息
60
+     */
61
+    @PreAuthorize("@ss.hasPermi('system:checkCategory:query')")
62
+    @GetMapping(value = "/{id}")
63
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
64
+        return success(baseCheckCategoryService.selectBaseCheckCategoryById(id));
65
+    }
66
+
67
+    /**
68
+     * 新增检查项分类
69
+     */
70
+    @PreAuthorize("@ss.hasPermi('system:checkCategory:add')")
71
+    @Log(title = "检查项分类", businessType = BusinessType.INSERT)
72
+    @PostMapping
73
+    public AjaxResult add(@RequestBody BaseCheckCategory baseCheckCategory) {
74
+        baseCheckCategory.setCreateBy(getUsername());
75
+        return toAjax(baseCheckCategoryService.insertBaseCheckCategory(baseCheckCategory));
76
+    }
77
+
78
+    /**
79
+     * 修改检查项分类
80
+     */
81
+    @PreAuthorize("@ss.hasPermi('system:checkCategory:edit')")
82
+    @Log(title = "检查项分类", businessType = BusinessType.UPDATE)
83
+    @PutMapping
84
+    public AjaxResult edit(@RequestBody BaseCheckCategory baseCheckCategory) {
85
+        baseCheckCategory.setUpdateBy(getUsername());
86
+        return toAjax(baseCheckCategoryService.updateBaseCheckCategory(baseCheckCategory));
87
+    }
88
+
89
+    /**
90
+     * 删除检查项分类
91
+     */
92
+    @PreAuthorize("@ss.hasPermi('system:checkCategory:remove')")
93
+    @Log(title = "检查项分类", businessType = BusinessType.DELETE)
94
+    @DeleteMapping("/{ids}")
95
+    public AjaxResult remove(@PathVariable Long[] ids) {
96
+        return toAjax(baseCheckCategoryService.deleteBaseCheckCategoryByIds(ids));
97
+    }
98
+
99
+    /**
100
+     * 查询检查项分类列表树形结构
101
+     */
102
+    @PreAuthorize("@ss.hasPermi('system:checkCategory:list')")
103
+    @GetMapping("/listTree")
104
+    public AjaxResult listTree(BaseCheckCategory baseCheckCategory) {
105
+        List<BaseCheckCategory> list = baseCheckCategoryService.selectBaseCheckCategoryListTree(baseCheckCategory);
106
+        return success(list);
107
+    }
108
+}

+ 108 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseCheckPointController.java

@@ -0,0 +1,108 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.security.access.prepost.PreAuthorize;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.PostMapping;
10
+import org.springframework.web.bind.annotation.PutMapping;
11
+import org.springframework.web.bind.annotation.DeleteMapping;
12
+import org.springframework.web.bind.annotation.PathVariable;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.RestController;
16
+import com.sundot.airport.common.annotation.Log;
17
+import com.sundot.airport.common.core.controller.BaseController;
18
+import com.sundot.airport.common.core.domain.AjaxResult;
19
+import com.sundot.airport.common.enums.BusinessType;
20
+import com.sundot.airport.system.domain.BaseCheckPoint;
21
+import com.sundot.airport.system.service.IBaseCheckPointService;
22
+import com.sundot.airport.common.utils.poi.ExcelUtil;
23
+
24
+/**
25
+ * 检查部位Controller
26
+ *
27
+ * @author ruoyi
28
+ * @date 2025-07-08
29
+ */
30
+@RestController
31
+@RequestMapping("/system/point")
32
+public class BaseCheckPointController extends BaseController {
33
+    @Autowired
34
+    private IBaseCheckPointService baseCheckPointService;
35
+
36
+    /**
37
+     * 查询检查部位列表
38
+     */
39
+    @PreAuthorize("@ss.hasPermi('system:point:list')")
40
+    @GetMapping("/list")
41
+    public AjaxResult list(BaseCheckPoint baseCheckPoint) {
42
+        List<BaseCheckPoint> list = baseCheckPointService.selectBaseCheckPointList(baseCheckPoint);
43
+        return success(list);
44
+    }
45
+
46
+    /**
47
+     * 导出检查部位列表
48
+     */
49
+    @PreAuthorize("@ss.hasPermi('system:point:export')")
50
+    @Log(title = "检查部位", businessType = BusinessType.EXPORT)
51
+    @PostMapping("/export")
52
+    public void export(HttpServletResponse response, BaseCheckPoint baseCheckPoint) {
53
+        List<BaseCheckPoint> list = baseCheckPointService.selectBaseCheckPointList(baseCheckPoint);
54
+        ExcelUtil<BaseCheckPoint> util = new ExcelUtil<BaseCheckPoint>(BaseCheckPoint.class);
55
+        util.exportExcel(response, list, "检查部位数据");
56
+    }
57
+
58
+    /**
59
+     * 获取检查部位详细信息
60
+     */
61
+    @PreAuthorize("@ss.hasPermi('system:point:query')")
62
+    @GetMapping(value = "/{id}")
63
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
64
+        return success(baseCheckPointService.selectBaseCheckPointById(id));
65
+    }
66
+
67
+    /**
68
+     * 新增检查部位
69
+     */
70
+    @PreAuthorize("@ss.hasPermi('system:point:add')")
71
+    @Log(title = "检查部位", businessType = BusinessType.INSERT)
72
+    @PostMapping
73
+    public AjaxResult add(@RequestBody BaseCheckPoint baseCheckPoint) {
74
+        baseCheckPoint.setCreateBy(getUsername());
75
+        return toAjax(baseCheckPointService.insertBaseCheckPoint(baseCheckPoint));
76
+    }
77
+
78
+    /**
79
+     * 修改检查部位
80
+     */
81
+    @PreAuthorize("@ss.hasPermi('system:point:edit')")
82
+    @Log(title = "检查部位", businessType = BusinessType.UPDATE)
83
+    @PutMapping
84
+    public AjaxResult edit(@RequestBody BaseCheckPoint baseCheckPoint) {
85
+        baseCheckPoint.setUpdateBy(getUsername());
86
+        return toAjax(baseCheckPointService.updateBaseCheckPoint(baseCheckPoint));
87
+    }
88
+
89
+    /**
90
+     * 删除检查部位
91
+     */
92
+    @PreAuthorize("@ss.hasPermi('system:point:remove')")
93
+    @Log(title = "检查部位", businessType = BusinessType.DELETE)
94
+    @DeleteMapping("/{ids}")
95
+    public AjaxResult remove(@PathVariable Long[] ids) {
96
+        return toAjax(baseCheckPointService.deleteBaseCheckPointByIds(ids));
97
+    }
98
+
99
+    /**
100
+     * 查询检查部位列表树形结构
101
+     */
102
+    @PreAuthorize("@ss.hasPermi('system:point:list')")
103
+    @GetMapping("/listTree")
104
+    public AjaxResult listTree(BaseCheckPoint baseCheckPoint) {
105
+        List<BaseCheckPoint> list = baseCheckPointService.selectBaseCheckPointListTree(baseCheckPoint);
106
+        return success(list);
107
+    }
108
+}

+ 109 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseDefaultChoiseController.java

@@ -0,0 +1,109 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.security.access.prepost.PreAuthorize;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.PostMapping;
10
+import org.springframework.web.bind.annotation.PutMapping;
11
+import org.springframework.web.bind.annotation.DeleteMapping;
12
+import org.springframework.web.bind.annotation.PathVariable;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.RestController;
16
+import com.sundot.airport.common.annotation.Log;
17
+import com.sundot.airport.common.annotation.Anonymous;
18
+import com.sundot.airport.common.core.controller.BaseController;
19
+import com.sundot.airport.common.core.domain.AjaxResult;
20
+import com.sundot.airport.common.enums.BusinessType;
21
+import com.sundot.airport.system.domain.BaseDefaultChoise;
22
+import com.sundot.airport.system.service.IBaseDefaultChoiseService;
23
+import com.sundot.airport.common.utils.poi.ExcelUtil;
24
+import com.sundot.airport.common.core.page.TableDataInfo;
25
+
26
+/**
27
+ * 默认选择配置Controller
28
+ *
29
+ * @author ruoyi
30
+ * @date 2025-08-25
31
+ */
32
+@RestController
33
+@RequestMapping("/system/defaultChoise")
34
+public class BaseDefaultChoiseController extends BaseController {
35
+    @Autowired
36
+    private IBaseDefaultChoiseService baseDefaultChoiseService;
37
+
38
+    /**
39
+     * 查询默认选择配置列表
40
+     */
41
+    @Anonymous
42
+    @GetMapping("/list")
43
+    public TableDataInfo list(BaseDefaultChoise baseDefaultChoise) {
44
+        startPage();
45
+        List<BaseDefaultChoise> list = baseDefaultChoiseService.selectBaseDefaultChoiseList(baseDefaultChoise);
46
+        return getDataTable(list);
47
+    }
48
+
49
+    /**
50
+     * 导出默认选择配置列表
51
+     */
52
+    @PreAuthorize("@ss.hasPermi('system:defaultChoise:export')")
53
+    @Log(title = "默认选择配置", businessType = BusinessType.EXPORT)
54
+    @PostMapping("/export")
55
+    public void export(HttpServletResponse response, BaseDefaultChoise baseDefaultChoise) {
56
+        List<BaseDefaultChoise> list = baseDefaultChoiseService.selectBaseDefaultChoiseList(baseDefaultChoise);
57
+        ExcelUtil<BaseDefaultChoise> util = new ExcelUtil<BaseDefaultChoise>(BaseDefaultChoise.class);
58
+        util.exportExcel(response, list, "默认选择配置数据");
59
+    }
60
+
61
+    /**
62
+     * 获取默认选择配置详细信息
63
+     */
64
+    @PreAuthorize("@ss.hasPermi('system:defaultChoise:query')")
65
+    @GetMapping(value = "/{id}")
66
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
67
+        return success(baseDefaultChoiseService.selectBaseDefaultChoiseById(id));
68
+    }
69
+
70
+    /**
71
+     * 新增默认选择配置
72
+     */
73
+    @PreAuthorize("@ss.hasPermi('system:defaultChoise:add')")
74
+    @Log(title = "默认选择配置", businessType = BusinessType.INSERT)
75
+    @PostMapping
76
+    public AjaxResult add(@RequestBody BaseDefaultChoise baseDefaultChoise) {
77
+        return toAjax(baseDefaultChoiseService.insertBaseDefaultChoise(baseDefaultChoise));
78
+    }
79
+
80
+    /**
81
+     * 修改默认选择配置
82
+     */
83
+    @PreAuthorize("@ss.hasPermi('system:defaultChoise:edit')")
84
+    @Log(title = "默认选择配置", businessType = BusinessType.UPDATE)
85
+    @PutMapping
86
+    public AjaxResult edit(@RequestBody BaseDefaultChoise baseDefaultChoise) {
87
+        return toAjax(baseDefaultChoiseService.updateBaseDefaultChoise(baseDefaultChoise));
88
+    }
89
+
90
+    /**
91
+     * 删除默认选择配置
92
+     */
93
+    @PreAuthorize("@ss.hasPermi('system:defaultChoise:remove')")
94
+    @Log(title = "默认选择配置", businessType = BusinessType.DELETE)
95
+    @DeleteMapping("/{ids}")
96
+    public AjaxResult remove(@PathVariable Long[] ids) {
97
+        return toAjax(baseDefaultChoiseService.deleteBaseDefaultChoiseByIds(ids));
98
+    }
99
+
100
+    /**
101
+     * 获取默认选择的分类信息(包含ID、名称及父类信息)
102
+     */
103
+    @Anonymous
104
+    @GetMapping("/categoryInfo/{categoryType}")
105
+    public AjaxResult getCategoryInfo(@PathVariable("categoryType") Integer categoryType) {
106
+        List<BaseDefaultChoise> list = baseDefaultChoiseService.selectDefaultCategoryWithInfo(categoryType);
107
+        return success(list);
108
+    }
109
+}

+ 107 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/BasePositionController.java

@@ -0,0 +1,107 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.security.access.prepost.PreAuthorize;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.PostMapping;
10
+import org.springframework.web.bind.annotation.PutMapping;
11
+import org.springframework.web.bind.annotation.DeleteMapping;
12
+import org.springframework.web.bind.annotation.PathVariable;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.RestController;
16
+import com.sundot.airport.common.annotation.Log;
17
+import com.sundot.airport.common.core.controller.BaseController;
18
+import com.sundot.airport.common.core.domain.AjaxResult;
19
+import com.sundot.airport.common.enums.BusinessType;
20
+import com.sundot.airport.system.domain.BasePosition;
21
+import com.sundot.airport.system.service.IBasePositionService;
22
+import com.sundot.airport.common.utils.poi.ExcelUtil;
23
+
24
+/**
25
+ * 位置Controller
26
+ *
27
+ * @author ruoyi
28
+ * @date 2025-07-09
29
+ */
30
+@RestController
31
+@RequestMapping("/system/position")
32
+public class BasePositionController extends BaseController {
33
+    @Autowired
34
+    private IBasePositionService basePositionService;
35
+
36
+    /**
37
+     * 查询位置列表
38
+     */
39
+    @PreAuthorize("@ss.hasPermi('system:position:list')")
40
+    @GetMapping("/list")
41
+    public AjaxResult list(BasePosition basePosition) {
42
+        List<BasePosition> list = basePositionService.selectBasePositionList(basePosition);
43
+        return success(list);
44
+    }
45
+
46
+    /**
47
+     * 导出位置列表
48
+     */
49
+    @PreAuthorize("@ss.hasPermi('system:position:export')")
50
+    @Log(title = "位置", businessType = BusinessType.EXPORT)
51
+    @PostMapping("/export")
52
+    public void export(HttpServletResponse response, BasePosition basePosition) {
53
+        List<BasePosition> list = basePositionService.selectBasePositionList(basePosition);
54
+        ExcelUtil<BasePosition> util = new ExcelUtil<BasePosition>(BasePosition.class);
55
+        util.exportExcel(response, list, "位置数据");
56
+    }
57
+
58
+    /**
59
+     * 获取位置详细信息
60
+     */
61
+    @PreAuthorize("@ss.hasPermi('system:position:query')")
62
+    @GetMapping(value = "/{id}")
63
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
64
+        return success(basePositionService.selectBasePositionById(id));
65
+    }
66
+
67
+    /**
68
+     * 新增位置
69
+     */
70
+    @PreAuthorize("@ss.hasPermi('system:position:add')")
71
+    @Log(title = "位置", businessType = BusinessType.INSERT)
72
+    @PostMapping
73
+    public AjaxResult add(@RequestBody BasePosition basePosition) {
74
+        basePosition.setCreateBy(getUsername());
75
+        return toAjax(basePositionService.insertBasePosition(basePosition));
76
+    }
77
+
78
+    /**
79
+     * 修改位置
80
+     */
81
+    @PreAuthorize("@ss.hasPermi('system:position:edit')")
82
+    @Log(title = "位置", businessType = BusinessType.UPDATE)
83
+    @PutMapping
84
+    public AjaxResult edit(@RequestBody BasePosition basePosition) {
85
+        basePosition.setUpdateBy(getUsername());
86
+        return toAjax(basePositionService.updateBasePosition(basePosition));
87
+    }
88
+
89
+    /**
90
+     * 删除位置
91
+     */
92
+    @PreAuthorize("@ss.hasPermi('system:position:remove')")
93
+    @Log(title = "位置", businessType = BusinessType.DELETE)
94
+    @DeleteMapping("/{ids}")
95
+    public AjaxResult remove(@PathVariable Long[] ids) {
96
+        return toAjax(basePositionService.deleteBasePositionByIds(ids));
97
+    }
98
+
99
+    /**
100
+     * 查询位置列表树形结构
101
+     */
102
+    @PreAuthorize("@ss.hasPermi('system:position:list')")
103
+    @GetMapping("/listTree")
104
+    public AjaxResult listTree(BasePosition basePosition) {
105
+        return success(basePositionService.selectBasePositionListTree(basePosition));
106
+    }
107
+}

+ 194 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseProjectController.java

@@ -0,0 +1,194 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.ArrayList;
4
+import java.util.Collections;
5
+import java.util.List;
6
+import java.util.Map;
7
+import java.util.stream.Collectors;
8
+import javax.servlet.http.HttpServletResponse;
9
+
10
+import cn.hutool.core.collection.CollUtil;
11
+import com.sundot.airport.system.domain.vo.CheckProjectItemTreeVo;
12
+import com.sundot.airport.system.domain.vo.CheckProjectItemVo;
13
+import org.springframework.security.access.prepost.PreAuthorize;
14
+import org.springframework.beans.factory.annotation.Autowired;
15
+import org.springframework.web.bind.annotation.GetMapping;
16
+import org.springframework.web.bind.annotation.PostMapping;
17
+import org.springframework.web.bind.annotation.PutMapping;
18
+import org.springframework.web.bind.annotation.DeleteMapping;
19
+import org.springframework.web.bind.annotation.PathVariable;
20
+import org.springframework.web.bind.annotation.RequestBody;
21
+import org.springframework.web.bind.annotation.RequestMapping;
22
+import org.springframework.web.bind.annotation.RestController;
23
+import com.sundot.airport.common.annotation.Log;
24
+import com.sundot.airport.common.core.controller.BaseController;
25
+import com.sundot.airport.common.core.domain.AjaxResult;
26
+import com.sundot.airport.common.enums.BusinessType;
27
+import com.sundot.airport.system.domain.BaseProject;
28
+import com.sundot.airport.system.service.IBaseProjectService;
29
+import com.sundot.airport.common.utils.poi.ExcelUtil;
30
+import com.sundot.airport.common.core.page.TableDataInfo;
31
+
32
+/**
33
+ * 检查项目Controller
34
+ *
35
+ * @author ruoyi
36
+ * @date 2025-07-11
37
+ */
38
+@RestController
39
+@RequestMapping("/system/project")
40
+public class BaseProjectController extends BaseController {
41
+    @Autowired
42
+    private IBaseProjectService baseProjectService;
43
+
44
+    /**
45
+     * 查询检查项目列表
46
+     */
47
+    @PreAuthorize("@ss.hasPermi('system:project:list')")
48
+    @GetMapping("/list")
49
+    public TableDataInfo list(BaseProject baseProject) {
50
+        startPage();
51
+        List<BaseProject> list = baseProjectService.selectBaseProjectList(baseProject);
52
+        return getDataTable(list);
53
+    }
54
+
55
+    /**
56
+     * 导出检查项目列表
57
+     */
58
+    @PreAuthorize("@ss.hasPermi('system:project:export')")
59
+    @Log(title = "检查项目", businessType = BusinessType.EXPORT)
60
+    @PostMapping("/export")
61
+    public void export(HttpServletResponse response, BaseProject baseProject) {
62
+        List<BaseProject> list = baseProjectService.selectBaseProjectList(baseProject);
63
+        ExcelUtil<BaseProject> util = new ExcelUtil<BaseProject>(BaseProject.class);
64
+        util.exportExcel(response, list, "检查项目数据");
65
+    }
66
+
67
+    /**
68
+     * 获取检查项目详细信息
69
+     */
70
+    @PreAuthorize("@ss.hasPermi('system:project:query')")
71
+    @GetMapping(value = "/{id}")
72
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
73
+        return success(baseProjectService.selectBaseProjectById(id));
74
+    }
75
+
76
+    /**
77
+     * 新增检查项目
78
+     */
79
+    @PreAuthorize("@ss.hasPermi('system:project:add')")
80
+    @Log(title = "检查项目", businessType = BusinessType.INSERT)
81
+    @PostMapping
82
+    public AjaxResult add(@RequestBody BaseProject baseProject) {
83
+        baseProject.setCreateBy(getUsername());
84
+        return toAjax(baseProjectService.insertBaseProject(baseProject));
85
+    }
86
+
87
+    /**
88
+     * 修改检查项目
89
+     */
90
+    @PreAuthorize("@ss.hasPermi('system:project:edit')")
91
+    @Log(title = "检查项目", businessType = BusinessType.UPDATE)
92
+    @PutMapping
93
+    public AjaxResult edit(@RequestBody BaseProject baseProject) {
94
+        baseProject.setUpdateBy(getUsername());
95
+        return toAjax(baseProjectService.updateBaseProject(baseProject));
96
+    }
97
+
98
+    /**
99
+     * 删除检查项目
100
+     */
101
+    @PreAuthorize("@ss.hasPermi('system:project:remove')")
102
+    @Log(title = "检查项目", businessType = BusinessType.DELETE)
103
+    @DeleteMapping("/{ids}")
104
+    public AjaxResult remove(@PathVariable Long[] ids) {
105
+        return toAjax(baseProjectService.deleteBaseProjectByIds(ids));
106
+    }
107
+
108
+    /**
109
+     * 根据分类编码查询检查项目列表
110
+     */
111
+    @PreAuthorize("@ss.hasPermi('system:project:list')")
112
+    @GetMapping(value = "/list/{categoryCode}")
113
+    public AjaxResult list(@PathVariable("categoryCode") String categoryCode) {
114
+        return success(baseProjectService.selectProjectListByCategoryCode(categoryCode));
115
+    }
116
+
117
+    /**
118
+     * 根据检查级别查询检查项目列表
119
+     */
120
+    @PreAuthorize("@ss.hasPermi('system:project:list')")
121
+    @GetMapping(value = "/listByCheckLevel/{checkLevel}")
122
+    public AjaxResult listByCheckedLevel(@PathVariable("checkLevel") String checkLevel) {
123
+        List<CheckProjectItemVo> result = baseProjectService.listByCheckLevel(checkLevel);
124
+        return success(convertToTree(result));
125
+    }
126
+
127
+    /**
128
+     * 转换为树形结构
129
+     */
130
+    public List<CheckProjectItemTreeVo> convertToTree(List<CheckProjectItemVo> itemList) {
131
+        if (CollUtil.isEmpty(itemList)) {
132
+            return Collections.emptyList();
133
+        }
134
+        List<CheckProjectItemTreeVo> result = new ArrayList<>();
135
+
136
+        // 第一层分组:按一级分类
137
+        Map<String, List<CheckProjectItemVo>> levelOneMap = itemList.stream()
138
+                .collect(Collectors.groupingBy(CheckProjectItemVo::getCategoryCodeOne));
139
+
140
+
141
+        // 处理一级分类
142
+        for (Map.Entry<String, List<CheckProjectItemVo>> levelOneEntry : levelOneMap.entrySet()) {
143
+            String levelOneCode = levelOneEntry.getKey();
144
+            List<CheckProjectItemVo> levelOneItems = levelOneEntry.getValue();
145
+
146
+            // 获取一级分类名称(取第一个元素的名称)
147
+            String levelOneName = levelOneItems.get(0).getCategoryNameOne();
148
+
149
+            // 创建一级节点
150
+            CheckProjectItemTreeVo levelOneNode = new CheckProjectItemTreeVo();
151
+            levelOneNode.setCode(levelOneCode);
152
+            levelOneNode.setName(levelOneName);
153
+
154
+            // 第二层分组:按二级分类
155
+            Map<String, List<CheckProjectItemVo>> levelTwoMap = levelOneItems.stream()
156
+                    .collect(Collectors.groupingBy(CheckProjectItemVo::getCategoryCodeTwo));
157
+
158
+            List<CheckProjectItemTreeVo> levelTwoNodes = new ArrayList<>();
159
+
160
+            // 处理二级分类
161
+            for (Map.Entry<String, List<CheckProjectItemVo>> levelTwoEntry : levelTwoMap.entrySet()) {
162
+                String levelTwoCode = levelTwoEntry.getKey();
163
+                List<CheckProjectItemVo> levelTwoItems = levelTwoEntry.getValue();
164
+
165
+                // 获取二级分类名称(取第一个元素的名称)
166
+                String levelTwoName = levelTwoItems.get(0).getCategoryNameTwo();
167
+
168
+                // 创建二级节点
169
+                CheckProjectItemTreeVo levelTwoNode = new CheckProjectItemTreeVo();
170
+                levelTwoNode.setCode(levelTwoCode);
171
+                levelTwoNode.setName(levelTwoName);
172
+
173
+                // 处理检查项目
174
+                List<CheckProjectItemTreeVo> projectNodes = levelTwoItems.stream()
175
+                        .map(item -> {
176
+                            CheckProjectItemTreeVo projectNode = new CheckProjectItemTreeVo();
177
+                            projectNode.setCode(item.getProjectCode());
178
+                            projectNode.setName(item.getProjectName());
179
+                            projectNode.setList(null); // 项目没有子节点
180
+                            return projectNode;
181
+                        })
182
+                        .collect(Collectors.toList());
183
+
184
+                levelTwoNode.setList(projectNodes);
185
+                levelTwoNodes.add(levelTwoNode);
186
+            }
187
+
188
+            levelOneNode.setList(levelTwoNodes);
189
+            result.add(levelOneNode);
190
+        }
191
+        return result;
192
+    }
193
+
194
+}

+ 100 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseReadController.java

@@ -0,0 +1,100 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import com.sundot.airport.common.domain.BaseRead;
7
+import com.sundot.airport.common.service.IBaseReadService;
8
+import org.springframework.security.access.prepost.PreAuthorize;
9
+import org.springframework.beans.factory.annotation.Autowired;
10
+import org.springframework.web.bind.annotation.GetMapping;
11
+import org.springframework.web.bind.annotation.PostMapping;
12
+import org.springframework.web.bind.annotation.PutMapping;
13
+import org.springframework.web.bind.annotation.DeleteMapping;
14
+import org.springframework.web.bind.annotation.PathVariable;
15
+import org.springframework.web.bind.annotation.RequestBody;
16
+import org.springframework.web.bind.annotation.RequestMapping;
17
+import org.springframework.web.bind.annotation.RestController;
18
+import com.sundot.airport.common.annotation.Log;
19
+import com.sundot.airport.common.core.controller.BaseController;
20
+import com.sundot.airport.common.core.domain.AjaxResult;
21
+import com.sundot.airport.common.enums.BusinessType;
22
+import com.sundot.airport.common.utils.poi.ExcelUtil;
23
+import com.sundot.airport.common.core.page.TableDataInfo;
24
+
25
+/**
26
+ * 已读Controller
27
+ *
28
+ * @author ruoyi
29
+ * @date 2025-09-09
30
+ */
31
+@RestController
32
+@RequestMapping("/system/read")
33
+public class BaseReadController extends BaseController {
34
+    @Autowired
35
+    private IBaseReadService baseReadService;
36
+
37
+    /**
38
+     * 查询已读列表
39
+     */
40
+    @PreAuthorize("@ss.hasPermi('system:read:list')")
41
+    @GetMapping("/list")
42
+    public TableDataInfo list(BaseRead baseRead) {
43
+        startPage();
44
+        List<BaseRead> list = baseReadService.selectBaseReadList(baseRead);
45
+        return getDataTable(list);
46
+    }
47
+
48
+    /**
49
+     * 导出已读列表
50
+     */
51
+    @PreAuthorize("@ss.hasPermi('system:read:export')")
52
+    @Log(title = "已读", businessType = BusinessType.EXPORT)
53
+    @PostMapping("/export")
54
+    public void export(HttpServletResponse response, BaseRead baseRead) {
55
+        List<BaseRead> list = baseReadService.selectBaseReadList(baseRead);
56
+        ExcelUtil<BaseRead> util = new ExcelUtil<BaseRead>(BaseRead.class);
57
+        util.exportExcel(response, list, "已读数据");
58
+    }
59
+
60
+    /**
61
+     * 获取已读详细信息
62
+     */
63
+    @PreAuthorize("@ss.hasPermi('system:read:query')")
64
+    @GetMapping(value = "/{id}")
65
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
66
+        return success(baseReadService.selectBaseReadById(id));
67
+    }
68
+
69
+    /**
70
+     * 新增已读
71
+     */
72
+    @PreAuthorize("@ss.hasPermi('system:read:add')")
73
+    @Log(title = "已读", businessType = BusinessType.INSERT)
74
+    @PostMapping
75
+    public AjaxResult add(@RequestBody BaseRead baseRead) {
76
+        baseRead.setCreateBy(getUsername());
77
+        return toAjax(baseReadService.insertBaseRead(baseRead));
78
+    }
79
+
80
+    /**
81
+     * 修改已读
82
+     */
83
+    @PreAuthorize("@ss.hasPermi('system:read:edit')")
84
+    @Log(title = "已读", businessType = BusinessType.UPDATE)
85
+    @PutMapping
86
+    public AjaxResult edit(@RequestBody BaseRead baseRead) {
87
+        baseRead.setUpdateBy(getUsername());
88
+        return toAjax(baseReadService.updateBaseRead(baseRead));
89
+    }
90
+
91
+    /**
92
+     * 删除已读
93
+     */
94
+    @PreAuthorize("@ss.hasPermi('system:read:remove')")
95
+    @Log(title = "已读", businessType = BusinessType.DELETE)
96
+    @DeleteMapping("/{ids}")
97
+    public AjaxResult remove(@PathVariable Long[] ids) {
98
+        return toAjax(baseReadService.deleteBaseReadByIds(ids));
99
+    }
100
+}

+ 109 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseSeizeCategoryController.java

@@ -0,0 +1,109 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import com.sundot.airport.common.annotation.Anonymous;
7
+import org.springframework.security.access.prepost.PreAuthorize;
8
+import org.springframework.beans.factory.annotation.Autowired;
9
+import org.springframework.web.bind.annotation.GetMapping;
10
+import org.springframework.web.bind.annotation.PostMapping;
11
+import org.springframework.web.bind.annotation.PutMapping;
12
+import org.springframework.web.bind.annotation.DeleteMapping;
13
+import org.springframework.web.bind.annotation.PathVariable;
14
+import org.springframework.web.bind.annotation.RequestBody;
15
+import org.springframework.web.bind.annotation.RequestMapping;
16
+import org.springframework.web.bind.annotation.RestController;
17
+import com.sundot.airport.common.annotation.Log;
18
+import com.sundot.airport.common.core.controller.BaseController;
19
+import com.sundot.airport.common.core.domain.AjaxResult;
20
+import com.sundot.airport.common.enums.BusinessType;
21
+import com.sundot.airport.system.domain.BaseSeizeCategory;
22
+import com.sundot.airport.system.service.IBaseSeizeCategoryService;
23
+import com.sundot.airport.common.utils.poi.ExcelUtil;
24
+
25
+/**
26
+ * 查获物品分类Controller
27
+ *
28
+ * @author ruoyi
29
+ * @date 2025-07-08
30
+ */
31
+@RestController
32
+@RequestMapping("/system/category")
33
+public class BaseSeizeCategoryController extends BaseController {
34
+    @Autowired
35
+    private IBaseSeizeCategoryService baseSeizeCategoryService;
36
+
37
+    /**
38
+     * 查询查获物品分类列表
39
+     */
40
+    @Anonymous
41
+    @GetMapping("/list")
42
+    public AjaxResult list(BaseSeizeCategory baseSeizeCategory) {
43
+        List<BaseSeizeCategory> list = baseSeizeCategoryService.selectBaseSeizeCategoryList(baseSeizeCategory);
44
+        return success(list);
45
+    }
46
+
47
+    /**
48
+     * 导出查获物品分类列表
49
+     */
50
+    @PreAuthorize("@ss.hasPermi('system:category:export')")
51
+    @Log(title = "查获物品分类", businessType = BusinessType.EXPORT)
52
+    @PostMapping("/export")
53
+    public void export(HttpServletResponse response, BaseSeizeCategory baseSeizeCategory) {
54
+        List<BaseSeizeCategory> list = baseSeizeCategoryService.selectBaseSeizeCategoryList(baseSeizeCategory);
55
+        ExcelUtil<BaseSeizeCategory> util = new ExcelUtil<BaseSeizeCategory>(BaseSeizeCategory.class);
56
+        util.exportExcel(response, list, "查获物品分类数据");
57
+    }
58
+
59
+    /**
60
+     * 获取查获物品分类详细信息
61
+     */
62
+    @PreAuthorize("@ss.hasPermi('system:category:query')")
63
+    @GetMapping(value = "/{id}")
64
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
65
+        return success(baseSeizeCategoryService.selectBaseSeizeCategoryById(id));
66
+    }
67
+
68
+    /**
69
+     * 新增查获物品分类
70
+     */
71
+    @PreAuthorize("@ss.hasPermi('system:category:add')")
72
+    @Log(title = "查获物品分类", businessType = BusinessType.INSERT)
73
+    @PostMapping
74
+    public AjaxResult add(@RequestBody BaseSeizeCategory baseSeizeCategory) {
75
+        baseSeizeCategory.setCreateBy(getUsername());
76
+        return toAjax(baseSeizeCategoryService.insertBaseSeizeCategory(baseSeizeCategory));
77
+    }
78
+
79
+    /**
80
+     * 修改查获物品分类
81
+     */
82
+    @PreAuthorize("@ss.hasPermi('system:category:edit')")
83
+    @Log(title = "查获物品分类", businessType = BusinessType.UPDATE)
84
+    @PutMapping
85
+    public AjaxResult edit(@RequestBody BaseSeizeCategory baseSeizeCategory) {
86
+        baseSeizeCategory.setUpdateBy(getUsername());
87
+        return toAjax(baseSeizeCategoryService.updateBaseSeizeCategory(baseSeizeCategory));
88
+    }
89
+
90
+    /**
91
+     * 删除查获物品分类
92
+     */
93
+    @PreAuthorize("@ss.hasPermi('system:category:remove')")
94
+    @Log(title = "查获物品分类", businessType = BusinessType.DELETE)
95
+    @DeleteMapping("/{ids}")
96
+    public AjaxResult remove(@PathVariable Long[] ids) {
97
+        return toAjax(baseSeizeCategoryService.deleteBaseSeizeCategoryByIds(ids));
98
+    }
99
+
100
+    /**
101
+     * 查询查获物品分类列表树形结构
102
+     */
103
+    @PreAuthorize("@ss.hasPermi('system:category:list')")
104
+    @GetMapping("/listTree")
105
+    public AjaxResult listTree(BaseSeizeCategory baseSeizeCategory) {
106
+        List<BaseSeizeCategory> list = baseSeizeCategoryService.selectBaseSeizeCategoryListTree(baseSeizeCategory);
107
+        return success(list);
108
+    }
109
+}

+ 109 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/BaseSeizeItemController.java

@@ -0,0 +1,109 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.security.access.prepost.PreAuthorize;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.PostMapping;
10
+import org.springframework.web.bind.annotation.PutMapping;
11
+import org.springframework.web.bind.annotation.DeleteMapping;
12
+import org.springframework.web.bind.annotation.PathVariable;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.RestController;
16
+import com.sundot.airport.common.annotation.Log;
17
+import com.sundot.airport.common.core.controller.BaseController;
18
+import com.sundot.airport.common.core.domain.AjaxResult;
19
+import com.sundot.airport.common.enums.BusinessType;
20
+import com.sundot.airport.system.domain.BaseSeizeItem;
21
+import com.sundot.airport.system.service.IBaseSeizeItemService;
22
+import com.sundot.airport.common.utils.poi.ExcelUtil;
23
+import com.sundot.airport.common.core.page.TableDataInfo;
24
+
25
+/**
26
+ * 查获物品Controller
27
+ *
28
+ * @author ruoyi
29
+ * @date 2025-07-08
30
+ */
31
+@RestController
32
+@RequestMapping("/system/item")
33
+public class BaseSeizeItemController extends BaseController {
34
+    @Autowired
35
+    private IBaseSeizeItemService baseSeizeItemService;
36
+
37
+    /**
38
+     * 查询查获物品列表
39
+     */
40
+    @PreAuthorize("@ss.hasPermi('system:item:list')")
41
+    @GetMapping("/list")
42
+    public TableDataInfo list(BaseSeizeItem baseSeizeItem) {
43
+        startPage();
44
+        List<BaseSeizeItem> list = baseSeizeItemService.selectBaseSeizeItemList(baseSeizeItem);
45
+        return getDataTable(list);
46
+    }
47
+
48
+    /**
49
+     * 导出查获物品列表
50
+     */
51
+    @PreAuthorize("@ss.hasPermi('system:item:export')")
52
+    @Log(title = "查获物品", businessType = BusinessType.EXPORT)
53
+    @PostMapping("/export")
54
+    public void export(HttpServletResponse response, BaseSeizeItem baseSeizeItem) {
55
+        List<BaseSeizeItem> list = baseSeizeItemService.selectBaseSeizeItemList(baseSeizeItem);
56
+        ExcelUtil<BaseSeizeItem> util = new ExcelUtil<BaseSeizeItem>(BaseSeizeItem.class);
57
+        util.exportExcel(response, list, "查获物品数据");
58
+    }
59
+
60
+    /**
61
+     * 获取查获物品详细信息
62
+     */
63
+    @PreAuthorize("@ss.hasPermi('system:item:query')")
64
+    @GetMapping(value = "/{id}")
65
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
66
+        return success(baseSeizeItemService.selectBaseSeizeItemById(id));
67
+    }
68
+
69
+    /**
70
+     * 新增查获物品
71
+     */
72
+    @PreAuthorize("@ss.hasPermi('system:item:add')")
73
+    @Log(title = "查获物品", businessType = BusinessType.INSERT)
74
+    @PostMapping
75
+    public AjaxResult add(@RequestBody BaseSeizeItem baseSeizeItem) {
76
+        baseSeizeItem.setCreateBy(getUsername());
77
+        return toAjax(baseSeizeItemService.insertBaseSeizeItem(baseSeizeItem));
78
+    }
79
+
80
+    /**
81
+     * 修改查获物品
82
+     */
83
+    @PreAuthorize("@ss.hasPermi('system:item:edit')")
84
+    @Log(title = "查获物品", businessType = BusinessType.UPDATE)
85
+    @PutMapping
86
+    public AjaxResult edit(@RequestBody BaseSeizeItem baseSeizeItem) {
87
+        baseSeizeItem.setUpdateBy(getUsername());
88
+        return toAjax(baseSeizeItemService.updateBaseSeizeItem(baseSeizeItem));
89
+    }
90
+
91
+    /**
92
+     * 删除查获物品
93
+     */
94
+    @PreAuthorize("@ss.hasPermi('system:item:remove')")
95
+    @Log(title = "查获物品", businessType = BusinessType.DELETE)
96
+    @DeleteMapping("/{ids}")
97
+    public AjaxResult remove(@PathVariable Long[] ids) {
98
+        return toAjax(baseSeizeItemService.deleteBaseSeizeItemByIds(ids));
99
+    }
100
+
101
+    /**
102
+     * 根据分类编码查询查获物品列表
103
+     */
104
+    @PreAuthorize("@ss.hasPermi('system:item:list')")
105
+    @GetMapping(value = "/list/{categoryCode}")
106
+    public AjaxResult list(@PathVariable("categoryCode") String categoryCode) {
107
+        return success(baseSeizeItemService.selectSeizeItemListByCategoryCode(categoryCode));
108
+    }
109
+}

+ 54 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SmsController.java

@@ -0,0 +1,54 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import com.sundot.airport.common.core.domain.AjaxResult;
4
+import com.sundot.airport.common.service.SmsService;
5
+import com.sundot.airport.common.utils.StringUtils;
6
+import io.swagger.annotations.Api;
7
+import io.swagger.annotations.ApiOperation;
8
+import lombok.extern.slf4j.Slf4j;
9
+import org.springframework.beans.factory.annotation.Autowired;
10
+import org.springframework.web.bind.annotation.PostMapping;
11
+import org.springframework.web.bind.annotation.RequestMapping;
12
+import org.springframework.web.bind.annotation.RequestParam;
13
+import org.springframework.web.bind.annotation.RestController;
14
+
15
+/**
16
+ * 短信验证码控制器
17
+ *
18
+ * @author wangxx
19
+ */
20
+@Slf4j
21
+@RestController
22
+@RequestMapping("/sms")
23
+@Api(tags = "短信验证码管理")
24
+public class SmsController {
25
+
26
+    @Autowired
27
+    private SmsService smsService;
28
+
29
+    /**
30
+     * 发送短信验证码
31
+     *
32
+     * @param phoneNumber 手机号码
33
+     * @return 结果
34
+     */
35
+    @PostMapping("/code")
36
+    @ApiOperation("发送短信验证码")
37
+    public AjaxResult sendSmsCode(@RequestParam String phoneNumber) {
38
+        if (StringUtils.isEmpty(phoneNumber)) {
39
+            return AjaxResult.error("手机号码不能为空");
40
+        }
41
+
42
+        try {
43
+            boolean result = smsService.sendSmsCode(phoneNumber);
44
+            if (result) {
45
+                return AjaxResult.success("验证码发送成功,请注意查收短信");
46
+            } else {
47
+                return AjaxResult.error("验证码发送失败");
48
+            }
49
+        } catch (Exception e) {
50
+            log.error("发送短信验证码异常:{}", e.getMessage(), e);
51
+            return AjaxResult.error(e.getMessage());
52
+        }
53
+    }
54
+}

+ 132 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysAppController.java

@@ -0,0 +1,132 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.security.access.prepost.PreAuthorize;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.PostMapping;
10
+import org.springframework.web.bind.annotation.PutMapping;
11
+import org.springframework.web.bind.annotation.DeleteMapping;
12
+import org.springframework.web.bind.annotation.PathVariable;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.RestController;
16
+import com.sundot.airport.common.annotation.Log;
17
+import com.sundot.airport.common.core.controller.BaseController;
18
+import com.sundot.airport.common.core.domain.AjaxResult;
19
+import com.sundot.airport.common.enums.BusinessType;
20
+import com.sundot.airport.system.domain.SysApp;
21
+import com.sundot.airport.system.service.ISysAppService;
22
+import com.sundot.airport.common.utils.poi.ExcelUtil;
23
+import com.sundot.airport.common.core.page.TableDataInfo;
24
+
25
+/**
26
+ * 应用Controller
27
+ *
28
+ * @author ruoyi
29
+ * @date 2025-11-20
30
+ */
31
+@RestController
32
+@RequestMapping("/system/app")
33
+public class SysAppController extends BaseController {
34
+    @Autowired
35
+    private ISysAppService sysAppService;
36
+
37
+    /**
38
+     * 查询应用列表
39
+     */
40
+    @PreAuthorize("@ss.hasPermi('system:app:list')")
41
+    @GetMapping("/list")
42
+    public TableDataInfo list(SysApp sysApp) {
43
+        startPage();
44
+        List<SysApp> list = sysAppService.selectSysAppList(sysApp);
45
+        return getDataTable(list);
46
+    }
47
+
48
+    /**
49
+     * 导出应用列表
50
+     */
51
+    @PreAuthorize("@ss.hasPermi('system:app:export')")
52
+    @Log(title = "应用", businessType = BusinessType.EXPORT)
53
+    @PostMapping("/export")
54
+    public void export(HttpServletResponse response, SysApp sysApp) {
55
+        List<SysApp> list = sysAppService.selectSysAppList(sysApp);
56
+        ExcelUtil<SysApp> util = new ExcelUtil<SysApp>(SysApp.class);
57
+        util.exportExcel(response, list, "应用数据");
58
+    }
59
+
60
+    /**
61
+     * 获取应用详细信息
62
+     */
63
+    @PreAuthorize("@ss.hasPermi('system:app:query')")
64
+    @GetMapping(value = "/{appId}")
65
+    public AjaxResult getInfo(@PathVariable("appId") Long appId) {
66
+        return success(sysAppService.selectSysAppByAppId(appId));
67
+    }
68
+
69
+    /**
70
+     * 新增应用
71
+     */
72
+    @PreAuthorize("@ss.hasPermi('system:app:add')")
73
+    @Log(title = "应用", businessType = BusinessType.INSERT)
74
+    @PostMapping
75
+    public AjaxResult add(@RequestBody SysApp sysApp) {
76
+        sysApp.setCreateByUserId(getUserId());
77
+        sysApp.setCreateBy(getLoginUser().getUser().getNickName());
78
+        return toAjax(sysAppService.insertSysApp(sysApp));
79
+    }
80
+
81
+    /**
82
+     * 修改应用
83
+     */
84
+    @PreAuthorize("@ss.hasPermi('system:app:edit')")
85
+    @Log(title = "应用", businessType = BusinessType.UPDATE)
86
+    @PutMapping
87
+    public AjaxResult edit(@RequestBody SysApp sysApp) {
88
+        sysApp.setUpdateByUserId(getUserId());
89
+        sysApp.setUpdateBy(getLoginUser().getUser().getNickName());
90
+        return toAjax(sysAppService.updateSysApp(sysApp));
91
+    }
92
+
93
+    /**
94
+     * 删除应用
95
+     */
96
+    @PreAuthorize("@ss.hasPermi('system:app:remove')")
97
+    @Log(title = "应用", businessType = BusinessType.DELETE)
98
+    @DeleteMapping("/{appIds}")
99
+    public AjaxResult remove(@PathVariable Long[] appIds) {
100
+        return toAjax(sysAppService.deleteSysAppByAppIds(appIds));
101
+    }
102
+
103
+    /**
104
+     * 查询全部应用列表
105
+     */
106
+    @PreAuthorize("@ss.hasPermi('system:app:list')")
107
+    @GetMapping("/listAll")
108
+    public AjaxResult listAll(SysApp sysApp) {
109
+        List<SysApp> list = sysAppService.selectSysAppList(sysApp);
110
+        return success(list);
111
+    }
112
+
113
+    /**
114
+     * 移动端查询全部应用列表
115
+     * homePage:0-非首页应用;1-首页应用
116
+     */
117
+//    @PreAuthorize("@ss.hasPermi('system:app:list')")
118
+    @GetMapping("/listBy")
119
+    public AjaxResult listBy(SysApp sysApp) {
120
+        List<SysApp> list = sysAppService.listBy(sysApp);
121
+        return success(list);
122
+    }
123
+
124
+    /**
125
+     * 根据角色ID查询应用列表
126
+     */
127
+    @GetMapping(value = "/roleAppSelect/{roleId}")
128
+    public AjaxResult roleAppSelect(@PathVariable("roleId") Long roleId) {
129
+        AjaxResult result = sysAppService.roleAppSelect(roleId);
130
+        return result;
131
+    }
132
+}

+ 68 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysCheckAnalysisReportController.java

@@ -0,0 +1,68 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import cn.hutool.core.util.StrUtil;
4
+import com.sundot.airport.check.service.ICheckLargeScreenService;
5
+import com.sundot.airport.common.core.controller.BaseController;
6
+import com.sundot.airport.common.core.domain.AjaxResult;
7
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
8
+import com.sundot.airport.common.core.domain.SysAnalysisReportDateRangeDto;
9
+import com.sundot.airport.common.core.domain.SysAnalysisReportParamDto;
10
+import com.sundot.airport.common.core.domain.SysCheckAnalysisReportCheckProblemDistributionDto;
11
+import com.sundot.airport.common.core.domain.SysCheckAnalysisReportDto;
12
+import com.sundot.airport.common.enums.DateRangeQueryTypeEnum;
13
+import com.sundot.airport.common.utils.DateRangeQueryUtils;
14
+import org.springframework.beans.factory.annotation.Autowired;
15
+import org.springframework.web.bind.annotation.GetMapping;
16
+import org.springframework.web.bind.annotation.PostMapping;
17
+import org.springframework.web.bind.annotation.RequestBody;
18
+import org.springframework.web.bind.annotation.RequestMapping;
19
+import org.springframework.web.bind.annotation.RestController;
20
+
21
+import java.util.ArrayList;
22
+import java.util.List;
23
+
24
+/**
25
+ * 质控分析报告
26
+ *
27
+ * @author ruoyi
28
+ */
29
+@RestController
30
+@RequestMapping("/system/analysisReport")
31
+public class SysCheckAnalysisReportController extends BaseController {
32
+
33
+    @Autowired
34
+    private ICheckLargeScreenService checkLargeScreenService;
35
+
36
+    /**
37
+     * 质控活动
38
+     */
39
+    @PostMapping("/check")
40
+    public AjaxResult checkAnalysisReport(@RequestBody SysAnalysisReportParamDto paramDto) {
41
+        List<SysAnalysisReportDateRangeDto> dateRangeList = new ArrayList<>();
42
+        if (StrUtil.equals(DateRangeQueryTypeEnum.YEAR.getCode(), paramDto.getDateRangeQueryType())) {
43
+            dateRangeList.add(DateRangeQueryUtils.getPreviousYearRange(paramDto.getYear()));
44
+            dateRangeList.add(DateRangeQueryUtils.getYearRange(paramDto.getYear(), null));
45
+        } else if (StrUtil.equals(DateRangeQueryTypeEnum.QUARTER.getCode(), paramDto.getDateRangeQueryType())) {
46
+            dateRangeList.add(DateRangeQueryUtils.getLastYearSameQuarterRange(paramDto.getYear(), paramDto.getQuarter()));
47
+            dateRangeList.add(DateRangeQueryUtils.getPreviousQuarterRange(paramDto.getYear(), paramDto.getQuarter()));
48
+            dateRangeList.add(DateRangeQueryUtils.getQuarterRange(paramDto.getYear(), paramDto.getQuarter(), null));
49
+        } else if (StrUtil.equals(DateRangeQueryTypeEnum.MONTH.getCode(), paramDto.getDateRangeQueryType())) {
50
+            dateRangeList.add(DateRangeQueryUtils.getLastYearSameMonthRange(paramDto.getYear(), paramDto.getMonth()));
51
+            dateRangeList.add(DateRangeQueryUtils.getPreviousMonthRange(paramDto.getYear(), paramDto.getMonth()));
52
+            dateRangeList.add(DateRangeQueryUtils.getMonthRange(paramDto.getYear(), paramDto.getMonth(), null));
53
+        }
54
+        paramDto.setDateRangeList(dateRangeList);
55
+        SysCheckAnalysisReportDto result = checkLargeScreenService.checkAnalysisReport(paramDto);
56
+        return success(result);
57
+    }
58
+
59
+    /**
60
+     * 质控活动-问题分布统计
61
+     */
62
+    @GetMapping("/checkProblemDistribution")
63
+    public AjaxResult checkAnalysisReportCheckProblemDistributionDto(BaseLargeScreenQueryParamDto paramDto) {
64
+        SysCheckAnalysisReportCheckProblemDistributionDto result = checkLargeScreenService.getCheckProblemDistribution(paramDto);
65
+        return success(result);
66
+    }
67
+
68
+}

+ 133 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysConfigController.java

@@ -0,0 +1,133 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+import org.springframework.beans.factory.annotation.Autowired;
6
+import org.springframework.security.access.prepost.PreAuthorize;
7
+import org.springframework.validation.annotation.Validated;
8
+import org.springframework.web.bind.annotation.DeleteMapping;
9
+import org.springframework.web.bind.annotation.GetMapping;
10
+import org.springframework.web.bind.annotation.PathVariable;
11
+import org.springframework.web.bind.annotation.PostMapping;
12
+import org.springframework.web.bind.annotation.PutMapping;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.RestController;
16
+import com.sundot.airport.common.annotation.Log;
17
+import com.sundot.airport.common.core.controller.BaseController;
18
+import com.sundot.airport.common.core.domain.AjaxResult;
19
+import com.sundot.airport.common.core.page.TableDataInfo;
20
+import com.sundot.airport.common.enums.BusinessType;
21
+import com.sundot.airport.common.utils.poi.ExcelUtil;
22
+import com.sundot.airport.system.domain.SysConfig;
23
+import com.sundot.airport.system.service.ISysConfigService;
24
+
25
+/**
26
+ * 参数配置 信息操作处理
27
+ * 
28
+ * @author ruoyi
29
+ */
30
+@RestController
31
+@RequestMapping("/system/config")
32
+public class SysConfigController extends BaseController
33
+{
34
+    @Autowired
35
+    private ISysConfigService configService;
36
+
37
+    /**
38
+     * 获取参数配置列表
39
+     */
40
+    @PreAuthorize("@ss.hasPermi('system:config:list')")
41
+    @GetMapping("/list")
42
+    public TableDataInfo list(SysConfig config)
43
+    {
44
+        startPage();
45
+        List<SysConfig> list = configService.selectConfigList(config);
46
+        return getDataTable(list);
47
+    }
48
+
49
+    @Log(title = "参数管理", businessType = BusinessType.EXPORT)
50
+    @PreAuthorize("@ss.hasPermi('system:config:export')")
51
+    @PostMapping("/export")
52
+    public void export(HttpServletResponse response, SysConfig config)
53
+    {
54
+        List<SysConfig> list = configService.selectConfigList(config);
55
+        ExcelUtil<SysConfig> util = new ExcelUtil<SysConfig>(SysConfig.class);
56
+        util.exportExcel(response, list, "参数数据");
57
+    }
58
+
59
+    /**
60
+     * 根据参数编号获取详细信息
61
+     */
62
+    @PreAuthorize("@ss.hasPermi('system:config:query')")
63
+    @GetMapping(value = "/{configId}")
64
+    public AjaxResult getInfo(@PathVariable Long configId)
65
+    {
66
+        return success(configService.selectConfigById(configId));
67
+    }
68
+
69
+    /**
70
+     * 根据参数键名查询参数值
71
+     */
72
+    @GetMapping(value = "/configKey/{configKey}")
73
+    public AjaxResult getConfigKey(@PathVariable String configKey)
74
+    {
75
+        return success(configService.selectConfigByKey(configKey));
76
+    }
77
+
78
+    /**
79
+     * 新增参数配置
80
+     */
81
+    @PreAuthorize("@ss.hasPermi('system:config:add')")
82
+    @Log(title = "参数管理", businessType = BusinessType.INSERT)
83
+    @PostMapping
84
+    public AjaxResult add(@Validated @RequestBody SysConfig config)
85
+    {
86
+        if (!configService.checkConfigKeyUnique(config))
87
+        {
88
+            return error("新增参数'" + config.getConfigName() + "'失败,参数键名已存在");
89
+        }
90
+        config.setCreateBy(getUsername());
91
+        return toAjax(configService.insertConfig(config));
92
+    }
93
+
94
+    /**
95
+     * 修改参数配置
96
+     */
97
+    @PreAuthorize("@ss.hasPermi('system:config:edit')")
98
+    @Log(title = "参数管理", businessType = BusinessType.UPDATE)
99
+    @PutMapping
100
+    public AjaxResult edit(@Validated @RequestBody SysConfig config)
101
+    {
102
+        if (!configService.checkConfigKeyUnique(config))
103
+        {
104
+            return error("修改参数'" + config.getConfigName() + "'失败,参数键名已存在");
105
+        }
106
+        config.setUpdateBy(getUsername());
107
+        return toAjax(configService.updateConfig(config));
108
+    }
109
+
110
+    /**
111
+     * 删除参数配置
112
+     */
113
+    @PreAuthorize("@ss.hasPermi('system:config:remove')")
114
+    @Log(title = "参数管理", businessType = BusinessType.DELETE)
115
+    @DeleteMapping("/{configIds}")
116
+    public AjaxResult remove(@PathVariable Long[] configIds)
117
+    {
118
+        configService.deleteConfigByIds(configIds);
119
+        return success();
120
+    }
121
+
122
+    /**
123
+     * 刷新参数缓存
124
+     */
125
+    @PreAuthorize("@ss.hasPermi('system:config:remove')")
126
+    @Log(title = "参数管理", businessType = BusinessType.CLEAN)
127
+    @DeleteMapping("/refreshCache")
128
+    public AjaxResult refreshCache()
129
+    {
130
+        configService.resetConfigCache();
131
+        return success();
132
+    }
133
+}

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

@@ -0,0 +1,162 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import cn.hutool.core.collection.CollectionUtil;
4
+import cn.hutool.core.util.StrUtil;
5
+import com.sundot.airport.common.annotation.Log;
6
+import com.sundot.airport.common.constant.UserConstants;
7
+import com.sundot.airport.common.core.controller.BaseController;
8
+import com.sundot.airport.common.core.domain.AjaxResult;
9
+import com.sundot.airport.common.core.domain.entity.SysDept;
10
+import com.sundot.airport.common.enums.BusinessType;
11
+import com.sundot.airport.common.enums.DeptTypeEnum;
12
+import com.sundot.airport.common.utils.StringUtils;
13
+import com.sundot.airport.system.service.ISysDeptService;
14
+import org.apache.commons.lang3.ArrayUtils;
15
+import org.springframework.beans.factory.annotation.Autowired;
16
+import org.springframework.security.access.prepost.PreAuthorize;
17
+import org.springframework.validation.annotation.Validated;
18
+import org.springframework.web.bind.annotation.*;
19
+
20
+import java.util.*;
21
+import java.util.stream.Collectors;
22
+
23
+/**
24
+ * 部门信息
25
+ *
26
+ * @author ruoyi
27
+ */
28
+@RestController
29
+@RequestMapping("/system/dept")
30
+public class SysDeptController extends BaseController {
31
+    @Autowired
32
+    private ISysDeptService deptService;
33
+
34
+    /**
35
+     * 获取部门列表
36
+     */
37
+    @PreAuthorize("@ss.hasPermi('system:dept:list')")
38
+    @GetMapping("/list")
39
+    public AjaxResult list(SysDept dept) {
40
+        List<SysDept> depts = deptService.selectDeptList(dept);
41
+        return success(depts);
42
+    }
43
+
44
+    /**
45
+     * 查询部门列表(排除节点)
46
+     */
47
+    @PreAuthorize("@ss.hasPermi('system:dept:list')")
48
+    @GetMapping("/list/exclude/{deptId}")
49
+    public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId) {
50
+        List<SysDept> depts = deptService.selectDeptList(new SysDept());
51
+        depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + ""));
52
+        return success(depts);
53
+    }
54
+
55
+    /**
56
+     * 根据部门编号获取详细信息
57
+     */
58
+    @PreAuthorize("@ss.hasPermi('system:dept:query')")
59
+    @GetMapping(value = "/{deptId}")
60
+    public AjaxResult getInfo(@PathVariable Long deptId) {
61
+        deptService.checkDeptDataScope(deptId);
62
+        return success(deptService.selectDeptById(deptId));
63
+    }
64
+
65
+    /**
66
+     * 新增部门
67
+     */
68
+    @PreAuthorize("@ss.hasPermi('system:dept:add')")
69
+    @Log(title = "部门管理", businessType = BusinessType.INSERT)
70
+    @PostMapping
71
+    public AjaxResult add(@Validated @RequestBody SysDept dept) {
72
+        if (!deptService.checkDeptNameUnique(dept)) {
73
+            return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
74
+        }
75
+        dept.setCreateBy(getUsername());
76
+        return toAjax(deptService.insertDept(dept));
77
+    }
78
+
79
+    /**
80
+     * 修改部门
81
+     */
82
+    @PreAuthorize("@ss.hasPermi('system:dept:edit')")
83
+    @Log(title = "部门管理", businessType = BusinessType.UPDATE)
84
+    @PutMapping
85
+    public AjaxResult edit(@Validated @RequestBody SysDept dept) {
86
+        Long deptId = dept.getDeptId();
87
+        deptService.checkDeptDataScope(deptId);
88
+        if (!deptService.checkDeptNameUnique(dept)) {
89
+            return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
90
+        } else if (dept.getParentId().equals(deptId)) {
91
+            return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
92
+        } else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0) {
93
+            return error("该部门包含未停用的子部门!");
94
+        }
95
+        dept.setUpdateBy(getUsername());
96
+        return toAjax(deptService.updateDept(dept));
97
+    }
98
+
99
+    /**
100
+     * 删除部门
101
+     */
102
+    @PreAuthorize("@ss.hasPermi('system:dept:remove')")
103
+    @Log(title = "部门管理", businessType = BusinessType.DELETE)
104
+    @DeleteMapping("/{deptId}")
105
+    public AjaxResult remove(@PathVariable Long deptId) {
106
+        if (deptService.hasChildByDeptId(deptId)) {
107
+            return warn("存在下级部门,不允许删除");
108
+        }
109
+        if (deptService.checkDeptExistUser(deptId)) {
110
+            return warn("部门存在用户,不允许删除");
111
+        }
112
+        deptService.checkDeptDataScope(deptId);
113
+        return toAjax(deptService.deleteDeptById(deptId));
114
+    }
115
+
116
+    @PostMapping("/teamList")
117
+    public AjaxResult teamList(@RequestBody SysDept dept) {
118
+        return success(buildTree(deptService.selectDeptInfoAll(dept)));
119
+    }
120
+
121
+    private Map<String, String> buildTree(List<SysDept> list) {
122
+        Map<String, String> result = new HashMap<>();
123
+        // 从班组开始往上推,最多3级
124
+        List<SysDept> teams = list.stream().filter(x -> StrUtil.equals(x.getDeptType(), DeptTypeEnum.TEAMS.getCode())).collect(Collectors.toList());
125
+        if (CollectionUtil.isEmpty(teams)) {
126
+            return result;
127
+        }
128
+        teams.forEach(team -> {
129
+            String val = team.getDeptId().toString();
130
+            StringBuffer label = new StringBuffer(team.getDeptName());
131
+            if (StrUtil.isNotBlank(team.getAncestors())) {
132
+                String[] splits = team.getAncestors().split(",");
133
+                // splits 去掉 第一个元素,并且反转
134
+                List<String> reverse = Arrays.stream(splits).skip(1).collect(Collectors.toList());
135
+                // reverse 元素反转
136
+                Collections.reverse(reverse);
137
+                reverse.forEach(parent -> {
138
+                    SysDept department = list.stream().filter(x -> StrUtil.equals(parent, x.getDeptId().toString())).findFirst().orElse(new SysDept());
139
+                    label.insert(0, department.getDeptName() + "/");
140
+                });
141
+            }
142
+            result.put(val, label.toString());
143
+        });
144
+        return result;
145
+    }
146
+
147
+    /**
148
+     * 查询部门负责人
149
+     */
150
+    @GetMapping(value = "/deptLeader/{deptId}")
151
+    public AjaxResult deptLeader(@PathVariable Long deptId) {
152
+        return success(deptService.deptLeader(deptId));
153
+    }
154
+
155
+    /**
156
+     * 查询部门指定角色人员
157
+     */
158
+    @PostMapping(value = "/deptRole/{deptId}")
159
+    public AjaxResult deptRole(@PathVariable Long deptId, @RequestBody List<String> roleKeyList) {
160
+        return success(deptService.deptRole(deptId, roleKeyList));
161
+    }
162
+}

+ 121 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysDictDataController.java

@@ -0,0 +1,121 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.ArrayList;
4
+import java.util.List;
5
+import javax.servlet.http.HttpServletResponse;
6
+import org.springframework.beans.factory.annotation.Autowired;
7
+import org.springframework.security.access.prepost.PreAuthorize;
8
+import org.springframework.validation.annotation.Validated;
9
+import org.springframework.web.bind.annotation.DeleteMapping;
10
+import org.springframework.web.bind.annotation.GetMapping;
11
+import org.springframework.web.bind.annotation.PathVariable;
12
+import org.springframework.web.bind.annotation.PostMapping;
13
+import org.springframework.web.bind.annotation.PutMapping;
14
+import org.springframework.web.bind.annotation.RequestBody;
15
+import org.springframework.web.bind.annotation.RequestMapping;
16
+import org.springframework.web.bind.annotation.RestController;
17
+import com.sundot.airport.common.annotation.Log;
18
+import com.sundot.airport.common.core.controller.BaseController;
19
+import com.sundot.airport.common.core.domain.AjaxResult;
20
+import com.sundot.airport.common.core.domain.entity.SysDictData;
21
+import com.sundot.airport.common.core.page.TableDataInfo;
22
+import com.sundot.airport.common.enums.BusinessType;
23
+import com.sundot.airport.common.utils.StringUtils;
24
+import com.sundot.airport.common.utils.poi.ExcelUtil;
25
+import com.sundot.airport.system.service.ISysDictDataService;
26
+import com.sundot.airport.system.service.ISysDictTypeService;
27
+
28
+/**
29
+ * 数据字典信息
30
+ * 
31
+ * @author ruoyi
32
+ */
33
+@RestController
34
+@RequestMapping("/system/dict/data")
35
+public class SysDictDataController extends BaseController
36
+{
37
+    @Autowired
38
+    private ISysDictDataService dictDataService;
39
+
40
+    @Autowired
41
+    private ISysDictTypeService dictTypeService;
42
+
43
+    @PreAuthorize("@ss.hasPermi('system:dict:list')")
44
+    @GetMapping("/list")
45
+    public TableDataInfo list(SysDictData dictData)
46
+    {
47
+        startPage();
48
+        List<SysDictData> list = dictDataService.selectDictDataList(dictData);
49
+        return getDataTable(list);
50
+    }
51
+
52
+    @Log(title = "字典数据", businessType = BusinessType.EXPORT)
53
+    @PreAuthorize("@ss.hasPermi('system:dict:export')")
54
+    @PostMapping("/export")
55
+    public void export(HttpServletResponse response, SysDictData dictData)
56
+    {
57
+        List<SysDictData> list = dictDataService.selectDictDataList(dictData);
58
+        ExcelUtil<SysDictData> util = new ExcelUtil<SysDictData>(SysDictData.class);
59
+        util.exportExcel(response, list, "字典数据");
60
+    }
61
+
62
+    /**
63
+     * 查询字典数据详细
64
+     */
65
+    @PreAuthorize("@ss.hasPermi('system:dict:query')")
66
+    @GetMapping(value = "/{dictCode}")
67
+    public AjaxResult getInfo(@PathVariable Long dictCode)
68
+    {
69
+        return success(dictDataService.selectDictDataById(dictCode));
70
+    }
71
+
72
+    /**
73
+     * 根据字典类型查询字典数据信息
74
+     */
75
+    @GetMapping(value = "/type/{dictType}")
76
+    public AjaxResult dictType(@PathVariable String dictType)
77
+    {
78
+        List<SysDictData> data = dictTypeService.selectDictDataByType(dictType);
79
+        if (StringUtils.isNull(data))
80
+        {
81
+            data = new ArrayList<SysDictData>();
82
+        }
83
+        return success(data);
84
+    }
85
+
86
+    /**
87
+     * 新增字典类型
88
+     */
89
+    @PreAuthorize("@ss.hasPermi('system:dict:add')")
90
+    @Log(title = "字典数据", businessType = BusinessType.INSERT)
91
+    @PostMapping
92
+    public AjaxResult add(@Validated @RequestBody SysDictData dict)
93
+    {
94
+        dict.setCreateBy(getUsername());
95
+        return toAjax(dictDataService.insertDictData(dict));
96
+    }
97
+
98
+    /**
99
+     * 修改保存字典类型
100
+     */
101
+    @PreAuthorize("@ss.hasPermi('system:dict:edit')")
102
+    @Log(title = "字典数据", businessType = BusinessType.UPDATE)
103
+    @PutMapping
104
+    public AjaxResult edit(@Validated @RequestBody SysDictData dict)
105
+    {
106
+        dict.setUpdateBy(getUsername());
107
+        return toAjax(dictDataService.updateDictData(dict));
108
+    }
109
+
110
+    /**
111
+     * 删除字典类型
112
+     */
113
+    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
114
+    @Log(title = "字典类型", businessType = BusinessType.DELETE)
115
+    @DeleteMapping("/{dictCodes}")
116
+    public AjaxResult remove(@PathVariable Long[] dictCodes)
117
+    {
118
+        dictDataService.deleteDictDataByIds(dictCodes);
119
+        return success();
120
+    }
121
+}

+ 121 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysDictTypeController.java

@@ -0,0 +1,121 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.beans.factory.annotation.Autowired;
7
+import org.springframework.security.access.prepost.PreAuthorize;
8
+import org.springframework.validation.annotation.Validated;
9
+import org.springframework.web.bind.annotation.DeleteMapping;
10
+import org.springframework.web.bind.annotation.GetMapping;
11
+import org.springframework.web.bind.annotation.PathVariable;
12
+import org.springframework.web.bind.annotation.PostMapping;
13
+import org.springframework.web.bind.annotation.PutMapping;
14
+import org.springframework.web.bind.annotation.RequestBody;
15
+import org.springframework.web.bind.annotation.RequestMapping;
16
+import org.springframework.web.bind.annotation.RestController;
17
+import com.sundot.airport.common.annotation.Log;
18
+import com.sundot.airport.common.core.controller.BaseController;
19
+import com.sundot.airport.common.core.domain.AjaxResult;
20
+import com.sundot.airport.common.core.domain.entity.SysDictType;
21
+import com.sundot.airport.common.core.page.TableDataInfo;
22
+import com.sundot.airport.common.enums.BusinessType;
23
+import com.sundot.airport.common.utils.poi.ExcelUtil;
24
+import com.sundot.airport.system.service.ISysDictTypeService;
25
+
26
+/**
27
+ * 数据字典信息
28
+ *
29
+ * @author ruoyi
30
+ */
31
+@RestController
32
+@RequestMapping("/system/dict/type")
33
+public class SysDictTypeController extends BaseController {
34
+    @Autowired
35
+    private ISysDictTypeService dictTypeService;
36
+
37
+    @PreAuthorize("@ss.hasPermi('system:dict:list')")
38
+    @GetMapping("/list")
39
+    public TableDataInfo list(SysDictType dictType) {
40
+        startPage();
41
+        List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
42
+        return getDataTable(list);
43
+    }
44
+
45
+    @Log(title = "字典类型", businessType = BusinessType.EXPORT)
46
+    @PreAuthorize("@ss.hasPermi('system:dict:export')")
47
+    @PostMapping("/export")
48
+    public void export(HttpServletResponse response, SysDictType dictType) {
49
+        List<SysDictType> list = dictTypeService.selectDictTypeList(dictType);
50
+        ExcelUtil<SysDictType> util = new ExcelUtil<SysDictType>(SysDictType.class);
51
+        util.exportExcel(response, list, "字典类型");
52
+    }
53
+
54
+    /**
55
+     * 查询字典类型详细
56
+     */
57
+    @PreAuthorize("@ss.hasPermi('system:dict:query')")
58
+    @GetMapping(value = "/{dictId}")
59
+    public AjaxResult getInfo(@PathVariable Long dictId) {
60
+        return success(dictTypeService.selectDictTypeById(dictId));
61
+    }
62
+
63
+    /**
64
+     * 新增字典类型
65
+     */
66
+    @PreAuthorize("@ss.hasPermi('system:dict:add')")
67
+    @Log(title = "字典类型", businessType = BusinessType.INSERT)
68
+    @PostMapping
69
+    public AjaxResult add(@Validated @RequestBody SysDictType dict) {
70
+        if (!dictTypeService.checkDictTypeUnique(dict)) {
71
+            return error("新增字典'" + dict.getDictName() + "'失败,字典类型已存在");
72
+        }
73
+        dict.setCreateBy(getUsername());
74
+        return toAjax(dictTypeService.insertDictType(dict));
75
+    }
76
+
77
+    /**
78
+     * 修改字典类型
79
+     */
80
+    @PreAuthorize("@ss.hasPermi('system:dict:edit')")
81
+    @Log(title = "字典类型", businessType = BusinessType.UPDATE)
82
+    @PutMapping
83
+    public AjaxResult edit(@Validated @RequestBody SysDictType dict) {
84
+        if (!dictTypeService.checkDictTypeUnique(dict)) {
85
+            return error("修改字典'" + dict.getDictName() + "'失败,字典类型已存在");
86
+        }
87
+        dict.setUpdateBy(getUsername());
88
+        return toAjax(dictTypeService.updateDictType(dict));
89
+    }
90
+
91
+    /**
92
+     * 删除字典类型
93
+     */
94
+    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
95
+    @Log(title = "字典类型", businessType = BusinessType.DELETE)
96
+    @DeleteMapping("/{dictIds}")
97
+    public AjaxResult remove(@PathVariable Long[] dictIds) {
98
+        dictTypeService.deleteDictTypeByIds(dictIds);
99
+        return success();
100
+    }
101
+
102
+    /**
103
+     * 刷新字典缓存
104
+     */
105
+    @PreAuthorize("@ss.hasPermi('system:dict:remove')")
106
+    @Log(title = "字典类型", businessType = BusinessType.CLEAN)
107
+    @DeleteMapping("/refreshCache")
108
+    public AjaxResult refreshCache() {
109
+        dictTypeService.resetDictCache();
110
+        return success();
111
+    }
112
+
113
+    /**
114
+     * 获取字典选择框列表
115
+     */
116
+    @GetMapping("/optionselect")
117
+    public AjaxResult optionselect() {
118
+        List<SysDictType> dictTypes = dictTypeService.selectDictTypeAll();
119
+        return success(dictTypes);
120
+    }
121
+}

+ 93 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysDynamicSqlController.java

@@ -0,0 +1,93 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import com.sundot.airport.common.annotation.Log;
4
+import com.sundot.airport.common.core.controller.BaseController;
5
+import com.sundot.airport.common.core.domain.AjaxResult;
6
+import com.sundot.airport.common.core.page.TableDataInfo;
7
+import com.sundot.airport.common.enums.BusinessType;
8
+import com.sundot.airport.common.utils.poi.ExcelUtil;
9
+import com.sundot.airport.system.domain.SysDynamicSql;
10
+import com.sundot.airport.system.service.ISysDynamicSqlService;
11
+import org.springframework.beans.factory.annotation.Autowired;
12
+import org.springframework.web.bind.annotation.*;
13
+
14
+import javax.servlet.http.HttpServletResponse;
15
+import java.util.List;
16
+import java.util.Map;
17
+
18
+/**
19
+ * 动态sql执行configController
20
+ *
21
+ * @author ruoyi
22
+ * @date 2025-06-20
23
+ */
24
+@RestController
25
+@RequestMapping("/system/sql")
26
+public class SysDynamicSqlController extends BaseController {
27
+    @Autowired
28
+    private ISysDynamicSqlService sysDynamicSqlService;
29
+
30
+    /**
31
+     * 查询动态sql执行config列表
32
+     */
33
+    @GetMapping("/list")
34
+    public TableDataInfo list(SysDynamicSql sysDynamicSql) {
35
+        startPage();
36
+        List<SysDynamicSql> list = sysDynamicSqlService.selectSysDynamicSqlList(sysDynamicSql);
37
+        return getDataTable(list);
38
+    }
39
+
40
+    /**
41
+     * 导出动态sql执行config列表
42
+     */
43
+    @Log(title = "动态sql执行config", businessType = BusinessType.EXPORT)
44
+    @PostMapping("/export")
45
+    public void export(HttpServletResponse response, SysDynamicSql sysDynamicSql) {
46
+        List<SysDynamicSql> list = sysDynamicSqlService.selectSysDynamicSqlList(sysDynamicSql);
47
+        ExcelUtil<SysDynamicSql> util = new ExcelUtil<SysDynamicSql>(SysDynamicSql.class);
48
+        util.exportExcel(response, list, "动态sql执行config数据");
49
+    }
50
+
51
+    /**
52
+     * 获取动态sql执行config详细信息
53
+     */
54
+    @GetMapping(value = "/{id}")
55
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
56
+        return success(sysDynamicSqlService.selectSysDynamicSqlById(id));
57
+    }
58
+
59
+    /**
60
+     * 新增动态sql执行config
61
+     */
62
+    @Log(title = "动态sql执行config", businessType = BusinessType.INSERT)
63
+    @PostMapping
64
+    public AjaxResult add(@RequestBody SysDynamicSql sysDynamicSql) {
65
+        return toAjax(sysDynamicSqlService.insertSysDynamicSql(sysDynamicSql));
66
+    }
67
+
68
+    /**
69
+     * 修改动态sql执行config
70
+     */
71
+    @Log(title = "动态sql执行config", businessType = BusinessType.UPDATE)
72
+    @PutMapping
73
+    public AjaxResult edit(@RequestBody SysDynamicSql sysDynamicSql) {
74
+        return toAjax(sysDynamicSqlService.updateSysDynamicSql(sysDynamicSql));
75
+    }
76
+
77
+    /**
78
+     * 删除动态sql执行config
79
+     */
80
+    @Log(title = "动态sql执行config", businessType = BusinessType.DELETE)
81
+    @DeleteMapping("/{ids}")
82
+    public AjaxResult remove(@PathVariable Long[] ids) {
83
+        return toAjax(sysDynamicSqlService.deleteSysDynamicSqlByIds(ids));
84
+    }
85
+
86
+    /**
87
+     * 删除动态sql执行config
88
+     */
89
+    @PostMapping("/executeSqlByKeyAndParam/{sqlKey}")
90
+    public AjaxResult executeSqlByKeyAndParam(@PathVariable(name = "sqlKey") String sqlkey, @RequestBody Map<String, Object> params) {
91
+        return success(sysDynamicSqlService.executeSqlByKeyAndParam(sqlkey, params));
92
+    }
93
+}

+ 563 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysHomePageController.java

@@ -0,0 +1,563 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import cn.hutool.core.collection.CollUtil;
4
+import cn.hutool.core.util.ObjectUtil;
5
+import cn.hutool.core.util.StrUtil;
6
+import com.sundot.airport.attendance.service.IAttendancePostRecordService;
7
+import com.sundot.airport.check.service.ICheckLargeScreenService;
8
+import com.sundot.airport.common.core.controller.BaseController;
9
+import com.sundot.airport.common.core.domain.AjaxResult;
10
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
11
+import com.sundot.airport.common.core.domain.DataPermissionResult;
12
+import com.sundot.airport.common.core.domain.SysHomePageDetailDto;
13
+import com.sundot.airport.common.core.domain.SysHomePageDetailQueryParamDto;
14
+import com.sundot.airport.common.core.domain.SysHomePageRankingDto;
15
+import com.sundot.airport.common.core.domain.entity.SysDept;
16
+import com.sundot.airport.common.core.domain.entity.SysUser;
17
+import com.sundot.airport.common.enums.DataPermissionType;
18
+import com.sundot.airport.common.enums.DeptType;
19
+import com.sundot.airport.common.enums.DeptTypeEnum;
20
+import com.sundot.airport.common.enums.HomePageQueryEnum;
21
+import com.sundot.airport.common.enums.RoleTypeEnum;
22
+import com.sundot.airport.common.enums.SourceTypeEnum;
23
+import com.sundot.airport.common.exception.ServiceException;
24
+import com.sundot.airport.common.utils.DateUtils;
25
+import com.sundot.airport.exam.service.IAccuracyStatisticsService;
26
+import com.sundot.airport.item.mapper.SeizureReportMapper;
27
+import com.sundot.airport.system.service.ISysDeptService;
28
+import com.sundot.airport.system.service.ISysLearningGrowthService;
29
+import com.sundot.airport.system.service.ISysUserService;
30
+import com.sundot.airport.web.core.utils.DataPermissionUtils;
31
+import org.springframework.beans.factory.annotation.Autowired;
32
+import org.springframework.web.bind.annotation.GetMapping;
33
+import org.springframework.web.bind.annotation.PostMapping;
34
+import org.springframework.web.bind.annotation.RequestBody;
35
+import org.springframework.web.bind.annotation.RequestHeader;
36
+import org.springframework.web.bind.annotation.RequestMapping;
37
+import org.springframework.web.bind.annotation.RestController;
38
+
39
+import java.math.BigDecimal;
40
+import java.math.RoundingMode;
41
+import java.util.ArrayList;
42
+import java.util.Arrays;
43
+import java.util.Calendar;
44
+import java.util.Collections;
45
+import java.util.List;
46
+import java.util.stream.Collectors;
47
+
48
+/**
49
+ * 首页
50
+ *
51
+ * @author ruoyi
52
+ */
53
+@RestController
54
+@RequestMapping("/system/homePage")
55
+public class SysHomePageController extends BaseController {
56
+
57
+    @Autowired
58
+    private ISysUserService sysUserService;
59
+
60
+    @Autowired
61
+    private ISysDeptService deptService;
62
+
63
+    @Autowired
64
+    private ICheckLargeScreenService checkLargeScreenService;
65
+
66
+    @Autowired
67
+    private ISysLearningGrowthService sysLearningGrowthService;
68
+
69
+    @Autowired
70
+    private SeizureReportMapper seizureReportMapper;
71
+
72
+    @Autowired
73
+    private IAttendancePostRecordService attendancePostRecordService;
74
+
75
+    @Autowired
76
+    private IAccuracyStatisticsService accuracyStatisticsService;
77
+
78
+    /**
79
+     * 首页-排名
80
+     */
81
+    @GetMapping("/homePageRanking")
82
+    public AjaxResult homePageRanking(BaseLargeScreenQueryParamDto dto) {
83
+        SysHomePageRankingDto result = new SysHomePageRankingDto();
84
+        result.setCheckLargeScreenHomePageRankingDto(checkLargeScreenService.checkRanking(dto));
85
+        result.setDailyTaskAccuracyRankingDto(accuracyStatisticsService.getAccuracyRanking(dto));
86
+        return success(result);
87
+    }
88
+
89
+    /**
90
+     * 首页-整体
91
+     */
92
+    @GetMapping("/homePageWhole")
93
+    public AjaxResult homePageWhole(BaseLargeScreenQueryParamDto dto, @RequestHeader(value = "X-Request-Source", defaultValue = "mobile") String source) {
94
+        List<SysHomePageDetailQueryParamDto> dtoList = new ArrayList<>();
95
+        if (StrUtil.equals(SourceTypeEnum.mobile.getCode(), source)) {
96
+            DataPermissionResult dataPermission = DataPermissionUtils.getDataPermission(getUserId(), getDeptId(), getLoginUser());
97
+            if (DataPermissionType.SELF == dataPermission.getPermissionType()) {
98
+                SysHomePageDetailQueryParamDto userDto = new SysHomePageDetailQueryParamDto();
99
+                userDto.setId(getUserId());
100
+                userDto.setName(getLoginUser().getUser().getNickName());
101
+                userDto.setType(HomePageQueryEnum.USER.getCode());
102
+                dtoList.add(userDto);
103
+                List<SysDept> deptList = deptService.selectAllDept(getDeptId());
104
+                SysDept team = deptList.stream().filter(item -> StrUtil.equals(DeptType.TEAMS.getCode(), item.getDeptType())).findFirst().orElse(null);
105
+                SysDept department = deptList.stream().filter(item -> StrUtil.equals(DeptType.MANAGER.getCode(), item.getDeptType())).findFirst().orElse(null);
106
+                SysDept brigade = deptList.stream().filter(item -> StrUtil.equals(DeptType.BRIGADE.getCode(), item.getDeptType())).findFirst().orElse(null);
107
+                SysDept station = deptList.stream().filter(item -> StrUtil.equals(DeptType.STATION.getCode(), item.getDeptType())).findFirst().orElse(null);
108
+                if (ObjectUtil.isNotNull(team)) {
109
+                    SysHomePageDetailQueryParamDto teamDto = new SysHomePageDetailQueryParamDto();
110
+                    teamDto.setId(team.getDeptId());
111
+                    teamDto.setName(team.getDeptName());
112
+                    teamDto.setType(HomePageQueryEnum.DEPT.getCode());
113
+                    dtoList.add(teamDto);
114
+                }
115
+                if (ObjectUtil.isNotNull(department)) {
116
+                    SysHomePageDetailQueryParamDto departmentDto = new SysHomePageDetailQueryParamDto();
117
+                    departmentDto.setId(department.getDeptId());
118
+                    departmentDto.setName(department.getDeptName());
119
+                    departmentDto.setType(HomePageQueryEnum.DEPT.getCode());
120
+                    dtoList.add(departmentDto);
121
+                }
122
+                if (ObjectUtil.isNotNull(brigade)) {
123
+                    SysHomePageDetailQueryParamDto departmentDto = new SysHomePageDetailQueryParamDto();
124
+                    departmentDto.setId(brigade.getDeptId());
125
+                    departmentDto.setName(brigade.getDeptName());
126
+                    departmentDto.setType(HomePageQueryEnum.DEPT.getCode());
127
+                    dtoList.add(departmentDto);
128
+                }
129
+                if (ObjectUtil.isNotNull(station)) {
130
+                    SysHomePageDetailQueryParamDto stationDto = new SysHomePageDetailQueryParamDto();
131
+                    stationDto.setId(station.getDeptId());
132
+                    stationDto.setName(station.getDeptName());
133
+                    stationDto.setType(HomePageQueryEnum.DEPT.getCode());
134
+                    dtoList.add(stationDto);
135
+                }
136
+            } else if (DataPermissionType.TEAM == dataPermission.getPermissionType() || DataPermissionType.DEPARTMENT == dataPermission.getPermissionType()) {
137
+                List<SysDept> deptList = deptService.selectAllDept(getDeptId());
138
+                SysDept team = deptList.stream().filter(item -> StrUtil.equals(DeptType.TEAMS.getCode(), item.getDeptType())).findFirst().orElse(null);
139
+                SysDept department = deptList.stream().filter(item -> StrUtil.equals(DeptType.MANAGER.getCode(), item.getDeptType())).findFirst().orElse(null);
140
+                SysDept brigade = deptList.stream().filter(item -> StrUtil.equals(DeptType.BRIGADE.getCode(), item.getDeptType())).findFirst().orElse(null);
141
+                SysDept station = deptList.stream().filter(item -> StrUtil.equals(DeptType.STATION.getCode(), item.getDeptType())).findFirst().orElse(null);
142
+
143
+                // 支持dataSource参数:individual=个人数据,team=班组数据(默认)
144
+                // 仅对班组长权限生效
145
+                if (DataPermissionType.TEAM == dataPermission.getPermissionType() && "individual".equalsIgnoreCase(dto.getDataSource())) {
146
+                    // 返回班组长个人数据(类似安检员视角)
147
+                    SysHomePageDetailQueryParamDto userDto = new SysHomePageDetailQueryParamDto();
148
+                    userDto.setId(getUserId());
149
+                    userDto.setName(getLoginUser().getUser().getNickName());
150
+                    userDto.setType(HomePageQueryEnum.USER.getCode());
151
+                    dtoList.add(userDto);
152
+                }
153
+
154
+                // 添加班组、主管、大队、站点信息
155
+                if (ObjectUtil.isNotNull(team)) {
156
+                    SysHomePageDetailQueryParamDto teamDto = new SysHomePageDetailQueryParamDto();
157
+                    teamDto.setId(team.getDeptId());
158
+                    teamDto.setName(team.getDeptName());
159
+                    teamDto.setType(HomePageQueryEnum.DEPT.getCode());
160
+                    dtoList.add(teamDto);
161
+                }
162
+                if (ObjectUtil.isNotNull(department)) {
163
+                    SysHomePageDetailQueryParamDto departmentDto = new SysHomePageDetailQueryParamDto();
164
+                    departmentDto.setId(department.getDeptId());
165
+                    departmentDto.setName(department.getDeptName());
166
+                    departmentDto.setType(HomePageQueryEnum.DEPT.getCode());
167
+                    dtoList.add(departmentDto);
168
+                }
169
+                if (ObjectUtil.isNotNull(brigade)) {
170
+                    SysHomePageDetailQueryParamDto departmentDto = new SysHomePageDetailQueryParamDto();
171
+                    departmentDto.setId(brigade.getDeptId());
172
+                    departmentDto.setName(brigade.getDeptName());
173
+                    departmentDto.setType(HomePageQueryEnum.DEPT.getCode());
174
+                    dtoList.add(departmentDto);
175
+                }
176
+                if (ObjectUtil.isNotNull(station)) {
177
+                    SysHomePageDetailQueryParamDto stationDto = new SysHomePageDetailQueryParamDto();
178
+                    stationDto.setId(station.getDeptId());
179
+                    stationDto.setName(station.getDeptName());
180
+                    stationDto.setType(HomePageQueryEnum.DEPT.getCode());
181
+                    dtoList.add(stationDto);
182
+                }
183
+            } else if (DataPermissionType.BRIGADE == dataPermission.getPermissionType()) {
184
+                SysDept sysDept = deptService.selectDeptById(getDeptId());
185
+                SysDept sysDeptStation = deptService.selectDeptById(sysDept.getParentId());
186
+                SysHomePageDetailQueryParamDto stationDto = new SysHomePageDetailQueryParamDto();
187
+                stationDto.setId(sysDeptStation.getDeptId());
188
+                stationDto.setName(sysDeptStation.getDeptName());
189
+                stationDto.setType(HomePageQueryEnum.DEPT.getCode());
190
+                dtoList.add(stationDto);
191
+                SysDept query = new SysDept();
192
+                query.setParentId(sysDeptStation.getDeptId());
193
+                List<SysDept> deptList = deptService.selectDeptInfoAll(query);
194
+                deptList.forEach(item -> {
195
+                    SysHomePageDetailQueryParamDto departmentDto = new SysHomePageDetailQueryParamDto();
196
+                    departmentDto.setId(item.getDeptId());
197
+                    departmentDto.setName(item.getDeptName());
198
+                    departmentDto.setType(HomePageQueryEnum.DEPT.getCode());
199
+                    dtoList.add(departmentDto);
200
+                });
201
+            } else if (DataPermissionType.STATION == dataPermission.getPermissionType()) {
202
+                SysDept sysDept = deptService.selectDeptById(getDeptId());
203
+                SysHomePageDetailQueryParamDto stationDto = new SysHomePageDetailQueryParamDto();
204
+                stationDto.setId(sysDept.getDeptId());
205
+                stationDto.setName(sysDept.getDeptName());
206
+                stationDto.setType(HomePageQueryEnum.DEPT.getCode());
207
+                dtoList.add(stationDto);
208
+                SysDept query = new SysDept();
209
+                query.setParentId(sysDept.getDeptId());
210
+                List<SysDept> deptList = deptService.selectDeptInfoAll(query);
211
+                deptList.forEach(item -> {
212
+                    SysHomePageDetailQueryParamDto departmentDto = new SysHomePageDetailQueryParamDto();
213
+                    departmentDto.setId(item.getDeptId());
214
+                    departmentDto.setName(item.getDeptName());
215
+                    departmentDto.setType(HomePageQueryEnum.DEPT.getCode());
216
+                    dtoList.add(departmentDto);
217
+                });
218
+            } else {
219
+                List<SysDept> deptListAll = deptService.selectDeptInfoAll(new SysDept());
220
+                List<SysDept> SysDeptStation = deptListAll.stream().filter(item -> ObjectUtil.equal(DeptTypeEnum.STATION.getCode(), item.getDeptType())).collect(Collectors.toList());
221
+                SysDeptStation.forEach(item -> {
222
+                    SysDept sysDept = deptService.selectDeptById(item.getDeptId());
223
+                    SysHomePageDetailQueryParamDto stationDto = new SysHomePageDetailQueryParamDto();
224
+                    stationDto.setId(sysDept.getDeptId());
225
+                    stationDto.setName(sysDept.getDeptName());
226
+                    stationDto.setType(HomePageQueryEnum.DEPT.getCode());
227
+                    dtoList.add(stationDto);
228
+                    SysDept query = new SysDept();
229
+                    query.setParentId(sysDept.getDeptId());
230
+                    List<SysDept> deptList = deptService.selectDeptInfoAll(query);
231
+                    deptList.forEach(department -> {
232
+                        SysHomePageDetailQueryParamDto departmentDto = new SysHomePageDetailQueryParamDto();
233
+                        departmentDto.setId(department.getDeptId());
234
+                        departmentDto.setName(department.getDeptName());
235
+                        departmentDto.setType(HomePageQueryEnum.DEPT.getCode());
236
+                        dtoList.add(departmentDto);
237
+                    });
238
+                });
239
+            }
240
+        } else {
241
+            List<SysDept> sysDeptList = deptService.selectAllDept(getDeptId());
242
+            Collections.reverse(sysDeptList);
243
+            SysDept sysDept = sysDeptList.stream().filter(x -> StrUtil.equals(DeptType.STATION.getCode(), x.getDeptType())).findFirst().orElse(null);
244
+            if (ObjectUtil.isNull(sysDept)) {
245
+                throw new ServiceException("未查询到站级部门信息");
246
+            }
247
+            SysHomePageDetailQueryParamDto stationDto = new SysHomePageDetailQueryParamDto();
248
+            stationDto.setId(sysDept.getDeptId());
249
+            stationDto.setName(sysDept.getDeptName());
250
+            stationDto.setType(HomePageQueryEnum.DEPT.getCode());
251
+            dtoList.add(stationDto);
252
+            SysDept query = new SysDept();
253
+            query.setParentId(sysDept.getDeptId());
254
+            List<SysDept> deptList = deptService.selectDeptInfoAll(query);
255
+            deptList.forEach(item -> {
256
+                SysHomePageDetailQueryParamDto departmentDto = new SysHomePageDetailQueryParamDto();
257
+                departmentDto.setId(item.getDeptId());
258
+                departmentDto.setName(item.getDeptName());
259
+                departmentDto.setType(HomePageQueryEnum.DEPT.getCode());
260
+                dtoList.add(departmentDto);
261
+            });
262
+        }
263
+        dtoList.forEach(item -> {
264
+            item.setSpecifiedDate(dto.getSpecifiedDate());
265
+            item.setStartDate(dto.getStartDate());
266
+            item.setEndDate(dto.getEndDate());
267
+        });
268
+        List<SysHomePageDetailDto> result = getSysHomePageDetailDtoList(dtoList);
269
+        return success(result);
270
+    }
271
+
272
+    /**
273
+     * 首页-明细(能力对比)
274
+     * 支持班组长角色使用dataSource参数:
275
+     * - 如果列表中包含班组数据,且dataSource=individual,则将班组数据替换为班组长个人数据
276
+     */
277
+    @PostMapping("/homePageDetail")
278
+    public AjaxResult homePageDetail(@RequestBody List<SysHomePageDetailQueryParamDto> dtoList) {
279
+        List<SysHomePageDetailDto> result = getSysHomePageDetailDtoList(dtoList);
280
+        return success(result);
281
+    }
282
+
283
+    /**
284
+     * 获取首页-明细
285
+     */
286
+    private List<SysHomePageDetailDto> getSysHomePageDetailDtoList(List<SysHomePageDetailQueryParamDto> dtoList) {
287
+        List<SysHomePageDetailDto> result = new ArrayList<>();
288
+        // 批量调用前统一初始化部门缓存,避免每次 getAnswersAccuracy 都重复查询数据库
289
+        accuracyStatisticsService.initDeptCache();
290
+        try {
291
+            for (SysHomePageDetailQueryParamDto item : dtoList) {
292
+                SysHomePageDetailDto resultItem = new SysHomePageDetailDto();
293
+                resultItem.setId(item.getId());
294
+                resultItem.setName(item.getName());
295
+                resultItem.setType(item.getType());
296
+                resultItem.setLearningGrowthScore(getLearningGrowthScore(item));
297
+                resultItem.setLearningGrowthScoreGraph(resultItem.getLearningGrowthScore().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP));
298
+                resultItem.setCheckPassRate(getCheckPassRate(item));
299
+                resultItem.setCheckPassRateGraph(resultItem.getCheckPassRate());
300
+                resultItem.setSeizureCount(getSeizureCount(item));
301
+                resultItem.setSeizureCountGraph(getSeizureCountGraph(item));
302
+                resultItem.setWorkingHours(getWorkingHours(item));
303
+                resultItem.setWorkingHoursGraph(getWorkingHoursGraph(item));
304
+                // 抽问抽答正确率
305
+                resultItem.setAnswersAccuracy(getAnswersAccuracy(item));
306
+                resultItem.setAnswersAccuracyGraph(resultItem.getAnswersAccuracy());
307
+                result.add(resultItem);
308
+            }
309
+        } finally {
310
+            accuracyStatisticsService.clearDeptCache();
311
+        }
312
+        return result;
313
+    }
314
+
315
+    /**
316
+     * 获取抽问抽答正确率
317
+     */
318
+    private BigDecimal getAnswersAccuracy(SysHomePageDetailQueryParamDto item) {
319
+        Long userId = null;
320
+        Long deptId = null;
321
+        if (StrUtil.equals(HomePageQueryEnum.USER.getCode(), item.getType())) {
322
+            userId = item.getId();
323
+        } else {
324
+            deptId = item.getId();
325
+        }
326
+        return accuracyStatisticsService.getAccuracyRate(userId, deptId, item.getStartDate(), item.getEndDate());
327
+    }
328
+
329
+    /**
330
+     * 获取巡检合格率
331
+     */
332
+    private BigDecimal getCheckPassRate(SysHomePageDetailQueryParamDto item) {
333
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
334
+        if (StrUtil.equals(HomePageQueryEnum.USER.getCode(), item.getType())) {
335
+            dto.setUserId(item.getId());
336
+        } else {
337
+            dto.setDeptId(item.getId());
338
+        }
339
+        dto.setSpecifiedDate(item.getSpecifiedDate());
340
+        dto.setStartDate(item.getStartDate());
341
+        dto.setEndDate(item.getEndDate());
342
+        return checkLargeScreenService.checkPassRate(dto);
343
+    }
344
+
345
+    /**
346
+     * 获取培训答题平均分
347
+     */
348
+    private BigDecimal getLearningGrowthScore(SysHomePageDetailQueryParamDto item) {
349
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
350
+        if (StrUtil.equals(HomePageQueryEnum.USER.getCode(), item.getType())) {
351
+            dto.setUserId(item.getId());
352
+        } else {
353
+            dto.setDeptId(item.getId());
354
+        }
355
+        dto.setSpecifiedDate(item.getSpecifiedDate());
356
+        dto.setStartDate(item.getStartDate());
357
+        dto.setEndDate(item.getEndDate());
358
+        return sysLearningGrowthService.learningGrowthAverage(dto);
359
+    }
360
+
361
+    /**
362
+     * 部门值计算器函数式接口
363
+     */
364
+    @FunctionalInterface
365
+    private interface DeptValueCalculator {
366
+        BigDecimal calculate(String deptType, SysHomePageDetailQueryParamDto item);
367
+    }
368
+
369
+    /**
370
+     * 获取查获数量
371
+     */
372
+    private BigDecimal getSeizureCount(SysHomePageDetailQueryParamDto item) {
373
+        if (item.getEndDate() != null) {
374
+            item.setEndDate(DateUtils.addSeconds(DateUtils.truncate(item.getEndDate(), Calendar.DAY_OF_MONTH), 86399));
375
+        }
376
+        if (StrUtil.equals(HomePageQueryEnum.USER.getCode(), item.getType())) {
377
+            return seizureReportMapper.selectSelfSeizureCount(item.getId(), item.getStartDate(), item.getEndDate());
378
+        } else {
379
+            return calculateDeptBasedValue(item, this::calculateAverageSeizureCountForDeptType);
380
+        }
381
+    }
382
+
383
+    /**
384
+     * 获取查获数量图形
385
+     */
386
+    private BigDecimal getSeizureCountGraph(SysHomePageDetailQueryParamDto item) {
387
+        if (item.getEndDate() != null) {
388
+            item.setEndDate(DateUtils.addSeconds(DateUtils.truncate(item.getEndDate(), Calendar.DAY_OF_MONTH), 86399));
389
+        }
390
+        if (StrUtil.equals(HomePageQueryEnum.USER.getCode(), item.getType())) {
391
+            BigDecimal totalSeizure = seizureReportMapper.selectSelfSeizureCount(item.getId(), item.getStartDate(), item.getEndDate());
392
+            Long stationId = getStationIdByUserId(item.getId());
393
+            BigDecimal maxSeizureCount = seizureReportMapper.selectMaxSeizureCountInStation(stationId, item.getStartDate(), item.getEndDate());
394
+            return (maxSeizureCount != null && maxSeizureCount.compareTo(BigDecimal.ZERO) > 0) ?
395
+                    totalSeizure.divide(maxSeizureCount, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;
396
+        } else {
397
+            return calculateDeptBasedValue(item, this::calculateSeizureCountGraphForDeptType);
398
+        }
399
+    }
400
+
401
+    /**
402
+     * 获取部门相关数值的通用方法
403
+     */
404
+    private BigDecimal calculateDeptBasedValue(SysHomePageDetailQueryParamDto item,
405
+                                               DeptValueCalculator calculator) {
406
+        SysDept sysDept = deptService.selectDeptById(item.getId());
407
+        if (sysDept == null) {
408
+            return BigDecimal.ZERO;
409
+        }
410
+        return calculator.calculate(sysDept.getDeptType(), item);
411
+    }
412
+
413
+    /**
414
+     * 平均查获数量计算
415
+     */
416
+    private BigDecimal calculateAverageSeizureCountForDeptType(String deptType, SysHomePageDetailQueryParamDto item) {
417
+        switch (deptType) {
418
+            case "TEAMS":
419
+                BigDecimal totalTeamSeizure = seizureReportMapper.selectTeamAverage(item.getId(), item.getStartDate(), item.getEndDate());
420
+                Integer teamUserCount = seizureReportMapper.selectUserCountByTeamId(item.getId());
421
+                return (teamUserCount != null && teamUserCount > 0) ?
422
+                        totalTeamSeizure.divide(new BigDecimal(teamUserCount), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO;
423
+            case "MANAGER":
424
+                BigDecimal totalDepartmentSeizure = seizureReportMapper.selectDepartmentAverage(item.getId(), item.getStartDate(), item.getEndDate());
425
+                Integer deptUserCount = seizureReportMapper.selectUserCountByDepartmentId(item.getId());
426
+                return (deptUserCount != null && deptUserCount > 0) ?
427
+                        totalDepartmentSeizure.divide(new BigDecimal(deptUserCount), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO;
428
+            case "BRIGADE":
429
+                BigDecimal totalBrigadeSeizure = seizureReportMapper.selectBrigadeAverage(item.getId(), item.getStartDate(), item.getEndDate());
430
+                Integer brigadeUserCount = seizureReportMapper.selectUserCountByBrigadeId(item.getId());
431
+                return (brigadeUserCount != null && brigadeUserCount > 0) ?
432
+                        totalBrigadeSeizure.divide(new BigDecimal(brigadeUserCount), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO;
433
+            case "STATION":
434
+                BigDecimal totalStationSeizure = seizureReportMapper.selectStationAverage(item.getId(), item.getStartDate(), item.getEndDate());
435
+                Integer stationUserCount = seizureReportMapper.selectUserCountByStationId(item.getId());
436
+                return (stationUserCount != null && stationUserCount > 0) ?
437
+                        totalStationSeizure.divide(new BigDecimal(stationUserCount), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO;
438
+            default:
439
+                return BigDecimal.ZERO;
440
+        }
441
+    }
442
+
443
+    /**
444
+     * 查获数量图表计算
445
+     */
446
+    private BigDecimal calculateSeizureCountGraphForDeptType(String deptType, SysHomePageDetailQueryParamDto item) {
447
+        switch (deptType) {
448
+            case "TEAMS":
449
+                Long sectionDeptId = seizureReportMapper.selectSectionDeptIdByTeamId(item.getId());
450
+                Long brigadeDeptId = seizureReportMapper.selectBrigadeDeptIdByDepartmentId(sectionDeptId);
451
+                Long stationId = seizureReportMapper.selectStationDeptIdByDeptId(brigadeDeptId);
452
+                BigDecimal totalTeamSeizure = seizureReportMapper.selectTeamAverage(item.getId(), item.getStartDate(), item.getEndDate());
453
+                //查询班级人数
454
+                Integer teamUserCount = seizureReportMapper.selectUserCountByTeamId(item.getId());
455
+                BigDecimal maxSeizureCount = seizureReportMapper.selectMaxSeizureCountInStation(stationId, item.getStartDate(), item.getEndDate());
456
+                return (maxSeizureCount != null && maxSeizureCount.compareTo(BigDecimal.ZERO) > 0 && teamUserCount != null && teamUserCount > 0) ?
457
+                        totalTeamSeizure.divide(new BigDecimal(teamUserCount), 2, RoundingMode.HALF_UP).divide(maxSeizureCount, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;
458
+            case "MANAGER":
459
+                Long brigadeId = seizureReportMapper.selectBrigadeDeptIdByDepartmentId(item.getId());
460
+                Long deptStationId = seizureReportMapper.selectStationDeptIdByDeptId(brigadeId);
461
+                BigDecimal totalDepartmentSeizure = seizureReportMapper.selectDepartmentAverage(item.getId(), item.getStartDate(), item.getEndDate());
462
+                BigDecimal maxSeizureCountForDept = seizureReportMapper.selectMaxSeizureCountInStation(deptStationId, item.getStartDate(), item.getEndDate());
463
+                //查询科室人数
464
+                Integer deptUserCount = seizureReportMapper.selectUserCountByDepartmentId(item.getId());
465
+                return (maxSeizureCountForDept != null && maxSeizureCountForDept.compareTo(BigDecimal.ZERO) > 0 && deptUserCount != null && deptUserCount > 0) ?
466
+                        totalDepartmentSeizure.divide(new BigDecimal(deptUserCount), 2, RoundingMode.HALF_UP).divide(maxSeizureCountForDept, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;
467
+            case "BRIGADE":
468
+                Long stationDeptId = seizureReportMapper.selectStationDeptIdByDeptId(item.getId());
469
+                BigDecimal totalBrigadeSeizure = seizureReportMapper.selectBrigadeAverage(item.getId(), item.getStartDate(), item.getEndDate());
470
+                BigDecimal maxSeizureCountFor = seizureReportMapper.selectMaxSeizureCountInStation(stationDeptId, item.getStartDate(), item.getEndDate());
471
+                //查询大队人数
472
+                Integer deptBrigadeUserCount = seizureReportMapper.selectUserCountByBrigadeId(item.getId());
473
+                return (maxSeizureCountFor != null && maxSeizureCountFor.compareTo(BigDecimal.ZERO) > 0 && deptBrigadeUserCount != null && deptBrigadeUserCount > 0) ?
474
+                        totalBrigadeSeizure.divide(new BigDecimal(deptBrigadeUserCount), 2, RoundingMode.HALF_UP).divide(maxSeizureCountFor, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;
475
+            case "STATION":
476
+                BigDecimal totalStationSeizure = seizureReportMapper.selectStationAverage(item.getId(), item.getStartDate(), item.getEndDate());
477
+                BigDecimal maxSeizureCountForStation = seizureReportMapper.selectMaxSeizureCountInStation(item.getId(), item.getStartDate(), item.getEndDate());
478
+                //查询站人数
479
+                Integer stationUserCount = seizureReportMapper.selectUserCountByStationId(item.getId());
480
+                return (maxSeizureCountForStation != null && maxSeizureCountForStation.compareTo(BigDecimal.ZERO) > 0 && stationUserCount != null && stationUserCount > 0) ?
481
+                        totalStationSeizure.divide(new BigDecimal(stationUserCount), 2, RoundingMode.HALF_UP).divide(maxSeizureCountForStation, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;
482
+            default:
483
+                return BigDecimal.ZERO;
484
+        }
485
+    }
486
+
487
+    /**
488
+     * 根据用户ID获取对应站点ID
489
+     */
490
+    private Long getStationIdByUserId(Long userId) {
491
+        SysUser sysUser = sysUserService.selectUserById(userId);
492
+        List<SysDept> deptList = deptService.selectAllDept(sysUser.getDeptId());
493
+        Collections.reverse(deptList);
494
+        SysDept station = deptList.stream().filter(x -> StrUtil.equals(DeptType.STATION.getCode(), x.getDeptType())).findFirst().orElse(null);
495
+        return station != null ? station.getDeptId() : 0;
496
+    }
497
+
498
+    private BigDecimal getWorkingHours(SysHomePageDetailQueryParamDto item) {
499
+        if (item.getEndDate() != null) {
500
+            item.setEndDate(DateUtils.addSeconds(DateUtils.truncate(item.getEndDate(), Calendar.DAY_OF_MONTH), 86399));
501
+        }
502
+        if (StrUtil.equals(HomePageQueryEnum.USER.getCode(), item.getType())) {
503
+            BigDecimal bigDecimal = attendancePostRecordService.selectPersonalTotalWorkDurationInTimeRange(item.getId(), item.getStartDate(), item.getEndDate());
504
+            return bigDecimal != null ? bigDecimal.divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO;
505
+        } else {
506
+            return calculateDeptBasedValue(item, this::calculateAverageWorkDurationForDeptType);
507
+        }
508
+    }
509
+
510
+    private BigDecimal getWorkingHoursGraph(SysHomePageDetailQueryParamDto item) {
511
+        if (item.getEndDate() != null) {
512
+            item.setEndDate(DateUtils.addSeconds(DateUtils.truncate(item.getEndDate(), Calendar.DAY_OF_MONTH), 86399));
513
+        }
514
+        if (StrUtil.equals(HomePageQueryEnum.USER.getCode(), item.getType())) {
515
+            BigDecimal totalWorkDuration = attendancePostRecordService.selectPersonalTotalWorkDurationInTimeRange(item.getId(), item.getStartDate(), item.getEndDate());
516
+            Long stationId = getStationIdByUserId(item.getId());
517
+            BigDecimal maxUserWorkDuration = attendancePostRecordService.selectTopUserWorkDurationInTimeRange(stationId, item.getStartDate(), item.getEndDate());
518
+            return (maxUserWorkDuration != null && maxUserWorkDuration.compareTo(BigDecimal.ZERO) > 0) ?
519
+                    totalWorkDuration.divide(maxUserWorkDuration, 4, RoundingMode.HALF_UP) : BigDecimal.ZERO;
520
+        } else {
521
+            return calculateDeptBasedValue(item, this::calculateWorkDurationForDeptType);
522
+        }
523
+    }
524
+
525
+    /**
526
+     * 平均工作时长计算
527
+     */
528
+    private BigDecimal calculateAverageWorkDurationForDeptType(String deptType, SysHomePageDetailQueryParamDto item) {
529
+        List<SysUser> sysUserList = sysUserService.selectUserListByRoleKeyAndDeptId(Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()), item.getId());
530
+        if (CollUtil.isEmpty(sysUserList)) {
531
+            return BigDecimal.ZERO;
532
+        }
533
+        List<Long> list = sysUserList.stream().map(SysUser::getUserId).distinct().collect(Collectors.toList());
534
+        int size = list.size();
535
+        BigDecimal totalWorkDuration = attendancePostRecordService.selectTotalWorkDurationInTimeRange(list, item.getStartDate(), item.getEndDate());
536
+        return totalWorkDuration.divide(new BigDecimal(60), 2, RoundingMode.HALF_UP).divide(new BigDecimal(size), 2, RoundingMode.HALF_UP);
537
+    }
538
+
539
+    /**
540
+     * 工作时长图表计算
541
+     */
542
+    private BigDecimal calculateWorkDurationForDeptType(String deptType, SysHomePageDetailQueryParamDto item) {
543
+        List<SysDept> deptList = deptService.selectAllDept(item.getId());
544
+        if (CollUtil.isEmpty(deptList)) {
545
+            return BigDecimal.ZERO;
546
+        }
547
+        Collections.reverse(deptList);
548
+        SysDept station = deptList.stream().filter(x -> StrUtil.equals(DeptType.STATION.getCode(), x.getDeptType())).findFirst().orElse(null);
549
+        BigDecimal maxUserWorkDuration = attendancePostRecordService.selectTopUserWorkDurationInTimeRange(station.getDeptId(), item.getStartDate(), item.getEndDate());
550
+        if (maxUserWorkDuration == null || maxUserWorkDuration.compareTo(BigDecimal.ZERO) == 0) {
551
+            return BigDecimal.ZERO;
552
+        }
553
+        List<SysUser> sysUserList = sysUserService.selectUserListByRoleKeyAndDeptId(Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()), item.getId());
554
+        if (CollUtil.isEmpty(sysUserList)) {
555
+            return BigDecimal.ZERO;
556
+        }
557
+        List<Long> list = sysUserList.stream().map(SysUser::getUserId).distinct().collect(Collectors.toList());
558
+        int size = list.size();
559
+        BigDecimal totalWorkDuration = attendancePostRecordService.selectTotalWorkDurationInTimeRange(list, item.getStartDate(), item.getEndDate());
560
+        return totalWorkDuration.divide(new BigDecimal(size), 2, RoundingMode.HALF_UP).divide(maxUserWorkDuration, 4, RoundingMode.HALF_UP);
561
+    }
562
+
563
+}

+ 654 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysHomeReportController.java

@@ -0,0 +1,654 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import cn.hutool.core.collection.CollUtil;
4
+import cn.hutool.core.util.ObjectUtil;
5
+import cn.hutool.core.util.StrUtil;
6
+import com.alibaba.excel.EasyExcel;
7
+import com.alibaba.excel.ExcelWriter;
8
+import com.alibaba.excel.write.metadata.WriteSheet;
9
+import com.alibaba.excel.write.metadata.style.WriteCellStyle;
10
+import com.alibaba.excel.write.metadata.style.WriteFont;
11
+import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
12
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
13
+import com.sundot.airport.attendance.service.IAttendancePostRecordService;
14
+import com.sundot.airport.check.service.ICheckLargeScreenService;
15
+import com.sundot.airport.common.core.controller.BaseController;
16
+import com.sundot.airport.common.core.domain.*;
17
+import com.sundot.airport.common.core.domain.entity.SysDept;
18
+import com.sundot.airport.common.core.domain.entity.SysUser;
19
+import com.sundot.airport.common.dto.AttendanceWorkDurationDto;
20
+import com.sundot.airport.common.enums.DeptType;
21
+import com.sundot.airport.common.enums.HomePageQueryEnum;
22
+import com.sundot.airport.common.enums.RoleTypeEnum;
23
+import com.sundot.airport.common.exception.ServiceException;
24
+import com.sundot.airport.common.utils.DateUtils;
25
+import com.sundot.airport.exam.service.IAccuracyStatisticsService;
26
+import com.sundot.airport.item.service.SeizureReportService;
27
+import com.sundot.airport.system.service.ISysDeptService;
28
+import com.sundot.airport.system.service.ISysLearningGrowthService;
29
+import com.sundot.airport.system.service.ISysUserService;
30
+import lombok.SneakyThrows;
31
+import org.apache.poi.ss.usermodel.BorderStyle;
32
+import org.apache.poi.ss.usermodel.FillPatternType;
33
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
34
+import org.apache.poi.ss.usermodel.IndexedColors;
35
+import org.apache.poi.ss.usermodel.VerticalAlignment;
36
+import org.springframework.beans.factory.annotation.Autowired;
37
+import org.springframework.web.bind.annotation.GetMapping;
38
+import org.springframework.web.bind.annotation.PostMapping;
39
+import org.springframework.web.bind.annotation.RequestBody;
40
+import org.springframework.web.bind.annotation.RequestMapping;
41
+import org.springframework.web.bind.annotation.RestController;
42
+
43
+import javax.servlet.http.HttpServletResponse;
44
+import java.math.BigDecimal;
45
+import java.math.RoundingMode;
46
+import java.net.URLEncoder;
47
+import java.util.*;
48
+import java.util.stream.Collectors;
49
+
50
+/**
51
+ * 首页报表控制器
52
+ *
53
+ * @author ruoyi
54
+ */
55
+@RestController
56
+@RequestMapping("/system/homeReport")
57
+public class SysHomeReportController extends BaseController {
58
+
59
+    @Autowired
60
+    private ISysDeptService deptService;
61
+
62
+    @Autowired
63
+    private ICheckLargeScreenService checkLargeScreenService;
64
+
65
+    @Autowired
66
+    private ISysLearningGrowthService sysLearningGrowthService;
67
+
68
+    @Autowired
69
+    private IAccuracyStatisticsService accuracyStatisticsService;
70
+
71
+    @Autowired
72
+    private SeizureReportService seizureReportService;
73
+
74
+    @Autowired
75
+    private IAttendancePostRecordService attendancePostRecordService;
76
+
77
+    @Autowired
78
+    private ISysUserService sysUserService;
79
+
80
+    /**
81
+     * 首页报表-下载
82
+     */
83
+    @SneakyThrows
84
+    @GetMapping("/download")
85
+    public void downloadHomeReport(HttpServletResponse response, BaseLargeScreenQueryParamDto dto) {
86
+        // 获取首页报表-整体
87
+        SysHomeReportDto result = getHomeReportDto(dto);
88
+        // 设置响应头
89
+        String fileName = URLEncoder.encode("首页报表", "UTF-8");
90
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
91
+        response.setCharacterEncoding("UTF-8");
92
+        response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
93
+
94
+        // 1. 创建表头样式
95
+        WriteCellStyle headWriteCellStyle = new WriteCellStyle();
96
+
97
+        // 设置表头字体
98
+        WriteFont headWriteFont = new WriteFont();
99
+        headWriteFont.setFontHeightInPoints((short) 11);  // 字体大小
100
+        headWriteFont.setBold(true);                      // 加粗
101
+        headWriteFont.setFontName("宋体");                // 字体类型
102
+        headWriteFont.setColor(IndexedColors.BLACK.getIndex()); // 字体颜色
103
+        headWriteCellStyle.setWriteFont(headWriteFont);
104
+
105
+        // 设置表头单元格样式
106
+        headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); // 背景色
107
+        headWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND); // 填充模式
108
+        headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);    // 水平居中
109
+        headWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);      // 垂直居中
110
+        headWriteCellStyle.setWrapped(true);  // 自动换行
111
+
112
+        // 设置表头边框 - 细实线
113
+        headWriteCellStyle.setBorderLeft(BorderStyle.THIN);
114
+        headWriteCellStyle.setBorderRight(BorderStyle.THIN);
115
+        headWriteCellStyle.setBorderTop(BorderStyle.THIN);
116
+        headWriteCellStyle.setBorderBottom(BorderStyle.THIN);
117
+
118
+        // 设置边框颜色
119
+        headWriteCellStyle.setTopBorderColor(IndexedColors.BLACK.getIndex());
120
+        headWriteCellStyle.setBottomBorderColor(IndexedColors.BLACK.getIndex());
121
+        headWriteCellStyle.setLeftBorderColor(IndexedColors.BLACK.getIndex());
122
+        headWriteCellStyle.setRightBorderColor(IndexedColors.BLACK.getIndex());
123
+
124
+        // 2. 创建内容样式
125
+        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
126
+
127
+        // 设置内容字体
128
+        WriteFont contentWriteFont = new WriteFont();
129
+        contentWriteFont.setFontHeightInPoints((short) 11);  // 内容字体大小
130
+        contentWriteFont.setFontName("宋体");
131
+        contentWriteFont.setColor(IndexedColors.BLACK.getIndex());
132
+        contentWriteCellStyle.setWriteFont(contentWriteFont);
133
+
134
+        // 设置内容单元格样式
135
+        contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
136
+        contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
137
+        contentWriteCellStyle.setWrapped(true);
138
+
139
+        // 设置内容边框 - 细实线
140
+        contentWriteCellStyle.setBorderLeft(BorderStyle.THIN);
141
+        contentWriteCellStyle.setBorderRight(BorderStyle.THIN);
142
+        contentWriteCellStyle.setBorderTop(BorderStyle.THIN);
143
+        contentWriteCellStyle.setBorderBottom(BorderStyle.THIN);
144
+
145
+        // 设置边框颜色
146
+        contentWriteCellStyle.setTopBorderColor(IndexedColors.BLACK.getIndex());
147
+        contentWriteCellStyle.setBottomBorderColor(IndexedColors.BLACK.getIndex());
148
+        contentWriteCellStyle.setLeftBorderColor(IndexedColors.BLACK.getIndex());
149
+        contentWriteCellStyle.setRightBorderColor(IndexedColors.BLACK.getIndex());
150
+
151
+        HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
152
+
153
+        // 创建ExcelWriter
154
+        ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream())
155
+                .registerWriteHandler(horizontalCellStyleStrategy)
156
+                .build();
157
+
158
+        try {
159
+            // 第一个sheet:巡检合格率
160
+            WriteSheet checkSheet = EasyExcel.writerSheet(0, "巡检合格率")
161
+                    .head(SysHomeReportDetailCheckExcelDto.class)
162
+                    .build();
163
+            List<SysHomeReportDetailCheckExcelDto> checkExcelData = getCheckExcelDtoData(result.getCheckList());
164
+            excelWriter.write(checkExcelData, checkSheet);
165
+
166
+            // 第二个sheet:培训答题分数
167
+            WriteSheet learningSheet = EasyExcel.writerSheet(1, "培训答题分数")
168
+                    .head(SysHomeReportDetailLearningExcelDto.class)
169
+                    .build();
170
+            List<SysHomeReportDetailLearningExcelDto> learningExcelData = getLearningExcelDtoData(result.getLearningList());
171
+            excelWriter.write(learningExcelData, learningSheet);
172
+
173
+            // 第三个sheet:抽问抽答正确率
174
+            WriteSheet answerSheet = EasyExcel.writerSheet(2, "抽问抽答正确率")
175
+                    .head(SysHomeReportDetailAnswerExcelDto.class)
176
+                    .build();
177
+            List<SysHomeReportDetailAnswerExcelDto> answerExcelData = getAnswerExcelDtoData(result.getAnswerList());
178
+            excelWriter.write(answerExcelData, answerSheet);
179
+
180
+            // 第四个sheet:查获数量
181
+            WriteSheet seizureSheet = EasyExcel.writerSheet(3, "查获数量")
182
+                    .head(SysHomeReportDetailSeizureExcelDto.class)
183
+                    .build();
184
+            List<SysHomeReportDetailSeizureExcelDto> seizureExcelData = getSeizureExcelDtoData(result.getSeizureList());
185
+            excelWriter.write(seizureExcelData, seizureSheet);
186
+
187
+            // 第五个sheet:工作时长
188
+            WriteSheet workingSheet = EasyExcel.writerSheet(4, "工作时长")
189
+                    .head(SysHomeReportDetailWorkingExcelDto.class)
190
+                    .build();
191
+            List<SysHomeReportDetailWorkingExcelDto> workingExcelData = getWorkingExcelDtoData(result.getWorkingList());
192
+            excelWriter.write(workingExcelData, workingSheet);
193
+        } catch (Exception e) {
194
+            throw new RuntimeException("导出报表失败", e);
195
+        } finally {
196
+            if (excelWriter != null) {
197
+                excelWriter.finish();
198
+            }
199
+        }
200
+    }
201
+
202
+    /**
203
+     * 获取首页报表下载数据-工作时长
204
+     *
205
+     * @param workingList
206
+     * @return
207
+     */
208
+    private List<SysHomeReportDetailWorkingExcelDto> getWorkingExcelDtoData(List<SysHomeReportDetailDto> workingList) {
209
+        List<SysHomeReportDetailWorkingExcelDto> result = new ArrayList<>();
210
+        workingList.forEach(item -> {
211
+            SysHomeReportDetailWorkingExcelDto excelDto = new SysHomeReportDetailWorkingExcelDto();
212
+            excelDto.setName(item.getName());
213
+            excelDto.setTotalNumber(item.getTotalNumber());
214
+            excelDto.setAverageNumber(item.getAverageNumber());
215
+            excelDto.setMedianNumber(item.getMedianNumber());
216
+            excelDto.setMaxNumber(item.getMaxNumber());
217
+            excelDto.setMinNumber(item.getMinNumber());
218
+            result.add(excelDto);
219
+        });
220
+        return result;
221
+    }
222
+
223
+    /**
224
+     * 获取首页报表下载数据-巡检合格率
225
+     */
226
+    private List<SysHomeReportDetailCheckExcelDto> getCheckExcelDtoData(List<SysHomeReportDetailDto> checkList) {
227
+        List<SysHomeReportDetailCheckExcelDto> result = new ArrayList<>();
228
+        checkList.forEach(item -> {
229
+            SysHomeReportDetailCheckExcelDto excelDto = new SysHomeReportDetailCheckExcelDto();
230
+            excelDto.setName(item.getName());
231
+            excelDto.setAverageNumber(item.getAverageNumber().multiply(BigDecimal.valueOf(100)));
232
+            excelDto.setMedianNumber(item.getMedianNumber().multiply(BigDecimal.valueOf(100)));
233
+            excelDto.setMaxNumber(item.getMaxNumber().multiply(BigDecimal.valueOf(100)));
234
+            excelDto.setMinNumber(item.getMinNumber().multiply(BigDecimal.valueOf(100)));
235
+            result.add(excelDto);
236
+        });
237
+        return result;
238
+    }
239
+
240
+    /**
241
+     * 获取首页报表下载数据-培训答题分数
242
+     */
243
+    private List<SysHomeReportDetailLearningExcelDto> getLearningExcelDtoData(List<SysHomeReportDetailDto> learningList) {
244
+        List<SysHomeReportDetailLearningExcelDto> result = new ArrayList<>();
245
+        learningList.forEach(item -> {
246
+            SysHomeReportDetailLearningExcelDto excelDto = new SysHomeReportDetailLearningExcelDto();
247
+            excelDto.setName(item.getName());
248
+            excelDto.setAverageNumber(item.getAverageNumber());
249
+            excelDto.setMedianNumber(item.getMedianNumber());
250
+            excelDto.setMaxNumber(item.getMaxNumber());
251
+            excelDto.setMinNumber(item.getMinNumber());
252
+            result.add(excelDto);
253
+        });
254
+        return result;
255
+    }
256
+
257
+    /**
258
+     * 获取首页报表下载数据-抽问抽答正确率
259
+     */
260
+    private List<SysHomeReportDetailAnswerExcelDto> getAnswerExcelDtoData(List<SysHomeReportDetailDto> answerList) {
261
+        List<SysHomeReportDetailAnswerExcelDto> result = new ArrayList<>();
262
+        if (answerList != null) {
263
+            answerList.forEach(item -> {
264
+                SysHomeReportDetailAnswerExcelDto excelDto = new SysHomeReportDetailAnswerExcelDto();
265
+                excelDto.setName(item.getName());
266
+                excelDto.setAverageNumber(item.getAverageNumber());
267
+                excelDto.setMedianNumber(item.getMedianNumber());
268
+                excelDto.setMaxNumber(item.getMaxNumber());
269
+                excelDto.setMinNumber(item.getMinNumber());
270
+                result.add(excelDto);
271
+            });
272
+        }
273
+        return result;
274
+    }
275
+
276
+    /**
277
+     * 获取首页报表下载数据-查获数量
278
+     */
279
+    private List<SysHomeReportDetailSeizureExcelDto> getSeizureExcelDtoData(List<SysHomeReportDetailDto> learningList) {
280
+        List<SysHomeReportDetailSeizureExcelDto> result = new ArrayList<>();
281
+        learningList.forEach(item -> {
282
+            SysHomeReportDetailSeizureExcelDto excelDto = new SysHomeReportDetailSeizureExcelDto();
283
+            excelDto.setName(item.getName());
284
+            excelDto.setTotalNumber(item.getTotalNumber());
285
+            excelDto.setAverageNumber(item.getAverageNumber());
286
+            excelDto.setMedianNumber(item.getMedianNumber());
287
+            excelDto.setMaxNumber(item.getMaxNumber());
288
+            excelDto.setMinNumber(item.getMinNumber());
289
+            result.add(excelDto);
290
+        });
291
+        return result;
292
+    }
293
+
294
+    /**
295
+     * 首页报表-整体
296
+     */
297
+    @GetMapping("/homeReportWhole")
298
+    public AjaxResult homePageWhole(BaseLargeScreenQueryParamDto dto) {
299
+        SysHomeReportDto result = getHomeReportDto(dto);
300
+        return success(result);
301
+    }
302
+
303
+    /**
304
+     * 获取首页报表-整体
305
+     */
306
+    private SysHomeReportDto getHomeReportDto(BaseLargeScreenQueryParamDto dto) {
307
+        List<SysHomePageDetailQueryParamDto> dtoList = new ArrayList<>();
308
+        List<SysDept> sysDeptList = deptService.selectAllDept(getDeptId());
309
+        Collections.reverse(sysDeptList);
310
+        SysDept stationDept = sysDeptList.stream().filter(x -> StrUtil.equals(DeptType.STATION.getCode(), x.getDeptType())).findFirst().orElse(null);
311
+        if (ObjectUtil.isNull(stationDept)) {
312
+            throw new ServiceException("未查询到站级部门信息");
313
+        }
314
+        SysHomePageDetailQueryParamDto stationDto = new SysHomePageDetailQueryParamDto();
315
+        stationDto.setId(stationDept.getDeptId());
316
+        stationDto.setName(stationDept.getDeptName());
317
+        stationDto.setType(HomePageQueryEnum.DEPT.getCode());
318
+        dtoList.add(stationDto);
319
+        SysDept queryDept = new SysDept();
320
+        queryDept.setParentId(stationDept.getDeptId());
321
+        List<SysDept> deptList = deptService.selectDeptInfoAll(queryDept);
322
+        deptList.forEach(item -> {
323
+            SysHomePageDetailQueryParamDto departmentDto = new SysHomePageDetailQueryParamDto();
324
+            departmentDto.setId(item.getDeptId());
325
+            departmentDto.setName(item.getDeptName());
326
+            departmentDto.setType(HomePageQueryEnum.DEPT.getCode());
327
+            dtoList.add(departmentDto);
328
+        });
329
+
330
+        dtoList.forEach(item -> {
331
+            item.setSpecifiedDate(dto.getSpecifiedDate());
332
+            item.setStartDate(dto.getStartDate());
333
+            item.setEndDate(dto.getEndDate());
334
+        });
335
+        SysHomeReportDto result = getSysHomeReportDto(dtoList);
336
+        return result;
337
+    }
338
+
339
+    /**
340
+     * 首页报表-明细
341
+     */
342
+    @PostMapping("/homeReportDetail")
343
+    public AjaxResult homePageDetail(@RequestBody List<SysHomePageDetailQueryParamDto> dtoList) {
344
+        SysHomeReportDto result = getSysHomeReportDto(dtoList);
345
+        return success(result);
346
+    }
347
+
348
+    /**
349
+     * 获取首页报表-明细
350
+     */
351
+    private SysHomeReportDto getSysHomeReportDto(List<SysHomePageDetailQueryParamDto> dtoList) {
352
+        SysHomeReportDto result = new SysHomeReportDto();
353
+        // 培训答题分数
354
+        result.setLearningList(getLearningList(dtoList));
355
+        // 巡检合格率
356
+        result.setCheckList(getCheckList(dtoList));
357
+        // 查获数量
358
+        result.setSeizureList(getSeizureList(dtoList));
359
+        // 在岗时长
360
+        result.setWorkingList(getWorkingList(dtoList));
361
+        // 抽问抽答正确率
362
+        result.setAnswerList(getAnswerList(dtoList));
363
+        return result;
364
+    }
365
+
366
+
367
+    /**
368
+     * 获取首页报表-明细-查获数量
369
+     */
370
+    private List<SysHomeReportDetailDto> getSeizureList(List<SysHomePageDetailQueryParamDto> dtoList) {
371
+        List<SysHomeReportDetailDto> result = new ArrayList<>();
372
+        for (SysHomePageDetailQueryParamDto item : dtoList) {
373
+            result.add(getSeizure(item));
374
+        }
375
+        return result;
376
+    }
377
+
378
+    /**
379
+     * 获取首页报表-明细-培训答题分数
380
+     */
381
+    private List<SysHomeReportDetailDto> getLearningList(List<SysHomePageDetailQueryParamDto> dtoList) {
382
+        List<SysHomeReportDetailDto> result = new ArrayList<>();
383
+        for (SysHomePageDetailQueryParamDto item : dtoList) {
384
+            result.add(getLearning(item));
385
+        }
386
+        return result;
387
+    }
388
+
389
+    /**
390
+     * 获取首页报表-明细-巡检合格率
391
+     */
392
+    private List<SysHomeReportDetailDto> getCheckList(List<SysHomePageDetailQueryParamDto> dtoList) {
393
+        List<SysHomeReportDetailDto> result = new ArrayList<>();
394
+        for (SysHomePageDetailQueryParamDto item : dtoList) {
395
+            result.add(getCheck(item));
396
+        }
397
+        return result;
398
+    }
399
+
400
+    /**
401
+     * 获取巡检合格率数据
402
+     */
403
+    private SysHomeReportDetailDto getCheck(SysHomePageDetailQueryParamDto item) {
404
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
405
+        if (StrUtil.equals(HomePageQueryEnum.USER.getCode(), item.getType())) {
406
+            dto.setUserId(item.getId());
407
+        } else {
408
+            dto.setDeptId(item.getId());
409
+        }
410
+        dto.setSpecifiedDate(item.getSpecifiedDate());
411
+        dto.setStartDate(item.getStartDate());
412
+        dto.setEndDate(item.getEndDate());
413
+        return checkLargeScreenService.checkData(dto);
414
+    }
415
+
416
+    /**
417
+     * 获取培训答题分数数据
418
+     */
419
+    private SysHomeReportDetailDto getLearning(SysHomePageDetailQueryParamDto item) {
420
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
421
+        if (StrUtil.equals(HomePageQueryEnum.USER.getCode(), item.getType())) {
422
+            dto.setUserId(item.getId());
423
+        } else {
424
+            dto.setDeptId(item.getId());
425
+        }
426
+        dto.setSpecifiedDate(item.getSpecifiedDate());
427
+        dto.setStartDate(item.getStartDate());
428
+        dto.setEndDate(item.getEndDate());
429
+        return sysLearningGrowthService.learningGrowthData(dto);
430
+    }
431
+
432
+    /**
433
+     * 获取查获数量数据
434
+     */
435
+    private SysHomeReportDetailDto getSeizure(SysHomePageDetailQueryParamDto item) {
436
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
437
+        if (StrUtil.equals(HomePageQueryEnum.USER.getCode(), item.getType())) {
438
+            dto.setUserId(item.getId());
439
+        } else {
440
+            dto.setDeptId(item.getId());
441
+        }
442
+        dto.setSpecifiedDate(item.getSpecifiedDate());
443
+        dto.setStartDate(item.getStartDate());
444
+        dto.setEndDate(item.getEndDate());
445
+        return seizureReportService.seizureData(dto);
446
+    }
447
+
448
+    /**
449
+     * 获取首页报表-明细-抽问抽答正确率
450
+     */
451
+    private List<SysHomeReportDetailDto> getAnswerList(List<SysHomePageDetailQueryParamDto> dtoList) {
452
+        List<SysHomeReportDetailDto> result = new ArrayList<>();
453
+        // 批量调用前统一初始化部门缓存,避免每次 answerData 都重复查询数据库
454
+        accuracyStatisticsService.initDeptCache();
455
+        try {
456
+            for (SysHomePageDetailQueryParamDto item : dtoList) {
457
+                result.add(getAnswer(item));
458
+            }
459
+        } finally {
460
+            accuracyStatisticsService.clearDeptCache();
461
+        }
462
+        return result;
463
+    }
464
+
465
+    /**
466
+     * 获取抽问抽答正确率数据
467
+     */
468
+    private SysHomeReportDetailDto getAnswer(SysHomePageDetailQueryParamDto item) {
469
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
470
+        if (StrUtil.equals(HomePageQueryEnum.USER.getCode(), item.getType())) {
471
+            dto.setUserId(item.getId());
472
+        } else {
473
+            dto.setDeptId(item.getId());
474
+        }
475
+        dto.setSpecifiedDate(item.getSpecifiedDate());
476
+        dto.setStartDate(item.getStartDate());
477
+        dto.setEndDate(item.getEndDate());
478
+        return accuracyStatisticsService.answerData(dto);
479
+    }
480
+
481
+    private List<SysHomeReportDetailDto> getWorkingList(List<SysHomePageDetailQueryParamDto> dtoList) {
482
+        List<SysHomeReportDetailDto> result = new ArrayList<>();
483
+        for (SysHomePageDetailQueryParamDto item : dtoList) {
484
+            result.add(getWorking(item));
485
+        }
486
+        return result;
487
+    }
488
+
489
+    private SysHomeReportDetailDto getWorking(SysHomePageDetailQueryParamDto item) {
490
+        // 构建查询参数
491
+        BaseLargeScreenQueryParamDto dto = new BaseLargeScreenQueryParamDto();
492
+        if (StrUtil.equals(HomePageQueryEnum.USER.getCode(), item.getType())) {
493
+            dto.setUserId(item.getId());
494
+        } else {
495
+            dto.setDeptId(item.getId());
496
+        }
497
+        dto.setSpecifiedDate(item.getSpecifiedDate());
498
+        dto.setStartDate(item.getStartDate());
499
+        dto.setEndDate(item.getEndDate());
500
+        return getWorkingHoursData(dto);
501
+    }
502
+
503
+    /**
504
+     * 获取在岗时长数据
505
+     */
506
+    public SysHomeReportDetailDto getWorkingHoursData(BaseLargeScreenQueryParamDto dto) {
507
+        if (ObjectUtils.isNull(dto.getUserId()) && ObjectUtils.isNull(dto.getDeptId())) {
508
+            throw new ServiceException("用户ID和部门ID不能同时为空");
509
+        }
510
+        // 默认筛选条件
511
+        if (ObjectUtils.isNull(dto.getStartDate()) && ObjectUtils.isNull(dto.getEndDate()) && ObjectUtils.isNull(dto.getSpecifiedDate())) {
512
+            dto.setEndDate(DateUtils.addSeconds(DateUtils.truncate(DateUtils.getNowDate(), Calendar.DAY_OF_MONTH), 86399));
513
+            dto.setStartDate(DateUtils.addDays(DateUtils.truncate(DateUtils.getNowDate(), Calendar.DAY_OF_MONTH), -6));
514
+        }
515
+
516
+        SysHomeReportDetailDto result = new SysHomeReportDetailDto();
517
+
518
+        // 全量数据 - 根据用户ID或部门ID获取对应的在岗记录
519
+        List<AttendanceWorkDurationDto> itemListAll = getAttendanceWorkDurationList(dto);
520
+
521
+        List<AttendanceWorkDurationDto> targetList;
522
+        if (ObjectUtils.isNotNull(dto.getUserId())) {
523
+            SysUser sysUser = sysUserService.selectUserById(dto.getUserId());
524
+            if (ObjectUtils.isNull(sysUser)) {
525
+                throw new ServiceException("【" + dto.getUserId() + "】用户不存在");
526
+            }
527
+            result.setId(sysUser.getUserId());
528
+            result.setName(sysUser.getNickName());
529
+            result.setType(HomePageQueryEnum.USER.getCode());
530
+            targetList = itemListAll.stream().filter(item -> ObjectUtil.equals(item.getId(), dto.getUserId())).collect(Collectors.toList());
531
+        } else {
532
+            SysDept sysDept = deptService.selectDeptById(dto.getDeptId());
533
+            if (ObjectUtils.isNull(sysDept)) {
534
+                throw new ServiceException("部门不存在");
535
+            }
536
+            result.setId(sysDept.getDeptId());
537
+            result.setName(sysDept.getDeptName());
538
+            result.setType(HomePageQueryEnum.DEPT.getCode());
539
+            // 根据部门ID查询该部门下的用户列表(这里可以根据实际角色进行调整)
540
+            List<SysUser> sysUserList = sysUserService.selectUserListByRoleKeyAndDeptId(
541
+                    Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()),
542
+                    dto.getDeptId()
543
+            );
544
+            List<Long> userIdList = sysUserList.stream().map(SysUser::getUserId).collect(Collectors.toList());
545
+            targetList = itemListAll.stream().filter(item -> userIdList.contains(item.getId())).collect(Collectors.toList());
546
+        }
547
+
548
+        // 提取工作时长数值并排序
549
+        List<BigDecimal> targetValueList = targetList.stream()
550
+                .map(AttendanceWorkDurationDto::getWorkDuration)
551
+                .filter(Objects::nonNull)
552
+                .sorted()
553
+                .collect(Collectors.toList());
554
+
555
+        if (CollUtil.isEmpty(targetValueList)) {
556
+            return result;
557
+        }
558
+
559
+        // 最大值
560
+        result.setMaxNumber(targetValueList.get(targetValueList.size() - 1));
561
+        // 最小值
562
+        result.setMinNumber(targetValueList.get(0));
563
+        // 总和
564
+        BigDecimal sum = targetValueList.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
565
+        result.setTotalNumber(sum);
566
+        // 总数
567
+        long totalCount = targetList.stream().map(AttendanceWorkDurationDto::getId).distinct().count();
568
+        // 平均值
569
+        result.setAverageNumber(sum.divide(new BigDecimal(totalCount), 2, RoundingMode.HALF_UP));
570
+        // 中位数
571
+        int size = targetValueList.size();
572
+        int middle = size / 2;
573
+        if (size % 2 == 1) {
574
+            result.setMedianNumber(targetValueList.get(middle));
575
+        } else {
576
+            result.setMedianNumber(targetValueList.get(middle - 1).add(targetValueList.get(middle)).divide(new BigDecimal(2), 2, RoundingMode.HALF_UP));
577
+        }
578
+        return result;
579
+    }
580
+
581
+    /**
582
+     * 获取在岗工作时长列表
583
+     */
584
+    private List<AttendanceWorkDurationDto> getAttendanceWorkDurationList(BaseLargeScreenQueryParamDto dto) {
585
+        List<AttendanceWorkDurationDto> result = new ArrayList<>();
586
+
587
+        if (ObjectUtils.isNotNull(dto.getUserId())) {
588
+            // 查询指定用户的总工作时长
589
+            BigDecimal workDuration = attendancePostRecordService.selectPersonalTotalWorkDurationInTimeRange(
590
+                    dto.getUserId(),
591
+                    dto.getStartDate(),
592
+                    dto.getEndDate()
593
+            );
594
+            if (workDuration != null) {
595
+                // 将分钟转换为小时
596
+                workDuration = workDuration.divide(new BigDecimal(60), 2, RoundingMode.HALF_UP);
597
+                AttendanceWorkDurationDto item = new AttendanceWorkDurationDto();
598
+                item.setId(dto.getUserId());
599
+                item.setWorkDuration(workDuration);
600
+                result.add(item);
601
+            }
602
+        } else if (ObjectUtils.isNotNull(dto.getDeptId())) {
603
+            // 根据部门查询该部门下所有用户的工作时长
604
+            SysDept sysDept = deptService.selectDeptById(dto.getDeptId());
605
+            if (sysDept != null) {
606
+                // 根据部门查询该部门下的所有用户
607
+                List<SysUser> sysUserList = sysUserService.selectUserListByRoleKeyAndDeptId(
608
+                        Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()),
609
+                        dto.getDeptId()
610
+                );
611
+
612
+                // 提取用户ID列表
613
+                List<Long> userIds = sysUserList.stream()
614
+                        .map(SysUser::getUserId).distinct()
615
+                        .collect(Collectors.toList());
616
+
617
+                // 批量查询所有用户的工作时长
618
+                List<AttendanceWorkDurationDto> workDurationList = attendancePostRecordService.selectBatchPersonalTotalWorkDurationInTimeRange(
619
+                        userIds,
620
+                        dto.getStartDate(),
621
+                        dto.getEndDate()
622
+                );
623
+
624
+                // 将查询结果转换为Map便于查找
625
+                Map<Long, BigDecimal> workDurationMap = workDurationList.stream()
626
+                        .collect(Collectors.toMap(
627
+                                AttendanceWorkDurationDto::getId,
628
+                                AttendanceWorkDurationDto::getWorkDuration,
629
+                                (existing, replacement) -> existing
630
+                        ));
631
+
632
+                // 组装结果
633
+                for (SysUser user : sysUserList) {
634
+                    BigDecimal workDuration = workDurationMap.get(user.getUserId());
635
+                    if (workDuration == null) {
636
+                        workDuration = BigDecimal.ZERO;
637
+                    }
638
+                    if (workDuration.compareTo(BigDecimal.ZERO) > 0) {
639
+                        // 将分钟转换为小时
640
+                        workDuration = workDuration.divide(new BigDecimal(60), 2, RoundingMode.HALF_UP);
641
+                    }
642
+                    AttendanceWorkDurationDto item = new AttendanceWorkDurationDto();
643
+                    item.setId(user.getUserId());
644
+                    item.setWorkDuration(workDuration);
645
+                    result.add(item);
646
+                }
647
+            }
648
+        }
649
+
650
+        return result;
651
+    }
652
+
653
+
654
+}

+ 29 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysIndexController.java

@@ -0,0 +1,29 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import org.springframework.beans.factory.annotation.Autowired;
4
+import org.springframework.web.bind.annotation.RequestMapping;
5
+import org.springframework.web.bind.annotation.RestController;
6
+import com.sundot.airport.common.config.RuoYiConfig;
7
+import com.sundot.airport.common.utils.StringUtils;
8
+
9
+/**
10
+ * 首页
11
+ *
12
+ * @author ruoyi
13
+ */
14
+@RestController
15
+public class SysIndexController
16
+{
17
+    /** 系统基础配置 */
18
+    @Autowired
19
+    private RuoYiConfig ruoyiConfig;
20
+
21
+    /**
22
+     * 访问首页,提示语
23
+     */
24
+    @RequestMapping("/")
25
+    public String index()
26
+    {
27
+        return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
28
+    }
29
+}

+ 175 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysLearningGrowthController.java

@@ -0,0 +1,175 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.math.BigDecimal;
4
+import java.util.List;
5
+import javax.servlet.http.HttpServletResponse;
6
+
7
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
8
+import com.sundot.airport.system.domain.SysLargeScreenLearningGrowthDto;
9
+import com.sundot.airport.system.domain.SysLargeScreenLearningGrowthOrganizationalSupportDto;
10
+import com.sundot.airport.system.domain.SysLargeScreenLearningGrowthQueryParamDto;
11
+import lombok.SneakyThrows;
12
+import org.springframework.cache.annotation.Cacheable;
13
+import org.springframework.security.access.prepost.PreAuthorize;
14
+import org.springframework.beans.factory.annotation.Autowired;
15
+import org.springframework.web.bind.annotation.GetMapping;
16
+import org.springframework.web.bind.annotation.PostMapping;
17
+import org.springframework.web.bind.annotation.DeleteMapping;
18
+import org.springframework.web.bind.annotation.PathVariable;
19
+import org.springframework.web.bind.annotation.RequestBody;
20
+import org.springframework.web.bind.annotation.RequestMapping;
21
+import org.springframework.web.bind.annotation.RestController;
22
+import com.sundot.airport.common.annotation.Log;
23
+import com.sundot.airport.common.core.controller.BaseController;
24
+import com.sundot.airport.common.core.domain.AjaxResult;
25
+import com.sundot.airport.common.enums.BusinessType;
26
+import com.sundot.airport.system.domain.SysLearningGrowth;
27
+import com.sundot.airport.system.service.ISysLearningGrowthService;
28
+import com.sundot.airport.common.utils.poi.ExcelUtil;
29
+import com.sundot.airport.common.core.page.TableDataInfo;
30
+import org.springframework.web.multipart.MultipartFile;
31
+
32
+/**
33
+ * 学习成长Controller
34
+ *
35
+ * @author ruoyi
36
+ * @date 2025-10-22
37
+ */
38
+@RestController
39
+@RequestMapping("/system/growth")
40
+public class SysLearningGrowthController extends BaseController {
41
+    @Autowired
42
+    private ISysLearningGrowthService sysLearningGrowthService;
43
+
44
+    /**
45
+     * 查询学习成长列表
46
+     */
47
+    @PreAuthorize("@ss.hasPermi('system:growth:list')")
48
+    @GetMapping("/list")
49
+    public TableDataInfo list(SysLearningGrowth sysLearningGrowth) {
50
+        startPage();
51
+        List<SysLearningGrowth> list = sysLearningGrowthService.selectSysLearningGrowthList(sysLearningGrowth);
52
+        return getDataTable(list);
53
+    }
54
+
55
+    /**
56
+     * 导出学习成长列表
57
+     */
58
+    @PreAuthorize("@ss.hasPermi('system:growth:export')")
59
+    @Log(title = "学习成长", businessType = BusinessType.EXPORT)
60
+    @PostMapping("/export")
61
+    public void export(HttpServletResponse response, SysLearningGrowth sysLearningGrowth) {
62
+        List<SysLearningGrowth> list = sysLearningGrowthService.selectSysLearningGrowthList(sysLearningGrowth);
63
+        ExcelUtil<SysLearningGrowth> util = new ExcelUtil<SysLearningGrowth>(SysLearningGrowth.class);
64
+        util.exportExcel(response, list, "学习成长数据");
65
+    }
66
+
67
+    /**
68
+     * 获取学习成长详细信息
69
+     */
70
+    @PreAuthorize("@ss.hasPermi('system:growth:query')")
71
+    @GetMapping(value = "/{id}")
72
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
73
+        return success(sysLearningGrowthService.selectSysLearningGrowthById(id));
74
+    }
75
+
76
+    /**
77
+     * 新增学习成长
78
+     */
79
+    @PreAuthorize("@ss.hasPermi('system:growth:add')")
80
+    @Log(title = "学习成长", businessType = BusinessType.INSERT)
81
+    @PostMapping("/add")
82
+    public AjaxResult add(@RequestBody SysLearningGrowth sysLearningGrowth) {
83
+        sysLearningGrowth.setCreateBy(getUsername());
84
+        return toAjax(sysLearningGrowthService.insertSysLearningGrowth(sysLearningGrowth));
85
+    }
86
+
87
+    /**
88
+     * 修改学习成长
89
+     */
90
+    @PreAuthorize("@ss.hasPermi('system:growth:edit')")
91
+    @Log(title = "学习成长", businessType = BusinessType.UPDATE)
92
+    @PostMapping("/edit")
93
+    public AjaxResult edit(@RequestBody SysLearningGrowth sysLearningGrowth) {
94
+        sysLearningGrowth.setUpdateBy(getUsername());
95
+        return toAjax(sysLearningGrowthService.updateSysLearningGrowth(sysLearningGrowth));
96
+    }
97
+
98
+    /**
99
+     * 删除学习成长
100
+     */
101
+    @PreAuthorize("@ss.hasPermi('system:growth:remove')")
102
+    @Log(title = "学习成长", businessType = BusinessType.DELETE)
103
+    @DeleteMapping("/{ids}")
104
+    public AjaxResult remove(@PathVariable Long[] ids) {
105
+        return toAjax(sysLearningGrowthService.deleteSysLearningGrowthByIds(ids));
106
+    }
107
+
108
+    /**
109
+     * 下载模板
110
+     */
111
+    @Log(title = "学习成长", businessType = BusinessType.IMPORT)
112
+    @PreAuthorize("@ss.hasPermi('system:growth:import')")
113
+    @PostMapping("/importTemplate")
114
+    public void importTemplate(HttpServletResponse response) {
115
+        ExcelUtil<SysLearningGrowth> util = new ExcelUtil<SysLearningGrowth>(SysLearningGrowth.class);
116
+        util.importTemplateExcel(response, "学习成长");
117
+    }
118
+
119
+    /**
120
+     * 导入数据
121
+     */
122
+    @SneakyThrows
123
+    @Log(title = "学习成长", businessType = BusinessType.IMPORT)
124
+    @PreAuthorize("@ss.hasPermi('system:growth:import')")
125
+    @PostMapping("/importData")
126
+    public AjaxResult importData(MultipartFile file, boolean updateSupport) {
127
+        ExcelUtil<SysLearningGrowth> util = new ExcelUtil<SysLearningGrowth>(SysLearningGrowth.class);
128
+        List<SysLearningGrowth> list = util.importExcel(file.getInputStream());
129
+        String operName = getUsername();
130
+        String message = sysLearningGrowthService.importData(list, updateSupport, operName);
131
+        return success(message);
132
+    }
133
+
134
+    /**
135
+     * 能力画像-学习成长
136
+     */
137
+    @Cacheable(
138
+            value = "statistics_list",
139
+            keyGenerator = "statisticsKeyGenerator",
140
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
141
+    )
142
+    @GetMapping("/portrait")
143
+    public AjaxResult portrait(SysLargeScreenLearningGrowthQueryParamDto dto) {
144
+        List<SysLargeScreenLearningGrowthDto> result = sysLearningGrowthService.portrait(dto);
145
+        return success(result);
146
+    }
147
+
148
+    /**
149
+     * 工作画像-组织支撑-培训测试平均分趋势图
150
+     */
151
+    @Cacheable(
152
+            value = "statistics_list",
153
+            keyGenerator = "statisticsKeyGenerator",
154
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
155
+    )
156
+    @GetMapping("/organizationalSupport")
157
+    public AjaxResult organizationalSupport(SysLargeScreenLearningGrowthQueryParamDto dto) {
158
+        List<SysLargeScreenLearningGrowthOrganizationalSupportDto> result = sysLearningGrowthService.organizationalSupport(dto);
159
+        return success(result);
160
+    }
161
+
162
+    /**
163
+     * 首页-学习成长-平均分
164
+     */
165
+//    @Cacheable(
166
+//            value = "statistics_learningGrowth_average",
167
+//            keyGenerator = "statisticsKeyGenerator",
168
+//            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
169
+//    )
170
+    @GetMapping("/learningGrowthAverage")
171
+    public AjaxResult learningGrowthAverage(BaseLargeScreenQueryParamDto dto) {
172
+        BigDecimal result = sysLearningGrowthService.learningGrowthAverage(dto);
173
+        return success(result);
174
+    }
175
+}

+ 137 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysLoginController.java

@@ -0,0 +1,137 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.Date;
4
+import java.util.List;
5
+import java.util.Set;
6
+
7
+import com.sundot.airport.common.dto.UserInfo;
8
+import com.sundot.airport.web.core.cache.UserCache;
9
+import org.springframework.beans.factory.annotation.Autowired;
10
+import org.springframework.web.bind.annotation.GetMapping;
11
+import org.springframework.web.bind.annotation.PostMapping;
12
+import org.springframework.web.bind.annotation.RequestBody;
13
+import org.springframework.web.bind.annotation.RestController;
14
+import com.sundot.airport.common.constant.Constants;
15
+import com.sundot.airport.common.core.domain.AjaxResult;
16
+import com.sundot.airport.common.core.domain.entity.SysMenu;
17
+import com.sundot.airport.common.core.domain.entity.SysUser;
18
+import com.sundot.airport.common.core.domain.model.LoginBody;
19
+import com.sundot.airport.common.core.domain.model.LoginUser;
20
+import com.sundot.airport.common.core.text.Convert;
21
+import com.sundot.airport.common.utils.DateUtils;
22
+import com.sundot.airport.common.utils.SecurityUtils;
23
+import com.sundot.airport.common.utils.StringUtils;
24
+import com.sundot.airport.framework.web.service.SysLoginService;
25
+import com.sundot.airport.framework.web.service.SysPermissionService;
26
+import com.sundot.airport.framework.web.service.TokenService;
27
+import com.sundot.airport.system.service.ISysConfigService;
28
+import com.sundot.airport.system.service.ISysMenuService;
29
+
30
+/**
31
+ * 登录验证
32
+ *
33
+ * @author ruoyi
34
+ */
35
+@RestController
36
+public class SysLoginController {
37
+    @Autowired
38
+    private UserCache userCache;
39
+
40
+    @Autowired
41
+    private SysLoginService loginService;
42
+
43
+    @Autowired
44
+    private ISysMenuService menuService;
45
+
46
+    @Autowired
47
+    private SysPermissionService permissionService;
48
+
49
+    @Autowired
50
+    private TokenService tokenService;
51
+
52
+    @Autowired
53
+    private ISysConfigService configService;
54
+
55
+    /**
56
+     * 登录方法
57
+     *
58
+     * @param loginBody 登录信息
59
+     * @return 结果
60
+     */
61
+    @PostMapping("/login")
62
+    public AjaxResult login(@RequestBody LoginBody loginBody) {
63
+        AjaxResult ajax = AjaxResult.success();
64
+        String token;
65
+        // 判断登录类型
66
+        if ("sms".equals(loginBody.getLoginType())) {
67
+            // 短信验证码登录
68
+            token = loginService.smsLogin(loginBody);
69
+        } else {
70
+            // 密码登录(默认)
71
+            token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
72
+                    loginBody.getUuid());
73
+        }
74
+        ajax.put(Constants.TOKEN, token);
75
+        return ajax;
76
+    }
77
+
78
+    /**
79
+     * 获取用户信息
80
+     *
81
+     * @return 用户信息
82
+     */
83
+    @GetMapping("getInfo")
84
+    public AjaxResult getInfo() {
85
+        LoginUser loginUser = SecurityUtils.getLoginUser();
86
+        SysUser user = loginUser.getUser();
87
+        // 角色集合
88
+        Set<String> roles = permissionService.getRolePermission(user);
89
+        // 权限集合
90
+        Set<String> permissions = permissionService.getMenuPermission(user);
91
+        if (!loginUser.getPermissions().equals(permissions)) {
92
+            loginUser.setPermissions(permissions);
93
+            tokenService.refreshToken(loginUser);
94
+        }
95
+        UserInfo userInfo = userCache.getUserInfo(user.getUserId());
96
+        AjaxResult ajax = AjaxResult.success();
97
+        ajax.put("user", user);
98
+        ajax.put("roles", roles);
99
+        ajax.put("permissions", permissions);
100
+        ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
101
+        ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
102
+        ajax.put("userInfo", userInfo);
103
+        return ajax;
104
+    }
105
+
106
+    /**
107
+     * 获取路由信息
108
+     *
109
+     * @return 路由信息
110
+     */
111
+    @GetMapping("getRouters")
112
+    public AjaxResult getRouters() {
113
+        Long userId = SecurityUtils.getUserId();
114
+        List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
115
+        return AjaxResult.success(menuService.buildMenus(menus));
116
+    }
117
+
118
+    // 检查初始密码是否提醒修改
119
+    public boolean initPasswordIsModify(Date pwdUpdateDate) {
120
+        Integer initPasswordModify = Convert.toInt(configService.selectConfigByKey("sys.account.initPasswordModify"));
121
+        return initPasswordModify != null && initPasswordModify == 1 && pwdUpdateDate == null;
122
+    }
123
+
124
+    // 检查密码是否过期
125
+    public boolean passwordIsExpiration(Date pwdUpdateDate) {
126
+        Integer passwordValidateDays = Convert.toInt(configService.selectConfigByKey("sys.account.passwordValidateDays"));
127
+        if (passwordValidateDays != null && passwordValidateDays > 0) {
128
+            if (StringUtils.isNull(pwdUpdateDate)) {
129
+                // 如果从未修改过初始密码,直接提醒过期
130
+                return true;
131
+            }
132
+            Date nowDate = DateUtils.getNowDate();
133
+            return DateUtils.differentDaysByMillisecond(nowDate, pwdUpdateDate) > passwordValidateDays;
134
+        }
135
+        return false;
136
+    }
137
+}

+ 125 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysMenuController.java

@@ -0,0 +1,125 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+
5
+import org.springframework.beans.factory.annotation.Autowired;
6
+import org.springframework.security.access.prepost.PreAuthorize;
7
+import org.springframework.validation.annotation.Validated;
8
+import org.springframework.web.bind.annotation.DeleteMapping;
9
+import org.springframework.web.bind.annotation.GetMapping;
10
+import org.springframework.web.bind.annotation.PathVariable;
11
+import org.springframework.web.bind.annotation.PostMapping;
12
+import org.springframework.web.bind.annotation.PutMapping;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.RestController;
16
+import com.sundot.airport.common.annotation.Log;
17
+import com.sundot.airport.common.constant.UserConstants;
18
+import com.sundot.airport.common.core.controller.BaseController;
19
+import com.sundot.airport.common.core.domain.AjaxResult;
20
+import com.sundot.airport.common.core.domain.entity.SysMenu;
21
+import com.sundot.airport.common.enums.BusinessType;
22
+import com.sundot.airport.common.utils.StringUtils;
23
+import com.sundot.airport.system.service.ISysMenuService;
24
+
25
+/**
26
+ * 菜单信息
27
+ *
28
+ * @author ruoyi
29
+ */
30
+@RestController
31
+@RequestMapping("/system/menu")
32
+public class SysMenuController extends BaseController {
33
+    @Autowired
34
+    private ISysMenuService menuService;
35
+
36
+    /**
37
+     * 获取菜单列表
38
+     */
39
+    @PreAuthorize("@ss.hasPermi('system:menu:list')")
40
+    @GetMapping("/list")
41
+    public AjaxResult list(SysMenu menu) {
42
+        List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
43
+        return success(menus);
44
+    }
45
+
46
+    /**
47
+     * 根据菜单编号获取详细信息
48
+     */
49
+    @PreAuthorize("@ss.hasPermi('system:menu:query')")
50
+    @GetMapping(value = "/{menuId}")
51
+    public AjaxResult getInfo(@PathVariable Long menuId) {
52
+        return success(menuService.selectMenuById(menuId));
53
+    }
54
+
55
+    /**
56
+     * 获取菜单下拉树列表
57
+     */
58
+    @GetMapping("/treeselect")
59
+    public AjaxResult treeselect(SysMenu menu) {
60
+        List<SysMenu> menus = menuService.selectMenuList(menu, getUserId());
61
+        return success(menuService.buildMenuTreeSelect(menus));
62
+    }
63
+
64
+    /**
65
+     * 加载对应角色菜单列表树
66
+     */
67
+    @GetMapping(value = "/roleMenuTreeselect/{roleId}")
68
+    public AjaxResult roleMenuTreeselect(@PathVariable("roleId") Long roleId) {
69
+        List<SysMenu> menus = menuService.selectMenuList(getUserId());
70
+        AjaxResult ajax = AjaxResult.success();
71
+        ajax.put("checkedKeys", menuService.selectMenuListByRoleId(roleId));
72
+        ajax.put("menus", menuService.buildMenuTreeSelect(menus));
73
+        return ajax;
74
+    }
75
+
76
+    /**
77
+     * 新增菜单
78
+     */
79
+    @PreAuthorize("@ss.hasPermi('system:menu:add')")
80
+    @Log(title = "菜单管理", businessType = BusinessType.INSERT)
81
+    @PostMapping
82
+    public AjaxResult add(@Validated @RequestBody SysMenu menu) {
83
+        if (!menuService.checkMenuNameUnique(menu)) {
84
+            return error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
85
+        } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) {
86
+            return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
87
+        }
88
+        menu.setCreateBy(getUsername());
89
+        return toAjax(menuService.insertMenu(menu));
90
+    }
91
+
92
+    /**
93
+     * 修改菜单
94
+     */
95
+    @PreAuthorize("@ss.hasPermi('system:menu:edit')")
96
+    @Log(title = "菜单管理", businessType = BusinessType.UPDATE)
97
+    @PutMapping
98
+    public AjaxResult edit(@Validated @RequestBody SysMenu menu) {
99
+        if (!menuService.checkMenuNameUnique(menu)) {
100
+            return error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
101
+        } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) {
102
+            return error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
103
+        } else if (menu.getMenuId().equals(menu.getParentId())) {
104
+            return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
105
+        }
106
+        menu.setUpdateBy(getUsername());
107
+        return toAjax(menuService.updateMenu(menu));
108
+    }
109
+
110
+    /**
111
+     * 删除菜单
112
+     */
113
+    @PreAuthorize("@ss.hasPermi('system:menu:remove')")
114
+    @Log(title = "菜单管理", businessType = BusinessType.DELETE)
115
+    @DeleteMapping("/{menuId}")
116
+    public AjaxResult remove(@PathVariable("menuId") Long menuId) {
117
+        if (menuService.hasChildByMenuId(menuId)) {
118
+            return warn("存在子菜单,不允许删除");
119
+        }
120
+        if (menuService.checkMenuExistRole(menuId)) {
121
+            return warn("菜单已分配,不允许删除");
122
+        }
123
+        return toAjax(menuService.deleteMenuById(menuId));
124
+    }
125
+}

+ 88 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysNoticeController.java

@@ -0,0 +1,88 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+
5
+import org.springframework.beans.factory.annotation.Autowired;
6
+import org.springframework.security.access.prepost.PreAuthorize;
7
+import org.springframework.validation.annotation.Validated;
8
+import org.springframework.web.bind.annotation.DeleteMapping;
9
+import org.springframework.web.bind.annotation.GetMapping;
10
+import org.springframework.web.bind.annotation.PathVariable;
11
+import org.springframework.web.bind.annotation.PostMapping;
12
+import org.springframework.web.bind.annotation.PutMapping;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.RestController;
16
+import com.sundot.airport.common.annotation.Log;
17
+import com.sundot.airport.common.core.controller.BaseController;
18
+import com.sundot.airport.common.core.domain.AjaxResult;
19
+import com.sundot.airport.common.core.page.TableDataInfo;
20
+import com.sundot.airport.common.enums.BusinessType;
21
+import com.sundot.airport.system.domain.SysNotice;
22
+import com.sundot.airport.system.service.ISysNoticeService;
23
+
24
+/**
25
+ * 公告 信息操作处理
26
+ *
27
+ * @author ruoyi
28
+ */
29
+@RestController
30
+@RequestMapping("/system/notice")
31
+public class SysNoticeController extends BaseController {
32
+    @Autowired
33
+    private ISysNoticeService noticeService;
34
+
35
+    /**
36
+     * 获取通知公告列表
37
+     */
38
+    @PreAuthorize("@ss.hasPermi('system:notice:list')")
39
+    @GetMapping("/list")
40
+    public TableDataInfo list(SysNotice notice) {
41
+        startPage();
42
+        List<SysNotice> list = noticeService.selectNoticeList(notice);
43
+        return getDataTable(list);
44
+    }
45
+
46
+    /**
47
+     * 根据通知公告编号获取详细信息
48
+     */
49
+    @PreAuthorize("@ss.hasPermi('system:notice:query')")
50
+    @GetMapping(value = "/{noticeId}")
51
+    public AjaxResult getInfo(@PathVariable Long noticeId) {
52
+        return success(noticeService.selectNoticeById(noticeId));
53
+    }
54
+
55
+    /**
56
+     * 新增通知公告
57
+     */
58
+    @PreAuthorize("@ss.hasPermi('system:notice:add')")
59
+    @Log(title = "通知公告", businessType = BusinessType.INSERT)
60
+    @PostMapping
61
+    public AjaxResult add(@Validated @RequestBody SysNotice notice) {
62
+        notice.setCreateByUserId(getUserId());
63
+        notice.setCreateBy(getLoginUser().getUser().getNickName());
64
+        return toAjax(noticeService.insertNotice(notice));
65
+    }
66
+
67
+    /**
68
+     * 修改通知公告
69
+     */
70
+    @PreAuthorize("@ss.hasPermi('system:notice:edit')")
71
+    @Log(title = "通知公告", businessType = BusinessType.UPDATE)
72
+    @PutMapping
73
+    public AjaxResult edit(@Validated @RequestBody SysNotice notice) {
74
+        notice.setUpdateByUserId(getUserId());
75
+        notice.setUpdateBy(getLoginUser().getUser().getNickName());
76
+        return toAjax(noticeService.updateNotice(notice));
77
+    }
78
+
79
+    /**
80
+     * 删除通知公告
81
+     */
82
+    @PreAuthorize("@ss.hasPermi('system:notice:remove')")
83
+    @Log(title = "通知公告", businessType = BusinessType.DELETE)
84
+    @DeleteMapping("/{noticeIds}")
85
+    public AjaxResult remove(@PathVariable Long[] noticeIds) {
86
+        return toAjax(noticeService.deleteNoticeByIds(noticeIds));
87
+    }
88
+}

+ 136 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysPostController.java

@@ -0,0 +1,136 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.beans.factory.annotation.Autowired;
7
+import org.springframework.security.access.prepost.PreAuthorize;
8
+import org.springframework.validation.annotation.Validated;
9
+import org.springframework.web.bind.annotation.DeleteMapping;
10
+import org.springframework.web.bind.annotation.GetMapping;
11
+import org.springframework.web.bind.annotation.PathVariable;
12
+import org.springframework.web.bind.annotation.PostMapping;
13
+import org.springframework.web.bind.annotation.PutMapping;
14
+import org.springframework.web.bind.annotation.RequestBody;
15
+import org.springframework.web.bind.annotation.RequestMapping;
16
+import org.springframework.web.bind.annotation.RestController;
17
+import com.sundot.airport.common.annotation.Log;
18
+import com.sundot.airport.common.core.controller.BaseController;
19
+import com.sundot.airport.common.core.domain.AjaxResult;
20
+import com.sundot.airport.common.core.page.TableDataInfo;
21
+import com.sundot.airport.common.enums.BusinessType;
22
+import com.sundot.airport.common.utils.poi.ExcelUtil;
23
+import com.sundot.airport.system.domain.SysPost;
24
+import com.sundot.airport.system.service.ISysPostService;
25
+
26
+/**
27
+ * 岗位信息操作处理
28
+ *
29
+ * @author ruoyi
30
+ */
31
+@RestController
32
+@RequestMapping("/system/post")
33
+public class SysPostController extends BaseController {
34
+    @Autowired
35
+    private ISysPostService postService;
36
+
37
+    /**
38
+     * 获取岗位列表
39
+     */
40
+    @PreAuthorize("@ss.hasPermi('system:post:list')")
41
+    @GetMapping("/list")
42
+    public TableDataInfo list(SysPost post) {
43
+        startPage();
44
+        List<SysPost> list = postService.selectPostList(post);
45
+        return getDataTable(list);
46
+    }
47
+
48
+    @Log(title = "岗位管理", businessType = BusinessType.EXPORT)
49
+    @PreAuthorize("@ss.hasPermi('system:post:export')")
50
+    @PostMapping("/export")
51
+    public void export(HttpServletResponse response, SysPost post) {
52
+        List<SysPost> list = postService.selectPostList(post);
53
+        ExcelUtil<SysPost> util = new ExcelUtil<SysPost>(SysPost.class);
54
+        util.exportExcel(response, list, "岗位数据");
55
+    }
56
+
57
+    /**
58
+     * 根据岗位编号获取详细信息
59
+     */
60
+    @PreAuthorize("@ss.hasPermi('system:post:query')")
61
+    @GetMapping(value = "/{postId}")
62
+    public AjaxResult getInfo(@PathVariable Long postId) {
63
+        return success(postService.selectPostById(postId));
64
+    }
65
+
66
+    /**
67
+     * 新增岗位
68
+     */
69
+    @PreAuthorize("@ss.hasPermi('system:post:add')")
70
+    @Log(title = "岗位管理", businessType = BusinessType.INSERT)
71
+    @PostMapping
72
+    public AjaxResult add(@Validated @RequestBody SysPost post) {
73
+        if (!postService.checkPostNameUnique(post)) {
74
+            return error("新增岗位'" + post.getPostName() + "'失败,岗位名称已存在");
75
+        } else if (!postService.checkPostCodeUnique(post)) {
76
+            return error("新增岗位'" + post.getPostName() + "'失败,岗位编码已存在");
77
+        }
78
+        post.setCreateBy(getUsername());
79
+        return toAjax(postService.insertPost(post));
80
+    }
81
+
82
+    /**
83
+     * 修改岗位
84
+     */
85
+    @PreAuthorize("@ss.hasPermi('system:post:edit')")
86
+    @Log(title = "岗位管理", businessType = BusinessType.UPDATE)
87
+    @PutMapping
88
+    public AjaxResult edit(@Validated @RequestBody SysPost post) {
89
+        if (!postService.checkPostNameUnique(post)) {
90
+            return error("修改岗位'" + post.getPostName() + "'失败,岗位名称已存在");
91
+        } else if (!postService.checkPostCodeUnique(post)) {
92
+            return error("修改岗位'" + post.getPostName() + "'失败,岗位编码已存在");
93
+        }
94
+        post.setUpdateBy(getUsername());
95
+        return toAjax(postService.updatePost(post));
96
+    }
97
+
98
+    /**
99
+     * 删除岗位
100
+     */
101
+    @PreAuthorize("@ss.hasPermi('system:post:remove')")
102
+    @Log(title = "岗位管理", businessType = BusinessType.DELETE)
103
+    @DeleteMapping("/{postIds}")
104
+    public AjaxResult remove(@PathVariable Long[] postIds) {
105
+        return toAjax(postService.deletePostByIds(postIds));
106
+    }
107
+
108
+    /**
109
+     * 获取岗位选择框列表
110
+     */
111
+    @GetMapping("/optionselect")
112
+    public AjaxResult optionselect() {
113
+        List<SysPost> posts = postService.selectPostAll();
114
+        return success(posts);
115
+    }
116
+
117
+    /**
118
+     * 获取所有岗位列表
119
+     */
120
+    @PreAuthorize("@ss.hasPermi('system:post:list')")
121
+    @GetMapping("/listAll")
122
+    public AjaxResult listAll(SysPost post) {
123
+        List<SysPost> list = postService.selectPostList(post);
124
+        return success(list);
125
+    }
126
+
127
+    /**
128
+     * 查询岗位信息集合树形结构
129
+     */
130
+    @PreAuthorize("@ss.hasPermi('system:post:list')")
131
+    @GetMapping("/listAllTree")
132
+    public AjaxResult listAllTree(SysPost post) {
133
+        List<SysPost> list = postService.selectPostListTree(post);
134
+        return success(list);
135
+    }
136
+}

+ 148 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysProfileController.java

@@ -0,0 +1,148 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.Map;
4
+import org.springframework.beans.factory.annotation.Autowired;
5
+import org.springframework.web.bind.annotation.GetMapping;
6
+import org.springframework.web.bind.annotation.PostMapping;
7
+import org.springframework.web.bind.annotation.PutMapping;
8
+import org.springframework.web.bind.annotation.RequestBody;
9
+import org.springframework.web.bind.annotation.RequestMapping;
10
+import org.springframework.web.bind.annotation.RequestParam;
11
+import org.springframework.web.bind.annotation.RestController;
12
+import org.springframework.web.multipart.MultipartFile;
13
+import com.sundot.airport.common.annotation.Log;
14
+import com.sundot.airport.common.config.RuoYiConfig;
15
+import com.sundot.airport.common.core.controller.BaseController;
16
+import com.sundot.airport.common.core.domain.AjaxResult;
17
+import com.sundot.airport.common.core.domain.entity.SysUser;
18
+import com.sundot.airport.common.core.domain.model.LoginUser;
19
+import com.sundot.airport.common.enums.BusinessType;
20
+import com.sundot.airport.common.utils.DateUtils;
21
+import com.sundot.airport.common.utils.SecurityUtils;
22
+import com.sundot.airport.common.utils.StringUtils;
23
+import com.sundot.airport.common.utils.file.FileUploadUtils;
24
+import com.sundot.airport.common.utils.file.FileUtils;
25
+import com.sundot.airport.common.utils.file.MimeTypeUtils;
26
+import com.sundot.airport.framework.web.service.TokenService;
27
+import com.sundot.airport.system.service.ISysUserService;
28
+
29
+/**
30
+ * 个人信息 业务处理
31
+ * 
32
+ * @author ruoyi
33
+ */
34
+@RestController
35
+@RequestMapping("/system/user/profile")
36
+public class SysProfileController extends BaseController
37
+{
38
+    @Autowired
39
+    private ISysUserService userService;
40
+
41
+    @Autowired
42
+    private TokenService tokenService;
43
+
44
+    /**
45
+     * 个人信息
46
+     */
47
+    @GetMapping
48
+    public AjaxResult profile()
49
+    {
50
+        LoginUser loginUser = getLoginUser();
51
+        SysUser user = loginUser.getUser();
52
+        AjaxResult ajax = AjaxResult.success(user);
53
+        ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername()));
54
+        ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername()));
55
+        return ajax;
56
+    }
57
+
58
+    /**
59
+     * 修改用户
60
+     */
61
+    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
62
+    @PutMapping
63
+    public AjaxResult updateProfile(@RequestBody SysUser user)
64
+    {
65
+        LoginUser loginUser = getLoginUser();
66
+        SysUser currentUser = loginUser.getUser();
67
+        currentUser.setNickName(user.getNickName());
68
+        currentUser.setEmail(user.getEmail());
69
+        currentUser.setPhonenumber(user.getPhonenumber());
70
+        currentUser.setSex(user.getSex());
71
+        if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser))
72
+        {
73
+            return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在");
74
+        }
75
+        if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser))
76
+        {
77
+            return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在");
78
+        }
79
+        if (userService.updateUserProfile(currentUser) > 0)
80
+        {
81
+            // 更新缓存用户信息
82
+            tokenService.setLoginUser(loginUser);
83
+            return success();
84
+        }
85
+        return error("修改个人信息异常,请联系管理员");
86
+    }
87
+
88
+    /**
89
+     * 重置密码
90
+     */
91
+    @Log(title = "个人信息", businessType = BusinessType.UPDATE)
92
+    @PutMapping("/updatePwd")
93
+    public AjaxResult updatePwd(@RequestBody Map<String, String> params)
94
+    {
95
+        String oldPassword = params.get("oldPassword");
96
+        String newPassword = params.get("newPassword");
97
+        LoginUser loginUser = getLoginUser();
98
+        Long userId = loginUser.getUserId();
99
+        String password = loginUser.getPassword();
100
+        if (!SecurityUtils.matchesPassword(oldPassword, password))
101
+        {
102
+            return error("修改密码失败,旧密码错误");
103
+        }
104
+        if (SecurityUtils.matchesPassword(newPassword, password))
105
+        {
106
+            return error("新密码不能与旧密码相同");
107
+        }
108
+        newPassword = SecurityUtils.encryptPassword(newPassword);
109
+        if (userService.resetUserPwd(userId, newPassword) > 0)
110
+        {
111
+            // 更新缓存用户密码&密码最后更新时间
112
+            loginUser.getUser().setPwdUpdateDate(DateUtils.getNowDate());
113
+            loginUser.getUser().setPassword(newPassword);
114
+            tokenService.setLoginUser(loginUser);
115
+            return success();
116
+        }
117
+        return error("修改密码异常,请联系管理员");
118
+    }
119
+
120
+    /**
121
+     * 头像上传
122
+     */
123
+    @Log(title = "用户头像", businessType = BusinessType.UPDATE)
124
+    @PostMapping("/avatar")
125
+    public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception
126
+    {
127
+        if (!file.isEmpty())
128
+        {
129
+            LoginUser loginUser = getLoginUser();
130
+            String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION, true);
131
+            if (userService.updateUserAvatar(loginUser.getUserId(), avatar))
132
+            {
133
+                String oldAvatar = loginUser.getUser().getAvatar();
134
+                if (StringUtils.isNotEmpty(oldAvatar))
135
+                {
136
+                    FileUtils.deleteFile(RuoYiConfig.getProfile() + FileUtils.stripPrefix(oldAvatar));
137
+                }
138
+                AjaxResult ajax = AjaxResult.success();
139
+                ajax.put("imgUrl", avatar);
140
+                // 更新缓存用户头像
141
+                loginUser.getUser().setAvatar(avatar);
142
+                tokenService.setLoginUser(loginUser);
143
+                return ajax;
144
+            }
145
+        }
146
+        return error("上传图片异常,请联系管理员");
147
+    }
148
+}

+ 38 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysRegisterController.java

@@ -0,0 +1,38 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import org.springframework.beans.factory.annotation.Autowired;
4
+import org.springframework.web.bind.annotation.PostMapping;
5
+import org.springframework.web.bind.annotation.RequestBody;
6
+import org.springframework.web.bind.annotation.RestController;
7
+import com.sundot.airport.common.core.controller.BaseController;
8
+import com.sundot.airport.common.core.domain.AjaxResult;
9
+import com.sundot.airport.common.core.domain.model.RegisterBody;
10
+import com.sundot.airport.common.utils.StringUtils;
11
+import com.sundot.airport.framework.web.service.SysRegisterService;
12
+import com.sundot.airport.system.service.ISysConfigService;
13
+
14
+/**
15
+ * 注册验证
16
+ * 
17
+ * @author ruoyi
18
+ */
19
+@RestController
20
+public class SysRegisterController extends BaseController
21
+{
22
+    @Autowired
23
+    private SysRegisterService registerService;
24
+
25
+    @Autowired
26
+    private ISysConfigService configService;
27
+
28
+    @PostMapping("/register")
29
+    public AjaxResult register(@RequestBody RegisterBody user)
30
+    {
31
+        if (!("true".equals(configService.selectConfigByKey("sys.account.registerUser"))))
32
+        {
33
+            return error("当前系统没有开启注册功能!");
34
+        }
35
+        String msg = registerService.register(user);
36
+        return StringUtils.isEmpty(msg) ? success() : error(msg);
37
+    }
38
+}

+ 239 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysRoleController.java

@@ -0,0 +1,239 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.beans.factory.annotation.Autowired;
7
+import org.springframework.security.access.prepost.PreAuthorize;
8
+import org.springframework.validation.annotation.Validated;
9
+import org.springframework.web.bind.annotation.DeleteMapping;
10
+import org.springframework.web.bind.annotation.GetMapping;
11
+import org.springframework.web.bind.annotation.PathVariable;
12
+import org.springframework.web.bind.annotation.PostMapping;
13
+import org.springframework.web.bind.annotation.PutMapping;
14
+import org.springframework.web.bind.annotation.RequestBody;
15
+import org.springframework.web.bind.annotation.RequestMapping;
16
+import org.springframework.web.bind.annotation.RestController;
17
+import com.sundot.airport.common.annotation.Log;
18
+import com.sundot.airport.common.core.controller.BaseController;
19
+import com.sundot.airport.common.core.domain.AjaxResult;
20
+import com.sundot.airport.common.core.domain.entity.SysDept;
21
+import com.sundot.airport.common.core.domain.entity.SysRole;
22
+import com.sundot.airport.common.core.domain.entity.SysUser;
23
+import com.sundot.airport.common.core.domain.model.LoginUser;
24
+import com.sundot.airport.common.core.page.TableDataInfo;
25
+import com.sundot.airport.common.enums.BusinessType;
26
+import com.sundot.airport.common.utils.StringUtils;
27
+import com.sundot.airport.common.utils.poi.ExcelUtil;
28
+import com.sundot.airport.framework.web.service.SysPermissionService;
29
+import com.sundot.airport.framework.web.service.TokenService;
30
+import com.sundot.airport.system.domain.SysUserRole;
31
+import com.sundot.airport.system.service.ISysDeptService;
32
+import com.sundot.airport.system.service.ISysRoleService;
33
+import com.sundot.airport.system.service.ISysUserService;
34
+
35
+/**
36
+ * 角色信息
37
+ *
38
+ * @author ruoyi
39
+ */
40
+@RestController
41
+@RequestMapping("/system/role")
42
+public class SysRoleController extends BaseController {
43
+    @Autowired
44
+    private ISysRoleService roleService;
45
+
46
+    @Autowired
47
+    private TokenService tokenService;
48
+
49
+    @Autowired
50
+    private SysPermissionService permissionService;
51
+
52
+    @Autowired
53
+    private ISysUserService userService;
54
+
55
+    @Autowired
56
+    private ISysDeptService deptService;
57
+
58
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
59
+    @GetMapping("/list")
60
+    public TableDataInfo list(SysRole role) {
61
+        startPage();
62
+        List<SysRole> list = roleService.selectRoleList(role);
63
+        return getDataTable(list);
64
+    }
65
+
66
+    @Log(title = "角色管理", businessType = BusinessType.EXPORT)
67
+    @PreAuthorize("@ss.hasPermi('system:role:export')")
68
+    @PostMapping("/export")
69
+    public void export(HttpServletResponse response, SysRole role) {
70
+        List<SysRole> list = roleService.selectRoleList(role);
71
+        ExcelUtil<SysRole> util = new ExcelUtil<SysRole>(SysRole.class);
72
+        util.exportExcel(response, list, "角色数据");
73
+    }
74
+
75
+    /**
76
+     * 根据角色编号获取详细信息
77
+     */
78
+    @PreAuthorize("@ss.hasPermi('system:role:query')")
79
+    @GetMapping(value = "/{roleId}")
80
+    public AjaxResult getInfo(@PathVariable Long roleId) {
81
+        roleService.checkRoleDataScope(roleId);
82
+        return success(roleService.selectRoleById(roleId));
83
+    }
84
+
85
+    /**
86
+     * 新增角色
87
+     */
88
+    @PreAuthorize("@ss.hasPermi('system:role:add')")
89
+    @Log(title = "角色管理", businessType = BusinessType.INSERT)
90
+    @PostMapping
91
+    public AjaxResult add(@Validated @RequestBody SysRole role) {
92
+        if (!roleService.checkRoleNameUnique(role)) {
93
+            return error("新增角色'" + role.getRoleName() + "'失败,角色名称已存在");
94
+        } else if (!roleService.checkRoleKeyUnique(role)) {
95
+            return error("新增角色'" + role.getRoleName() + "'失败,角色权限已存在");
96
+        }
97
+        role.setCreateBy(getUsername());
98
+        return toAjax(roleService.insertRole(role));
99
+
100
+    }
101
+
102
+    /**
103
+     * 修改保存角色
104
+     */
105
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
106
+    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
107
+    @PutMapping
108
+    public AjaxResult edit(@Validated @RequestBody SysRole role) {
109
+        roleService.checkRoleAllowed(role);
110
+        roleService.checkRoleDataScope(role.getRoleId());
111
+        if (!roleService.checkRoleNameUnique(role)) {
112
+            return error("修改角色'" + role.getRoleName() + "'失败,角色名称已存在");
113
+        } else if (!roleService.checkRoleKeyUnique(role)) {
114
+            return error("修改角色'" + role.getRoleName() + "'失败,角色权限已存在");
115
+        }
116
+        role.setUpdateBy(getUsername());
117
+
118
+        if (roleService.updateRole(role) > 0) {
119
+            // 更新缓存用户权限
120
+            LoginUser loginUser = getLoginUser();
121
+            if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin()) {
122
+                loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName()));
123
+                loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser()));
124
+                tokenService.setLoginUser(loginUser);
125
+            }
126
+            return success();
127
+        }
128
+        return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员");
129
+    }
130
+
131
+    /**
132
+     * 修改保存数据权限
133
+     */
134
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
135
+    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
136
+    @PutMapping("/dataScope")
137
+    public AjaxResult dataScope(@RequestBody SysRole role) {
138
+        roleService.checkRoleAllowed(role);
139
+        roleService.checkRoleDataScope(role.getRoleId());
140
+        return toAjax(roleService.authDataScope(role));
141
+    }
142
+
143
+    /**
144
+     * 状态修改
145
+     */
146
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
147
+    @Log(title = "角色管理", businessType = BusinessType.UPDATE)
148
+    @PutMapping("/changeStatus")
149
+    public AjaxResult changeStatus(@RequestBody SysRole role) {
150
+        roleService.checkRoleAllowed(role);
151
+        roleService.checkRoleDataScope(role.getRoleId());
152
+        role.setUpdateBy(getUsername());
153
+        return toAjax(roleService.updateRoleStatus(role));
154
+    }
155
+
156
+    /**
157
+     * 删除角色
158
+     */
159
+    @PreAuthorize("@ss.hasPermi('system:role:remove')")
160
+    @Log(title = "角色管理", businessType = BusinessType.DELETE)
161
+    @DeleteMapping("/{roleIds}")
162
+    public AjaxResult remove(@PathVariable Long[] roleIds) {
163
+        return toAjax(roleService.deleteRoleByIds(roleIds));
164
+    }
165
+
166
+    /**
167
+     * 获取角色选择框列表
168
+     */
169
+    @PreAuthorize("@ss.hasPermi('system:role:query')")
170
+    @GetMapping("/optionselect")
171
+    public AjaxResult optionselect() {
172
+        return success(roleService.selectRoleAll());
173
+    }
174
+
175
+    /**
176
+     * 查询已分配用户角色列表
177
+     */
178
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
179
+    @GetMapping("/authUser/allocatedList")
180
+    public TableDataInfo allocatedList(SysUser user) {
181
+        startPage();
182
+        List<SysUser> list = userService.selectAllocatedList(user);
183
+        return getDataTable(list);
184
+    }
185
+
186
+    /**
187
+     * 查询未分配用户角色列表
188
+     */
189
+    @PreAuthorize("@ss.hasPermi('system:role:list')")
190
+    @GetMapping("/authUser/unallocatedList")
191
+    public TableDataInfo unallocatedList(SysUser user) {
192
+        startPage();
193
+        List<SysUser> list = userService.selectUnallocatedList(user);
194
+        return getDataTable(list);
195
+    }
196
+
197
+    /**
198
+     * 取消授权用户
199
+     */
200
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
201
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
202
+    @PutMapping("/authUser/cancel")
203
+    public AjaxResult cancelAuthUser(@RequestBody SysUserRole userRole) {
204
+        return toAjax(roleService.deleteAuthUser(userRole));
205
+    }
206
+
207
+    /**
208
+     * 批量取消授权用户
209
+     */
210
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
211
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
212
+    @PutMapping("/authUser/cancelAll")
213
+    public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds) {
214
+        return toAjax(roleService.deleteAuthUsers(roleId, userIds));
215
+    }
216
+
217
+    /**
218
+     * 批量选择用户授权
219
+     */
220
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
221
+    @Log(title = "角色管理", businessType = BusinessType.GRANT)
222
+    @PutMapping("/authUser/selectAll")
223
+    public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds) {
224
+        roleService.checkRoleDataScope(roleId);
225
+        return toAjax(roleService.insertAuthUsers(roleId, userIds));
226
+    }
227
+
228
+    /**
229
+     * 获取对应角色部门树列表
230
+     */
231
+    @PreAuthorize("@ss.hasPermi('system:role:query')")
232
+    @GetMapping(value = "/deptTree/{roleId}")
233
+    public AjaxResult deptTree(@PathVariable("roleId") Long roleId) {
234
+        AjaxResult ajax = AjaxResult.success();
235
+        ajax.put("checkedKeys", deptService.selectDeptListByRoleId(roleId));
236
+        ajax.put("depts", deptService.selectDeptTreeList(new SysDept()));
237
+        return ajax;
238
+    }
239
+}

+ 264 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysUsageReportController.java

@@ -0,0 +1,264 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import cn.hutool.core.collection.CollUtil;
4
+import cn.hutool.core.date.DatePattern;
5
+import cn.hutool.core.date.DateUtil;
6
+import cn.hutool.core.util.ObjectUtil;
7
+import cn.hutool.core.util.StrUtil;
8
+import com.sundot.airport.attendance.domain.AttendanceRecord;
9
+import com.sundot.airport.attendance.service.IAttendanceRecordService;
10
+import com.sundot.airport.check.service.ICheckLargeScreenService;
11
+import com.sundot.airport.common.core.controller.BaseController;
12
+import com.sundot.airport.common.core.domain.AjaxResult;
13
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
14
+import com.sundot.airport.common.core.domain.SysUsageReportDto;
15
+import com.sundot.airport.common.core.domain.entity.SysDept;
16
+import com.sundot.airport.common.core.domain.entity.SysUser;
17
+import com.sundot.airport.common.dto.BaseCommonDto;
18
+import com.sundot.airport.common.dto.QuizAccuracyAnalysisResultDto;
19
+import com.sundot.airport.common.enums.DeptTypeEnum;
20
+import com.sundot.airport.common.exception.ServiceException;
21
+import com.sundot.airport.common.utils.DeptUtils;
22
+import com.sundot.airport.exam.dto.DashboardOverviewDTO;
23
+import com.sundot.airport.exam.dto.DashboardQueryDTO;
24
+import com.sundot.airport.exam.dto.DashboardUserRankingDTO;
25
+import com.sundot.airport.exam.service.IAccuracyStatisticsService;
26
+import com.sundot.airport.exam.service.IDashboardService;
27
+import com.sundot.airport.item.service.ItemLargeScreenService;
28
+import com.sundot.airport.system.service.ISysDeptService;
29
+import com.sundot.airport.system.service.ISysUserService;
30
+import org.springframework.beans.factory.annotation.Autowired;
31
+import org.springframework.web.bind.annotation.GetMapping;
32
+import org.springframework.web.bind.annotation.RequestMapping;
33
+import org.springframework.web.bind.annotation.RestController;
34
+
35
+import java.math.BigDecimal;
36
+import java.time.DayOfWeek;
37
+import java.time.LocalDate;
38
+import java.time.ZoneId;
39
+import java.util.ArrayList;
40
+import java.util.Date;
41
+import java.util.List;
42
+import java.util.stream.Collectors;
43
+
44
+/**
45
+ * 使用报告
46
+ *
47
+ * @author ruoyi
48
+ */
49
+@RestController
50
+@RequestMapping("/system/usageReport")
51
+public class SysUsageReportController extends BaseController {
52
+
53
+    @Autowired
54
+    private ISysDeptService sysDeptService;
55
+
56
+    @Autowired
57
+    private ISysUserService sysUserService;
58
+
59
+    @Autowired
60
+    private ICheckLargeScreenService checkLargeScreenService;
61
+
62
+    @Autowired
63
+    private ItemLargeScreenService itemLargeScreenService;
64
+
65
+    @Autowired
66
+    private IAttendanceRecordService attendanceRecordService;
67
+
68
+    @Autowired
69
+    private IDashboardService dashboardService;
70
+
71
+    @Autowired
72
+    private IAccuracyStatisticsService accuracyStatisticsService;
73
+
74
+    /**
75
+     * 使用报告
76
+     */
77
+    @GetMapping("/report")
78
+    public AjaxResult checkAnalysisReportCheckProblemDistributionDto(BaseLargeScreenQueryParamDto paramDto) {
79
+        SysUsageReportDto result = getSysUsageReportDto(paramDto);
80
+        return success(result);
81
+    }
82
+
83
+    /**
84
+     * 获取使用报告
85
+     */
86
+    private SysUsageReportDto getSysUsageReportDto(BaseLargeScreenQueryParamDto paramDto) {
87
+        if (ObjectUtil.isNull(paramDto.getStartDate()) || ObjectUtil.isNull(paramDto.getEndDate())) {
88
+            Date lastMonday = getLastWeekMonday();
89
+            Date lastSunday = getLastWeekSunday();
90
+            paramDto.setStartDate(lastMonday);
91
+            paramDto.setEndDate(lastSunday);
92
+        }
93
+        SysUsageReportDto result = new SysUsageReportDto();
94
+        result.setStartDate(paramDto.getStartDate());
95
+        result.setEndDate(paramDto.getEndDate());
96
+        result.setBasicSituation(getBasicSituation(paramDto));
97
+        result.setRunSituation(getRunSituation(paramDto));
98
+        return result;
99
+    }
100
+
101
+    /**
102
+     * 获取上周周一的日期 (java.util.Date)
103
+     */
104
+    public static Date getLastWeekMonday() {
105
+        LocalDate today = LocalDate.now();
106
+        // 计算本周周一(ISO标准:周一为1,周日为7)
107
+        LocalDate mondayThisWeek = today.with(DayOfWeek.MONDAY);
108
+        // 上周周一 = 本周周一 - 7天
109
+        LocalDate lastMonday = mondayThisWeek.minusWeeks(1);
110
+
111
+        return convertToUtilDate(lastMonday);
112
+    }
113
+
114
+    /**
115
+     * 获取上周周日的日期 (java.util.Date)
116
+     */
117
+    public static Date getLastWeekSunday() {
118
+        LocalDate today = LocalDate.now();
119
+        // 计算本周周一
120
+        LocalDate mondayThisWeek = today.with(DayOfWeek.MONDAY);
121
+        // 上周周日 = 本周周一 - 1天
122
+        LocalDate lastSunday = mondayThisWeek.minusDays(1);
123
+
124
+        return convertToUtilDate(lastSunday);
125
+    }
126
+
127
+    /**
128
+     * 将LocalDate转换为java.util.Date
129
+     */
130
+    private static Date convertToUtilDate(LocalDate localDate) {
131
+        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
132
+    }
133
+
134
+    /**
135
+     * 获取基本情况
136
+     */
137
+    private SysUsageReportDto.BasicSituation getBasicSituation(BaseLargeScreenQueryParamDto paramDto) {
138
+        SysUsageReportDto.BasicSituation basicSituation = new SysUsageReportDto.BasicSituation();
139
+        List<SysUsageReportDto.SysDeptInfo> sysDeptInfoList = new ArrayList<>();
140
+        // 获取当前用户所在站点ID
141
+        Long topSiteId = DeptUtils.getTopSiteId(sysDeptService.selectDeptById(getDeptId()));
142
+        if (ObjectUtil.isNull(topSiteId)) {
143
+            throw new ServiceException("无法找到有效的站点信息");
144
+        }
145
+        List<SysDept> deptList = sysDeptService.selectChildrenDeptById(topSiteId);
146
+        deptList = deptList.stream().filter(item -> StrUtil.equals(DeptTypeEnum.BRIGADE.getCode(), item.getDeptType())).collect(Collectors.toList());
147
+        deptList.forEach(item -> {
148
+            SysUsageReportDto.SysDeptInfo sysDeptInfo = new SysUsageReportDto.SysDeptInfo();
149
+            List<SysUser> sysUserList = sysUserService.selectUserByDeptId(item.getDeptId());
150
+//            List<SysUser> sysUserList = sysUserService.selectUserListByRoleKeyAndDeptId(Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()), item.getId());
151
+            sysDeptInfo.setDeptId(item.getDeptId());
152
+            sysDeptInfo.setDeptName(item.getDeptName());
153
+            sysDeptInfo.setPeopleNumber(sysUserList.size());
154
+            sysDeptInfoList.add(sysDeptInfo);
155
+        });
156
+        basicSituation.setSysDeptInfoList(sysDeptInfoList);
157
+        basicSituation.setSysDeptInfoListDesc(sysDeptInfoList.stream().map(SysUsageReportDto.SysDeptInfo::getDeptName).collect(Collectors.joining("、")));
158
+        basicSituation.setTotalPeopleNumber(sysDeptInfoList.stream().mapToInt(SysUsageReportDto.SysDeptInfo::getPeopleNumber).sum());
159
+        return basicSituation;
160
+    }
161
+
162
+    /**
163
+     * 获取运行情况
164
+     */
165
+    private SysUsageReportDto.RunSituation getRunSituation(BaseLargeScreenQueryParamDto paramDto) {
166
+        SysUsageReportDto.RunSituation runSituation = new SysUsageReportDto.RunSituation();
167
+        runSituation.setAttendanceModule(getAttendanceModule(paramDto));
168
+        runSituation.setSeizureModule(getSeizureModule(paramDto));
169
+        runSituation.setExamModule(getExamModule(paramDto));
170
+        runSituation.setCheckModule(getCheckModule(paramDto));
171
+        return runSituation;
172
+    }
173
+
174
+    /**
175
+     * 获取勤务模块
176
+     */
177
+    private SysUsageReportDto.AttendanceModule getAttendanceModule(BaseLargeScreenQueryParamDto paramDto) {
178
+        SysUsageReportDto.AttendanceModule attendanceModule = new SysUsageReportDto.AttendanceModule();
179
+        AttendanceRecord attendanceRecordReq = new AttendanceRecord();
180
+        attendanceRecordReq.setAttendanceDateStart(paramDto.getStartDate());
181
+        attendanceRecordReq.setAttendanceDateEnd(paramDto.getEndDate());
182
+        List<AttendanceRecord> attendanceRecordList = attendanceRecordService.selectAttendanceRecordList(attendanceRecordReq);
183
+        if (CollUtil.isEmpty(attendanceRecordList)) {
184
+            return null;
185
+        }
186
+        List<SysUsageReportDto.SysDeptInfo> sysDeptInfoList = new ArrayList<>();
187
+        attendanceRecordList.stream().filter(item -> StrUtil.isNotBlank(item.getBrigadeCode())).collect(Collectors.groupingBy(item -> item.getBrigadeCode() + "_" + item.getBrigadeName())).forEach((key, value) -> {
188
+            SysUsageReportDto.SysDeptInfo sysDeptInfo = new SysUsageReportDto.SysDeptInfo();
189
+            sysDeptInfo.setDeptId(Long.valueOf(key.split("_")[0]));
190
+            sysDeptInfo.setDeptName(key.split("_")[1]);
191
+            sysDeptInfo.setPeopleNumber((int) value.stream().map(AttendanceRecord::getUserId).distinct().count());
192
+            sysDeptInfoList.add(sysDeptInfo);
193
+        });
194
+        attendanceModule.setSysDeptInfoList(sysDeptInfoList);
195
+        attendanceModule.setSysDeptInfoListDesc(sysDeptInfoList.stream().map(item -> item.getDeptName() + item.getPeopleNumber() + "人").collect(Collectors.joining("、")));
196
+        attendanceModule.setTotalPeopleNumber(sysDeptInfoList.stream().mapToInt(SysUsageReportDto.SysDeptInfo::getPeopleNumber).sum());
197
+        return attendanceModule;
198
+    }
199
+
200
+    /**
201
+     * 获取查获模块
202
+     */
203
+    private SysUsageReportDto.SeizureModule getSeizureModule(BaseLargeScreenQueryParamDto paramDto) {
204
+        SysUsageReportDto.SeizureModule seizureModule = itemLargeScreenService.getSeizureModule(paramDto);
205
+        return seizureModule;
206
+    }
207
+
208
+    /**
209
+     * 获取抽问抽答模块
210
+     */
211
+    private SysUsageReportDto.ExamModule getExamModule(BaseLargeScreenQueryParamDto paramDto) {
212
+        SysUsageReportDto.ExamModule examModule = new SysUsageReportDto.ExamModule();
213
+
214
+        String startDateStr = DateUtil.format(paramDto.getStartDate(), DatePattern.NORM_DATE_PATTERN);
215
+        String endDateStr = DateUtil.format(paramDto.getEndDate(), DatePattern.NORM_DATE_PATTERN);
216
+
217
+        DashboardQueryDTO query = new DashboardQueryDTO();
218
+        query.setStartDate(startDateStr);
219
+        query.setEndDate(endDateStr);
220
+        query.setTimeRange("custom");
221
+
222
+        // 总项数、平均分
223
+        DashboardOverviewDTO overview = dashboardService.getOverview(query);
224
+        if (overview != null) {
225
+            examModule.setTotalItem(overview.getCompletedTasks() != null ? overview.getCompletedTasks().intValue() : 0);
226
+            examModule.setAverageScore(overview.getAvgScore() != null ? BigDecimal.valueOf(overview.getAvgScore()) : BigDecimal.ZERO);
227
+        }
228
+
229
+        // 参与人数(有完成任务的用户数)
230
+        List<DashboardUserRankingDTO> allUsers = dashboardService.getAllUsersRanking(query);
231
+        if (CollUtil.isNotEmpty(allUsers)) {
232
+            long participantCount = allUsers.stream()
233
+                    .filter(user -> user.getCompletedTasks() != null && user.getCompletedTasks() > 0)
234
+                    .count();
235
+            examModule.setTotalPeopleNumber((int) participantCount);
236
+        } else {
237
+            examModule.setTotalPeopleNumber(0);
238
+        }
239
+
240
+        // 各分类正确率列表
241
+        QuizAccuracyAnalysisResultDto accuracyAnalysis = accuracyStatisticsService.getQuizAccuracyAnalysis(startDateStr, endDateStr);
242
+        if (accuracyAnalysis != null && CollUtil.isNotEmpty(accuracyAnalysis.getCategoryList())) {
243
+            List<BaseCommonDto> typeList = accuracyAnalysis.getCategoryList().stream().map(item -> {
244
+                BaseCommonDto dto = new BaseCommonDto();
245
+                dto.setName(item.getCategoryName());
246
+                dto.setScale(item.getCorrectRate());
247
+                dto.setRemark(item.getLabel());
248
+                return dto;
249
+            }).collect(Collectors.toList());
250
+            examModule.setTypeList(typeList);
251
+        }
252
+
253
+        return examModule;
254
+    }
255
+
256
+    /**
257
+     * 获取巡检模块
258
+     */
259
+    private SysUsageReportDto.CheckModule getCheckModule(BaseLargeScreenQueryParamDto paramDto) {
260
+        SysUsageReportDto.CheckModule checkModule = checkLargeScreenService.getCheckModule(paramDto);
261
+        return checkModule;
262
+    }
263
+
264
+}

+ 927 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysUserController.java

@@ -0,0 +1,927 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.math.BigDecimal;
4
+import java.math.RoundingMode;
5
+import java.sql.Date;
6
+import java.util.*;
7
+import java.util.stream.Collectors;
8
+import javax.servlet.http.HttpServletResponse;
9
+
10
+import cn.hutool.core.collection.CollUtil;
11
+import cn.hutool.core.collection.CollectionUtil;
12
+import cn.hutool.core.util.ObjectUtil;
13
+import cn.hutool.core.util.StrUtil;
14
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
15
+import com.sundot.airport.attendance.domain.portrait.WorkDateStats;
16
+import com.sundot.airport.attendance.domain.portrait.WorkHoursStats;
17
+import com.sundot.airport.check.domain.CheckLargeScreenCommonDto;
18
+import com.sundot.airport.check.domain.CheckLargeScreenCorrectionDto;
19
+import com.sundot.airport.check.domain.CheckLargeScreenCorrectionQueryParamDto;
20
+import com.sundot.airport.common.core.domain.ResetUserPostDto;
21
+import com.sundot.airport.common.core.domain.ResetUserRoleDto;
22
+import com.sundot.airport.common.core.domain.SysLargeScreenWorkingPortraitDto;
23
+import com.sundot.airport.common.dto.SysUserConditionDto;
24
+import com.sundot.airport.common.enums.RoleTypeEnum;
25
+import com.sundot.airport.exam.domain.DailyTask;
26
+import com.sundot.airport.attendance.portrait.AttendanceModuleIndicatorResult;
27
+import com.sundot.airport.check.service.ICheckLargeScreenService;
28
+import com.sundot.airport.common.constant.CacheConstants;
29
+import com.sundot.airport.common.core.domain.BaseLargeScreenQueryParamDto;
30
+import com.sundot.airport.common.core.domain.SysLargeScreenDetailDto;
31
+import com.sundot.airport.common.core.redis.RedisCache;
32
+import com.sundot.airport.common.domain.portrait.IndicatorCalculateParams;
33
+import com.sundot.airport.common.domain.portrait.ModularIndicatorResult;
34
+import com.sundot.airport.common.enums.DeptTypeEnum;
35
+import com.sundot.airport.common.enums.portrait.UserType;
36
+import com.sundot.airport.item.domain.portrait.ItemSeizureModuleIndicatorResult;
37
+import com.sundot.airport.item.domain.portrait.SeizureStats;
38
+import com.sundot.airport.system.domain.SysLargeScreenCooperationDto;
39
+import com.sundot.airport.system.domain.SysLargeScreenCooperationQueryParamDto;
40
+import com.sundot.airport.system.domain.portrait.IndicatorResult;
41
+import com.sundot.airport.system.domain.portrait.QualificationStats;
42
+import com.sundot.airport.system.domain.portrait.WorkExperienceStats;
43
+import com.sundot.airport.system.service.ISysLearningGrowthService;
44
+import com.sundot.airport.system.service.portrait.UserPortraitService;
45
+import org.apache.commons.lang3.ArrayUtils;
46
+import org.springframework.beans.factory.annotation.Autowired;
47
+import org.springframework.cache.annotation.Cacheable;
48
+import org.springframework.security.access.prepost.PreAuthorize;
49
+import org.springframework.validation.annotation.Validated;
50
+import org.springframework.web.bind.annotation.DeleteMapping;
51
+import org.springframework.web.bind.annotation.GetMapping;
52
+import org.springframework.web.bind.annotation.PathVariable;
53
+import org.springframework.web.bind.annotation.PostMapping;
54
+import org.springframework.web.bind.annotation.PutMapping;
55
+import org.springframework.web.bind.annotation.RequestBody;
56
+import org.springframework.web.bind.annotation.RequestMapping;
57
+import org.springframework.web.bind.annotation.RequestParam;
58
+import org.springframework.web.bind.annotation.RestController;
59
+import org.springframework.web.multipart.MultipartFile;
60
+import com.sundot.airport.common.annotation.Log;
61
+import com.sundot.airport.common.constant.UserConstants;
62
+import com.sundot.airport.common.core.controller.BaseController;
63
+import com.sundot.airport.common.core.domain.AjaxResult;
64
+import com.sundot.airport.common.core.domain.entity.SysDept;
65
+import com.sundot.airport.common.core.domain.entity.SysRole;
66
+import com.sundot.airport.common.core.domain.entity.SysUser;
67
+import com.sundot.airport.common.core.page.TableDataInfo;
68
+import com.sundot.airport.common.enums.BusinessType;
69
+import com.sundot.airport.common.utils.SecurityUtils;
70
+import com.sundot.airport.common.utils.StringUtils;
71
+import com.sundot.airport.common.utils.poi.ExcelUtil;
72
+import com.sundot.airport.system.service.ISysDeptService;
73
+import com.sundot.airport.system.service.ISysPostService;
74
+import com.sundot.airport.system.service.ISysRoleService;
75
+import com.sundot.airport.system.service.ISysUserService;
76
+
77
+/**
78
+ * 用户信息
79
+ *
80
+ * @author ruoyi
81
+ */
82
+@RestController
83
+@RequestMapping("/system/user")
84
+public class SysUserController extends BaseController {
85
+    @Autowired
86
+    private ISysUserService userService;
87
+
88
+    @Autowired
89
+    private ISysRoleService roleService;
90
+
91
+    @Autowired
92
+    private ISysDeptService deptService;
93
+
94
+    @Autowired
95
+    private ISysPostService postService;
96
+
97
+    @Autowired
98
+    private RedisCache redisCache;
99
+
100
+    @Autowired
101
+    private ICheckLargeScreenService checkLargeScreenService;
102
+
103
+    @Autowired
104
+    private ISysLearningGrowthService sysLearningGrowthService;
105
+
106
+    @Autowired
107
+    private ISysUserService iSysUserService;
108
+
109
+    @Autowired
110
+    private UserPortraitService userPortraitService;
111
+
112
+    @Autowired
113
+    private com.sundot.airport.exam.mapper.DailyTaskMapper dailyTaskMapper;
114
+
115
+    /**
116
+     * 获取用户列表
117
+     */
118
+    @PreAuthorize("@ss.hasPermi('system:user:list')")
119
+    @GetMapping("/list")
120
+    public TableDataInfo list(SysUser user) {
121
+        startPage();
122
+        List<SysUser> list = userService.selectUserList(user);
123
+        return getDataTable(list);
124
+    }
125
+
126
+    @Log(title = "用户管理", businessType = BusinessType.EXPORT)
127
+    @PreAuthorize("@ss.hasPermi('system:user:export')")
128
+    @PostMapping("/export")
129
+    public void export(HttpServletResponse response, SysUser user) {
130
+        List<SysUser> list = userService.selectUserList(user);
131
+        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
132
+        util.exportExcel(response, list, "用户数据");
133
+    }
134
+
135
+    @Log(title = "用户管理", businessType = BusinessType.IMPORT)
136
+    @PreAuthorize("@ss.hasPermi('system:user:import')")
137
+    @PostMapping("/importData")
138
+    public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
139
+        ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
140
+        List<SysUser> userList = util.importExcel(file.getInputStream(), 1);
141
+        String operName = getUsername();
142
+        String message = userService.importUser(userList, updateSupport, operName);
143
+        return success(message);
144
+    }
145
+
146
+    @PostMapping("/importTemplate")
147
+    public void importTemplate(HttpServletResponse response) {
148
+        List<SysDept> deptList = deptService.selectDeptList(new SysDept());
149
+        ExcelUtil<SysUser> util = new ExcelUtil<>(SysUser.class);
150
+        util.exportTemplateWithDeptRef(response, deptList, "用户数据导入模板.xlsx");
151
+    }
152
+
153
+    /**
154
+     * 查询用户列表
155
+     */
156
+    @PostMapping("/selectUserListByCondition")
157
+    public AjaxResult selectUserListByCondition(@RequestBody SysUserConditionDto dto) {
158
+        List<SysUser> result = userService.selectUserListByDeptIdAndRoleKeyListAndUserName(dto);
159
+        return success(result);
160
+    }
161
+
162
+    /**
163
+     * 能力画像-协同配合
164
+     */
165
+    @Cacheable(
166
+            value = "statistics_data",
167
+            keyGenerator = "statisticsKeyGenerator",
168
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
169
+    )
170
+    @GetMapping("/cooperation")
171
+    public AjaxResult cooperation(SysLargeScreenCooperationQueryParamDto dto) {
172
+        SysLargeScreenCooperationDto sysLargeScreenCooperationDto = userService.cooperation(dto);
173
+        return success(sysLargeScreenCooperationDto);
174
+    }
175
+
176
+    /**
177
+     * 能力画像-总体概览
178
+     */
179
+    @Cacheable(
180
+            value = "statistics_data",
181
+            keyGenerator = "statisticsKeyGenerator",
182
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
183
+    )
184
+    @GetMapping("/population")
185
+    public AjaxResult population(BaseLargeScreenQueryParamDto dto) {
186
+        // 默认查询当前登录用户的数据
187
+        if (ObjectUtil.isNull(dto.getUserId()) && ObjectUtil.isNull(dto.getDeptId())) {
188
+            dto.setUserId(getUserId());
189
+        }
190
+        // 传入的用户角色为部门管理员时,则查询部门数据。其中班组长不做处理,由前端处理
191
+        if (Objects.nonNull(dto.getUserId())) {
192
+            SysUser sysUser = userService.selectUserById(dto.getUserId());
193
+            List<SysRole> roles = sysUser.getRoles();
194
+            if (CollUtil.isNotEmpty(roles)) {
195
+                List<String> roleKeyList = roles.stream().map(SysRole::getRoleKey).collect(Collectors.toList());
196
+                if (roleKeyList.contains(RoleTypeEnum.admin.getCode()) || roleKeyList.contains(RoleTypeEnum.test.getCode()) || roleKeyList.contains(RoleTypeEnum.zhijianke.getCode()) || roleKeyList.contains(RoleTypeEnum.jingli.getCode()) || roleKeyList.contains(RoleTypeEnum.xingzheng.getCode()) || roleKeyList.contains(RoleTypeEnum.kezhang.getCode())) {
197
+                    dto.setUserId(null);
198
+                    dto.setDeptId(sysUser.getDeptId());
199
+                }
200
+            }
201
+        }
202
+
203
+        // 返回结果
204
+        SysLargeScreenDetailDto result = new SysLargeScreenDetailDto();
205
+        IndicatorCalculateParams indicatorCalculateParams = this.appendParams(dto);
206
+        if (Objects.nonNull(dto.getDeptId())) {
207
+            SysDept sysDept = deptService.selectDeptById(dto.getDeptId());
208
+            if (StrUtil.equals(DeptTypeEnum.STATION.getCode(), sysDept.getDeptType()) || StrUtil.equals(DeptTypeEnum.BRIGADE.getCode(), sysDept.getDeptType()) || StrUtil.equals(DeptTypeEnum.MANAGER.getCode(), sysDept.getDeptType()) || StrUtil.equals(DeptTypeEnum.TEAMS.getCode(), sysDept.getDeptType())) {
209
+                indicatorCalculateParams.setUserType(StrUtil.equals(DeptTypeEnum.STATION.getCode(), sysDept.getDeptType()) ? UserType.STATION : StrUtil.equals(DeptTypeEnum.BRIGADE.getCode(), sysDept.getDeptType()) ? UserType.BRIGADE : StrUtil.equals(DeptTypeEnum.MANAGER.getCode(), sysDept.getDeptType()) ? UserType.DEPARTMENT : UserType.TEAM);
210
+                List<SysUser> sysUsers = iSysUserService.selectUserListByRoleKeyAndDeptId(Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()), dto.getDeptId());
211
+                indicatorCalculateParams.setUserIds(sysUsers.stream().map(SysUser::getUserId).collect(Collectors.toList()));
212
+                indicatorCalculateParams.setDeptId(dto.getDeptId());
213
+            }
214
+        } else {
215
+            indicatorCalculateParams.setUserType(UserType.PERSONAL);
216
+            indicatorCalculateParams.setUserId(dto.getUserId());
217
+        }
218
+
219
+        // 巡检问题总次数
220
+        result.setCheckCount(checkLargeScreenService.population(dto));
221
+        // 学习成长平均分
222
+        result.setLearningGrowthScore(sysLearningGrowthService.population(dto));
223
+        // 工作风格
224
+        result.setWorkingStyle(userService.getWorkingStyle(dto));
225
+        // 主观印象
226
+        result.setSubjectiveImpression(userService.getSubjectiveImpression(dto));
227
+        // 测试平均分(抽问抽答)
228
+        result.setTestScore(calculatePopulationTestScore(dto));
229
+
230
+        //工作产出(有效查获数量)
231
+        this.avgSeizureCount(indicatorCalculateParams, result);
232
+        // 资质等级
233
+        this.qualificationLevel(indicatorCalculateParams, result);
234
+        // 工作履历(安检工作年限)
235
+        this.workingYears(indicatorCalculateParams, result);
236
+        // 出勤投入(上岗时长)
237
+        this.avgWorkingHours(indicatorCalculateParams, result);
238
+
239
+        return success(result);
240
+    }
241
+
242
+    /**
243
+     * 能力画像-明细
244
+     */
245
+    @Cacheable(
246
+            value = "statistics_list",
247
+            keyGenerator = "statisticsKeyGenerator",
248
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
249
+    )
250
+    @GetMapping("/detail")
251
+    public AjaxResult detail(BaseLargeScreenQueryParamDto dto) {
252
+        // 判空处理
253
+        if (ObjectUtil.isNull(dto.getDeptId())) {
254
+            dto.setDeptId(getDeptId());
255
+        }
256
+        if (ObjectUtil.isNull(dto.getDeptId())) {
257
+            return error("部门ID不能为空");
258
+        }
259
+
260
+        IndicatorCalculateParams indicatorCalculateParams = this.appendParams(dto);
261
+
262
+        // 返回结果
263
+        List<SysLargeScreenDetailDto> result = new ArrayList<>();
264
+
265
+        // 人均巡检问题数
266
+        List<SysLargeScreenDetailDto> checkList = checkLargeScreenService.detail(dto);
267
+        Map<Long, BigDecimal> checkCountMap = checkList.stream().collect(Collectors.toMap(SysLargeScreenDetailDto::getId, SysLargeScreenDetailDto::getCheckCount, (oldValue, newValue) -> newValue));
268
+
269
+        // 返回结果处理
270
+        SysDept sysDept = deptService.selectDeptById(dto.getDeptId());
271
+        if (StrUtil.equals(DeptTypeEnum.STATION.getCode(), sysDept.getDeptType()) || StrUtil.equals(DeptTypeEnum.BRIGADE.getCode(), sysDept.getDeptType()) || StrUtil.equals(DeptTypeEnum.MANAGER.getCode(), sysDept.getDeptType())) {
272
+            indicatorCalculateParams.setUserType(StrUtil.equals(DeptTypeEnum.STATION.getCode(), sysDept.getDeptType()) ? UserType.BRIGADE : StrUtil.equals(DeptTypeEnum.BRIGADE.getCode(), sysDept.getDeptType()) ? UserType.DEPARTMENT : UserType.TEAM);
273
+            SysDept temp = new SysDept();
274
+            temp.setParentId(dto.getDeptId());
275
+            List<SysDept> deptList = deptService.selectDeptInfoAll(temp);
276
+            deptList.forEach(item -> {
277
+                SysLargeScreenDetailDto resultItem = new SysLargeScreenDetailDto();
278
+                resultItem.setId(item.getDeptId());
279
+                resultItem.setName(item.getDeptName());
280
+                resultItem.setCheckCount(checkCountMap.getOrDefault(item.getDeptId(), BigDecimal.ZERO));
281
+                //获取指标
282
+                indicatorCalculateParams.setDeptId(item.getDeptId());
283
+                List<SysUser> sysUsers = iSysUserService.selectUserListByRoleKeyAndDeptId(Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()), item.getDeptId());
284
+                indicatorCalculateParams.setUserIds(sysUsers.stream().map(SysUser::getUserId).collect(Collectors.toList()));
285
+                this.qualificationLevel(indicatorCalculateParams, resultItem);
286
+                this.avgSeizureCount(indicatorCalculateParams, resultItem);
287
+                this.avgWorkingDays(indicatorCalculateParams, resultItem);
288
+                this.avgWorkingHours(indicatorCalculateParams, resultItem);
289
+                this.avgTestScore(indicatorCalculateParams, resultItem);
290
+                result.add(resultItem);
291
+            });
292
+        } else if (StrUtil.equals(DeptTypeEnum.TEAMS.getCode(), sysDept.getDeptType())) {
293
+            indicatorCalculateParams.setUserType(UserType.PERSONAL);
294
+            List<SysUser> sysUsers = iSysUserService.selectUserListByRoleKeyAndDeptId(Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()), dto.getDeptId());
295
+            sysUsers.forEach(item -> {
296
+                SysLargeScreenDetailDto resultItem = new SysLargeScreenDetailDto();
297
+                resultItem.setId(item.getUserId());
298
+                resultItem.setName(item.getNickName());
299
+                resultItem.setCheckCount(checkCountMap.getOrDefault(item.getUserId(), BigDecimal.ZERO));
300
+                //获取指标
301
+                indicatorCalculateParams.setUserId(item.getUserId());
302
+                this.qualificationLevel(indicatorCalculateParams, resultItem);
303
+                this.avgSeizureCount(indicatorCalculateParams, resultItem);
304
+                this.avgWorkingDays(indicatorCalculateParams, resultItem);
305
+                this.avgWorkingHours(indicatorCalculateParams, resultItem);
306
+                this.avgTestScore(indicatorCalculateParams, resultItem);
307
+                result.add(resultItem);
308
+            });
309
+        } else {
310
+            return error("部门类型数据错误");
311
+        }
312
+
313
+        return success(result);
314
+    }
315
+
316
+    /**
317
+     * 工作画像-总体概览
318
+     */
319
+    @Cacheable(
320
+            value = "statistics_data",
321
+            keyGenerator = "statisticsKeyGenerator",
322
+            unless = "!T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isSuccess(#result) ||T(com.sundot.airport.common.cache.StatisticsCacheConditionUtil).isEmptyData(#result)"
323
+    )
324
+    @GetMapping("/workingPortrait")
325
+    public AjaxResult workingPortrait(BaseLargeScreenQueryParamDto dto) {
326
+        // 返回结果
327
+        SysLargeScreenWorkingPortraitDto result = new SysLargeScreenWorkingPortraitDto();
328
+
329
+        // 工作产出和管理推动查询条件
330
+        CheckLargeScreenCorrectionQueryParamDto checkQueryParam = new CheckLargeScreenCorrectionQueryParamDto();
331
+        checkQueryParam.setDeptId(dto.getDeptId());
332
+        checkQueryParam.setStartDate(dto.getStartDate());
333
+        checkQueryParam.setEndDate(dto.getEndDate());
334
+        checkQueryParam.setSpecifiedDate(dto.getSpecifiedDate());
335
+        checkQueryParam.setProcessStatus("3");//ProcessStatusEnum.ARCHIVED.getCode()-归档
336
+        // 工作产出
337
+        List<CheckLargeScreenCommonDto> workOutputCheckList = checkLargeScreenService.workOutputCheck(checkQueryParam);
338
+        result.setWorkOutput(workOutputCheckList.stream().map(CheckLargeScreenCommonDto::getTotal).reduce(BigDecimal.ZERO, BigDecimal::add));
339
+        // 管理推动
340
+        CheckLargeScreenCorrectionDto checkLargeScreenCorrectionDto = checkLargeScreenService.managementPromotionCorrection(checkQueryParam);
341
+        result.setManagementPromotion(checkLargeScreenCorrectionDto.getOverTimeCompletedCount().add(checkLargeScreenCorrectionDto.getOverTimeUnfinishedCount()));
342
+        //在岗时长
343
+        SysDept sysDept = deptService.selectDeptById(dto.getDeptId());
344
+        IndicatorCalculateParams indicatorCalculateParams = this.appendParams(dto);
345
+        indicatorCalculateParams.setUserType(StrUtil.equals(DeptTypeEnum.STATION.getCode(), sysDept.getDeptType()) ? UserType.STATION : UserType.BRIGADE);
346
+        indicatorCalculateParams.setDeptId(dto.getDeptId());
347
+        List<SysUser> sysUsers = iSysUserService.selectUserListByRoleKeyAndDeptId(Arrays.asList(RoleTypeEnum.banzuzhang.getCode(), RoleTypeEnum.SecurityCheck.getCode()), dto.getDeptId());
348
+        indicatorCalculateParams.setUserIds(sysUsers.stream().map(SysUser::getUserId).collect(Collectors.toList()));
349
+        SysLargeScreenDetailDto detail = new SysLargeScreenDetailDto();
350
+        this.avgWorkingHours(indicatorCalculateParams, detail);
351
+        result.setAvgWorkingHours(detail.getAvgWorkingHours());
352
+        return success(result);
353
+    }
354
+
355
+    /**
356
+     * 根据用户编号获取详细信息
357
+     */
358
+    @PreAuthorize("@ss.hasPermi('system:user:query')")
359
+    @GetMapping(value = {"/", "/{userId}"})
360
+    public AjaxResult getInfo(@PathVariable(value = "userId", required = false) Long userId) {
361
+        AjaxResult ajax = AjaxResult.success();
362
+        if (StringUtils.isNotNull(userId)) {
363
+            userService.checkUserDataScope(userId);
364
+            SysUser sysUser = userService.selectUserById(userId);
365
+            if (StringUtils.isNotEmpty(sysUser.getTeamCooperation())) {
366
+                List<Long> list = Arrays.stream(sysUser.getTeamCooperation().split(",")).map(Long::valueOf).collect(Collectors.toList());
367
+                sysUser.setTeamCooperationList(userService.selectByUserIdList(list));
368
+            }
369
+            ajax.put(AjaxResult.DATA_TAG, sysUser);
370
+            ajax.put("postIds", postService.selectPostListByUserId(userId));
371
+            ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList()));
372
+        }
373
+        List<SysRole> roles = roleService.selectRoleAll();
374
+        ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
375
+        ajax.put("posts", postService.selectPostAll());
376
+        return ajax;
377
+    }
378
+
379
+    /**
380
+     * 新增用户
381
+     */
382
+    @PreAuthorize("@ss.hasPermi('system:user:add')")
383
+    @Log(title = "用户管理", businessType = BusinessType.INSERT)
384
+    @PostMapping
385
+    public AjaxResult add(@Validated @RequestBody SysUser user) {
386
+        deptService.checkDeptDataScope(user.getDeptId());
387
+        roleService.checkRoleDataScope(user.getRoleIds());
388
+        if (!userService.checkUserNameUnique(user)) {
389
+            return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
390
+        } else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) {
391
+            return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
392
+        } else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) {
393
+            return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
394
+        }
395
+        user.setCreateBy(getUsername());
396
+        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
397
+        return toAjax(userService.insertUser(user));
398
+    }
399
+
400
+    /**
401
+     * 修改用户
402
+     */
403
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
404
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
405
+    @PutMapping
406
+    public AjaxResult edit(@Validated @RequestBody SysUser user) {
407
+        userService.checkUserAllowed(user);
408
+        userService.checkUserDataScope(user.getUserId());
409
+        deptService.checkDeptDataScope(user.getDeptId());
410
+        roleService.checkRoleDataScope(user.getRoleIds());
411
+        if (!userService.checkUserNameUnique(user)) {
412
+            return error("修改用户'" + user.getUserName() + "'失败,登录账号已存在");
413
+        } else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) {
414
+            return error("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
415
+        } else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) {
416
+            return error("修改用户'" + user.getUserName() + "'失败,邮箱账号已存在");
417
+        }
418
+        user.setUpdateBy(getUsername());
419
+        redisCache.expire(CacheConstants.USER_INFO_KEY + user.getUserId(), 0);
420
+        redisCache.expire(CacheConstants.USER_INFO_KEY + user.getUserName(), 0);
421
+        return toAjax(userService.updateUser(user));
422
+    }
423
+
424
+    /**
425
+     * 删除用户
426
+     */
427
+    @PreAuthorize("@ss.hasPermi('system:user:remove')")
428
+    @Log(title = "用户管理", businessType = BusinessType.DELETE)
429
+    @DeleteMapping("/{userIds}")
430
+    public AjaxResult remove(@PathVariable Long[] userIds) {
431
+        if (ArrayUtils.contains(userIds, getUserId())) {
432
+            return error("当前用户不能删除");
433
+        }
434
+        return toAjax(userService.deleteUserByIds(userIds));
435
+    }
436
+
437
+    /**
438
+     * 重置密码
439
+     */
440
+    @PreAuthorize("@ss.hasPermi('system:user:resetPwd')")
441
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
442
+    @PutMapping("/resetPwd")
443
+    public AjaxResult resetPwd(@RequestBody SysUser user) {
444
+        userService.checkUserAllowed(user);
445
+        userService.checkUserDataScope(user.getUserId());
446
+        user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
447
+        user.setUpdateBy(getUsername());
448
+        return toAjax(userService.resetPwd(user));
449
+    }
450
+
451
+    /**
452
+     * 状态修改
453
+     */
454
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
455
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
456
+    @PutMapping("/changeStatus")
457
+    public AjaxResult changeStatus(@RequestBody SysUser user) {
458
+        userService.checkUserAllowed(user);
459
+        userService.checkUserDataScope(user.getUserId());
460
+        user.setUpdateBy(getUsername());
461
+        return toAjax(userService.updateUserStatus(user));
462
+    }
463
+
464
+    /**
465
+     * 根据用户编号获取授权角色
466
+     */
467
+    @PreAuthorize("@ss.hasPermi('system:user:query')")
468
+    @GetMapping("/authRole/{userId}")
469
+    public AjaxResult authRole(@PathVariable("userId") Long userId) {
470
+        AjaxResult ajax = AjaxResult.success();
471
+        SysUser user = userService.selectUserById(userId);
472
+        List<SysRole> roles = roleService.selectRolesByUserId(userId);
473
+        ajax.put("user", user);
474
+        ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
475
+        redisCache.expire(CacheConstants.USER_INFO_KEY + user.getUserId(), 0);
476
+        redisCache.expire(CacheConstants.USER_INFO_KEY + user.getUserName(), 0);
477
+        return ajax;
478
+    }
479
+
480
+    /**
481
+     * 用户授权角色
482
+     */
483
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
484
+    @Log(title = "用户管理", businessType = BusinessType.GRANT)
485
+    @PutMapping("/authRole")
486
+    public AjaxResult insertAuthRole(Long userId, Long[] roleIds) {
487
+        userService.checkUserDataScope(userId);
488
+        roleService.checkRoleDataScope(roleIds);
489
+        userService.insertUserAuth(userId, roleIds);
490
+        redisCache.expire(CacheConstants.USER_INFO_KEY + userId, 0);
491
+        return success();
492
+    }
493
+
494
+    /**
495
+     * 获取部门树列表
496
+     */
497
+    @PreAuthorize("@ss.hasPermi('system:user:list')")
498
+    @GetMapping("/deptTree")
499
+    public AjaxResult deptTree(SysDept dept) {
500
+        return success(deptService.selectDeptTreeList(dept));
501
+    }
502
+
503
+    /**
504
+     * 获取部门及用户树列表
505
+     */
506
+    @PreAuthorize("@ss.hasPermi('system:user:list')")
507
+    @GetMapping("/deptUserTree")
508
+    public AjaxResult deptUserTree(SysDept dept) {
509
+        return success(deptService.selectDeptUserTreeList(dept));
510
+    }
511
+
512
+    /**
513
+     * 获取全部用户列表
514
+     */
515
+    @PreAuthorize("@ss.hasPermi('system:user:list')")
516
+    @GetMapping("/listAll")
517
+    public AjaxResult listAll(SysUser user) {
518
+        List<SysUser> list = userService.selectUserList(user);
519
+        return success(list);
520
+    }
521
+
522
+    /**
523
+     * 通过用户ID查询用户各级领导
524
+     */
525
+    @PreAuthorize("@ss.hasPermi('system:user:query')")
526
+    @GetMapping("/selectDeptLeaderByUserId")
527
+    public AjaxResult selectDeptLeaderByUserId(@RequestParam("userId") Long userId, @RequestParam("deptType") String deptType) {
528
+        return AjaxResult.success(userService.selectDeptLeaderByUserId(userId, deptType));
529
+    }
530
+
531
+    /**
532
+     * 重置班组长和安检员的岗位为旅检的所有二级岗位
533
+     */
534
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
535
+    @Log(title = "重置班组长和安检员的岗位为旅检的所有二级岗位", businessType = BusinessType.UPDATE)
536
+    @GetMapping("/resetUserPost")
537
+    public AjaxResult resetUserPost() {
538
+        return toAjax(userService.resetUserPost());
539
+    }
540
+
541
+    /**
542
+     * 重置指定部门中指定角色的人员的指定岗位
543
+     */
544
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
545
+    @Log(title = "重置指定部门中指定角色的人员的指定岗位", businessType = BusinessType.UPDATE)
546
+    @PostMapping("/resetUserPostByDept")
547
+    public AjaxResult resetUserPostByDept(@RequestBody ResetUserPostDto resetUserPostDto) {
548
+        return toAjax(userService.resetUserPostByDept(resetUserPostDto));
549
+    }
550
+
551
+    /**
552
+     * 重置指定人员的指定角色
553
+     */
554
+    @PreAuthorize("@ss.hasPermi('system:user:edit')")
555
+    @Log(title = "重置指定人员的指定角色", businessType = BusinessType.UPDATE)
556
+    @PostMapping("/resetUserRole")
557
+    public AjaxResult resetUserRole(@RequestBody ResetUserRoleDto resetUserRoleDto) {
558
+        return toAjax(userService.resetUserRole(resetUserRoleDto));
559
+    }
560
+
561
+    /**
562
+     * 根据用户ID获取岗位选择框列表
563
+     */
564
+    @PreAuthorize("@ss.hasPermi('system:user:query')")
565
+    @GetMapping(value = {"/getPostListsByUserId/{userId}"})
566
+    public AjaxResult getPostListsByUserId(@PathVariable("userId") Long userId) {
567
+        return AjaxResult.success(postService.selectPostListsByUserId(userId));
568
+    }
569
+
570
+    /**
571
+     * 获取用户画像-指标
572
+     *
573
+     * @return
574
+     */
575
+    private IndicatorCalculateParams appendParams(BaseLargeScreenQueryParamDto dto) {
576
+        IndicatorCalculateParams params = new IndicatorCalculateParams();
577
+        params.setIndicatorTypes(Arrays.asList("qualificationLevel", "workingDate", "workingHours", "effectiveSeizureCount", "workYears", "securityWorkYears"));
578
+        // 设置默认时间范围
579
+        java.util.Date endTime = Optional.ofNullable(dto.getEndDate()).orElse(new java.util.Date());
580
+        java.util.Date startTime = Optional.ofNullable(dto.getStartDate())
581
+                .orElse(java.util.Date.from(endTime.toInstant().minusSeconds(7862400L))); // 91天
582
+
583
+        params.setStartTime(startTime);
584
+        params.setEndTime(endTime);
585
+        return params;
586
+    }
587
+
588
+    /**
589
+     * 获取平均查获数量
590
+     *
591
+     * @param params
592
+     * @param detail
593
+     */
594
+    private void avgSeizureCount(IndicatorCalculateParams params, SysLargeScreenDetailDto detail) {
595
+        ModularIndicatorResult indicators = userPortraitService.getIndicators(params, Arrays.asList("item"));
596
+        if (indicators == null) {
597
+            detail.setAvgSeizureCount(BigDecimal.ZERO);
598
+            return;
599
+        }
600
+
601
+        ItemSeizureModuleIndicatorResult itemResult = (ItemSeizureModuleIndicatorResult) indicators.getModuleResult("item");
602
+        if (itemResult == null) {
603
+            detail.setAvgSeizureCount(BigDecimal.ZERO);
604
+            return;
605
+        }
606
+
607
+        if (UserType.PERSONAL.equals(params.getUserType())) {
608
+            detail.setAvgSeizureCount(itemResult.getEffectiveSeizureCount() != null ?
609
+                    new BigDecimal(itemResult.getEffectiveSeizureCount().toString()) : BigDecimal.ZERO);
610
+        } else {
611
+            Object effectiveSeizureCount = itemResult.getEffectiveSeizureCount();
612
+            if (effectiveSeizureCount instanceof SeizureStats) {
613
+                SeizureStats seizureStats = (SeizureStats) effectiveSeizureCount;
614
+                if (seizureStats.getTotalCount() != 0) {
615
+                    detail.setAvgSeizureCount(new BigDecimal(seizureStats.getTotalCount()));
616
+                } else {
617
+                    detail.setAvgSeizureCount(BigDecimal.ZERO);
618
+                }
619
+            } else {
620
+                detail.setAvgSeizureCount(BigDecimal.ZERO);
621
+            }
622
+        }
623
+    }
624
+
625
+    /**
626
+     * 获取平均上岗时长
627
+     *
628
+     * @param params
629
+     * @param detail
630
+     */
631
+    private void avgWorkingHours(IndicatorCalculateParams params, SysLargeScreenDetailDto detail) {
632
+        ModularIndicatorResult indicators = userPortraitService.getIndicators(params, Arrays.asList("attendance"));
633
+        if (indicators == null) {
634
+            detail.setAvgWorkingHours(BigDecimal.ZERO);
635
+            return;
636
+        }
637
+
638
+        AttendanceModuleIndicatorResult attendanceResult = (AttendanceModuleIndicatorResult) indicators.getModuleResult("attendance");
639
+        if (attendanceResult == null) {
640
+            detail.setAvgWorkingHours(BigDecimal.ZERO);
641
+            return;
642
+        }
643
+
644
+        if (UserType.PERSONAL.equals(params.getUserType())) {
645
+            detail.setAvgWorkingHours(attendanceResult.getWorkingHours() != null ?
646
+                    new BigDecimal(attendanceResult.getWorkingHours().toString()) : BigDecimal.ZERO);
647
+        } else {
648
+            Object workingHours = attendanceResult.getWorkingHours();
649
+            if (workingHours instanceof WorkHoursStats) {
650
+                WorkHoursStats workHoursStats = (WorkHoursStats) workingHours;
651
+                detail.setAvgWorkingHours(workHoursStats.getAveragePersonnel() != null ?
652
+                        workHoursStats.getAveragePersonnel() : BigDecimal.ZERO);
653
+            } else {
654
+                detail.setAvgWorkingHours(BigDecimal.ZERO);
655
+            }
656
+        }
657
+    }
658
+
659
+    /**
660
+     * 获取平均出勤天数
661
+     *
662
+     * @param params
663
+     * @param detail
664
+     */
665
+    private void avgWorkingDays(IndicatorCalculateParams params, SysLargeScreenDetailDto detail) {
666
+        ModularIndicatorResult indicators = userPortraitService.getIndicators(params, Arrays.asList("attendance"));
667
+        if (indicators == null) {
668
+            detail.setAvgWorkingDays(BigDecimal.ZERO);
669
+            return;
670
+        }
671
+
672
+        AttendanceModuleIndicatorResult attendanceResult = (AttendanceModuleIndicatorResult) indicators.getModuleResult("attendance");
673
+        if (attendanceResult == null) {
674
+            detail.setAvgWorkingDays(BigDecimal.ZERO);
675
+            return;
676
+        }
677
+
678
+        if (UserType.PERSONAL.equals(params.getUserType())) {
679
+            detail.setAvgWorkingDays(attendanceResult.getWorkingDate() != null ?
680
+                    new BigDecimal(attendanceResult.getWorkingDate().toString()) : BigDecimal.ZERO);
681
+        } else {
682
+            Object workingDate = attendanceResult.getWorkingDate();
683
+            if (workingDate instanceof WorkDateStats) {
684
+                WorkDateStats workDateStats = (WorkDateStats) workingDate;
685
+                detail.setAvgWorkingDays(workDateStats.getAveragePersonnel() != null ?
686
+                        workDateStats.getAveragePersonnel() : BigDecimal.ZERO);
687
+            } else {
688
+                detail.setAvgWorkingDays(BigDecimal.ZERO);
689
+            }
690
+        }
691
+    }
692
+
693
+    /**
694
+     * 获取资质等级
695
+     *
696
+     * @param params
697
+     * @param detail
698
+     */
699
+    private void qualificationLevel(IndicatorCalculateParams params, SysLargeScreenDetailDto detail) {
700
+        ModularIndicatorResult indicators = userPortraitService.getIndicators(params, Arrays.asList("system"));
701
+        if (indicators == null) {
702
+            detail.setQualificationLevel("0");
703
+            return;
704
+        }
705
+
706
+        IndicatorResult systemResult = (IndicatorResult) indicators.getModuleResult("system");
707
+        if (systemResult == null) {
708
+            detail.setQualificationLevel("0");
709
+            return;
710
+        }
711
+
712
+        if (UserType.PERSONAL.equals(params.getUserType())) {
713
+            String qualificationLevel = systemResult.getQualificationLevel();
714
+            detail.setQualificationLevel(qualificationLevel != null ? qualificationLevel : "0");
715
+        } else {
716
+            List<QualificationStats> qualificationLevelStats = systemResult.getQualificationLevelStats();
717
+            if (qualificationLevelStats != null) {
718
+                final String FIRST_LEVEL = "高级";
719
+                Integer reduce = qualificationLevelStats.stream()
720
+                        .filter(qualificationStats -> FIRST_LEVEL.equals(qualificationStats.getLevelName()))
721
+                        .map(QualificationStats::getCount)
722
+                        .reduce(0, Integer::sum);
723
+                detail.setQualificationLevel(reduce.toString());
724
+            } else {
725
+                detail.setQualificationLevel("0");
726
+            }
727
+        }
728
+    }
729
+
730
+    /**
731
+     * 个人获取工作年限
732
+     */
733
+    private void workingYears(IndicatorCalculateParams params, SysLargeScreenDetailDto detail) {
734
+        ModularIndicatorResult indicators = userPortraitService.getIndicators(params, Arrays.asList("system"));
735
+        if (indicators == null) {
736
+            detail.setWorkYears(BigDecimal.ZERO);
737
+        }
738
+        IndicatorResult systemResult = (IndicatorResult) indicators.getModuleResult("system");
739
+        if (systemResult == null) {
740
+            detail.setWorkYears(BigDecimal.ZERO);
741
+            return;
742
+        }
743
+        if (UserType.PERSONAL.equals(params.getUserType())) {
744
+            Integer workYears = systemResult.getSecurityWorkYears();
745
+            detail.setWorkYears(workYears != null ? new BigDecimal(workYears) : BigDecimal.ZERO);
746
+        } else {
747
+            WorkExperienceStats securityWorkYearsStats = systemResult.getSecurityWorkYearsStats();
748
+            if (securityWorkYearsStats != null && securityWorkYearsStats.getAverageWorkYears() != null) {
749
+                detail.setWorkYears(BigDecimal.valueOf(securityWorkYearsStats.getAverageWorkYears()));
750
+            } else {
751
+                detail.setWorkYears(BigDecimal.ZERO);
752
+            }
753
+        }
754
+    }
755
+
756
+
757
+    /**
758
+     * 获取测试平均分
759
+     *
760
+     * @param params
761
+     * @param detail
762
+     */
763
+    private void avgTestScore(IndicatorCalculateParams params, SysLargeScreenDetailDto detail) {
764
+        java.util.Date startTime = params.getStartTime();
765
+        java.util.Date endTime = params.getEndTime();
766
+
767
+        // 根据用户类型查询
768
+        if (UserType.PERSONAL.equals(params.getUserType())) {
769
+            // 个人:查询该用户的平均分
770
+            Long userId = params.getUserId();
771
+            BigDecimal avgScore = calculateUserAvgScore(userId, startTime, endTime);
772
+            detail.setTestScore(avgScore);
773
+        } else {
774
+            // 组织:查询该部门下所有成员的平均分
775
+            Long deptId = params.getDeptId();
776
+            BigDecimal avgScore = calculateDeptAvgScore(deptId, startTime, endTime);
777
+            detail.setTestScore(avgScore);
778
+        }
779
+    }
780
+
781
+    /**
782
+     * 计算个人平均分
783
+     */
784
+    private BigDecimal calculateUserAvgScore(Long userId, java.util.Date startTime, java.util.Date endTime) {
785
+        LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
786
+        wrapper.eq(DailyTask::getDtUserId, userId);
787
+        wrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(
788
+                new java.text.SimpleDateFormat("yyyy-MM-dd").format(startTime)));
789
+        wrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(
790
+                new java.text.SimpleDateFormat("yyyy-MM-dd").format(endTime)));
791
+        wrapper.in(DailyTask::getDtStatus, "COMPLETED", "EXPIRED");
792
+        wrapper.eq(DailyTask::getDelStatus, 0);
793
+
794
+        List<DailyTask> tasks = dailyTaskMapper.selectList(wrapper);
795
+
796
+        if (tasks.isEmpty()) {
797
+            return BigDecimal.ZERO;
798
+        }
799
+
800
+        BigDecimal totalScore = tasks.stream()
801
+                .map(task -> "EXPIRED".equals(task.getDtStatus()) ? BigDecimal.ZERO : task.getDtTotalScore())
802
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
803
+
804
+        return totalScore.divide(new BigDecimal(tasks.size()), 2, RoundingMode.HALF_UP);
805
+    }
806
+
807
+    /**
808
+     * 计算部门平均分(所有成员的平均分)
809
+     */
810
+    private BigDecimal calculateDeptAvgScore(Long deptId, java.util.Date startTime, java.util.Date endTime) {
811
+        // 1. 获取部门下所有用户
812
+        List<SysUser> users = iSysUserService.selectUserByDeptId(deptId);
813
+
814
+        if (users.isEmpty()) {
815
+            return BigDecimal.ZERO;
816
+        }
817
+
818
+        // 2. 收集所有用户ID
819
+        List<Long> userIds = users.stream()
820
+                .map(SysUser::getUserId)
821
+                .collect(Collectors.toList());
822
+
823
+        // 3. 查询这些用户的所有任务
824
+        LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
825
+        wrapper.in(DailyTask::getDtUserId, userIds);
826
+        wrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(
827
+                new java.text.SimpleDateFormat("yyyy-MM-dd").format(startTime)));
828
+        wrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(
829
+                new java.text.SimpleDateFormat("yyyy-MM-dd").format(endTime)));
830
+        wrapper.in(DailyTask::getDtStatus, "COMPLETED", "EXPIRED");
831
+        wrapper.eq(DailyTask::getDelStatus, 0);
832
+
833
+        List<DailyTask> tasks = dailyTaskMapper.selectList(wrapper);
834
+
835
+        if (tasks.isEmpty()) {
836
+            return BigDecimal.ZERO;
837
+        }
838
+
839
+        // 4. 计算所有任务的平均分
840
+        BigDecimal totalScore = tasks.stream()
841
+                .map(task -> "EXPIRED".equals(task.getDtStatus()) ? BigDecimal.ZERO : task.getDtTotalScore())
842
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
843
+
844
+        return totalScore.divide(new BigDecimal(tasks.size()), 2, RoundingMode.HALF_UP);
845
+    }
846
+
847
+    /**
848
+     * 计算population接口的测试平均分
849
+     * 根据userId或deptId查询抽问抽答的平均分
850
+     */
851
+    private BigDecimal calculatePopulationTestScore(BaseLargeScreenQueryParamDto dto) {
852
+        // 设置时间范围
853
+        java.util.Date endTime = Optional.ofNullable(dto.getEndDate()).orElse(new java.util.Date());
854
+        java.util.Date startTime = Optional.ofNullable(dto.getStartDate())
855
+                .orElse(java.util.Date.from(endTime.toInstant().minusSeconds(7862400L))); // 91天
856
+
857
+        // 判断是个人还是部门
858
+        if (ObjectUtil.isNotNull(dto.getUserId())) {
859
+            // 个人维度:直接查询该用户的平均分
860
+            return calculateUserAvgScore(dto.getUserId(), startTime, endTime);
861
+        } else if (ObjectUtil.isNotNull(dto.getDeptId())) {
862
+            // 部门维度:查询该部门及所有下级部门的平均分
863
+            return calculateDeptAvgScoreByDeptId(dto.getDeptId(), startTime, endTime);
864
+        }
865
+
866
+        return BigDecimal.ZERO;
867
+    }
868
+
869
+    /**
870
+     * 根据部门ID计算平均分(基于dt_dept_id查询)
871
+     * 需要递归查找所有下级部门
872
+     */
873
+    private BigDecimal calculateDeptAvgScoreByDeptId(Long deptId, java.util.Date startTime, java.util.Date endTime) {
874
+        // 1. 获取该部门及所有下级部门的ID列表
875
+        List<Long> deptIds = getAllSubDeptIds(deptId);
876
+
877
+        if (deptIds.isEmpty()) {
878
+            return BigDecimal.ZERO;
879
+        }
880
+
881
+        // 2. 查询这些部门的所有任务(通过dt_dept_id)
882
+        LambdaQueryWrapper<DailyTask> wrapper = new LambdaQueryWrapper<>();
883
+        wrapper.in(DailyTask::getDtDeptId, deptIds);
884
+        wrapper.ge(DailyTask::getDtBusinessDate, Date.valueOf(
885
+                new java.text.SimpleDateFormat("yyyy-MM-dd").format(startTime)));
886
+        wrapper.le(DailyTask::getDtBusinessDate, Date.valueOf(
887
+                new java.text.SimpleDateFormat("yyyy-MM-dd").format(endTime)));
888
+        wrapper.in(DailyTask::getDtStatus, "COMPLETED", "EXPIRED");
889
+        wrapper.eq(DailyTask::getDelStatus, 0);
890
+
891
+        List<DailyTask> tasks = dailyTaskMapper.selectList(wrapper);
892
+
893
+        if (tasks.isEmpty()) {
894
+            return BigDecimal.ZERO;
895
+        }
896
+
897
+        // 3. 计算平均分(EXPIRED的任务分数算0)
898
+        BigDecimal totalScore = tasks.stream()
899
+                .map(task -> "EXPIRED".equals(task.getDtStatus()) ? BigDecimal.ZERO : task.getDtTotalScore())
900
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
901
+
902
+        return totalScore.divide(new BigDecimal(tasks.size()), 2, RoundingMode.HALF_UP);
903
+    }
904
+
905
+    /**
906
+     * 获取指定部门及其所有下级部门的ID列表
907
+     * 根据sys_dept表的层级关系递归查找
908
+     */
909
+    private List<Long> getAllSubDeptIds(Long deptId) {
910
+        List<Long> result = new ArrayList<>();
911
+        result.add(deptId); // 包含自己
912
+
913
+        // 查询直接下级部门
914
+        SysDept queryDept = new SysDept();
915
+        queryDept.setParentId(deptId);
916
+        List<SysDept> subDepts = deptService.selectDeptInfoAll(queryDept);
917
+
918
+        // 递归查询所有下级部门
919
+        for (SysDept subDept : subDepts) {
920
+            if ("0".equals(subDept.getDelFlag())) { // 只包含未删除的部门
921
+                result.addAll(getAllSubDeptIds(subDept.getDeptId()));
922
+            }
923
+        }
924
+
925
+        return result;
926
+    }
927
+}

+ 102 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/system/SysWorkingDocumentController.java

@@ -0,0 +1,102 @@
1
+package com.sundot.airport.web.controller.system;
2
+
3
+import java.util.List;
4
+import javax.servlet.http.HttpServletResponse;
5
+
6
+import org.springframework.security.access.prepost.PreAuthorize;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.PostMapping;
10
+import org.springframework.web.bind.annotation.PutMapping;
11
+import org.springframework.web.bind.annotation.DeleteMapping;
12
+import org.springframework.web.bind.annotation.PathVariable;
13
+import org.springframework.web.bind.annotation.RequestBody;
14
+import org.springframework.web.bind.annotation.RequestMapping;
15
+import org.springframework.web.bind.annotation.RestController;
16
+import com.sundot.airport.common.annotation.Log;
17
+import com.sundot.airport.common.core.controller.BaseController;
18
+import com.sundot.airport.common.core.domain.AjaxResult;
19
+import com.sundot.airport.common.enums.BusinessType;
20
+import com.sundot.airport.system.domain.SysWorkingDocument;
21
+import com.sundot.airport.system.service.ISysWorkingDocumentService;
22
+import com.sundot.airport.common.utils.poi.ExcelUtil;
23
+import com.sundot.airport.common.core.page.TableDataInfo;
24
+
25
+/**
26
+ * 工作文档Controller
27
+ *
28
+ * @author ruoyi
29
+ * @date 2025-11-18
30
+ */
31
+@RestController
32
+@RequestMapping("/system/workingDocument")
33
+public class SysWorkingDocumentController extends BaseController {
34
+    @Autowired
35
+    private ISysWorkingDocumentService sysWorkingDocumentService;
36
+
37
+    /**
38
+     * 查询工作文档列表
39
+     */
40
+    @PreAuthorize("@ss.hasPermi('system:workingDocument:list')")
41
+    @GetMapping("/list")
42
+    public TableDataInfo list(SysWorkingDocument sysWorkingDocument) {
43
+        startPage();
44
+        List<SysWorkingDocument> list = sysWorkingDocumentService.selectSysWorkingDocumentList(sysWorkingDocument);
45
+        return getDataTable(list);
46
+    }
47
+
48
+    /**
49
+     * 导出工作文档列表
50
+     */
51
+    @PreAuthorize("@ss.hasPermi('system:workingDocument:export')")
52
+    @Log(title = "工作文档", businessType = BusinessType.EXPORT)
53
+    @PostMapping("/export")
54
+    public void export(HttpServletResponse response, SysWorkingDocument sysWorkingDocument) {
55
+        List<SysWorkingDocument> list = sysWorkingDocumentService.selectSysWorkingDocumentList(sysWorkingDocument);
56
+        ExcelUtil<SysWorkingDocument> util = new ExcelUtil<SysWorkingDocument>(SysWorkingDocument.class);
57
+        util.exportExcel(response, list, "工作文档数据");
58
+    }
59
+
60
+    /**
61
+     * 获取工作文档详细信息
62
+     */
63
+    @PreAuthorize("@ss.hasPermi('system:workingDocument:query')")
64
+    @GetMapping(value = "/{documentId}")
65
+    public AjaxResult getInfo(@PathVariable("documentId") Long documentId) {
66
+        return success(sysWorkingDocumentService.selectSysWorkingDocumentByDocumentId(documentId));
67
+    }
68
+
69
+    /**
70
+     * 新增工作文档
71
+     */
72
+    @PreAuthorize("@ss.hasPermi('system:workingDocument:add')")
73
+    @Log(title = "工作文档", businessType = BusinessType.INSERT)
74
+    @PostMapping
75
+    public AjaxResult add(@RequestBody SysWorkingDocument sysWorkingDocument) {
76
+        sysWorkingDocument.setCreateByUserId(getUserId());
77
+        sysWorkingDocument.setCreateBy(getLoginUser().getUser().getNickName());
78
+        return toAjax(sysWorkingDocumentService.insertSysWorkingDocument(sysWorkingDocument));
79
+    }
80
+
81
+    /**
82
+     * 修改工作文档
83
+     */
84
+    @PreAuthorize("@ss.hasPermi('system:workingDocument:edit')")
85
+    @Log(title = "工作文档", businessType = BusinessType.UPDATE)
86
+    @PutMapping
87
+    public AjaxResult edit(@RequestBody SysWorkingDocument sysWorkingDocument) {
88
+        sysWorkingDocument.setUpdateByUserId(getUserId());
89
+        sysWorkingDocument.setUpdateBy(getLoginUser().getUser().getNickName());
90
+        return toAjax(sysWorkingDocumentService.updateSysWorkingDocument(sysWorkingDocument));
91
+    }
92
+
93
+    /**
94
+     * 删除工作文档
95
+     */
96
+    @PreAuthorize("@ss.hasPermi('system:workingDocument:remove')")
97
+    @Log(title = "工作文档", businessType = BusinessType.DELETE)
98
+    @DeleteMapping("/{documentIds}")
99
+    public AjaxResult remove(@PathVariable Long[] documentIds) {
100
+        return toAjax(sysWorkingDocumentService.deleteSysWorkingDocumentByDocumentIds(documentIds));
101
+    }
102
+}

+ 183 - 0
airport-admin/src/main/java/com/sundot/airport/web/controller/tool/TestController.java

@@ -0,0 +1,183 @@
1
+package com.sundot.airport.web.controller.tool;
2
+
3
+import java.util.ArrayList;
4
+import java.util.LinkedHashMap;
5
+import java.util.List;
6
+import java.util.Map;
7
+import org.springframework.web.bind.annotation.DeleteMapping;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.PathVariable;
10
+import org.springframework.web.bind.annotation.PostMapping;
11
+import org.springframework.web.bind.annotation.PutMapping;
12
+import org.springframework.web.bind.annotation.RequestBody;
13
+import org.springframework.web.bind.annotation.RequestMapping;
14
+import org.springframework.web.bind.annotation.RestController;
15
+import com.sundot.airport.common.core.controller.BaseController;
16
+import com.sundot.airport.common.core.domain.R;
17
+import com.sundot.airport.common.utils.StringUtils;
18
+import io.swagger.annotations.Api;
19
+import io.swagger.annotations.ApiImplicitParam;
20
+import io.swagger.annotations.ApiImplicitParams;
21
+import io.swagger.annotations.ApiModel;
22
+import io.swagger.annotations.ApiModelProperty;
23
+import io.swagger.annotations.ApiOperation;
24
+
25
+/**
26
+ * swagger 用户测试方法
27
+ * 
28
+ * @author ruoyi
29
+ */
30
+@Api("用户信息管理")
31
+@RestController
32
+@RequestMapping("/test/user")
33
+public class TestController extends BaseController
34
+{
35
+    private final static Map<Integer, UserEntity> users = new LinkedHashMap<Integer, UserEntity>();
36
+    {
37
+        users.put(1, new UserEntity(1, "admin", "admin123", "15888888888"));
38
+        users.put(2, new UserEntity(2, "ry", "admin123", "15666666666"));
39
+    }
40
+
41
+    @ApiOperation("获取用户列表")
42
+    @GetMapping("/list")
43
+    public R<List<UserEntity>> userList()
44
+    {
45
+        List<UserEntity> userList = new ArrayList<UserEntity>(users.values());
46
+        return R.ok(userList);
47
+    }
48
+
49
+    @ApiOperation("获取用户详细")
50
+    @ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class)
51
+    @GetMapping("/{userId}")
52
+    public R<UserEntity> getUser(@PathVariable Integer userId)
53
+    {
54
+        if (!users.isEmpty() && users.containsKey(userId))
55
+        {
56
+            return R.ok(users.get(userId));
57
+        }
58
+        else
59
+        {
60
+            return R.fail("用户不存在");
61
+        }
62
+    }
63
+
64
+    @ApiOperation("新增用户")
65
+    @ApiImplicitParams({
66
+        @ApiImplicitParam(name = "userId", value = "用户id", dataType = "Integer", dataTypeClass = Integer.class),
67
+        @ApiImplicitParam(name = "username", value = "用户名称", dataType = "String", dataTypeClass = String.class),
68
+        @ApiImplicitParam(name = "password", value = "用户密码", dataType = "String", dataTypeClass = String.class),
69
+        @ApiImplicitParam(name = "mobile", value = "用户手机", dataType = "String", dataTypeClass = String.class)
70
+    })
71
+    @PostMapping("/save")
72
+    public R<String> save(UserEntity user)
73
+    {
74
+        if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
75
+        {
76
+            return R.fail("用户ID不能为空");
77
+        }
78
+        users.put(user.getUserId(), user);
79
+        return R.ok();
80
+    }
81
+
82
+    @ApiOperation("更新用户")
83
+    @PutMapping("/update")
84
+    public R<String> update(@RequestBody UserEntity user)
85
+    {
86
+        if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
87
+        {
88
+            return R.fail("用户ID不能为空");
89
+        }
90
+        if (users.isEmpty() || !users.containsKey(user.getUserId()))
91
+        {
92
+            return R.fail("用户不存在");
93
+        }
94
+        users.remove(user.getUserId());
95
+        users.put(user.getUserId(), user);
96
+        return R.ok();
97
+    }
98
+
99
+    @ApiOperation("删除用户信息")
100
+    @ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class)
101
+    @DeleteMapping("/{userId}")
102
+    public R<String> delete(@PathVariable Integer userId)
103
+    {
104
+        if (!users.isEmpty() && users.containsKey(userId))
105
+        {
106
+            users.remove(userId);
107
+            return R.ok();
108
+        }
109
+        else
110
+        {
111
+            return R.fail("用户不存在");
112
+        }
113
+    }
114
+}
115
+
116
+@ApiModel(value = "UserEntity", description = "用户实体")
117
+class UserEntity
118
+{
119
+    @ApiModelProperty("用户ID")
120
+    private Integer userId;
121
+
122
+    @ApiModelProperty("用户名称")
123
+    private String username;
124
+
125
+    @ApiModelProperty("用户密码")
126
+    private String password;
127
+
128
+    @ApiModelProperty("用户手机")
129
+    private String mobile;
130
+
131
+    public UserEntity()
132
+    {
133
+
134
+    }
135
+
136
+    public UserEntity(Integer userId, String username, String password, String mobile)
137
+    {
138
+        this.userId = userId;
139
+        this.username = username;
140
+        this.password = password;
141
+        this.mobile = mobile;
142
+    }
143
+
144
+    public Integer getUserId()
145
+    {
146
+        return userId;
147
+    }
148
+
149
+    public void setUserId(Integer userId)
150
+    {
151
+        this.userId = userId;
152
+    }
153
+
154
+    public String getUsername()
155
+    {
156
+        return username;
157
+    }
158
+
159
+    public void setUsername(String username)
160
+    {
161
+        this.username = username;
162
+    }
163
+
164
+    public String getPassword()
165
+    {
166
+        return password;
167
+    }
168
+
169
+    public void setPassword(String password)
170
+    {
171
+        this.password = password;
172
+    }
173
+
174
+    public String getMobile()
175
+    {
176
+        return mobile;
177
+    }
178
+
179
+    public void setMobile(String mobile)
180
+    {
181
+        this.mobile = mobile;
182
+    }
183
+}

+ 100 - 0
airport-admin/src/main/java/com/sundot/airport/web/core/cache/UserCache.java

@@ -0,0 +1,100 @@
1
+package com.sundot.airport.web.core.cache;
2
+
3
+import cn.hutool.core.collection.CollectionUtil;
4
+import cn.hutool.core.util.StrUtil;
5
+import com.sundot.airport.common.constant.CacheConstants;
6
+import com.sundot.airport.common.core.domain.entity.SysDept;
7
+import com.sundot.airport.common.core.domain.entity.SysUser;
8
+import com.sundot.airport.common.core.redis.RedisCache;
9
+import com.sundot.airport.common.dto.UserInfo;
10
+import com.sundot.airport.common.enums.DeptType;
11
+import com.sundot.airport.system.service.ISysDeptService;
12
+import com.sundot.airport.system.service.ISysUserService;
13
+import org.springframework.beans.BeanUtils;
14
+import org.springframework.beans.factory.annotation.Autowired;
15
+import org.springframework.stereotype.Component;
16
+
17
+import java.util.Collections;
18
+import java.util.List;
19
+import java.util.Objects;
20
+
21
+/**
22
+ * 用户信息缓存
23
+ *
24
+ * @Author: wangchong
25
+ * @Date: 2025/7/11 14:49
26
+ **/
27
+@Component
28
+public class UserCache {
29
+
30
+    @Autowired
31
+    private ISysUserService sysUserService;
32
+
33
+    @Autowired
34
+    private ISysDeptService sysDeptService;
35
+
36
+    @Autowired
37
+    private RedisCache redisCache;
38
+
39
+    /**
40
+     * 获取用户信息
41
+     *
42
+     * @param userId 用户id
43
+     * @return 用户信息
44
+     */
45
+    public UserInfo getUserInfo(Long userId) {
46
+        UserInfo userInfo = redisCache.getCacheObject(CacheConstants.USER_INFO_KEY + userId);
47
+        if (Objects.isNull(userInfo)) {
48
+            userInfo = new UserInfo();
49
+            SysUser sysUser = sysUserService.selectUserById(userId);
50
+            BeanUtils.copyProperties(sysUser, userInfo);
51
+            updateDeptInfo(userInfo);
52
+            redisCache.setCacheObject(CacheConstants.USER_INFO_KEY + userId, userInfo);
53
+            redisCache.expire(CacheConstants.USER_INFO_KEY + userId, 300);
54
+        }
55
+        return userInfo;
56
+    }
57
+
58
+
59
+    /**
60
+     * 获取用户信息
61
+     *
62
+     * @param userName 用户登录账号
63
+     * @return 用户信息
64
+     */
65
+    public UserInfo getUserInfo(String userName) {
66
+        UserInfo userInfo = redisCache.getCacheObject(CacheConstants.USER_INFO_KEY + userName);
67
+        if (Objects.isNull(userInfo)) {
68
+            userInfo = new UserInfo();
69
+            SysUser sysUser = sysUserService.selectUserByUserName(userName);
70
+            BeanUtils.copyProperties(sysUser, userInfo);
71
+            updateDeptInfo(userInfo);
72
+            redisCache.setCacheObject(CacheConstants.USER_INFO_KEY + userName, userInfo);
73
+            redisCache.expire(CacheConstants.USER_INFO_KEY + userName, 300);
74
+        }
75
+        return userInfo;
76
+    }
77
+
78
+    private void updateDeptInfo(UserInfo userInfo) {
79
+        if (Objects.isNull(userInfo.getDeptId())) {
80
+            return;
81
+        }
82
+        List<SysDept> deptList = sysDeptService.selectAllDept(userInfo.getDeptId());
83
+        if (CollectionUtil.isEmpty(deptList)) {
84
+            return;
85
+        }
86
+        Collections.reverse(deptList);
87
+        SysDept teams = deptList.stream().filter(x -> StrUtil.equals(DeptType.TEAMS.getCode(), x.getDeptType())).findFirst().orElse(new SysDept());
88
+        SysDept department = deptList.stream().filter(x -> StrUtil.equals(DeptType.MANAGER.getCode(), x.getDeptType())).findFirst().orElse(new SysDept());
89
+        SysDept brigade = deptList.stream().filter(x -> StrUtil.equals(DeptType.BRIGADE.getCode(), x.getDeptType())).findFirst().orElse(new SysDept());
90
+        SysDept station = deptList.stream().filter(x -> StrUtil.equals(DeptType.STATION.getCode(), x.getDeptType())).findFirst().orElse(new SysDept());
91
+        userInfo.setTeamsId(teams.getDeptId());
92
+        userInfo.setTeamsName(teams.getDeptName());
93
+        userInfo.setDepartmentId(department.getDeptId());
94
+        userInfo.setDepartmentName(department.getDeptName());
95
+        userInfo.setBrigadeId(brigade.getDeptId());
96
+        userInfo.setBrigadeName(brigade.getDeptName());
97
+        userInfo.setStationId(station.getDeptId());
98
+        userInfo.setStationName(station.getDeptName());
99
+    }
100
+}

+ 0 - 0
airport-admin/src/main/java/com/sundot/airport/web/core/config/SwaggerConfig.java


Some files were not shown because too many files changed in this diff