From 8d19836ea02819ea372aa8e1204abb57da8d729f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=91=A8=E7=AB=9E=E7=94=B1?= <1193626695@qq.com>
Date: Fri, 5 Dec 2025 00:19:12 +0800
Subject: [PATCH] =?UTF-8?q?=E6=9D=83=E9=99=90=E7=BB=86=E5=8C=96=EF=BC=9A?=
=?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=8E=A5=E5=8F=A3=E7=BA=A7=E6=9D=83=E9=99=90?=
=?UTF-8?q?=E6=8E=A7=E5=88=B6=EF=BC=88=E5=AD=A6=E7=94=9F=E4=BB=85=E8=AE=BF?=
=?UTF-8?q?=E9=97=AE=E4=B8=AA=E4=BA=BA=E7=94=A8=E6=B0=B4=20/=20=E6=B0=B4?=
=?UTF-8?q?=E8=B4=A8=E6=9F=A5=E8=AF=A2=EF=BC=8C=E7=BB=B4=E4=BF=AE=E4=BA=BA?=
=?UTF-8?q?=E5=91=98=E4=BB=85=E8=AE=BF=E9=97=AE=E8=BE=96=E5=8C=BA=E5=B7=A5?=
=?UTF-8?q?=E5=8D=95=20/=20=E8=AE=BE=E5=A4=87=EF=BC=8C=E7=AE=A1=E7=90=86?=
=?UTF-8?q?=E5=91=98=E5=85=A8=E6=9D=83=E9=99=90=EF=BC=89=EF=BC=9B=20?=
=?UTF-8?q?=E5=91=8A=E8=AD=A6=E5=8A=9F=E8=83=BD=E5=AE=8C=E5=96=84=EF=BC=9A?=
=?UTF-8?q?=E5=91=8A=E8=AD=A6=E5=88=86=E7=BA=A7=EF=BC=88=E4=B8=80=E8=88=AC?=
=?UTF-8?q?=20/=20=E7=B4=A7=E6=80=A5=EF=BC=89=E3=80=81=E5=91=8A=E8=AD=A6?=
=?UTF-8?q?=E6=8E=A8=E9=80=81=EF=BC=88Web/APP=20=E6=B6=88=E6=81=AF?=
=?UTF-8?q?=E6=8F=90=E9=86=92=EF=BC=89=E3=80=81=E5=91=8A=E8=AD=A6=E5=8E=86?=
=?UTF-8?q?=E5=8F=B2=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3=EF=BC=9B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 29 ++++++
.../campus/water/config/SecurityConfig.java | 96 +++++++++++++++++++
.../water/controller/AlertController.java | 75 +++++++++++++++
.../controller/GlobalExceptionHandler.java | 67 ++++++++++---
.../water/controller/WorkOrderController.java | 16 +++-
.../java/com/campus/water/entity/Alert.java | 23 ++++-
.../campus/water/mapper/AlertRepository.java | 30 ++++--
.../security/JwtAuthenticationFilter.java | 55 +++++++++++
.../water/security/JwtTokenProvider.java | 90 +++++++++++++++++
.../campus/water/security/RoleConstants.java | 16 ++++
.../security/UserDetailsServiceImpl.java | 79 +++++++++++++++
.../water/service/AlertPushService.java | 77 +++++++++++++++
.../water/service/AlertTriggerService.java | 2 +
.../water/service/app/StudentAppService.java | 10 +-
14 files changed, 637 insertions(+), 28 deletions(-)
create mode 100644 src/main/java/com/campus/water/config/SecurityConfig.java
create mode 100644 src/main/java/com/campus/water/controller/AlertController.java
create mode 100644 src/main/java/com/campus/water/security/JwtAuthenticationFilter.java
create mode 100644 src/main/java/com/campus/water/security/JwtTokenProvider.java
create mode 100644 src/main/java/com/campus/water/security/RoleConstants.java
create mode 100644 src/main/java/com/campus/water/security/UserDetailsServiceImpl.java
create mode 100644 src/main/java/com/campus/water/service/AlertPushService.java
diff --git a/pom.xml b/pom.xml
index 0e80add..dfe31eb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -93,6 +93,35 @@
spring-boot-starter-test
test
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.11.5
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.11.5
+ runtime
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.2.0
+
+
diff --git a/src/main/java/com/campus/water/config/SecurityConfig.java b/src/main/java/com/campus/water/config/SecurityConfig.java
new file mode 100644
index 0000000..c94cbdc
--- /dev/null
+++ b/src/main/java/com/campus/water/config/SecurityConfig.java
@@ -0,0 +1,96 @@
+// com/campus/water/config/SecurityConfig.java
+package com.campus.water.config;
+
+import com.campus.water.security.JwtAuthenticationFilter;
+import com.campus.water.security.RoleConstants;
+import com.campus.water.security.UserDetailsServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+/**
+ * 安全配置类
+ */
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity(prePostEnabled = true) // 启用方法级权限控制
+public class SecurityConfig {
+
+ @Autowired
+ private UserDetailsServiceImpl userDetailsService;
+
+ @Autowired
+ private JwtAuthenticationFilter jwtAuthenticationFilter;
+
+ /**
+ * 认证提供者
+ */
+ @Bean
+ public DaoAuthenticationProvider authenticationProvider() {
+ DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
+ authProvider.setUserDetailsService(userDetailsService);
+ authProvider.setPasswordEncoder(passwordEncoder());
+ return authProvider;
+ }
+
+ /**
+ * 密码加密器
+ */
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ /**
+ * 认证管理器
+ */
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
+ return authConfig.getAuthenticationManager();
+ }
+
+ /**
+ * 安全过滤链
+ */
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ // 关闭CSRF
+ .csrf(csrf -> csrf.disable())
+ // 无状态会话
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ // 权限控制
+ .authorizeHttpRequests(auth -> auth
+ // 登录接口放行
+ .requestMatchers("/api/app/student/login", "/api/app/repair/login", "/api/web/login").permitAll()
+ // 静态资源放行
+ .requestMatchers("/static/**", "/templates/**").permitAll()
+ // 新增告警接口权限控制(URL级)
+ .requestMatchers("/api/alerts/**").hasAnyRole("ADMIN", "REPAIRMAN")
+ // 基础权限控制(细粒度在Controller层通过注解控制)
+ .requestMatchers("/api/app/student/**").hasAnyRole("STUDENT", "ADMIN")
+ .requestMatchers("/api/app/repair/**").hasAnyRole("REPAIRMAN", "ADMIN")
+ .requestMatchers("/api/web/**").hasRole("ADMIN")
+ // 其他接口需要认证
+ .anyRequest().authenticated()
+ )
+ // 添加JWT过滤器
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
+
+ // 设置认证提供者
+ http.authenticationProvider(authenticationProvider());
+
+ return http.build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/campus/water/controller/AlertController.java b/src/main/java/com/campus/water/controller/AlertController.java
new file mode 100644
index 0000000..f12fde9
--- /dev/null
+++ b/src/main/java/com/campus/water/controller/AlertController.java
@@ -0,0 +1,75 @@
+package com.campus.water.controller;
+
+import com.campus.water.entity.Alert;
+import com.campus.water.mapper.AlertRepository;
+import com.campus.water.util.ResultVO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/alerts")
+@RequiredArgsConstructor
+@Tag(name = "告警管理接口") // 替换 @Api
+public class AlertController {
+
+ private final AlertRepository alertRepository;
+
+ @GetMapping("/history")
+ @PreAuthorize("hasAnyRole('ADMIN', 'REPAIRMAN')")
+ @Operation(summary = "分页查询告警历史(支持多条件筛选)") // 替换 @ApiOperation(若有)
+ public ResultVO> getAlertHistory(
+ @Parameter(description = "设备ID(可选)") @RequestParam(required = false) String deviceId, // 替换 @ApiParam
+ @Parameter(description = "告警级别(可选,如error、critical)") @RequestParam(required = false) String level,
+ @Parameter(description = "告警状态(可选,如pending、resolved)") @RequestParam(required = false) String status,
+ @Parameter(description = "开始时间(可选,格式:yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false) LocalDateTime startTime,
+ @Parameter(description = "结束时间(可选)") @RequestParam(required = false) LocalDateTime endTime,
+ @Parameter(description = "所属区域(维修人员仅能查询自己的区域)") @RequestParam(required = false) String areaId
+ ) {
+ List alerts;
+
+ // 构建查询条件(根据参数动态拼接,实际可使用Specification更灵活)
+ if (deviceId != null) {
+ alerts = alertRepository.findByDeviceIdAndTimestampBetween(deviceId, startTime, endTime);
+ } else if (level != null) {
+ alerts = alertRepository.findByAlertLevelAndTimestampBetween(
+ Alert.AlertLevel.valueOf(level), startTime, endTime);
+ } else if (status != null) {
+ alerts = alertRepository.findByStatusAndTimestampBetween(
+ Alert.AlertStatus.valueOf(status), startTime, endTime);
+ } else if (areaId != null) {
+ alerts = alertRepository.findByAreaIdAndTimestampBetween(areaId, startTime, endTime);
+ } else {
+ alerts = alertRepository.findByTimestampBetween(startTime, endTime);
+ }
+
+ return ResultVO.success(alerts);
+ }
+
+ /**
+ * 查询未处理告警(紧急优先)
+ */
+ @GetMapping("/pending")
+ @PreAuthorize("hasAnyRole('ADMIN', 'REPAIRMAN')")
+ public ResultVO> getPendingAlerts(
+ @Parameter(description = "区域ID(可选)") @RequestParam(required = false) String areaId) { // 替换@ApiParam为@Parameter
+ List pendingAlerts = areaId != null
+ ? alertRepository.findByAreaIdAndStatus(areaId, Alert.AlertStatus.pending)
+ : alertRepository.findByStatus(Alert.AlertStatus.pending);
+
+ // 按优先级排序(紧急在前)
+ pendingAlerts.sort((a1, a2) ->
+ Integer.compare(a2.getAlertLevel().getPriority(), a1.getAlertLevel().getPriority()));
+
+ return ResultVO.success(pendingAlerts);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/campus/water/controller/GlobalExceptionHandler.java b/src/main/java/com/campus/water/controller/GlobalExceptionHandler.java
index 832f5e0..902e32b 100644
--- a/src/main/java/com/campus/water/controller/GlobalExceptionHandler.java
+++ b/src/main/java/com/campus/water/controller/GlobalExceptionHandler.java
@@ -1,25 +1,68 @@
-package main.java.com.campus.water.controller;
+package com.campus.water.controller;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
+import com.campus.water.util.ResultVO;
+import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
-import java.util.HashMap;
-import java.util.Map;
+import java.time.format.DateTimeParseException;
/**
- * 全局异常处理,统一响应格式
+ * 全局异常处理器 - 统一处理项目中所有控制器层异常
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
+ /**
+ * 处理参数格式/值错误(如枚举值非法、参数为空等)
+ */
+ @ExceptionHandler(IllegalArgumentException.class)
+ public ResultVO handleIllegalArgument(IllegalArgumentException e) {
+ // 针对告警级别/状态参数错误做友好提示
+ String msg = e.getMessage();
+ if (msg.contains("AlertLevel") || msg.contains("AlertStatus")) {
+ msg = "参数错误:告警级别可选值(info/warning/error/critical),告警状态可选值(pending/resolved/closed)";
+ }
+ return ResultVO.error(400, "参数错误:" + msg);
+ }
+
+ /**
+ * 处理参数类型不匹配(如时间格式错误、字符串转数字失败等)
+ */
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ public ResultVO handleTypeMismatch(MethodArgumentTypeMismatchException e) {
+ String errorMsg;
+ // 特殊处理时间格式错误(告警查询的时间参数)
+ if (e.getCause() instanceof DateTimeParseException) {
+ errorMsg = "时间参数格式错误,正确格式:yyyy-MM-dd HH:mm:ss(示例:2025-12-05 10:30:00)";
+ } else {
+ // 通用类型不匹配提示
+ errorMsg = String.format(
+ "参数[%s]类型错误,期望类型:%s,实际传入值:%s",
+ e.getName(),
+ e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "未知",
+ e.getValue()
+ );
+ }
+ return ResultVO.error(400, errorMsg);
+ }
+
+ /**
+ * 处理权限不足异常(如非管理员/维修人员访问告警接口)
+ */
+ @ExceptionHandler(AccessDeniedException.class)
+ public ResultVO handleAccessDenied(AccessDeniedException e) {
+ return ResultVO.error(403, "权限不足:仅管理员/维修人员可访问告警相关功能");
+ }
+
+ /**
+ * 处理通用运行时异常(兜底)
+ */
@ExceptionHandler(RuntimeException.class)
- public ResponseEntity