Merge remote-tracking branch 'origin/develop' into zhanghongwei_branch

pull/166/head
ZHW 4 months ago
commit 013baee5d5

@ -0,0 +1,3 @@
Manifest-Version: 1.0
Main-Class: com.campus.water.CampusWaterApplication

@ -125,13 +125,34 @@
</dependencies>
<build>
<sourceDirectory>src</sourceDirectory>
<sourceDirectory>src/main/java</sourceDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
<plugins>
<!-- 1. 编译插件解决Lombok注解处理器问题 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>23</source>
<target>23</target>
<encoding>UTF-8</encoding>
<!-- 核心Lombok注解处理器配置 -->
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- 2. Spring Boot打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
@ -142,7 +163,17 @@
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<!-- 必加:指定启动类(替换为你的实际路径) -->
<mainClass>com.campus.water.CampusWaterApplication</mainClass>
</configuration>
<!-- 必加生成可执行JAR -->
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

@ -0,0 +1,46 @@
package com.campus.water.config;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import java.io.IOException;
@Configuration
public class HttpsConfig {
// 优先级最高的过滤器实现HTTP→HTTPS跳转
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public Filter httpToHttpsRedirectFilter() {
return new Filter() {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// 仅处理HTTP请求跳转到HTTPS
if ("http".equals(req.getScheme())) {
String httpsUrl = "https://" + req.getServerName() + ":" + 8081 + req.getRequestURI();
if (req.getQueryString() != null) {
httpsUrl += "?" + req.getQueryString();
}
res.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
res.setHeader("Location", httpsUrl);
return;
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void destroy() {}
};
}
}

@ -98,7 +98,7 @@ public class SecurityConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173"));
configuration.setAllowedOriginPatterns(Arrays.asList("**"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));
configuration.setAllowCredentials(true);

@ -1,19 +1,35 @@
package com.campus.water.controller;
import com.campus.water.entity.BusinessException;
import com.campus.water.util.ResultVO;
import org.springframework.security.access.AccessDeniedException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.time.format.DateTimeParseException;
import java.util.Objects;
import java.io.IOException;
import java.nio.file.AccessDeniedException;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
/**
* -
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
@ -26,6 +42,10 @@ public class GlobalExceptionHandler {
if (msg.contains("AlertLevel") || msg.contains("AlertStatus")) {
msg = "参数错误:告警级别可选值(info/warning/error/critical),告警状态可选值(pending/resolved/closed)";
}
// 设备ID格式错误特殊处理
if (msg.contains("设备ID") || msg.contains("deviceId")) {
msg = "设备ID格式错误正确格式为WM/WS开头+3位数字如WM001、WS123";
}
return ResultVO.error(400, "参数错误:" + msg);
}
@ -36,8 +56,15 @@ public class GlobalExceptionHandler {
public ResultVO<Void> handleTypeMismatch(MethodArgumentTypeMismatchException e) {
String errorMsg;
// 特殊处理时间格式错误(告警查询的时间参数)
if (e.getCause() instanceof DateTimeParseException) {
if (e.getCause() instanceof java.time.format.DateTimeParseException) {
errorMsg = "时间参数格式错误正确格式yyyy-MM-dd HH:mm:ss示例2025-12-05 10:30:00";
} else if (e.getRequiredType() != null && e.getRequiredType().isEnum()) {
// 枚举类型转换错误处理
errorMsg = String.format(
"参数[%s]枚举值错误,允许值:%s",
e.getName(),
getEnumValues(e.getRequiredType())
);
} else {
// 通用类型不匹配提示
errorMsg = String.format(
@ -51,21 +78,35 @@ public class GlobalExceptionHandler {
}
/**
* /访
* /访
*/
@ExceptionHandler(AccessDeniedException.class)
public ResultVO<Void> handleAccessDenied(AccessDeniedException e) {
return ResultVO.error(403, "权限不足:仅管理员/维修人员可访问告警相关功能");
@ExceptionHandler({AccessDeniedException.class, org.springframework.security.access.AccessDeniedException.class})
public ResultVO<Void> handleAccessDenied(Exception e) {
String roleMsg = "仅超级管理员可操作";
// 区分不同接口的权限提示
if (e.getMessage().contains("AREA_ADMIN")) {
roleMsg = "仅区域管理员及以上权限可操作";
} else if (e.getMessage().contains("REPAIRMAN")) {
roleMsg = "仅维修人员及管理员可操作";
}
return ResultVO.error(403, "权限不足:" + roleMsg);
}
/**
*
* /
*/
@ExceptionHandler(RuntimeException.class)
public ResultVO<Void> handleRuntimeException(RuntimeException e) {
// 生产环境建议添加日志记录,此处简化
// log.error("服务器运行时异常", e);
return ResultVO.error(500, "服务器内部错误:" + e.getMessage());
@ExceptionHandler(NoSuchElementException.class)
public ResultVO<Void> handleNoSuchElement(NoSuchElementException e) {
String msg = e.getMessage();
// 标准化资源不存在提示
if (msg.contains("设备")) {
return ResultVO.error(404, "设备不存在:" + msg.replace("No value present", "").trim());
} else if (msg.contains("管理员") || msg.contains("Admin")) {
return ResultVO.error(404, "管理员不存在:" + msg.replace("No value present", "").trim());
} else if (msg.contains("区域") || msg.contains("Area")) {
return ResultVO.error(404, "区域不存在:" + msg.replace("No value present", "").trim());
}
return ResultVO.error(404, "请求的资源不存在:" + msg);
}
/**
@ -73,8 +114,176 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultVO<Void> handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
// 获取第一个验证失败的字段和消息
String errorMsg = Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage();
return ResultVO.badRequest(errorMsg); // 返回400状态码和具体错误信息
// 收集所有验证失败的字段和消息
List<String> errorMessages = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + "" + error.getDefaultMessage())
.collect(Collectors.toList());
return ResultVO.error(400, "参数验证失败:" + String.join("", errorMessages));
}
/**
* @RequestBody
*/
@ExceptionHandler(BindException.class)
public ResultVO<Void> handleBindException(BindException e) {
FieldError firstError = e.getBindingResult().getFieldError();
String errorMsg = firstError != null ?
firstError.getField() + "" + firstError.getDefaultMessage() :
"参数绑定失败";
return ResultVO.error(400, "表单参数错误:" + errorMsg);
}
/**
*
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResultVO<Void> handleMissingParam(MissingServletRequestParameterException e) {
return ResultVO.error(400,
String.format("缺少必填参数:%s类型%s",
e.getParameterName(),
e.getParameterType()));
}
/**
*
*/
@ExceptionHandler(DuplicateKeyException.class)
public ResultVO<Void> handleDuplicateKey(DuplicateKeyException e) {
log.error("数据库唯一约束冲突", e);
String msg = "数据已存在,无法重复添加";
// 针对设备ID冲突特殊处理
if (e.getMessage().contains("device_id")) {
msg = "设备ID已存在请更换设备ID后重试";
} else if (e.getMessage().contains("admin_name")) {
msg = "管理员用户名已存在,请更换用户名";
}
return ResultVO.error(409, msg);
}
/**
*
*/
@ExceptionHandler(DataIntegrityViolationException.class)
public ResultVO<Void> handleDataIntegrityViolation(DataIntegrityViolationException e) {
log.error("数据库完整性约束异常", e);
String msg = "数据操作失败,可能存在关联数据";
if (e.getMessage().contains("foreign key constraint")) {
msg = "无法删除,该数据已被其他记录关联引用";
} else if (e.getMessage().contains("not null")) {
msg = "必填字段不能为空,请检查输入";
}
return ResultVO.error(400, msg);
}
/**
* JSON
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResultVO<Void> handleHttpMessageNotReadable(HttpMessageNotReadableException e) {
log.error("请求体解析失败", e);
String msg = "请求数据格式错误请检查JSON格式是否正确";
if (e.getMessage().contains("date-time")) {
msg = "日期时间格式错误正确格式yyyy-MM-dd HH:mm:ss";
}
return ResultVO.error(400, msg);
}
/**
* HTTP
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResultVO<Void> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e) {
return ResultVO.error(405,
String.format("不支持的请求方法:%s支持的方法%s",
e.getMethod(),
String.join(",", e.getSupportedMethods())));
}
/**
*
*/
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ResultVO<Void> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException e) {
return ResultVO.error(415,
String.format("不支持的媒体类型:%s支持的类型%s",
e.getContentType(),
e.getSupportedMediaTypes()));
}
/**
*
*/
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResultVO<Void> handleMaxUploadSizeExceeded(MaxUploadSizeExceededException e) {
long maxSizeMB = e.getMaxUploadSize() / (1024 * 1024);
return ResultVO.error(413,
String.format("文件大小超限,最大支持:%dMB", maxSizeMB));
}
/**
* IO
*/
@ExceptionHandler(IOException.class)
public ResultVO<Void> handleIOException(IOException e) {
log.error("IO操作异常", e);
String msg = "文件操作失败:" + e.getMessage();
if (e.getMessage().contains("Permission denied")) {
msg = "文件操作权限不足";
}
return ResultVO.error(500, msg);
}
/**
*
*/
@ExceptionHandler(BusinessException.class)
public ResultVO<Void> handleBusinessException(BusinessException e) {
// 业务异常自带状态码和消息
return ResultVO.error(e.getCode(), e.getMessage());
}
/**
* 404
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ResultVO<Void> handleNoHandlerFound(NoHandlerFoundException e) {
return ResultVO.error(404,
String.format("请求的接口不存在:%s %s",
e.getHttpMethod(),
e.getRequestURL()));
}
/**
*
*/
@ExceptionHandler(RuntimeException.class)
public ResultVO<Void> handleRuntimeException(RuntimeException e) {
log.error("服务器运行时异常", e);
// 生产环境可根据异常类型返回更友好的提示
String msg = "服务器内部错误:" + e.getMessage();
// 对常见运行时异常进行特殊处理
if (e instanceof NullPointerException) {
msg = "系统处理异常:数据为空";
} else if (e instanceof IndexOutOfBoundsException) {
msg = "系统处理异常:数据索引越界";
}
return ResultVO.error(500, msg);
}
/**
*
*/
private String getEnumValues(Class<?> enumClass) {
if (!enumClass.isEnum()) {
return "未知";
}
StringBuilder values = new StringBuilder();
for (Object enumConstant : enumClass.getEnumConstants()) {
values.append(enumConstant).append(",");
}
if (values.length() > 0) {
values.deleteCharAt(values.length() - 1);
}
return values.toString();
}
}

@ -0,0 +1,55 @@
package com.campus.water.entity; // 请确保这个包名与你的项目结构一致
import org.springframework.http.HttpStatus;
/**
*
* <p>
*
*
* </p>
*/
public class BusinessException extends RuntimeException {
private static final long serialVersionUID = 1L;
/**
* HTTP
*/
private int code;
/**
*
*/
private String message;
/**
*
* @param code
* @param message
*/
public BusinessException(int code, String message) {
super(message);
this.code = code;
this.message = message;
}
/**
* 使 400 Bad Request
* @param message
*/
public BusinessException(String message) {
this(HttpStatus.BAD_REQUEST.value(), message);
}
// --- Getters ---
public int getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}

@ -58,4 +58,6 @@ public interface AlertRepository extends JpaRepository<Alert, Long> {
List<Alert.AlertStatus> activeStatus,
LocalDateTime timestamp
);
List<Alert> findByResolvedByAndStatus(String repairmanId, Alert.AlertStatus alertStatus);
}

