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> handleRuntimeException(RuntimeException e) { - Map response = new HashMap<>(); - response.put("code", 401); // 权限相关错误用401,其他可调整 - response.put("msg", e.getMessage()); - response.put("data", null); - return new ResponseEntity<>(response, HttpStatus.OK); + public ResultVO handleRuntimeException(RuntimeException e) { + // 生产环境建议添加日志记录,此处简化 + // log.error("服务器运行时异常", e); + return ResultVO.error(500, "服务器内部错误:" + e.getMessage()); } } \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/WorkOrderController.java b/src/main/java/com/campus/water/controller/WorkOrderController.java index 100ef56..b8d4051 100644 --- a/src/main/java/com/campus/water/controller/WorkOrderController.java +++ b/src/main/java/com/campus/water/controller/WorkOrderController.java @@ -5,6 +5,7 @@ import com.campus.water.mapper.WorkOrderRepository; import com.campus.water.mapper.RepairmanRepository; import com.campus.water.mapper.AlertRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; @@ -24,7 +25,8 @@ public class WorkOrderController { @Autowired private AlertRepository alertRepository; - // 抢单功能 + // 抢单功能 - 维修人员和管理员可访问 + @PreAuthorize("hasAnyRole('REPAIRMAN', 'ADMIN')") @Transactional public boolean grabOrder(String orderId, String repairmanId) { Optional orderOpt = workOrderRepository.findById(orderId); @@ -48,7 +50,8 @@ public class WorkOrderController { return false; } - // 拒单功能 + // 拒单功能 - 维修人员和管理员可访问 + @PreAuthorize("hasAnyRole('REPAIRMAN', 'ADMIN')") @Transactional public boolean rejectOrder(String orderId, String repairmanId, String reason) { Optional orderOpt = workOrderRepository.findById(orderId); @@ -75,7 +78,8 @@ public class WorkOrderController { return false; } - // 提交维修结果 + // 提交维修结果 - 维修人员和管理员可访问 + @PreAuthorize("hasAnyRole('REPAIRMAN', 'ADMIN')") @Transactional public boolean submitRepairResult(String orderId, String repairmanId, String dealNote, String imgUrl) { @@ -106,12 +110,14 @@ public class WorkOrderController { return false; } - // 获取可抢工单列表 + // 获取可抢工单列表 - 维修人员和管理员可访问 + @PreAuthorize("hasAnyRole('REPAIRMAN', 'ADMIN')") public List getAvailableOrders(String areaId) { return workOrderRepository.findByAreaIdAndStatus(areaId, WorkOrder.OrderStatus.pending); } - // 获取维修工自己的工单 + // 获取维修工自己的工单 - 维修人员和管理员可访问 + @PreAuthorize("hasAnyRole('REPAIRMAN', 'ADMIN')") public List getMyOrders(String repairmanId) { return workOrderRepository.findByAssignedRepairmanId(repairmanId); } diff --git a/src/main/java/com/campus/water/entity/Alert.java b/src/main/java/com/campus/water/entity/Alert.java index 2510cc4..43eb7e0 100644 --- a/src/main/java/com/campus/water/entity/Alert.java +++ b/src/main/java/com/campus/water/entity/Alert.java @@ -54,7 +54,28 @@ public class Alert { private LocalDateTime updatedTime = LocalDateTime.now(); public enum AlertLevel { - info, warning, error, critical + info("一般", 1), // 信息级(如状态通知,无需处理) + warning("一般", 2), // 警告级(需关注,非紧急) + error("紧急", 3), // 错误级(需立即处理) + critical("紧急", 4); // 严重级(影响服务,最高优先级) + + private final String levelName; // 分级名称(一般/紧急) + private final int priority; // 处理优先级(1-4,升序) + + AlertLevel(String levelName, int priority) { + this.levelName = levelName; + this.priority = priority; + } + + // 获取分级名称(用于前端展示) + public String getLevelName() { + return levelName; + } + + // 获取优先级(用于推送排序) + public int getPriority() { + return priority; + } } public enum AlertStatus { diff --git a/src/main/java/com/campus/water/mapper/AlertRepository.java b/src/main/java/com/campus/water/mapper/AlertRepository.java index 1ce22ea..bc43f1c 100644 --- a/src/main/java/com/campus/water/mapper/AlertRepository.java +++ b/src/main/java/com/campus/water/mapper/AlertRepository.java @@ -8,24 +8,40 @@ import java.util.List; @Repository public interface AlertRepository extends JpaRepository { + // 根据设备ID和时间范围查询告警 + List findByDeviceIdAndTimestampBetween(String deviceId, LocalDateTime startTime, LocalDateTime endTime); + + // 根据告警级别和时间范围查询告警 + List findByAlertLevelAndTimestampBetween(Alert.AlertLevel level, LocalDateTime startTime, LocalDateTime endTime); + + // 根据告警状态和时间范围查询告警 + List findByStatusAndTimestampBetween(Alert.AlertStatus status, LocalDateTime startTime, LocalDateTime endTime); + + // 根据区域ID和时间范围查询告警 + List findByAreaIdAndTimestampBetween(String areaId, LocalDateTime startTime, LocalDateTime endTime); + + // 根据时间范围查询告警 + List findByTimestampBetween(LocalDateTime startTime, LocalDateTime endTime); + + // 根据区域ID和告警状态查询告警 + List findByAreaIdAndStatus(String areaId, Alert.AlertStatus status); + + // 根据告警状态查询告警 + List findByStatus(Alert.AlertStatus status); + + // 保留原有的其他查询方法(如果有) // 根据设备ID查询告警 List findByDeviceId(String deviceId); // 根据告警类型查询 List findByAlertType(String alertType); - // 根据告警状态查询 - List findByStatus(Alert.AlertStatus status); - // 根据告警级别查询 List findByAlertLevel(Alert.AlertLevel alertLevel); // 根据区域ID查询告警 List findByAreaId(String areaId); - // 根据时间范围查询告警记录 - List findByTimestampBetween(LocalDateTime start, LocalDateTime end); - // 按状态和级别查询告警 List findByStatusAndAlertLevel(Alert.AlertStatus status, Alert.AlertLevel level); @@ -35,7 +51,7 @@ public interface AlertRepository extends JpaRepository { // 根据处理人查询告警 List findByResolvedBy(String resolvedBy); - // 新增:检查重复未处理告警 + // 检查重复未处理告警 List findByDeviceIdAndAlertTypeAndStatusAndTimestampAfter( String deviceId, String alertType, diff --git a/src/main/java/com/campus/water/security/JwtAuthenticationFilter.java b/src/main/java/com/campus/water/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..3223827 --- /dev/null +++ b/src/main/java/com/campus/water/security/JwtAuthenticationFilter.java @@ -0,0 +1,55 @@ +// com/campus/water/security/JwtAuthenticationFilter.java +package com.campus.water.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * JWT认证拦截器 + */ +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private JwtTokenProvider jwtTokenProvider; + + @Autowired + private UserDetailsServiceImpl userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + try { + // 提取JWT令牌 + String jwt = jwtTokenProvider.getJwtFromRequest(request); + + if (jwt != null && jwtTokenProvider.validateJwtToken(jwt)) { + // 获取用户名并加载用户信息 + String username = jwtTokenProvider.getUsernameFromJwtToken(jwt); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + // 设置认证信息 + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + logger.error("认证失败: ", e); + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/security/JwtTokenProvider.java b/src/main/java/com/campus/water/security/JwtTokenProvider.java new file mode 100644 index 0000000..8da2229 --- /dev/null +++ b/src/main/java/com/campus/water/security/JwtTokenProvider.java @@ -0,0 +1,90 @@ +// com/campus/water/security/JwtTokenProvider.java +package com.campus.water.security; + +import io.jsonwebtoken.*; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.Date; + +/** + * JWT令牌生成、验证工具类 + */ +@Component +public class JwtTokenProvider { + + @Value("${jwt.secret}") + private String jwtSecret; + + @Value("${jwt.expiration}") + private long jwtExpirationMs; + + /** + * 生成JWT令牌 + */ + public String generateToken(Authentication authentication) { + UserDetails userPrincipal = (UserDetails) authentication.getPrincipal(); + return Jwts.builder() + .setSubject(userPrincipal.getUsername()) + .claim("roles", userPrincipal.getAuthorities().toString()) + .setIssuedAt(new Date()) + .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)) + .signWith(SignatureAlgorithm.HS512, jwtSecret) + .compact(); + } + + /** + * 直接生成令牌(登录专用) + */ + public String generateToken(String username, String role) { + return Jwts.builder() + .setSubject(username) + .claim("roles", role) + .setIssuedAt(new Date()) + .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)) + .signWith(SignatureAlgorithm.HS512, jwtSecret) + .compact(); + } + + /** + * 从令牌中获取用户名 + */ + public String getUsernameFromJwtToken(String token) { + return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject(); + } + + /** + * 从令牌中获取角色 + */ + public String getRoleFromJwtToken(String token) { + Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody(); + return claims.get("roles", String.class); + } + + /** + * 验证JWT令牌 + */ + public boolean validateJwtToken(String authToken) { + try { + Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken); + return true; + } catch (SignatureException | MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException e) { + return false; + } + } + + /** + * 从请求头中提取JWT令牌 + */ + public String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/security/RoleConstants.java b/src/main/java/com/campus/water/security/RoleConstants.java new file mode 100644 index 0000000..231457e --- /dev/null +++ b/src/main/java/com/campus/water/security/RoleConstants.java @@ -0,0 +1,16 @@ +// com/campus/water/security/RoleConstants.java +package com.campus.water.security; + +/** + * 角色常量定义 + */ +public class RoleConstants { + /** 学生角色 */ + public static final String ROLE_STUDENT = "ROLE_STUDENT"; + /** 维修人员角色 */ + public static final String ROLE_REPAIRMAN = "ROLE_REPAIRMAN"; + /** 管理员角色 */ + public static final String ROLE_ADMIN = "ROLE_ADMIN"; + + private RoleConstants() {} +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/security/UserDetailsServiceImpl.java b/src/main/java/com/campus/water/security/UserDetailsServiceImpl.java new file mode 100644 index 0000000..a2c2eae --- /dev/null +++ b/src/main/java/com/campus/water/security/UserDetailsServiceImpl.java @@ -0,0 +1,79 @@ +// com/campus/water/security/UserDetailsServiceImpl.java +package com.campus.water.security; + +import com.campus.water.entity.po.AdminPO; +import com.campus.water.entity.po.RepairerAuthPO; +import com.campus.water.entity.po.UserPO; +import com.campus.water.mapper.AdminRepository; +import com.campus.water.mapper.RepairerAuthRepository; +import com.campus.water.mapper.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +/** + * 加载用户权限信息 + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + @Autowired + private UserRepository userRepository; // 学生用户仓库 + + @Autowired + private AdminRepository adminRepository; // 管理员仓库 + + @Autowired + private RepairerAuthRepository repairerAuthRepository; // 维修人员仓库 + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 1. 尝试查询学生用户 + UserPO student = userRepository.findByUsername(username).orElse(null); + if (student != null) { + return createUserDetails( + student.getUsername(), + student.getPassword(), + RoleConstants.ROLE_STUDENT + ); + } + + // 2. 尝试查询管理员用户 + AdminPO admin = adminRepository.findByUsername(username).orElse(null); + if (admin != null) { + return createUserDetails( + admin.getUsername(), + admin.getPassword(), + RoleConstants.ROLE_ADMIN + ); + } + + // 3. 尝试查询维修人员用户 + RepairerAuthPO repairer = repairerAuthRepository.findByUsername(username).orElse(null); + if (repairer != null) { + return createUserDetails( + repairer.getUsername(), + repairer.getPassword(), + RoleConstants.ROLE_REPAIRMAN + ); + } + + // 所有类型用户都不存在 + throw new UsernameNotFoundException("用户不存在: " + username); + } + + // 构建UserDetails对象的工具方法 + private UserDetails createUserDetails(String username, String password, String role) { + return new User( + username, + password, + Collections.singletonList(new SimpleGrantedAuthority(role)) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/AlertPushService.java b/src/main/java/com/campus/water/service/AlertPushService.java new file mode 100644 index 0000000..1c5c7b2 --- /dev/null +++ b/src/main/java/com/campus/water/service/AlertPushService.java @@ -0,0 +1,77 @@ +package com.campus.water.service; + +import com.campus.water.entity.Alert; +import com.campus.water.entity.MessagePush; +import com.campus.water.entity.Repairman; +import com.campus.water.mapper.MessagePushRepository; +import com.campus.water.mapper.RepairmanRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +public class AlertPushService { + + private final MessagePushRepository messagePushRepository; + private final RepairmanRepository repairmanRepository; // 维修人员数据访问 + + /** + * 推送告警消息给目标用户(根据告警级别和区域分配) + * @param alert 告警实体 + */ + @Transactional + public void pushAlertMessage(Alert alert) { + // 1. 确定推送目标(紧急告警推送给管理员+区域维修人员;一般告警仅推送给区域维修人员) + String alertType = alert.getAlertType(); + String areaId = alert.getAreaId(); + boolean isEmergency = alert.getAlertLevel().getPriority() >= 3; // 紧急级别(error/critical) + + // 2. 构建消息内容 + MessagePush message = new MessagePush(); + message.setTitle(String.format("[%s告警] %s", + alert.getAlertLevel().getLevelName(), alertType)); + message.setContent(alert.getAlertMessage()); + message.setMessageType("ALERT"); + message.setRelatedId(alert.getAlertId().toString()); // 关联告警ID + message.setPushTime(LocalDateTime.now()); + + // 3. 推送区域维修人员(所有级别都推送) + List areaRepairmen = repairmanRepository.findByAreaId(areaId); + for (Repairman repairman : areaRepairmen) { + MessagePush repairmanMsg = copyMessage(message); + repairmanMsg.setRepairmanId(repairman.getRepairmanId()); + repairmanMsg.setUserId(repairman.getRepairmanId()); + repairmanMsg.setUserType("REPAIRMAN"); + messagePushRepository.save(repairmanMsg); + } + log.info("告警推送区域维修人员完成 | 告警ID:{} | 区域:{} | 人数:{}", + alert.getAlertId(), areaId, areaRepairmen.size()); + + // 4. 紧急告警额外推送管理员(假设管理员userType为"ADMIN",可扩展查询逻辑) + if (isEmergency) { + MessagePush adminMsg = copyMessage(message); + adminMsg.setAdminId("ADMIN001"); // 实际应从管理员表查询 + adminMsg.setUserId("ADMIN001"); + adminMsg.setUserType("ADMIN"); + messagePushRepository.save(adminMsg); + log.info("紧急告警推送管理员完成 | 告警ID:{}", alert.getAlertId()); + } + } + + // 复制消息基础信息(避免重复代码) + private MessagePush copyMessage(MessagePush source) { + MessagePush target = new MessagePush(); + target.setTitle(source.getTitle()); + target.setContent(source.getContent()); + target.setMessageType(source.getMessageType()); + target.setRelatedId(source.getRelatedId()); + target.setPushTime(source.getPushTime()); + return target; + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/AlertTriggerService.java b/src/main/java/com/campus/water/service/AlertTriggerService.java index 817146c..cb45547 100644 --- a/src/main/java/com/campus/water/service/AlertTriggerService.java +++ b/src/main/java/com/campus/water/service/AlertTriggerService.java @@ -155,6 +155,7 @@ public class AlertTriggerService { } } + private final AlertPushService alertPushService; /** * 创建告警记录和对应的工单(支持repair/maintenance类型) * @param orderType 工单类型(repair:维修,maintenance:保养,inspection:巡检<告警不触发>) @@ -175,6 +176,7 @@ public class AlertTriggerService { alert.setStatus(Alert.AlertStatus.pending); alert.setTimestamp(LocalDateTime.now()); alertRepository.save(alert); + alertPushService.pushAlertMessage(alert); log.info("创建告警记录成功 | 告警ID:{} | 设备ID:{} | 告警类型:{}", alert.getAlertId(), deviceId, alertType); diff --git a/src/main/java/com/campus/water/service/app/StudentAppService.java b/src/main/java/com/campus/water/service/app/StudentAppService.java index db0d90c..092fa32 100644 --- a/src/main/java/com/campus/water/service/app/StudentAppService.java +++ b/src/main/java/com/campus/water/service/app/StudentAppService.java @@ -3,6 +3,7 @@ package com.campus.water.service.app; import com.campus.water.controller.WaterUsageController; import com.campus.water.util.ResultVO; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; import java.util.Map; @@ -13,7 +14,8 @@ public class StudentAppService { @Autowired private WaterUsageController waterUsageController; - // 扫码获取终端信息 + // 扫码获取终端信息 - 学生和管理员可访问 + @PreAuthorize("hasAnyRole('STUDENT', 'ADMIN')") public ResultVO> getTerminalInfo(String terminalId) { try { Map result = waterUsageController.getTerminalInfo(terminalId); @@ -23,7 +25,8 @@ public class StudentAppService { } } - // 扫码用水 + // 扫码用水 - 学生和管理员可访问 + @PreAuthorize("hasAnyRole('STUDENT', 'ADMIN')") public ResultVO> scanToDrink(Map request) { try { String terminalId = (String) request.get("terminalId"); @@ -37,7 +40,8 @@ public class StudentAppService { } } - // 查询水质信息 + // 查询水质信息 - 学生和管理员可访问 + @PreAuthorize("hasAnyRole('STUDENT', 'ADMIN')") public ResultVO> getWaterQuality(String deviceId) { try { Map result = waterUsageController.getWaterQualityInfo(deviceId);