@ -554,4 +554,250 @@ public class CommonUtils {
default -> "普通";
};
}
/**
* MAC12/线
*/
public static boolean validateMacAddress(String mac) {
if (isBlankWithFullWidth(mac)) {
return false;
}
// 去除分隔符转为纯12位十六进制字符串
String pureMac = mac.replace(":", "").replace("-", "").trim().toLowerCase();
if (pureMac.length() != 12) {
return false;
}
return pureMac.matches("[0-9a-f]+");
}
/**
* IPIPv4
*/
public static boolean validateIpv4Address(String ip) {
if (isBlankWithFullWidth(ip)) {
return false;
}
String[] ipSegments = ip.split("\\.");
if (ipSegments.length != 4) {
return false;
}
try {
for (String segment : ipSegments) {
int num = Integer.parseInt(segment);
if (num < 0 || num > 255) {
return false;
}
}
return true;
} catch (NumberFormatException e) {
log.warn("IP地址格式错误{}", ip);
return false;
}
}
/**
* IDID
*/
public static List<String> batchValidateDeviceId(List<String> deviceIdList) {
List<String> invalidIds = new ArrayList<>();
if (isEmpty(deviceIdList)) {
return invalidIds;
}
for (String deviceId : deviceIdList) {
if (!validateDeviceId(deviceId)) {
invalidIds.add(deviceId);
}
}
return invalidIds;
}
/**
* /
*/
public static String convertCnToDeviceStatus(String cnStatus) {
if (isBlankWithFullWidth(cnStatus)) {
return "unknown";
}
return switch (cnStatus.trim()) {
case "在线" -> "online";
case "离线" -> "offline";
case "故障" -> "fault";
default -> "unknown";
};
}
/**
* MPaBar1MPa=10Bar
*/
public static BigDecimal convertMpaToBar(BigDecimal mpa) {
if (!validateSensorValue(mpa)) {
return BigDecimal.ZERO;
}
return mpa.multiply(new BigDecimal("10")).setScale(2, RoundingMode.HALF_UP);
}
/**
* BarMPa
*/
public static BigDecimal convertBarToMpa(BigDecimal bar) {
if (!validateSensorValue(bar)) {
return BigDecimal.ZERO;
}
return bar.divide(new BigDecimal("10"), 2, RoundingMode.HALF_UP);
}
/**
* 0-100寿/
*/
public static String convertNumToCn(Integer num) {
if (num == null || num < 0 || num > 100) {
return "零";
}
String[] cnNums = {"零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"};
if (num <= 10) {
return cnNums[num];
} else if (num < 20) {
return "十" + cnNums[num - 10];
} else if (num % 10 == 0) {
return cnNums[num / 10] + "十";
} else {
return cnNums[num / 10] + "十" + cnNums[num % 10];
}
}
/**
*
*/
public static String convertOrderStatusToCn(String status) {
if (isBlankWithFullWidth(status)) {
return "未知状态";
}
return switch (status.toLowerCase()) {
case "pending" -> "待处理";
case "processing" -> "处理中";
case "completed" -> "已完成";
case "cancelled" -> "已取消";
default -> "未知状态";
};
}
/**
*
*/
public static String convertOrderTypeToCn(String type) {
if (isBlankWithFullWidth(type)) {
return "未知类型";
}
return switch (type.toLowerCase()) {
case "repair" -> "故障维修";
case "maintenance" -> "定期保养";
case "inspection" -> "设备巡检";
default -> "未知类型";
};
}
/**
*
*/
public static String generateMockAlertMessage(DeviceType type) {
String[] makerAlerts = {
"原水TDS值过高超出阈值",
"纯水TDS值异常滤芯可能失效",
"水压过低,设备无法正常制水",
"设备检测到漏水,需紧急处理",
"滤芯寿命不足,需尽快更换"
};
String[] supplyAlerts = {
"水位过低,需及时补水",
"水压异常,供水不稳定",
"水温过高,设备散热异常",
"出水流量过低,可能堵塞",
"设备离线,通信中断"
};
Random random = new Random();
if (DeviceType.water_maker.equals(type)) {
return makerAlerts[random.nextInt(makerAlerts.length)];
} else {
return supplyAlerts[random.nextInt(supplyAlerts.length)];
}
}
/**
* nm*
*/
public static String desensitizeString(String str, int keepPrefix, int keepSuffix) {
if (isBlankWithFullWidth(str)) {
return str;
}
int length = str.length();
if (length <= keepPrefix + keepSuffix) {
return str;
}
StringBuilder sb = new StringBuilder();
sb.append(str.substring(0, keepPrefix));
for (int i = 0; i < length - keepPrefix - keepSuffix; i++) {
sb.append("*");
}
sb.append(str.substring(length - keepSuffix));
return sb.toString();
}
/**
*
*/
public static <T> List<T> batchDefaultIfNull(List<T> list, T defaultValue) {
List<T> result = new ArrayList<>();
if (isEmpty(list)) {
return result;
}
for (T item : list) {
result.add(defaultIfNull(item, defaultValue));
}
return result;
}
/**
*
*/
public static String generateRandomNumStr(int length) {
if (length <= 0) {
return "";
}
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(random.nextInt(10));
}
return sb.toString();
}
/**
*
*/
public static String generateRandomLetterStr(int length) {
if (length <= 0) {
return "";
}
Random random = new Random();
StringBuilder sb = new StringBuilder();
String letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
for (int i = 0; i < length; i++) {
sb.append(letters.charAt(random.nextInt(letters.length())));
}
return sb.toString();
}
/**
*
*/
public static long calculateDayInterval(Date start, Date end) {
if (start == null || end == null) {
return 0;
}
long millisDiff = Math.abs(end.getTime() - start.getTime());
return millisDiff / (24 * 60 * 60 * 1000);
}
}

File diff suppressed because it is too large Load Diff

@ -14,6 +14,9 @@
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@capacitor/android": "^8.0.0",
"@capacitor/cli": "^8.0.0",
"@capacitor/core": "^8.0.0",
"pinia": "^3.0.4",
"vue": "^3.5.25",
"vue-router": "^4.6.3"

@ -2,7 +2,7 @@
import axios from 'axios'
const apiClient = axios.create({
baseURL: 'http://localhost:8080',
baseURL: 'http://120.46.151.248:8080',
headers: {
'Content-Type': 'application/json'
}

@ -0,0 +1,15 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
publicPath: './', // 重要:使用相对路径
outputDir: 'dist',
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080', // 后端地址
changeOrigin: true
}
}
}
})

@ -35,4 +35,12 @@ server:
charset: UTF-8
enabled: true
force: true
port: 8080
port: 8081
ssl:
key-store: classpath:keystore.p12
key-store-password: 123456
key-store-type: PKCS12
key-alias: myhttps
additional-ports:
- port: 8080
protocol: HTTP

Binary file not shown.

@ -4,7 +4,7 @@ import { useAuthStore } from '@/stores/auth'
// 创建最基本的axios实例
const apiClient = axios.create({
baseURL: 'http://localhost:8080',
baseURL: 'http://120.46.151.248:8080',
headers: {
'Content-Type': 'application/json'
}

Loading…
Cancel
Save