From cc503b5ee7a7288e744c73dcc3a26544bb5a0e30 Mon Sep 17 00:00:00 2001 From: wanglei <3085637232@qq.com> Date: Fri, 5 Dec 2025 18:30:55 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E7=8E=8B=E7=A3=8A=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E6=95=B0=E6=8D=AE=E7=BB=9F=E8=AE=A1=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=88=E6=8C=89=E8=AE=BE=E5=A4=87=20/=20=E5=9C=B0?= =?UTF-8?q?=E5=8C=BA=20/=20=E6=97=B6=E9=97=B4=E7=BB=9F=E8=AE=A1=E7=94=A8?= =?UTF-8?q?=E6=B0=B4=E9=87=8F=E3=80=81=E5=91=8A=E8=AD=A6=E6=AC=A1=E6=95=B0?= =?UTF-8?q?=EF=BC=89=E3=80=81=E8=AE=BE=E5=A4=87=E7=8A=B6=E6=80=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=8E=A5=E5=8F=A3=EF=BC=88=E5=9C=A8=E7=BA=BF=20/=20?= =?UTF-8?q?=E7=A6=BB=E7=BA=BF=E6=A0=87=E8=AE=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../campus/water/config/MyBatisConfig.java | 214 +++++ .../campus/water/config/SwaggerConfig.java | 410 +++++++++ .../app/AppStatisticsController.java | 84 ++ .../web/DeviceStatusController.java | 153 ++++ .../controller/web/StatisticsController.java | 103 +++ .../request/DeviceStatusUpdateRequest.java | 27 + .../dto/request/StatisticsQueryRequest.java | 35 + .../water/entity/vo/AlarmStatisticsVO.java | 40 + .../campus/water/entity/vo/StatisticsVO.java | 45 + .../com/campus/water/mapper/DeviceMapper.java | 301 +++++++ .../com/campus/water/mapper/DeviceMapper.xml | 383 ++++++++ .../campus/water/mapper/StatisticsMapper.java | 102 +++ .../campus/water/mapper/StatisticsMapper.xml | 251 ++++++ .../campus/water/service/AlertService.java | 828 ++++++++++++++++++ .../campus/water/service/DeviceService.java | 219 +++++ .../water/service/StatisticsService.java | 247 ++++++ .../water/task/DeviceStatusMonitorTask.java | 54 ++ 17 files changed, 3496 insertions(+) create mode 100644 src/main/java/com/campus/water/config/MyBatisConfig.java create mode 100644 src/main/java/com/campus/water/config/SwaggerConfig.java create mode 100644 src/main/java/com/campus/water/controller/app/AppStatisticsController.java create mode 100644 src/main/java/com/campus/water/controller/web/DeviceStatusController.java create mode 100644 src/main/java/com/campus/water/controller/web/StatisticsController.java create mode 100644 src/main/java/com/campus/water/entity/dto/request/DeviceStatusUpdateRequest.java create mode 100644 src/main/java/com/campus/water/entity/dto/request/StatisticsQueryRequest.java create mode 100644 src/main/java/com/campus/water/entity/vo/AlarmStatisticsVO.java create mode 100644 src/main/java/com/campus/water/entity/vo/StatisticsVO.java create mode 100644 src/main/java/com/campus/water/mapper/DeviceMapper.java create mode 100644 src/main/java/com/campus/water/mapper/DeviceMapper.xml create mode 100644 src/main/java/com/campus/water/mapper/StatisticsMapper.java create mode 100644 src/main/java/com/campus/water/mapper/StatisticsMapper.xml create mode 100644 src/main/java/com/campus/water/service/AlertService.java create mode 100644 src/main/java/com/campus/water/service/DeviceService.java create mode 100644 src/main/java/com/campus/water/service/StatisticsService.java create mode 100644 src/main/java/com/campus/water/task/DeviceStatusMonitorTask.java diff --git a/src/main/java/com/campus/water/config/MyBatisConfig.java b/src/main/java/com/campus/water/config/MyBatisConfig.java new file mode 100644 index 0000000..b78aedb --- /dev/null +++ b/src/main/java/com/campus/water/config/MyBatisConfig.java @@ -0,0 +1,214 @@ +package com.campus.water.config; + +import com.zaxxer.hikari.HikariDataSource; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * MyBatis配置类 + * 功能:配置MyBatis与Spring Boot的集成,包括数据源、事务管理、Mapper扫描等 + * 用途: + * 1. 数据源配置:连接池、连接参数 + * 2. MyBatis核心配置:SqlSessionFactory、事务管理器 + * 3. Mapper扫描:自动注册MyBatis映射接口 + * 4. 插件配置:分页插件、性能监控插件等 + * 核心注解: + * - @Configuration: 声明为配置类 + * - @MapperScan: 指定Mapper接口的扫描路径 + * - @EnableTransactionManagement: 启用声明式事务管理 + */ +@Configuration +@MapperScan(basePackages = "com.campus.water.mapper") // 扫描Mapper接口 +@EnableTransactionManagement // 启用事务管理 +public class MyBatisConfig { + + // ========== 数据源配置 ========== + + /** + * 创建HikariCP数据源 + * @return 数据源实例 + * 功能:配置数据库连接池 + * 优点: + * - HikariCP是目前性能最好的连接池 + * - 自动从application.yml读取配置 + * - 支持连接泄漏检测、空闲连接回收 + */ + @Bean + @ConfigurationProperties(prefix = "spring.datasource.hikari") // 绑定配置前缀 + public DataSource dataSource() { + HikariDataSource dataSource = new HikariDataSource(); + + // 设置连接池监控配置(可在application.yml中覆盖) + dataSource.setPoolName("CampusWaterHikariPool"); + dataSource.setMaximumPoolSize(20); // 最大连接数 + dataSource.setMinimumIdle(5); // 最小空闲连接 + dataSource.setIdleTimeout(600000); // 空闲连接超时时间(10分钟) + dataSource.setConnectionTimeout(30000); // 连接超时时间(30秒) + dataSource.setMaxLifetime(1800000); // 连接最大生命周期(30分钟) + + // 连接测试配置 + dataSource.setConnectionTestQuery("SELECT 1"); // MySQL测试语句 + dataSource.setValidationTimeout(5000); // 验证超时时间 + + // 连接泄漏检测 + dataSource.setLeakDetectionThreshold(60000); // 60秒泄漏检测阈值 + + return dataSource; + } + + // ========== MyBatis核心配置 ========== + + /** + * 创建SqlSessionFactory + * @param dataSource 数据源 + * @return SqlSessionFactory实例 + * 功能:MyBatis的核心工厂类,用于创建SqlSession + * 配置项: + * - 数据源绑定 + * - Mapper XML文件位置 + * - 类型别名包扫描 + * - 全局配置文件 + */ + @Bean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { + SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); + + // 1. 设置数据源 + sessionFactory.setDataSource(dataSource); + + // 2. 设置Mapper XML文件位置(支持Ant风格路径) + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + sessionFactory.setMapperLocations( + resolver.getResources("classpath:mapper/**/*.xml") + ); + + // 3. 设置类型别名包扫描路径(实体类包) + sessionFactory.setTypeAliasesPackage("com.campus.water.entity"); + + // 4. 创建Configuration对象,设置全局配置 + org.apache.ibatis.session.Configuration configuration = + new org.apache.ibatis.session.Configuration(); + configuration.setMapUnderscoreToCamelCase(true); // 下划线转驼峰 + configuration.setUseGeneratedKeys(true); // 使用JDBC的getGeneratedKeys获取主键 + configuration.setUseColumnLabel(true); // 使用列标签代替列名 + configuration.setCacheEnabled(true); // 启用二级缓存 + + // 5. 设置日志实现 + configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class); + + // 6. 设置默认的执行器类型 + configuration.setDefaultExecutorType(org.apache.ibatis.session.ExecutorType.SIMPLE); + + // 7. 设置配置 + sessionFactory.setConfiguration(configuration); + + // 8. 设置插件(分页插件、性能监控插件等) + sessionFactory.setPlugins( + // 分页插件 + pageInterceptor(), + // 性能分析插件(开发环境使用) + // performanceInterceptor() + ); + + return sessionFactory.getObject(); + } + + /** + * 分页插件配置 + * @return 分页拦截器 + * 功能:自动处理分页查询,支持MySQL、Oracle等多种数据库 + * 特性: + * - 自动识别数据库类型 + * - 支持多种分页方式 + * - 线程安全 + */ + @Bean + public com.github.pagehelper.PageInterceptor pageInterceptor() { + com.github.pagehelper.PageInterceptor pageInterceptor = + new com.github.pagehelper.PageInterceptor(); + + Properties properties = new Properties(); + + // 1. 分页合理化:pageNum<=0时查询第一页,pageNum>总页数时查询最后一页 + properties.setProperty("reasonable", "true"); + + // 2. 支持通过Mapper接口参数传递分页参数 + properties.setProperty("supportMethodsArguments", "true"); + + // 3. 自动检测数据库类型 + properties.setProperty("autoRuntimeDialect", "true"); + + // 4. 分页参数默认值 + properties.setProperty("pageSizeZero", "true"); // pageSize=0时返回全部结果 + properties.setProperty("params", "count=countSql"); + + // 5. 开启分页返回对象中的统计信息 + properties.setProperty("rowBoundsWithCount", "true"); + + // 6. 总是返回PageInfo对象 + properties.setProperty("returnPageInfo", "always"); + + pageInterceptor.setProperties(properties); + return pageInterceptor; + } + + /** + * 性能分析插件(开发环境使用) + * @return 性能分析拦截器 + * 功能:记录SQL执行时间,帮助优化慢查询 + * 注意:生产环境建议关闭或设置较高的阈值 + */ + /* + @Bean + @Profile({"dev", "test"}) // 只在开发测试环境启用 + public com.github.pagehelper.sql.SqlStatsInterceptor performanceInterceptor() { + com.github.pagehelper.sql.SqlStatsInterceptor interceptor = + new com.github.pagehelper.sql.SqlStatsInterceptor(); + + Properties properties = new Properties(); + properties.setProperty("maxTime", "1000"); // 最大执行时间阈值(毫秒) + properties.setProperty("format", "true"); // 格式化SQL输出 + + interceptor.setProperties(properties); + return interceptor; + } + */ + + // ========== 事务管理配置 ========== + + /** + * 创建事务管理器 + * @param dataSource 数据源 + * @return 平台事务管理器 + * 功能:管理数据库事务,支持声明式事务 + * 注解支持:@Transactional + */ + @Bean + public PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + + // ========== 其他配置 ========== + + /** + * MyBatis属性配置 + * @return MyBatis配置属性 + * 功能:集中管理MyBatis相关配置 + */ + @Bean + @ConfigurationProperties(prefix = "mybatis.configuration") + public org.apache.ibatis.session.Configuration mybatisConfiguration() { + return new org.apache.ibatis.session.Configuration(); + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/config/SwaggerConfig.java b/src/main/java/com/campus/water/config/SwaggerConfig.java new file mode 100644 index 0000000..5f4c669 --- /dev/null +++ b/src/main/java/com/campus/water/config/SwaggerConfig.java @@ -0,0 +1,410 @@ +package com.campus.water.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import java.util.Arrays; +import java.util.List; + +/** + * Swagger/OpenAPI配置类 + * 功能:配置API文档生成和展示 + * 用途: + * 1. 自动生成API文档:基于代码注解 + * 2. 在线API测试:提供交互式测试界面 + * 3. 接口规范定义:统一API响应格式和错误码 + * 4. 权限控制:JWT token验证配置 + * 访问地址: + * - Swagger UI: http://localhost:8080/swagger-ui/index.html + * - OpenAPI JSON: http://localhost:8080/v3/api-docs + * 核心注解: + * - @Configuration: 声明为配置类 + * - @Profile: 指定生效的环境(开发/测试环境) + */ +@Configuration +@Profile({"dev", "test"}) // 只在开发测试环境启用,生产环境关闭 +public class SwaggerConfig { + + @Value("${spring.application.name:校园直饮矿化水物联网运维平台}") + private String applicationName; + + @Value("${server.port:8080}") + private String serverPort; + + /** + * 创建OpenAPI配置 + * @return OpenAPI配置实例 + * 功能:定义API文档的基本信息、安全方案、服务器等 + * 结构: + * - info: API基本信息(标题、版本、描述等) + * - servers: API服务器地址 + * - components: 可重用的组件(安全方案、响应模型等) + * - security: 全局安全要求 + */ + @Bean + public OpenAPI campusWaterOpenAPI() { + return new OpenAPI() + // 1. API基本信息 + .info(buildApiInfo()) + + // 2. API服务器配置 + .servers(buildServers()) + + // 3. 安全方案配置(JWT Bearer Token) + .components(buildComponents()) + .addSecurityItem(buildSecurityRequirement()) + + // 4. 全局标签(可在此定义,也可以在Controller上使用@Tag) + // .tags(buildTags()) + + // 5. 外部文档链接 + .externalDocs(new io.swagger.v3.oas.models.ExternalDocumentation() + .description("项目GitHub仓库") + .url("https://github.com/campus-water/water-management-system")); + } + + /** + * 构建API基本信息 + * @return Info对象 + * 功能:定义API的标题、版本、描述、联系方式等 + */ + private Info buildApiInfo() { + return new Info() + .title("校园直饮矿化水物联网运维平台 API") + .version("v1.0.0") + .description(""" + ## 项目概述 + + 校园直饮矿化水物联网运维平台是一个集设备监控、数据统计、告警管理、运维调度于一体的智能化管理系统。 + + ## 功能模块 + + ### 1. 设备管理 + - 设备状态监控(在线/离线/故障) + - 设备信息维护 + - 设备区域分配 + + ### 2. 数据统计 + - 用水量统计(按设备/区域/时间) + - 告警统计分析 + - 设备使用率统计 + - 水质数据统计 + + ### 3. 告警管理 + - 实时告警监控 + - 告警处理流程 + - 告警统计分析 + + ### 4. 工单管理 + - 维修工单创建 + - 工单分配和跟踪 + - 维修结果反馈 + + ### 5. 用户管理 + - 多角色权限控制(学生/维修人员/管理员) + - 个人信息管理 + - 饮水记录查询 + + ## 接口规范 + + ### 响应格式 + ```json + { + "code": 200, // 状态码 + "msg": "success", // 消息 + "data": {} // 数据 + } + ``` + + ### 状态码说明 + - 200: 成功 + - 400: 请求参数错误 + - 401: 未授权 + - 403: 禁止访问 + - 404: 资源不存在 + - 500: 服务器内部错误 + """) + .termsOfService("https://campus-water.com/terms") + .contact(buildContact()) + .license(buildLicense()); + } + + /** + * 构建联系信息 + * @return Contact对象 + * 功能:提供项目联系人和联系方式 + */ + private Contact buildContact() { + return new Contact() + .name("开发团队") + .url("https://campus-water.com") + .email("dev@campus-water.com") + .extensions(new java.util.HashMap<>() {{ + put("技术支持", "support@campus-water.com"); + put("产品反馈", "feedback@campus-water.com"); + }}); + } + + /** + * 构建许可证信息 + * @return License对象 + * 功能:定义API的使用许可证 + */ + private License buildLicense() { + return new License() + .name("Apache 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0.html") + .identifier("Apache-2.0"); + } + + /** + * 构建服务器配置 + * @return 服务器列表 + * 功能:定义API的访问地址,支持多环境 + */ + private List buildServers() { + return Arrays.asList( + // 本地开发环境 + new Server() + .url("http://localhost:" + serverPort) + .description("本地开发环境") + .variables(new java.util.HashMap<>() {{ + put("port", new io.swagger.v3.oas.models.servers.ServerVariable() + ._default(serverPort) + .description("服务器端口")); + }}), + + // 测试环境 + new Server() + .url("https://test.campus-water.com") + .description("测试环境") + .variables(new java.util.HashMap<>() {{ + put("env", new io.swagger.v3.oas.models.servers.ServerVariable() + ._default("test") + .description("环境标识") + ._enum(Arrays.asList("test", "staging"))); + }}), + + // 生产环境 + new Server() + .url("https://api.campus-water.com") + .description("生产环境") + .variables(new java.util.HashMap<>() {{ + put("version", new io.swagger.v3.oas.models.servers.ServerVariable() + ._default("v1") + .description("API版本") + ._enum(Arrays.asList("v1", "v2"))); + }}) + ); + } + + /** + * 构建组件配置 + * @return Components对象 + * 功能:定义可重用的组件,如安全方案、响应模型等 + */ + private Components buildComponents() { + return new Components() + // 1. JWT Bearer Token安全方案 + .addSecuritySchemes("Bearer Token", buildJwtSecurityScheme()) + + // 2. API Key安全方案(备用) + .addSecuritySchemes("API Key", buildApiKeySecurityScheme()) + + // 3. 通用响应模型 + .addSchemas("ResultVO", buildResultVOSchema()) + .addSchemas("PageResult", buildPageResultSchema()) + + // 4. 通用请求头 + .addParameters("X-User-Id", buildUserIdHeader()) + .addParameters("X-User-Type", buildUserTypeHeader()); + } + + /** + * 构建JWT安全方案 + * @return SecurityScheme对象 + * 功能:定义JWT Bearer Token的认证方式 + */ + private SecurityScheme buildJwtSecurityScheme() { + return new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .description(""" + JWT Bearer Token认证 + + ### 获取Token + 1. 调用登录接口获取token + 2. 在请求头中添加:Authorization: Bearer {token} + + ### Token格式 + ```json + { + "sub": "用户ID", + "username": "用户名", + "userType": "用户类型", + "iat": 签发时间, + "exp": 过期时间 + } + ``` + """) + .in(SecurityScheme.In.HEADER) + .name("Authorization"); + } + + /** + * 构建API Key安全方案 + * @return SecurityScheme对象 + * 功能:备用认证方式,用于第三方系统集成 + */ + private SecurityScheme buildApiKeySecurityScheme() { + return new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .name("X-API-KEY") + .description("API Key认证,用于第三方系统集成"); + } + + /** + * 构建通用响应模型 + * @return Schema对象 + * 功能:定义统一的API响应格式 + */ + private io.swagger.v3.oas.models.media.Schema buildResultVOSchema() { + return new io.swagger.v3.oas.models.media.Schema<>() + .type("object") + .addProperties("code", new io.swagger.v3.oas.models.media.Schema<>() + .type("integer") + .description("状态码") + .example(200)) + .addProperties("msg", new io.swagger.v3.oas.models.media.Schema<>() + .type("string") + .description("消息") + .example("success")) + .addProperties("data", new io.swagger.v3.oas.models.media.Schema<>() + .type("object") + .description("数据") + .nullable(true)) + .addProperties("timestamp", new io.swagger.v3.oas.models.media.Schema<>() + .type("string") + .format("date-time") + .description("时间戳") + .example("2024-01-01T12:00:00Z")) + .description("通用响应格式") + .required(Arrays.asList("code", "msg", "timestamp")); + } + + /** + * 构建分页响应模型 + * @return Schema对象 + * 功能:定义统一的分页响应格式 + */ + private io.swagger.v3.oas.models.media.Schema buildPageResultSchema() { + return new io.swagger.v3.oas.models.media.Schema<>() + .type("object") + .addProperties("total", new io.swagger.v3.oas.models.media.Schema<>() + .type("integer") + .description("总记录数") + .example(100)) + .addProperties("pages", new io.swagger.v3.oas.models.media.Schema<>() + .type("integer") + .description("总页数") + .example(10)) + .addProperties("pageNum", new io.swagger.v3.oas.models.media.Schema<>() + .type("integer") + .description("当前页码") + .example(1)) + .addProperties("pageSize", new io.swagger.v3.oas.models.media.Schema<>() + .type("integer") + .description("每页大小") + .example(10)) + .addProperties("list", new io.swagger.v3.oas.models.media.Schema<>() + .type("array") + .description("数据列表") + .items(new io.swagger.v3.oas.models.media.Schema<>())) + .description("分页响应格式") + .required(Arrays.asList("total", "pages", "pageNum", "pageSize", "list")); + } + + /** + * 构建用户ID请求头 + * @return Parameter对象 + * 功能:定义用户ID请求头的规范 + */ + private io.swagger.v3.oas.models.parameters.Parameter buildUserIdHeader() { + return new io.swagger.v3.oas.models.parameters.Parameter() + .name("X-User-Id") + .in(io.swagger.v3.oas.models.parameters.Parameter.In.HEADER.toString()) + .description("用户ID(登录后由系统分配)") + .required(false) + .schema(new io.swagger.v3.oas.models.media.Schema<>() + .type("string") + .example("STU20240001")); + } + + /** + * 构建用户类型请求头 + * @return Parameter对象 + * 功能:定义用户类型请求头的规范 + */ + private io.swagger.v3.oas.models.parameters.Parameter buildUserTypeHeader() { + return new io.swagger.v3.oas.models.parameters.Parameter() + .name("X-User-Type") + .in(io.swagger.v3.oas.models.parameters.Parameter.In.HEADER.toString()) + .description("用户类型:student/repairer/admin") + .required(false) + .schema(new io.swagger.v3.oas.models.media.Schema<>() + .type("string") + .example("student")); + } + + /** + * 构建安全要求 + * @return SecurityRequirement对象 + * 功能:定义需要认证的接口范围 + */ + private SecurityRequirement buildSecurityRequirement() { + return new SecurityRequirement() + .addList("Bearer Token"); + } + + /** + * 构建API标签 + * @return 标签列表 + * 功能:组织API接口到不同的标签组 + */ + /* + private List buildTags() { + return Arrays.asList( + new io.swagger.v3.oas.models.tags.Tag() + .name("用户管理") + .description("用户认证、注册、个人信息等接口"), + new io.swagger.v3.oas.models.tags.Tag() + .name("设备管理") + .description("设备状态、信息、区域分配等接口"), + new io.swagger.v3.oas.models.tags.Tag() + .name("数据统计") + .description("用水量、告警、设备状态等统计接口"), + new io.swagger.v3.oas.models.tags.Tag() + .name("告警管理") + .description("告警创建、处理、查询等接口"), + new io.swagger.v3.oas.models.tags.Tag() + .name("工单管理") + .description("维修工单创建、分配、处理等接口"), + new io.swagger.v3.oas.models.tags.Tag() + .name("水质管理") + .description("水质数据查询、分析等接口") + ); + } + */ +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/app/AppStatisticsController.java b/src/main/java/com/campus/water/controller/app/AppStatisticsController.java new file mode 100644 index 0000000..a92a874 --- /dev/null +++ b/src/main/java/com/campus/water/controller/app/AppStatisticsController.java @@ -0,0 +1,84 @@ +/** + * 移动端(App)统计接口控制器 + * 功能:为移动端提供简化的统计查询接口 + * 用途:支持学生在手机上查看个人用水统计和设备状态 + * 接口特点: + * - 简化参数:减少查询维度,优化移动端体验 + * - 个人化:基于用户ID过滤数据 + * - 快速响应:返回核心数据,减少数据传输量 + * 接口列表: + * 1. GET /personal-water-usage: 个人用水统计(今日/本周/本月) + * 2. GET /device-status-overview: 设备状态概览 + * 技术:Spring MVC、Header参数验证、移动端优化 + */ +package com.campus.water.controller.app; + +import com.campus.water.entity.dto.request.StatisticsQueryRequest; +import com.campus.water.entity.vo.StatisticsVO; +import com.campus.water.service.StatisticsService; +import com.campus.water.util.ResultVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.Map; + +@RestController +@RequestMapping("/api/app/statistics") +@RequiredArgsConstructor +@Tag(name = "App统计接口", description = "移动端简化统计接口") +public class AppStatisticsController { + + private final StatisticsService statisticsService; + + @GetMapping("/personal-water-usage") + @Operation(summary = "个人用水统计", description = "获取当前用户的用水统计") + public ResponseEntity> getPersonalWaterUsage( + @RequestParam(required = false) String period, // today/week/month + @RequestHeader("X-User-Id") String userId) { + try { + StatisticsQueryRequest request = new StatisticsQueryRequest(); + request.setStatType("by_time"); + + LocalDate now = LocalDate.now(); + if ("today".equals(period)) { + request.setPeriod("day"); + request.setStartDate(now); + request.setEndDate(now); + } else if ("week".equals(period)) { + request.setPeriod("day"); + request.setStartDate(now.minusDays(7)); + request.setEndDate(now); + } else if ("month".equals(period)) { + request.setPeriod("day"); + request.setStartDate(now.withDayOfMonth(1)); + request.setEndDate(now); + } else { + request.setPeriod("day"); + request.setStartDate(now.minusDays(30)); + request.setEndDate(now); + } + + // 这里需要根据userId过滤数据,实际实现需要调整 + StatisticsVO result = statisticsService.getWaterUsageStatistics(request); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "获取个人用水统计失败: " + e.getMessage())); + } + } + + @GetMapping("/device-status-overview") + @Operation(summary = "设备状态概览", description = "获取设备状态概览信息") + public ResponseEntity>> getDeviceStatusOverview( + @RequestParam(required = false) String areaId) { + try { + Map result = statisticsService.getDeviceStatusStatistics(areaId, null); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "获取设备状态概览失败: " + e.getMessage())); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/web/DeviceStatusController.java b/src/main/java/com/campus/water/controller/web/DeviceStatusController.java new file mode 100644 index 0000000..05c71cf --- /dev/null +++ b/src/main/java/com/campus/water/controller/web/DeviceStatusController.java @@ -0,0 +1,153 @@ +/** + * Web端设备状态管理接口控制器 + * 功能:提供设备状态管理的RESTful API接口 + * 用途:支持Web管理端对设备状态的手动和自动管理 + * 接口列表: + * 1. 状态更新:单设备状态变更 + * 2. 状态标记:在线/离线/故障快捷操作 + * 3. 批量操作:批量更新设备状态 + * 4. 状态查询:按状态筛选设备列表 + * 5. 离线检测:查询超时离线设备 + * 6. 自动检测:触发离线设备检测任务 + * 安全:需要权限验证,记录操作日志 + */ +package com.campus.water.controller.web; + +import com.campus.water.entity.Device; +import com.campus.water.entity.dto.request.DeviceStatusUpdateRequest; +import com.campus.water.service.DeviceService; +import com.campus.water.util.ResultVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/web/device-status") +@RequiredArgsConstructor +@Tag(name = "设备状态管理接口", description = "Web管理端设备状态管理接口") +public class DeviceStatusController { + + private final DeviceService deviceService; + + @PostMapping("/update") + @Operation(summary = "更新设备状态", description = "手动更新设备状态(在线/离线/故障)") + public ResponseEntity> updateDeviceStatus( + @Valid @RequestBody DeviceStatusUpdateRequest request) { + try { + boolean result = deviceService.updateDeviceStatus(request); + return ResponseEntity.ok(ResultVO.success(result, "设备状态更新成功")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "设备状态更新失败: " + e.getMessage())); + } + } + + @PostMapping("/{deviceId}/online") + @Operation(summary = "标记设备在线", description = "将设备标记为在线状态") + public ResponseEntity> markDeviceOnline(@PathVariable String deviceId) { + try { + boolean result = deviceService.markDeviceOnline(deviceId); + return ResponseEntity.ok(ResultVO.success(result, "设备已标记为在线")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "标记设备在线失败: " + e.getMessage())); + } + } + + @PostMapping("/{deviceId}/offline") + @Operation(summary = "标记设备离线", description = "将设备标记为离线状态") + public ResponseEntity> markDeviceOffline( + @PathVariable String deviceId, + @RequestParam(required = false) String reason) { + try { + boolean result = deviceService.markDeviceOffline(deviceId, reason); + return ResponseEntity.ok(ResultVO.success(result, "设备已标记为离线")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "标记设备离线失败: " + e.getMessage())); + } + } + + @PostMapping("/{deviceId}/fault") + @Operation(summary = "标记设备故障", description = "将设备标记为故障状态") + public ResponseEntity> markDeviceFault( + @PathVariable String deviceId, + @RequestParam String faultType, + @RequestParam String description) { + try { + boolean result = deviceService.markDeviceFault(deviceId, faultType, description); + return ResponseEntity.ok(ResultVO.success(result, "设备已标记为故障")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "标记设备故障失败: " + e.getMessage())); + } + } + + @PostMapping("/batch-update") + @Operation(summary = "批量更新设备状态", description = "批量更新多个设备的状态") + public ResponseEntity> batchUpdateDeviceStatus( + @RequestParam List deviceIds, + @RequestParam String status, + @RequestParam(required = false) String remark) { + try { + boolean result = deviceService.batchUpdateDeviceStatus(deviceIds, status, remark); + return ResponseEntity.ok(ResultVO.success(result, "批量更新设备状态成功")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "批量更新设备状态失败: " + e.getMessage())); + } + } + + @GetMapping("/by-status") + @Operation(summary = "按状态查询设备", description = "根据状态查询设备列表") + public ResponseEntity>> getDevicesByStatus( + @RequestParam String status, + @RequestParam(required = false) String areaId, + @RequestParam(required = false) String deviceType) { + try { + List devices = deviceService.getDevicesByStatus(status, areaId, deviceType); + return ResponseEntity.ok(ResultVO.success(devices)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "查询设备失败: " + e.getMessage())); + } + } + + @GetMapping("/status-count") + @Operation(summary = "设备状态数量统计", description = "统计各状态设备数量") + public ResponseEntity>> getDeviceStatusCount( + @RequestParam(required = false) String areaId, + @RequestParam(required = false) String deviceType) { + try { + Map result = deviceService.getDeviceStatusCount(areaId, deviceType); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "设备状态统计失败: " + e.getMessage())); + } + } + + @GetMapping("/offline-detection") + @Operation(summary = "离线设备检测", description = "检测离线时间超过阈值的设备") + public ResponseEntity>> getOfflineDevices( + @RequestParam(defaultValue = "30") Integer thresholdMinutes, + @RequestParam(required = false) String areaId) { + try { + List devices = deviceService.getOfflineDevicesExceedThreshold(thresholdMinutes, areaId); + return ResponseEntity.ok(ResultVO.success(devices)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "离线设备检测失败: " + e.getMessage())); + } + } + + @PostMapping("/auto-detect-offline") + @Operation(summary = "自动检测离线设备", description = "自动检测并标记离线设备") + public ResponseEntity> autoDetectOfflineDevices( + @RequestParam(defaultValue = "30") Integer thresholdMinutes) { + try { + deviceService.autoDetectOfflineDevices(thresholdMinutes); + return ResponseEntity.ok(ResultVO.success("离线设备检测完成")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "自动检测离线设备失败: " + e.getMessage())); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/web/StatisticsController.java b/src/main/java/com/campus/water/controller/web/StatisticsController.java new file mode 100644 index 0000000..b008925 --- /dev/null +++ b/src/main/java/com/campus/water/controller/web/StatisticsController.java @@ -0,0 +1,103 @@ +/** + * Web端统计接口控制器 + * 功能:提供Web管理端的统计数据查询API接口 + * 用途:前后端分离架构中的后端API服务 + * 接口列表: + * 1. POST /water-usage: 用水量统计(支持多维度) + * 2. POST /alarm: 告警统计(次数、处理情况) + * 3. GET /device-status: 设备状态数量统计 + * 4. GET /dashboard: 仪表板综合数据 + * 5. GET /hot-devices: 热门设备用水量排名 + * 技术:Spring MVC、参数验证、统一响应格式 + */ +package com.campus.water.controller.web; + +import com.campus.water.entity.dto.request.StatisticsQueryRequest; +import com.campus.water.entity.vo.AlarmStatisticsVO; +import com.campus.water.entity.vo.StatisticsVO; +import com.campus.water.service.StatisticsService; +import com.campus.water.util.ResultVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/web/statistics") +@RequiredArgsConstructor +@Tag(name = "统计分析接口", description = "Web管理端统计分析接口") +public class StatisticsController { + + private final StatisticsService statisticsService; + + @PostMapping("/water-usage") + @Operation(summary = "用水量统计", description = "按设备/区域/时间统计用水量") + public ResponseEntity> getWaterUsageStatistics( + @Valid @RequestBody StatisticsQueryRequest request) { + try { + StatisticsVO result = statisticsService.getWaterUsageStatistics(request); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "统计失败: " + e.getMessage())); + } + } + + @PostMapping("/alarm") + @Operation(summary = "告警统计", description = "统计告警次数和处理情况") + public ResponseEntity> getAlarmStatistics( + @Valid @RequestBody StatisticsQueryRequest request) { + try { + AlarmStatisticsVO result = statisticsService.getAlarmStatistics(request); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "告警统计失败: " + e.getMessage())); + } + } + + @GetMapping("/device-status") + @Operation(summary = "设备状态统计", description = "统计各状态设备数量") + public ResponseEntity>> getDeviceStatusStatistics( + @RequestParam(required = false) String areaId, + @RequestParam(required = false) String deviceType) { + try { + Map result = statisticsService.getDeviceStatusStatistics(areaId, deviceType); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "设备状态统计失败: " + e.getMessage())); + } + } + + @GetMapping("/dashboard") + @Operation(summary = "仪表盘数据", description = "获取综合仪表盘统计数据") + public ResponseEntity>> getDashboardStatistics() { + try { + Map result = statisticsService.getDashboardStatistics(); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "获取仪表盘数据失败: " + e.getMessage())); + } + } + + @GetMapping("/hot-devices") + @Operation(summary = "热门设备统计", description = "获取用水量最高的设备") + public ResponseEntity> getHotDevices( + @RequestParam(defaultValue = "7") Integer days, + @RequestParam(defaultValue = "10") Integer limit) { + try { + StatisticsQueryRequest request = new StatisticsQueryRequest(); + request.setStatType("by_device"); + request.setStartDate(java.time.LocalDate.now().minusDays(days)); + request.setEndDate(java.time.LocalDate.now()); + request.setLimit(limit); + + StatisticsVO result = statisticsService.getWaterUsageStatistics(request); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "获取热门设备失败: " + e.getMessage())); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/dto/request/DeviceStatusUpdateRequest.java b/src/main/java/com/campus/water/entity/dto/request/DeviceStatusUpdateRequest.java new file mode 100644 index 0000000..aa31b5f --- /dev/null +++ b/src/main/java/com/campus/water/entity/dto/request/DeviceStatusUpdateRequest.java @@ -0,0 +1,27 @@ +/** + * 设备状态更新请求数据传输对象(DTO) + * 功能:接收设备状态变更的请求参数 + * 用途:统一设备状态管理接口的入参规范 + * 参数: + * - deviceId: 设备唯一标识(必填) + * - status: 目标状态(online/offline/fault/maintenance) + * - remark: 状态变更备注(可选) + * 验证:非空验证、状态枚举验证 + */ +package com.campus.water.entity.dto.request; + +import lombok.Data; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Data +public class DeviceStatusUpdateRequest { + @NotBlank(message = "设备ID不能为空") + private String deviceId; + + @NotBlank(message = "设备状态不能为空") + private String status; // online/offline/fault/maintenance + + private String remark; // 状态变更备注 +} + diff --git a/src/main/java/com/campus/water/entity/dto/request/StatisticsQueryRequest.java b/src/main/java/com/campus/water/entity/dto/request/StatisticsQueryRequest.java new file mode 100644 index 0000000..9127415 --- /dev/null +++ b/src/main/java/com/campus/water/entity/dto/request/StatisticsQueryRequest.java @@ -0,0 +1,35 @@ +/** + * 统计查询请求数据传输对象(DTO) + * 功能:接收前端统计查询的参数,支持多种统计维度和过滤条件 + * 用途:统一统计查询接口的入参规范 + * 参数: + * - statType: 统计类型(water_usage/alarm/device_usage) + * - period: 统计周期(day/week/month/year/custom) + * - startDate/endDate: 时间范围 + * - deviceId/areaId: 设备/区域筛选 + */ +package com.campus.water.entity.dto.request; + +import lombok.Data; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDate; + +@Data +public class StatisticsQueryRequest { + @NotBlank(message = "统计类型不能为空") + private String statType; // water_usage/alarm/device_usage + + private String period; // day/week/month/year/custom + + private LocalDate startDate; + + private LocalDate endDate; + + private String deviceId; // 设备ID(可选) + + private String areaId; // 区域ID(可选) + + private String deviceType; // water_maker/water_supply + + private Integer limit = 10; // 返回条数限制 +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/vo/AlarmStatisticsVO.java b/src/main/java/com/campus/water/entity/vo/AlarmStatisticsVO.java new file mode 100644 index 0000000..183ff67 --- /dev/null +++ b/src/main/java/com/campus/water/entity/vo/AlarmStatisticsVO.java @@ -0,0 +1,40 @@ +/** + * 告警统计数据视图对象(VO) + * 功能:封装告警相关的统计数据,包括告警级别、类型、设备分布等 + * 用途:展示告警统计分析和处理情况 + * 结构: + * - AlarmStatisticsVO: 告警统计主类 + * - DeviceAlarmStatVO: 设备告警统计明细项 + */ +package com.campus.water.entity.vo; + +import lombok.Data; +import java.util.List; +import java.util.Map; + +@Data +public class AlarmStatisticsVO { + private Integer totalAlarms; // 总告警数 + private Integer pendingAlarms; // 未处理告警数 + private Integer resolvedAlarms; // 已处理告警数 + private Double averageResponseTime; // 平均响应时间(小时) + + // 按级别统计 + private Map alarmLevelCount; + + // 按类型统计 + private Map alarmTypeCount; + + // 按设备统计 + private List deviceAlarmStats; + + @Data + public static class DeviceAlarmStatVO { + private String deviceId; + private String deviceName; + private Integer totalAlarms; + private Integer pendingAlarms; + private Integer resolvedAlarms; + private String mostCommonAlarmType; // 最常见告警类型 + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/vo/StatisticsVO.java b/src/main/java/com/campus/water/entity/vo/StatisticsVO.java new file mode 100644 index 0000000..7892510 --- /dev/null +++ b/src/main/java/com/campus/water/entity/vo/StatisticsVO.java @@ -0,0 +1,45 @@ +/** + * 统计数据显示视图对象(VO) + * 功能:封装统计数据查询的响应结果,包含总计、明细、时间序列等数据结构 + * 用途:用于前后端数据交互,展示用水量、告警等统计信息 + * 结构: + * - StatisticsVO: 主统计响应类 + * - StatItemVO: 统计明细项(按设备/区域/时间维度) + * - TimeSeriesVO: 时间序列数据(用于图表展示) + */ +package com.campus.water.entity.vo; + +import lombok.Data; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Data +public class StatisticsVO { + private Integer totalCount; // 总数量 + private Double totalAmount; // 总用水量/总金额 + private Double avgAmount; // 平均值 + private String period; // 统计周期:day/week/month/year + private LocalDate startDate; // 统计开始日期 + private LocalDate endDate; // 统计结束日期 + + // 明细数据 + private List items; + + @Data + public static class StatItemVO { + private String dimensionKey; // 维度键:设备ID/区域ID/时间 + private String dimensionValue; // 维度值:设备名称/区域名称/时间标签 + private Integer count; // 数量 + private Double amount; // 用水量/金额 + private Double percentage; // 占比百分比 + } + + // 时间序列数据 + @Data + public static class TimeSeriesVO { + private List timeLabels; // 时间标签 + private List values; // 对应数值 + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/DeviceMapper.java b/src/main/java/com/campus/water/mapper/DeviceMapper.java new file mode 100644 index 0000000..a52b773 --- /dev/null +++ b/src/main/java/com/campus/water/mapper/DeviceMapper.java @@ -0,0 +1,301 @@ +package com.campus.water.mapper; + +import com.campus.water.entity.Device; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 设备数据访问接口(MyBatis Mapper) + * 功能:定义设备相关的数据库操作方法,包括设备信息CRUD和状态管理 + * 用途:为设备管理提供数据访问支持,支持设备状态查询、更新、统计等操作 + * 包含方法: + * - 设备基本信息CRUD操作 + * - 设备状态管理相关操作 + * - 设备统计和查询操作 + * 对应XML:DeviceMapper.xml中的SQL实现 + */ +@Mapper +public interface DeviceMapper { + + // ========== 设备基本信息CRUD操作 ========== + + /** + * 根据设备ID查询设备信息 + * @param deviceId 设备ID + * @return 设备实体对象 + */ + Device findById(@Param("deviceId") String deviceId); + + /** + * 查询所有设备 + * @return 设备列表 + */ + List findAll(); + + /** + * 根据设备名称模糊查询 + * @param deviceName 设备名称(模糊匹配) + * @return 匹配的设备列表 + */ + List findByDeviceNameLike(@Param("deviceName") String deviceName); + + /** + * 新增设备 + * @param device 设备实体 + * @return 影响的行数 + */ + int insert(Device device); + + /** + * 更新设备信息 + * @param device 设备实体 + * @return 影响的行数 + */ + int update(Device device); + + /** + * 删除设备 + * @param deviceId 设备ID + * @return 影响的行数 + */ + int delete(@Param("deviceId") String deviceId); + + // ========== 设备状态管理相关操作(新增) ========== + + /** + * 更新设备状态 + * @param deviceId 设备ID + * @param status 目标状态(online/offline/fault/maintenance) + * @param remark 状态变更备注 + * @return 影响的行数 + */ + int updateDeviceStatus(@Param("deviceId") String deviceId, + @Param("status") String status, + @Param("remark") String remark); + + /** + * 标记设备为在线状态 + * @param deviceId 设备ID + * @return 影响的行数 + */ + int markDeviceOnline(@Param("deviceId") String deviceId); + + /** + * 标记设备为离线状态 + * @param deviceId 设备ID + * @param reason 离线原因 + * @return 影响的行数 + */ + int markDeviceOffline(@Param("deviceId") String deviceId, + @Param("reason") String reason); + + /** + * 标记设备为故障状态 + * @param deviceId 设备ID + * @param faultType 故障类型 + * @param description 故障描述 + * @return 影响的行数 + */ + int markDeviceFault(@Param("deviceId") String deviceId, + @Param("faultType") String faultType, + @Param("description") String description); + + /** + * 批量更新设备状态 + * @param deviceIds 设备ID列表 + * @param status 目标状态 + * @param remark 状态变更备注 + * @return 影响的行数 + */ + int batchUpdateDeviceStatus(@Param("deviceIds") List deviceIds, + @Param("status") String status, + @Param("remark") String remark); + + // ========== 设备统计和查询操作 ========== + + /** + * 根据状态查询设备 + * @param status 设备状态(online/offline/fault/maintenance) + * @param areaId 区域ID(可选) + * @param deviceType 设备类型(water_maker/water_supply)(可选) + * @return 设备列表 + */ + List findByStatus(@Param("status") String status, + @Param("areaId") String areaId, + @Param("deviceType") String deviceType); + + /** + * 统计各状态设备数量 + * @param areaId 区域ID(可选) + * @param deviceType 设备类型(可选) + * @return 状态统计列表,每个元素包含status和count + */ + List> countByStatus(@Param("areaId") String areaId, + @Param("deviceType") String deviceType); + + /** + * 根据区域ID查询设备 + * @param areaId 区域ID + * @return 设备列表 + */ + List findByAreaId(@Param("areaId") String areaId); + + /** + * 根据设备类型查询设备 + * @param deviceType 设备类型(water_maker/water_supply) + * @return 设备列表 + */ + List findByDeviceType(@Param("deviceType") String deviceType); + + /** + * 根据安装位置模糊查询设备 + * @param location 安装位置关键词 + * @return 设备列表 + */ + List findByInstallLocationContaining(@Param("location") String location); + + /** + * 查询安装日期在指定范围内的设备 + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 设备列表 + */ + List findByInstallDateBetween(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + // ========== 设备监控相关操作 ========== + + /** + * 获取设备最后在线时间 + * @param deviceId 设备ID + * @return 设备信息(包含updated_time) + */ + Device getDeviceLastOnlineTime(@Param("deviceId") String deviceId); + + /** + * 查询离线时间超过阈值的设备 + * @param thresholdMinutes 离线阈值(分钟) + * @param areaId 区域ID(可选) + * @return 离线设备列表 + */ + List findOfflineDevicesExceedThreshold(@Param("thresholdMinutes") Integer thresholdMinutes, + @Param("areaId") String areaId); + + /** + * 更新设备最后通信时间 + * @param deviceId 设备ID + * @return 影响的行数 + */ + int updateLastCommunicationTime(@Param("deviceId") String deviceId); + + /** + * 查询需要维护的设备(根据维护计划) + * @param currentDate 当前日期 + * @return 需要维护的设备列表 + */ + List findDevicesNeedMaintenance(@Param("currentDate") LocalDate currentDate); + + // ========== 设备统计报表相关操作 ========== + + /** + * 统计各区域设备数量 + * @return 区域设备统计列表,每个元素包含areaId, areaName, deviceCount + */ + List> countDevicesByArea(); + + /** + * 统计各类型设备数量 + * @return 类型设备统计列表,每个元素包含deviceType, deviceCount + */ + List> countDevicesByType(); + + /** + * 统计设备在线率(按区域) + * @return 在线率统计列表,每个元素包含areaId, areaName, totalDevices, onlineDevices, onlineRate + */ + List> getOnlineRateByArea(); + + /** + * 查询设备运行时长统计 + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 设备运行时长列表,每个元素包含deviceId, deviceName, totalOnlineHours + */ + List> getDeviceRuntimeStats(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + // ========== 批量操作 ========== + + /** + * 批量插入设备 + * @param devices 设备列表 + * @return 影响的行数 + */ + int batchInsert(@Param("devices") List devices); + + /** + * 批量更新设备信息 + * @param devices 设备列表 + * @return 影响的行数 + */ + int batchUpdate(@Param("devices") List devices); + + // ========== 设备关联查询 ========== + + /** + * 根据设备ID查询关联的终端设备 + * @param deviceId 设备ID + * @return 终端映射列表 + */ + List> findTerminalsByDeviceId(@Param("deviceId") String deviceId); + + /** + * 根据设备ID查询最近的告警记录 + * @param deviceId 设备ID + * @param limit 限制条数 + * @return 告警记录列表 + */ + List> findRecentAlertsByDeviceId(@Param("deviceId") String deviceId, + @Param("limit") Integer limit); + + /** + * 根据设备ID查询最近的维修记录 + * @param deviceId 设备ID + * @param limit 限制条数 + * @return 工单记录列表 + */ + List> findRecentWorkOrdersByDeviceId(@Param("deviceId") String deviceId, + @Param("limit") Integer limit); + + // ========== 设备高级搜索 ========== + + /** + * 多条件组合查询设备 + * @param deviceName 设备名称(可选) + * @param deviceType 设备类型(可选) + * @param status 设备状态(可选) + * @param areaId 区域ID(可选) + * @param startDate 安装开始日期(可选) + * @param endDate 安装结束日期(可选) + * @return 设备列表 + */ + List searchDevices(@Param("deviceName") String deviceName, + @Param("deviceType") String deviceType, + @Param("status") String status, + @Param("areaId") String areaId, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + // ========== 设备导出相关 ========== + + /** + * 获取设备导出数据(用于Excel导出) + * @param conditions 查询条件 + * @return 设备导出数据列表 + */ + List> getDeviceExportData(@Param("conditions") Map conditions); +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/DeviceMapper.xml b/src/main/java/com/campus/water/mapper/DeviceMapper.xml new file mode 100644 index 0000000..9591e40 --- /dev/null +++ b/src/main/java/com/campus/water/mapper/DeviceMapper.xml @@ -0,0 +1,383 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO device ( + device_id, device_name, device_type, area_id, + install_location, install_date, status, + create_time, remark + ) VALUES ( + #{deviceId}, #{deviceName}, #{deviceType}, #{areaId}, + #{installLocation}, #{installDate}, #{status}, + #{createTime}, #{remark} + ) + + + + UPDATE device + SET device_name = #{deviceName}, + device_type = #{deviceType}, + area_id = #{areaId}, + install_location = #{installLocation}, + install_date = #{installDate}, + status = #{status}, + updated_time = NOW(), + remark = #{remark} + WHERE device_id = #{deviceId} + + + + DELETE FROM device WHERE device_id = #{deviceId} + + + + + + UPDATE device + SET status = #{status}, + updated_time = NOW(), + remark = CONCAT(IFNULL(remark, ''), ';', #{remark}) + WHERE device_id = #{deviceId} + + + + UPDATE device + SET status = 'online', + updated_time = NOW(), + remark = CONCAT(IFNULL(remark, ''), ';标记为在线:', DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')) + WHERE device_id = #{deviceId} + + + + UPDATE device + SET status = 'offline', + updated_time = NOW(), + remark = CONCAT(IFNULL(remark, ''), ';标记为离线:', DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'), ';原因:', #{reason}) + WHERE device_id = #{deviceId} + + + + UPDATE device + SET status = 'fault', + updated_time = NOW(), + remark = CONCAT(IFNULL(remark, ''), ';标记为故障:', DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'), + ';故障类型:', #{faultType}, ';描述:', #{description}) + WHERE device_id = #{deviceId} + + + + UPDATE device + SET status = #{status}, + updated_time = NOW(), + remark = CONCAT(IFNULL(remark, ''), ';批量更新:', #{remark}, ';时间:', DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')) + WHERE device_id IN + + #{deviceId} + + + + + + + + + + + + + + + + + + + + + + + + + UPDATE device + SET updated_time = NOW(), + status = 'online' + WHERE device_id = #{deviceId} + + + + + + + + + + + + + + + + + + INSERT INTO device ( + device_id, device_name, device_type, area_id, + install_location, install_date, status, + create_time, remark + ) VALUES + + ( + #{device.deviceId}, #{device.deviceName}, #{device.deviceType}, #{device.areaId}, + #{device.installLocation}, #{device.installDate}, #{device.status}, + #{device.createTime}, #{device.remark} + ) + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/StatisticsMapper.java b/src/main/java/com/campus/water/mapper/StatisticsMapper.java new file mode 100644 index 0000000..e00caca --- /dev/null +++ b/src/main/java/com/campus/water/mapper/StatisticsMapper.java @@ -0,0 +1,102 @@ +/** + * 统计数据处理映射接口(MyBatis Mapper) + * 功能:定义统计数据查询的数据库操作方法 + * 用途:与数据库交互,执行复杂的统计聚合查询 + * 核心方法: + * - 按设备/区域/时间维度统计用水量 + * - 按设备/区域统计告警次数 + * - 获取设备状态统计和告警处理统计 + * 备注:对应StatisticsMapper.xml中的SQL映射 + */ +package com.campus.water.mapper; + +import com.campus.water.entity.dto.request.StatisticsQueryRequest; +import com.campus.water.entity.vo.StatisticsVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Mapper +public interface StatisticsMapper { + + // 按设备统计用水量 + List> statWaterUsageByDevice( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("areaId") String areaId, + @Param("limit") Integer limit); + + // 按区域统计用水量 + List> statWaterUsageByArea( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("deviceType") String deviceType, + @Param("limit") Integer limit); + + // 按时间段统计用水量(日/周/月) + List> statWaterUsageByTime( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("period") String period, // day/week/month + @Param("deviceId") String deviceId, + @Param("areaId") String areaId); + + // 按设备统计告警次数 + List> statAlarmCountByDevice( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("areaId") String areaId, + @Param("alarmLevel") String alarmLevel, + @Param("limit") Integer limit); + + // 按区域统计告警次数 + List> statAlarmCountByArea( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("deviceType") String deviceType, + @Param("limit") Integer limit); + + // 按时间段统计告警趋势 + List> statAlarmTrendByTime( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("period") String period, + @Param("deviceId") String deviceId, + @Param("areaId") String areaId); + + // 统计设备使用情况(使用次数、总用水量) + List> statDeviceUsage( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("areaId") String areaId, + @Param("deviceType") String deviceType, + @Param("limit") Integer limit); + + // 统计终端使用情况 + List> statTerminalUsage( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("areaId") String areaId, + @Param("limit") Integer limit); + + // 统计水质达标率 + List> statWaterQualityRate( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("deviceId") String deviceId, + @Param("areaId") String areaId); + + // 获取设备状态统计 + Map getDeviceStatusStatistics( + @Param("areaId") String areaId, + @Param("deviceType") String deviceType); + + // 获取告警处理统计 + Map getAlarmHandleStatistics( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("areaId") String areaId); +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/StatisticsMapper.xml b/src/main/java/com/campus/water/mapper/StatisticsMapper.xml new file mode 100644 index 0000000..4883759 --- /dev/null +++ b/src/main/java/com/campus/water/mapper/StatisticsMapper.xml @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/AlertService.java b/src/main/java/com/campus/water/service/AlertService.java new file mode 100644 index 0000000..7f8995a --- /dev/null +++ b/src/main/java/com/campus/water/service/AlertService.java @@ -0,0 +1,828 @@ +package com.campus.water.service; + +import com.campus.water.entity.Alert; +import com.campus.water.entity.WorkOrder; +import com.campus.water.entity.Device; +import com.campus.water.entity.dto.request.AlarmStatisticsRequest; +import com.campus.water.entity.vo.AlarmStatisticsVO; +import com.campus.water.mapper.AlertRepository; +import com.campus.water.mapper.WorkOrderRepository; +import com.campus.water.mapper.DeviceRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import jakarta.persistence.criteria.Predicate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 告警服务层 + * 功能:处理告警相关的业务逻辑,包括告警创建、查询、统计、处理等 + * 用途: + * 1. 告警生命周期管理(创建、处理、关闭) + * 2. 告警统计和分析 + * 3. 告警与工单关联管理 + * 4. 告警通知和推送 + * 核心方法: + * - 告警创建:自动触发告警、手动创建告警 + * - 告警处理:更新告警状态、指派处理人 + * - 告警统计:多维度的告警数据分析 + * - 告警查询:支持多条件筛选和分页 + * 技术:Spring Data JPA、事务管理、复杂查询构建 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class AlertService { + + private final AlertRepository alertRepository; + private final WorkOrderRepository workOrderRepository; + private final DeviceRepository deviceRepository; + + // ========== 告警创建相关方法 ========== + + /** + * 自动创建告警(从传感器数据触发) + * @param deviceId 设备ID + * @param alertType 告警类型 + * @param alertLevel 告警级别 + * @param message 告警信息 + * @param areaId 区域ID + * @return 创建的告警对象 + */ + @Transactional + public Alert createAutoAlert(String deviceId, String alertType, + Alert.AlertLevel alertLevel, String message, + String areaId) { + try { + // 检查是否存在重复未处理告警 + if (hasDuplicatePendingAlert(deviceId, alertType)) { + log.info("存在重复未处理告警,跳过创建: deviceId={}, alertType={}", deviceId, alertType); + return null; + } + + Alert alert = new Alert(); + alert.setDeviceId(deviceId); + alert.setAlertType(alertType); + alert.setAlertLevel(alertLevel); + alert.setAlertMessage(message); + alert.setAreaId(areaId); + alert.setStatus(Alert.AlertStatus.pending); + alert.setTimestamp(LocalDateTime.now()); + alert.setCreatedTime(LocalDateTime.now()); + + Alert savedAlert = alertRepository.save(alert); + log.info("自动告警创建成功: alertId={}, deviceId={}, type={}, level={}", + savedAlert.getAlertId(), deviceId, alertType, alertLevel); + + // 根据告警类型自动创建工单 + if (shouldCreateWorkOrder(alertType, alertLevel)) { + createWorkOrderForAlert(savedAlert); + } + + return savedAlert; + } catch (Exception e) { + log.error("自动告警创建失败: deviceId={}, error={}", deviceId, e.getMessage(), e); + throw new RuntimeException("告警创建失败: " + e.getMessage()); + } + } + + /** + * 手动创建告警(由管理员或维修人员创建) + * @param deviceId 设备ID + * @param alertType 告警类型 + * @param alertLevel 告警级别字符串 + * @param message 告警信息 + * @param areaId 区域ID + * @return 创建的告警对象 + */ + @Transactional + public Alert createManualAlert(String deviceId, String alertType, + String alertLevel, String message, + String areaId) { + try { + Alert.AlertLevel level; + try { + level = Alert.AlertLevel.valueOf(alertLevel.toLowerCase()); + } catch (IllegalArgumentException e) { + level = Alert.AlertLevel.warning; // 默认级别 + } + + Alert alert = new Alert(); + alert.setDeviceId(deviceId); + alert.setAlertType(alertType); + alert.setAlertLevel(level); + alert.setAlertMessage(message); + alert.setAreaId(areaId); + alert.setStatus(Alert.AlertStatus.pending); + alert.setTimestamp(LocalDateTime.now()); + alert.setCreatedTime(LocalDateTime.now()); + + Alert savedAlert = alertRepository.save(alert); + log.info("手动告警创建成功: alertId={}, deviceId={}, type={}, level={}", + savedAlert.getAlertId(), deviceId, alertType, alertLevel); + + // 手动创建的告警默认创建工单 + createWorkOrderForAlert(savedAlert); + + return savedAlert; + } catch (Exception e) { + log.error("手动告警创建失败: deviceId={}, error={}", deviceId, e.getMessage(), e); + throw new RuntimeException("告警创建失败: " + e.getMessage()); + } + } + + /** + * 批量创建告警(用于批量导入或同步) + * @param alerts 告警列表 + * @return 成功创建的告警列表 + */ + @Transactional + public List batchCreateAlerts(List alerts) { + try { + List savedAlerts = alertRepository.saveAll(alerts); + log.info("批量创建告警成功: count={}", savedAlerts.size()); + + // 为每个告警创建工单 + for (Alert alert : savedAlerts) { + if (shouldCreateWorkOrder(alert.getAlertType(), alert.getAlertLevel())) { + createWorkOrderForAlert(alert); + } + } + + return savedAlerts; + } catch (Exception e) { + log.error("批量创建告警失败: error={}", e.getMessage(), e); + throw new RuntimeException("批量创建告警失败: " + e.getMessage()); + } + } + + // ========== 告警处理相关方法 ========== + + /** + * 处理告警(开始处理) + * @param alertId 告警ID + * @param repairmanId 维修人员ID + * @return 是否处理成功 + */ + @Transactional + public boolean processAlert(Long alertId, String repairmanId) { + try { + Optional alertOpt = alertRepository.findById(alertId); + if (alertOpt.isEmpty()) { + log.warn("告警不存在: alertId={}", alertId); + return false; + } + + Alert alert = alertOpt.get(); + if (alert.getStatus() != Alert.AlertStatus.pending) { + log.warn("告警状态不允许处理: alertId={}, currentStatus={}", alertId, alert.getStatus()); + return false; + } + + alert.setStatus(Alert.AlertStatus.processing); + alert.setResolvedBy(repairmanId); + alert.setUpdatedTime(LocalDateTime.now()); + + alertRepository.save(alert); + log.info("告警开始处理: alertId={}, repairmanId={}", alertId, repairmanId); + return true; + } catch (Exception e) { + log.error("处理告警失败: alertId={}, error={}", alertId, e.getMessage(), e); + throw new RuntimeException("处理告警失败: " + e.getMessage()); + } + } + + /** + * 解决告警(完成处理) + * @param alertId 告警ID + * @param resolvedBy 解决人 + * @param resolutionNotes 解决说明 + * @return 是否解决成功 + */ + @Transactional + public boolean resolveAlert(Long alertId, String resolvedBy, String resolutionNotes) { + try { + Optional alertOpt = alertRepository.findById(alertId); + if (alertOpt.isEmpty()) { + log.warn("告警不存在: alertId={}", alertId); + return false; + } + + Alert alert = alertOpt.get(); + if (alert.getStatus() != Alert.AlertStatus.processing && + alert.getStatus() != Alert.AlertStatus.pending) { + log.warn("告警状态不允许解决: alertId={}, currentStatus={}", alertId, alert.getStatus()); + return false; + } + + alert.setStatus(Alert.AlertStatus.resolved); + alert.setResolvedBy(resolvedBy); + alert.setResolvedTime(LocalDateTime.now()); + alert.setAlertMessage(alert.getAlertMessage() + " [解决方案: " + resolutionNotes + "]"); + alert.setUpdatedTime(LocalDateTime.now()); + + alertRepository.save(alert); + log.info("告警已解决: alertId={}, resolvedBy={}", alertId, resolvedBy); + return true; + } catch (Exception e) { + log.error("解决告警失败: alertId={}, error={}", alertId, e.getMessage(), e); + throw new RuntimeException("解决告警失败: " + e.getMessage()); + } + } + + /** + * 关闭告警(无需处理或误报) + * @param alertId 告警ID + * @param closedBy 关闭人 + * @param closeReason 关闭原因 + * @return 是否关闭成功 + */ + @Transactional + public boolean closeAlert(Long alertId, String closedBy, String closeReason) { + try { + Optional alertOpt = alertRepository.findById(alertId); + if (alertOpt.isEmpty()) { + log.warn("告警不存在: alertId={}", alertId); + return false; + } + + Alert alert = alertOpt.get(); + alert.setStatus(Alert.AlertStatus.closed); + alert.setResolvedBy(closedBy); + alert.setResolvedTime(LocalDateTime.now()); + alert.setAlertMessage(alert.getAlertMessage() + " [关闭原因: " + closeReason + "]"); + alert.setUpdatedTime(LocalDateTime.now()); + + alertRepository.save(alert); + log.info("告警已关闭: alertId={}, closedBy={}, reason={}", alertId, closedBy, closeReason); + return true; + } catch (Exception e) { + log.error("关闭告警失败: alertId={}, error={}", alertId, e.getMessage(), e); + throw new RuntimeException("关闭告警失败: " + e.getMessage()); + } + } + + /** + * 批量更新告警状态 + * @param alertIds 告警ID列表 + * @param status 目标状态 + * @param updatedBy 更新人 + * @return 成功更新的数量 + */ + @Transactional + public int batchUpdateAlertStatus(List alertIds, Alert.AlertStatus status, String updatedBy) { + try { + List alerts = alertRepository.findAllById(alertIds); + for (Alert alert : alerts) { + alert.setStatus(status); + alert.setResolvedBy(updatedBy); + if (status == Alert.AlertStatus.resolved || status == Alert.AlertStatus.closed) { + alert.setResolvedTime(LocalDateTime.now()); + } + alert.setUpdatedTime(LocalDateTime.now()); + } + + alertRepository.saveAll(alerts); + log.info("批量更新告警状态: count={}, status={}, updatedBy={}", + alerts.size(), status, updatedBy); + return alerts.size(); + } catch (Exception e) { + log.error("批量更新告警状态失败: error={}", e.getMessage(), e); + throw new RuntimeException("批量更新告警状态失败: " + e.getMessage()); + } + } + + // ========== 告警查询相关方法 ========== + + /** + * 根据ID查询告警 + * @param alertId 告警ID + * @return 告警对象(Optional) + */ + @Transactional(readOnly = true) + public Optional findById(Long alertId) { + return alertRepository.findById(alertId); + } + + /** + * 多条件分页查询告警 + * @param deviceId 设备ID(可选) + * @param alertType 告警类型(可选) + * @param alertLevel 告警级别(可选) + * @param status 告警状态(可选) + * @param areaId 区域ID(可选) + * @param startTime 开始时间(可选) + * @param endTime 结束时间(可选) + * @param pageable 分页参数 + * @return 分页告警列表 + */ + @Transactional(readOnly = true) + public Page findAlertsByConditions(String deviceId, String alertType, + Alert.AlertLevel alertLevel, Alert.AlertStatus status, + String areaId, LocalDateTime startTime, + LocalDateTime endTime, Pageable pageable) { + Specification spec = (root, query, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + + if (StringUtils.hasText(deviceId)) { + predicates.add(criteriaBuilder.equal(root.get("deviceId"), deviceId)); + } + + if (StringUtils.hasText(alertType)) { + predicates.add(criteriaBuilder.equal(root.get("alertType"), alertType)); + } + + if (alertLevel != null) { + predicates.add(criteriaBuilder.equal(root.get("alertLevel"), alertLevel)); + } + + if (status != null) { + predicates.add(criteriaBuilder.equal(root.get("status"), status)); + } + + if (StringUtils.hasText(areaId)) { + predicates.add(criteriaBuilder.equal(root.get("areaId"), areaId)); + } + + if (startTime != null) { + predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("timestamp"), startTime)); + } + + if (endTime != null) { + predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("timestamp"), endTime)); + } + + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + + return alertRepository.findAll(spec, pageable); + } + + /** + * 查询设备的最新告警 + * @param deviceId 设备ID + * @param limit 限制条数 + * @return 告警列表 + */ + @Transactional(readOnly = true) + public List findRecentAlertsByDeviceId(String deviceId, int limit) { + return alertRepository.findByDeviceIdOrderByTimestampDesc(deviceId) + .stream() + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * 查询未处理告警 + * @param areaId 区域ID(可选) + * @return 未处理告警列表 + */ + @Transactional(readOnly = true) + public List findPendingAlerts(String areaId) { + if (StringUtils.hasText(areaId)) { + return alertRepository.findByStatusAndAreaId(Alert.AlertStatus.pending, areaId); + } else { + return alertRepository.findByStatus(Alert.AlertStatus.pending); + } + } + + /** + * 查询正在处理的告警 + * @param repairmanId 维修人员ID(可选) + * @return 正在处理的告警列表 + */ + @Transactional(readOnly = true) + public List findProcessingAlerts(String repairmanId) { + if (StringUtils.hasText(repairmanId)) { + return alertRepository.findByStatusAndResolvedBy(Alert.AlertStatus.processing, repairmanId); + } else { + return alertRepository.findByStatus(Alert.AlertStatus.processing); + } + } + + // ========== 告警统计相关方法 ========== + + /** + * 获取告警统计概览 + * @param areaId 区域ID(可选) + * @param startTime 开始时间(可选) + * @param endTime 结束时间(可选) + * @return 告警统计概览 + */ + @Transactional(readOnly = true) + public Map getAlertOverview(String areaId, LocalDateTime startTime, LocalDateTime endTime) { + Map overview = new HashMap<>(); + + // 获取统计时间段内的告警 + List alerts; + if (startTime != null && endTime != null) { + alerts = alertRepository.findByTimestampBetween(startTime, endTime); + } else if (startTime != null) { + alerts = alertRepository.findByTimestampAfter(startTime); + } else { + alerts = alertRepository.findAll(); + } + + // 按区域过滤 + if (StringUtils.hasText(areaId)) { + alerts = alerts.stream() + .filter(alert -> areaId.equals(alert.getAreaId())) + .collect(Collectors.toList()); + } + + // 计算统计指标 + long totalAlerts = alerts.size(); + long pendingAlerts = alerts.stream() + .filter(a -> a.getStatus() == Alert.AlertStatus.pending) + .count(); + long processingAlerts = alerts.stream() + .filter(a -> a.getStatus() == Alert.AlertStatus.processing) + .count(); + long resolvedAlerts = alerts.stream() + .filter(a -> a.getStatus() == Alert.AlertStatus.resolved) + .count(); + + // 计算平均响应时间(小时) + double avgResponseHours = alerts.stream() + .filter(a -> a.getResolvedTime() != null && a.getTimestamp() != null) + .mapToDouble(a -> ChronoUnit.HOURS.between(a.getTimestamp(), a.getResolvedTime())) + .average() + .orElse(0.0); + + // 按级别统计 + Map levelCount = alerts.stream() + .collect(Collectors.groupingBy(Alert::getAlertLevel, Collectors.counting())); + + // 按类型统计 + Map typeCount = alerts.stream() + .collect(Collectors.groupingBy(Alert::getAlertType, Collectors.counting())); + + overview.put("totalAlerts", totalAlerts); + overview.put("pendingAlerts", pendingAlerts); + overview.put("processingAlerts", processingAlerts); + overview.put("resolvedAlerts", resolvedAlerts); + overview.put("resolvedRate", totalAlerts > 0 ? (double) resolvedAlerts / totalAlerts * 100 : 0); + overview.put("avgResponseHours", avgResponseHours); + overview.put("alertLevelCount", levelCount); + overview.put("alertTypeCount", typeCount); + + return overview; + } + + /** + * 获取告警趋势统计(按时间分组) + * @param period 统计周期(day/week/month) + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param areaId 区域ID(可选) + * @return 时间序列告警数据 + */ + @Transactional(readOnly = true) + public List> getAlertTrend(String period, + LocalDateTime startTime, + LocalDateTime endTime, + String areaId) { + List alerts = alertRepository.findByTimestampBetween(startTime, endTime); + + if (StringUtils.hasText(areaId)) { + alerts = alerts.stream() + .filter(alert -> areaId.equals(alert.getAreaId())) + .collect(Collectors.toList()); + } + + // 按时间分组 + Map> groupedAlerts = new TreeMap<>(); + + for (Alert alert : alerts) { + String timeKey = formatTimeKey(alert.getTimestamp(), period); + groupedAlerts.computeIfAbsent(timeKey, k -> new ArrayList<>()).add(alert); + } + + // 构建结果 + List> result = new ArrayList<>(); + for (Map.Entry> entry : groupedAlerts.entrySet()) { + Map item = new HashMap<>(); + item.put("timeLabel", entry.getKey()); + item.put("totalAlerts", entry.getValue().size()); + + // 按级别统计 + Map levelCount = entry.getValue().stream() + .collect(Collectors.groupingBy(Alert::getAlertLevel, Collectors.counting())); + item.put("alertLevelCount", levelCount); + + result.add(item); + } + + return result; + } + + /** + * 获取设备告警排名 + * @param topN 前N名 + * @param startTime 开始时间(可选) + * @param endTime 结束时间(可选) + * @return 设备告警排名 + */ + @Transactional(readOnly = true) + public List> getDeviceAlertRanking(int topN, + LocalDateTime startTime, + LocalDateTime endTime) { + List alerts; + if (startTime != null && endTime != null) { + alerts = alertRepository.findByTimestampBetween(startTime, endTime); + } else { + alerts = alertRepository.findAll(); + } + + // 按设备分组统计 + Map> deviceAlerts = alerts.stream() + .collect(Collectors.groupingBy(Alert::getDeviceId)); + + // 获取设备信息 + List> ranking = new ArrayList<>(); + for (Map.Entry> entry : deviceAlerts.entrySet()) { + String deviceId = entry.getKey(); + List deviceAlertList = entry.getValue(); + + Optional deviceOpt = deviceRepository.findById(deviceId); + String deviceName = deviceOpt.map(Device::getDeviceName).orElse("未知设备"); + + Map item = new HashMap<>(); + item.put("deviceId", deviceId); + item.put("deviceName", deviceName); + item.put("totalAlerts", deviceAlertList.size()); + item.put("pendingAlerts", deviceAlertList.stream() + .filter(a -> a.getStatus() == Alert.AlertStatus.pending) + .count()); + item.put("resolvedAlerts", deviceAlertList.stream() + .filter(a -> a.getStatus() == Alert.AlertStatus.resolved) + .count()); + + // 计算最常见的告警类型 + String mostCommonType = deviceAlertList.stream() + .collect(Collectors.groupingBy(Alert::getAlertType, Collectors.counting())) + .entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse("未知"); + item.put("mostCommonAlertType", mostCommonType); + + ranking.add(item); + } + + // 按告警总数排序并限制数量 + return ranking.stream() + .sorted((a, b) -> Integer.compare( + (Integer) b.get("totalAlerts"), + (Integer) a.get("totalAlerts"))) + .limit(topN) + .collect(Collectors.toList()); + } + + // ========== 告警分析相关方法 ========== + + /** + * 分析告警模式(发现频繁出现的告警组合) + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param areaId 区域ID(可选) + * @return 告警模式分析结果 + */ + @Transactional(readOnly = true) + public List> analyzeAlertPatterns(LocalDateTime startTime, + LocalDateTime endTime, + String areaId) { + List alerts = alertRepository.findByTimestampBetween(startTime, endTime); + + if (StringUtils.hasText(areaId)) { + alerts = alerts.stream() + .filter(alert -> areaId.equals(alert.getAreaId())) + .collect(Collectors.toList()); + } + + // 按设备和时间窗口分组 + Map>> deviceTimeAlerts = new HashMap<>(); + + for (Alert alert : alerts) { + String deviceId = alert.getDeviceId(); + String timeWindow = formatTimeWindow(alert.getTimestamp()); + + deviceTimeAlerts.computeIfAbsent(deviceId, k -> new HashMap<>()) + .computeIfAbsent(timeWindow, k -> new ArrayList<>()) + .add(alert); + } + + // 分析同一时间窗口内的告警组合 + List> patterns = new ArrayList<>(); + for (Map> timeAlerts : deviceTimeAlerts.values()) { + for (List windowAlerts : timeAlerts.values()) { + if (windowAlerts.size() >= 2) { + // 发现多个告警同时出现 + Set alertTypes = windowAlerts.stream() + .map(Alert::getAlertType) + .collect(Collectors.toSet()); + + if (alertTypes.size() > 1) { + Map pattern = new HashMap<>(); + pattern.put("alertTypes", alertTypes); + pattern.put("count", windowAlerts.size()); + pattern.put("deviceIds", deviceTimeAlerts.keySet()); + pattern.put("timestamp", windowAlerts.get(0).getTimestamp()); + patterns.add(pattern); + } + } + } + } + + return patterns; + } + + /** + * 计算告警预测指标 + * @param deviceId 设备ID + * @return 告警预测结果 + */ + @Transactional(readOnly = true) + public Map predictAlertRisk(String deviceId) { + Optional deviceOpt = deviceRepository.findById(deviceId); + if (deviceOpt.isEmpty()) { + return Collections.emptyMap(); + } + + Device device = deviceOpt.get(); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime oneMonthAgo = now.minusMonths(1); + + // 获取最近一个月的告警 + List recentAlerts = alertRepository.findByDeviceIdAndTimestampAfter(deviceId, oneMonthAgo); + + Map prediction = new HashMap<>(); + prediction.put("deviceId", deviceId); + prediction.put("deviceName", device.getDeviceName()); + prediction.put("deviceStatus", device.getStatus()); + + // 计算告警频率 + long alertCount = recentAlerts.size(); + double alertsPerDay = alertCount / 30.0; + prediction.put("alertFrequency", alertsPerDay); + + // 风险等级评估 + String riskLevel; + if (alertsPerDay >= 1.0) { + riskLevel = "高风险"; + } else if (alertsPerDay >= 0.5) { + riskLevel = "中风险"; + } else if (alertsPerDay >= 0.1) { + riskLevel = "低风险"; + } else { + riskLevel = "无风险"; + } + prediction.put("riskLevel", riskLevel); + + // 最常见的告警类型 + String mostCommonType = recentAlerts.stream() + .collect(Collectors.groupingBy(Alert::getAlertType, Collectors.counting())) + .entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse("无"); + prediction.put("mostCommonAlertType", mostCommonType); + + // 平均解决时间 + double avgResolveHours = recentAlerts.stream() + .filter(a -> a.getResolvedTime() != null) + .mapToDouble(a -> ChronoUnit.HOURS.between(a.getTimestamp(), a.getResolvedTime())) + .average() + .orElse(0.0); + prediction.put("avgResolveHours", avgResolveHours); + + return prediction; + } + + // ========== 私有辅助方法 ========== + + /** + * 检查是否存在重复的未处理告警 + */ + private boolean hasDuplicatePendingAlert(String deviceId, String alertType) { + List pendingAlerts = alertRepository.findByDeviceIdAndAlertTypeAndStatus( + deviceId, alertType, Alert.AlertStatus.pending); + + if (!pendingAlerts.isEmpty()) { + // 检查是否有30分钟内的重复告警 + LocalDateTime thirtyMinutesAgo = LocalDateTime.now().minusMinutes(30); + return pendingAlerts.stream() + .anyMatch(alert -> alert.getTimestamp().isAfter(thirtyMinutesAgo)); + } + + return false; + } + + /** + * 判断是否需要创建工单 + */ + private boolean shouldCreateWorkOrder(String alertType, Alert.AlertLevel alertLevel) { + // 严重级别以上的告警都需要创建工单 + if (alertLevel == Alert.AlertLevel.critical || alertLevel == Alert.AlertLevel.error) { + return true; + } + + // 特定类型的告警需要工单 + Set workOrderRequiredTypes = Set.of( + "WATER_MAKER_FAULT", + "WATER_SUPPLY_FAULT", + "DEVICE_FAULT", + "SAFETY_ALERT" + ); + + return workOrderRequiredTypes.contains(alertType); + } + + /** + * 为告警创建工单 + */ + private void createWorkOrderForAlert(Alert alert) { + try { + WorkOrder workOrder = new WorkOrder(); + workOrder.setOrderId(generateOrderId()); + workOrder.setAlertId(alert.getAlertId()); + workOrder.setDeviceId(alert.getDeviceId()); + workOrder.setAreaId(alert.getAreaId()); + workOrder.setOrderType(WorkOrder.OrderType.repair); + workOrder.setDescription("告警处理工单: " + alert.getAlertMessage()); + workOrder.setPriority(convertAlertLevelToPriority(alert.getAlertLevel())); + workOrder.setStatus(WorkOrder.OrderStatus.pending); + workOrder.setCreatedTime(LocalDateTime.now()); + + workOrderRepository.save(workOrder); + log.info("为告警创建工单成功: alertId={}, orderId={}", + alert.getAlertId(), workOrder.getOrderId()); + } catch (Exception e) { + log.error("为告警创建工单失败: alertId={}, error={}", + alert.getAlertId(), e.getMessage(), e); + } + } + + /** + * 生成工单ID + */ + private String generateOrderId() { + return String.format("WO%s%03d", + System.currentTimeMillis(), + (int)(Math.random() * 1000)); + } + + /** + * 将告警级别转换为工单优先级 + */ + private WorkOrder.OrderPriority convertAlertLevelToPriority(Alert.AlertLevel alertLevel) { + switch (alertLevel) { + case critical: + return WorkOrder.OrderPriority.urgent; + case error: + return WorkOrder.OrderPriority.high; + case warning: + return WorkOrder.OrderPriority.medium; + default: + return WorkOrder.OrderPriority.low; + } + } + + /** + * 格式化时间键(用于分组) + */ + private String formatTimeKey(LocalDateTime time, String period) { + switch (period) { + case "day": + return time.toLocalDate().toString(); + case "week": + return String.format("%d-W%d", + time.getYear(), + time.get(java.time.temporal.WeekFields.ISO.weekOfYear())); + case "month": + return String.format("%d-%02d", + time.getYear(), time.getMonthValue()); + default: + return time.toLocalDate().toString(); + } + } + + /** + * 格式化时间窗口(用于模式分析) + */ + private String formatTimeWindow(LocalDateTime time) { + // 按小时窗口分组 + return String.format("%s-%02d", + time.toLocalDate().toString(), + time.getHour()); + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/DeviceService.java b/src/main/java/com/campus/water/service/DeviceService.java new file mode 100644 index 0000000..2fc2e74 --- /dev/null +++ b/src/main/java/com/campus/water/service/DeviceService.java @@ -0,0 +1,219 @@ +/** + * 设备状态管理业务服务层 + * 功能:处理设备状态相关的业务逻辑 + * 用途:为设备状态控制器提供业务处理服务 + * 核心方法: + * - 状态更新:单设备状态变更 + * - 状态标记:在线/离线/故障标记 + * - 批量操作:批量状态更新 + * - 自动检测:定时检测离线设备 + * - 故障告警:设备故障时自动创建告警 + * 业务逻辑:状态验证、事务管理、日志记录 + */ +package com.campus.water.service; + +import com.campus.water.entity.Device; +import com.campus.water.entity.dto.request.DeviceStatusUpdateRequest; +import com.campus.water.mapper.DeviceMapper; +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; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DeviceService { + + private final DeviceMapper deviceMapper; + private final AlertService alertService; + + /** + * 更新设备状态 + */ + @Transactional + public boolean updateDeviceStatus(DeviceStatusUpdateRequest request) { + try { + int rows = deviceMapper.updateDeviceStatus( + request.getDeviceId(), + request.getStatus(), + request.getRemark() + ); + + if (rows > 0) { + log.info("设备状态更新成功: deviceId={}, status={}", + request.getDeviceId(), request.getStatus()); + + // 如果是故障状态,自动创建告警 + if ("fault".equals(request.getStatus())) { + createFaultAlert(request.getDeviceId(), request.getRemark()); + } + + return true; + } + return false; + } catch (Exception e) { + log.error("设备状态更新失败: deviceId={}, error={}", + request.getDeviceId(), e.getMessage(), e); + throw new RuntimeException("设备状态更新失败: " + e.getMessage()); + } + } + + /** + * 标记设备在线 + */ + @Transactional + public boolean markDeviceOnline(String deviceId) { + try { + int rows = deviceMapper.markDeviceOnline(deviceId); + if (rows > 0) { + log.info("设备标记为在线: deviceId={}", deviceId); + return true; + } + return false; + } catch (Exception e) { + log.error("标记设备在线失败: deviceId={}, error={}", deviceId, e.getMessage()); + throw new RuntimeException("标记设备在线失败: " + e.getMessage()); + } + } + + /** + * 标记设备离线 + */ + @Transactional + public boolean markDeviceOffline(String deviceId, String reason) { + try { + int rows = deviceMapper.markDeviceOffline(deviceId, reason); + if (rows > 0) { + log.info("设备标记为离线: deviceId={}, reason={}", deviceId, reason); + return true; + } + return false; + } catch (Exception e) { + log.error("标记设备离线失败: deviceId={}, error={}", deviceId, e.getMessage()); + throw new RuntimeException("标记设备离线失败: " + e.getMessage()); + } + } + + /** + * 标记设备故障 + */ + @Transactional + public boolean markDeviceFault(String deviceId, String faultType, String description) { + try { + int rows = deviceMapper.markDeviceFault(deviceId, faultType, description); + if (rows > 0) { + log.info("设备标记为故障: deviceId={}, type={}, desc={}", + deviceId, faultType, description); + + // 创建故障告警 + createFaultAlert(deviceId, String.format("故障类型: %s, 描述: %s", faultType, description)); + + return true; + } + return false; + } catch (Exception e) { + log.error("标记设备故障失败: deviceId={}, error={}", deviceId, e.getMessage()); + throw new RuntimeException("标记设备故障失败: " + e.getMessage()); + } + } + + /** + * 批量更新设备状态 + */ + @Transactional + public boolean batchUpdateDeviceStatus(List deviceIds, String status, String remark) { + try { + if (deviceIds == null || deviceIds.isEmpty()) { + return false; + } + + int rows = deviceMapper.batchUpdateDeviceStatus(deviceIds, status, remark); + log.info("批量更新设备状态: count={}, status={}, updated={}", + deviceIds.size(), status, rows); + + // 如果是故障状态,为每个设备创建告警 + if ("fault".equals(status)) { + for (String deviceId : deviceIds) { + createFaultAlert(deviceId, remark); + } + } + + return rows > 0; + } catch (Exception e) { + log.error("批量更新设备状态失败: error={}", e.getMessage(), e); + throw new RuntimeException("批量更新设备状态失败: " + e.getMessage()); + } + } + + /** + * 获取设备状态统计 + */ + @Transactional(readOnly = true) + public Map getDeviceStatusCount(String areaId, String deviceType) { + return deviceMapper.countByStatus(areaId, deviceType); + } + + /** + * 查询离线设备(超过阈值) + */ + @Transactional(readOnly = true) + public List getOfflineDevicesExceedThreshold(Integer thresholdMinutes, String areaId) { + return deviceMapper.findOfflineDevicesExceedThreshold(thresholdMinutes, areaId); + } + + /** + * 获取设备最后在线时间 + */ + @Transactional(readOnly = true) + public LocalDateTime getDeviceLastOnlineTime(String deviceId) { + Device device = deviceMapper.getDeviceLastOnlineTime(deviceId); + return device != null ? device.getUpdatedTime() : null; + } + + /** + * 根据状态查询设备 + */ + @Transactional(readOnly = true) + public List getDevicesByStatus(String status, String areaId, String deviceType) { + return deviceMapper.findByStatus(status, areaId, deviceType); + } + + /** + * 自动检测并标记离线设备 + */ + @Transactional + public void autoDetectOfflineDevices(Integer thresholdMinutes) { + List offlineDevices = getOfflineDevicesExceedThreshold(thresholdMinutes, null); + + for (Device device : offlineDevices) { + markDeviceOffline(device.getDeviceId(), + String.format("自动检测离线,超过%d分钟无数据", thresholdMinutes)); + } + + if (!offlineDevices.isEmpty()) { + log.warn("自动标记离线设备完成: count={}", offlineDevices.size()); + } + } + + /** + * 创建故障告警 + */ + private void createFaultAlert(String deviceId, String description) { + try { + alertService.createManualAlert( + deviceId, + "DEVICE_FAULT", + "设备故障", + String.format("设备故障告警 - 设备ID: %s, 描述: %s", deviceId, description), + "fault" + ); + } catch (Exception e) { + log.error("创建故障告警失败: deviceId={}, error={}", deviceId, e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/StatisticsService.java b/src/main/java/com/campus/water/service/StatisticsService.java new file mode 100644 index 0000000..d5e31e8 --- /dev/null +++ b/src/main/java/com/campus/water/service/StatisticsService.java @@ -0,0 +1,247 @@ +/** + * 统计业务逻辑服务层 + * 功能:处理统计数据的业务逻辑,包括数据转换、计算、聚合 + * 用途:为控制器提供统计数据处理服务 + * 核心方法: + * - getWaterUsageStatistics(): 用水量统计逻辑处理 + * - getAlarmStatistics(): 告警统计逻辑处理 + * - getDeviceStatusStatistics(): 设备状态统计 + * - getDashboardStatistics(): 仪表板综合数据 + * 业务逻辑:数据验证、结果格式化、异常处理 + */ +package com.campus.water.service; + +import com.campus.water.entity.dto.request.StatisticsQueryRequest; +import com.campus.water.entity.vo.AlarmStatisticsVO; +import com.campus.water.entity.vo.StatisticsVO; +import com.campus.water.mapper.StatisticsMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class StatisticsService { + + private final StatisticsMapper statisticsMapper; + private final DeviceService deviceService; + + /** + * 获取用水量统计 + */ + @Transactional(readOnly = true) + public StatisticsVO getWaterUsageStatistics(StatisticsQueryRequest request) { + StatisticsVO result = new StatisticsVO(); + result.setPeriod(request.getPeriod()); + result.setStartDate(request.getStartDate()); + result.setEndDate(request.getEndDate()); + + List> data; + + switch (request.getStatType()) { + case "by_device": + data = statisticsMapper.statWaterUsageByDevice( + request.getStartDate(), request.getEndDate(), + request.getAreaId(), request.getLimit()); + break; + case "by_area": + data = statisticsMapper.statWaterUsageByArea( + request.getStartDate(), request.getEndDate(), + request.getDeviceType(), request.getLimit()); + break; + case "by_time": + data = statisticsMapper.statWaterUsageByTime( + request.getStartDate(), request.getEndDate(), + request.getPeriod(), request.getDeviceId(), + request.getAreaId()); + break; + default: + throw new IllegalArgumentException("不支持的统计类型: " + request.getStatType()); + } + + // 计算总计 + double totalAmount = data.stream() + .mapToDouble(item -> item.get("totalWaterOutput") != null ? + Double.parseDouble(item.get("totalWaterOutput").toString()) : 0) + .sum(); + + int totalCount = data.stream() + .mapToInt(item -> item.get("usageCount") != null ? + Integer.parseInt(item.get("usageCount").toString()) : 0) + .sum(); + + result.setTotalCount(totalCount); + result.setTotalAmount(totalAmount); + result.setAvgAmount(totalCount > 0 ? totalAmount / totalCount : 0); + + // 构建明细项 + List items = new ArrayList<>(); + for (Map item : data) { + StatisticsVO.StatItemVO statItem = new StatisticsVO.StatItemVO(); + + if (request.getStatType().equals("by_time")) { + statItem.setDimensionKey(item.get("timeLabel").toString()); + statItem.setDimensionValue(item.get("timeLabel").toString()); + } else if (request.getStatType().equals("by_device")) { + statItem.setDimensionKey(item.get("deviceId").toString()); + statItem.setDimensionValue(item.get("deviceName") != null ? + item.get("deviceName").toString() : item.get("deviceId").toString()); + } else { + statItem.setDimensionKey(item.get("areaId").toString()); + statItem.setDimensionValue(item.get("areaName") != null ? + item.get("areaName").toString() : item.get("areaId").toString()); + } + + Integer count = item.get("usageCount") != null ? + Integer.parseInt(item.get("usageCount").toString()) : 0; + Double amount = item.get("totalWaterOutput") != null ? + Double.parseDouble(item.get("totalWaterOutput").toString()) : 0; + + statItem.setCount(count); + statItem.setAmount(amount); + statItem.setPercentage(totalAmount > 0 ? (amount / totalAmount) * 100 : 0); + + items.add(statItem); + } + + result.setItems(items); + return result; + } + + /** + * 获取告警统计 + */ + @Transactional(readOnly = true) + public AlarmStatisticsVO getAlarmStatistics(StatisticsQueryRequest request) { + AlarmStatisticsVO result = new AlarmStatisticsVO(); + + // 获取告警统计 + List> alarmStats; + + if ("by_device".equals(request.getStatType())) { + alarmStats = statisticsMapper.statAlarmCountByDevice( + request.getStartDate(), request.getEndDate(), + request.getAreaId(), null, request.getLimit()); + } else if ("by_area".equals(request.getStatType())) { + alarmStats = statisticsMapper.statAlarmCountByArea( + request.getStartDate(), request.getEndDate(), + request.getDeviceType(), request.getLimit()); + } else { + throw new IllegalArgumentException("不支持的告警统计类型: " + request.getStatType()); + } + + // 构建设备告警统计 + List deviceStats = alarmStats.stream() + .map(item -> { + AlarmStatisticsVO.DeviceAlarmStatVO deviceStat = new AlarmStatisticsVO.DeviceAlarmStatVO(); + deviceStat.setDeviceId(item.get("deviceId").toString()); + deviceStat.setDeviceName(item.get("deviceName") != null ? + item.get("deviceName").toString() : ""); + deviceStat.setTotalAlarms(Integer.parseInt(item.get("totalAlarms").toString())); + deviceStat.setPendingAlarms(Integer.parseInt(item.get("pendingAlarms").toString())); + deviceStat.setResolvedAlarms(Integer.parseInt(item.get("resolvedAlarms").toString())); + return deviceStat; + }) + .collect(Collectors.toList()); + + // 计算总数 + int totalAlarms = deviceStats.stream().mapToInt(AlarmStatisticsVO.DeviceAlarmStatVO::getTotalAlarms).sum(); + int pendingAlarms = deviceStats.stream().mapToInt(AlarmStatisticsVO.DeviceAlarmStatVO::getPendingAlarms).sum(); + int resolvedAlarms = deviceStats.stream().mapToInt(AlarmStatisticsVO.DeviceAlarmStatVO::getResolvedAlarms).sum(); + + result.setTotalAlarms(totalAlarms); + result.setPendingAlarms(pendingAlarms); + result.setResolvedAlarms(resolvedAlarms); + result.setDeviceAlarmStats(deviceStats); + + // 获取告警处理统计 + Map handleStats = statisticsMapper.getAlarmHandleStatistics( + request.getStartDate(), request.getEndDate(), request.getAreaId()); + + if (handleStats != null && handleStats.get("avgResponseHours") != null) { + result.setAverageResponseTime(Double.parseDouble(handleStats.get("avgResponseHours").toString())); + } + + return result; + } + + /** + * 获取设备状态统计 + */ + @Transactional(readOnly = true) + public Map getDeviceStatusStatistics(String areaId, String deviceType) { + return statisticsMapper.getDeviceStatusStatistics(areaId, deviceType); + } + + /** + * 获取综合仪表盘数据 + */ + @Transactional(readOnly = true) + public Map getDashboardStatistics() { + Map result = new HashMap<>(); + + // 今日数据 + LocalDate today = LocalDate.now(); + StatisticsVO todayWaterUsage = getWaterUsageStatistics(createTodayQuery()); + result.put("todayWaterUsage", todayWaterUsage); + + // 本月数据 + StatisticsVO monthWaterUsage = getWaterUsageStatistics(createMonthQuery()); + result.put("monthWaterUsage", monthWaterUsage); + + // 设备状态统计 + Map deviceStats = getDeviceStatusStatistics(null, null); + result.put("deviceStatus", deviceStats); + + // 告警统计 + AlarmStatisticsVO alarmStats = getAlarmStatistics(createAlarmQuery()); + result.put("alarmStatistics", alarmStats); + + // 热门设备(按用水量) + StatisticsQueryRequest hotDevicesQuery = new StatisticsQueryRequest(); + hotDevicesQuery.setStatType("by_device"); + hotDevicesQuery.setStartDate(today.minusDays(7)); + hotDevicesQuery.setEndDate(today); + hotDevicesQuery.setLimit(5); + StatisticsVO hotDevices = getWaterUsageStatistics(hotDevicesQuery); + result.put("hotDevices", hotDevices); + + return result; + } + + private StatisticsQueryRequest createTodayQuery() { + StatisticsQueryRequest request = new StatisticsQueryRequest(); + request.setStatType("by_time"); + request.setPeriod("day"); + request.setStartDate(LocalDate.now()); + request.setEndDate(LocalDate.now()); + return request; + } + + private StatisticsQueryRequest createMonthQuery() { + StatisticsQueryRequest request = new StatisticsQueryRequest(); + request.setStatType("by_time"); + request.setPeriod("month"); + request.setStartDate(LocalDate.now().withDayOfMonth(1)); + request.setEndDate(LocalDate.now()); + return request; + } + + private StatisticsQueryRequest createAlarmQuery() { + StatisticsQueryRequest request = new StatisticsQueryRequest(); + request.setStatType("by_device"); + request.setStartDate(LocalDate.now().minusDays(30)); + request.setEndDate(LocalDate.now()); + request.setLimit(10); + return request; + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/task/DeviceStatusMonitorTask.java b/src/main/java/com/campus/water/task/DeviceStatusMonitorTask.java new file mode 100644 index 0000000..dafd726 --- /dev/null +++ b/src/main/java/com/campus/water/task/DeviceStatusMonitorTask.java @@ -0,0 +1,54 @@ +/** + * 设备状态监控定时任务 + * 功能:定时执行设备状态检测和统计收集任务 + * 用途:自动化设备状态管理,减少人工干预 + * 核心任务: + * 1. 离线设备检测:每5分钟检测超时离线设备 + * 2. 状态统计收集:每小时收集设备状态统计数据 + * 技术:Spring @Scheduled定时任务、异常处理、日志记录 + * 配置:定时频率可在application.yml中配置 + */ +package com.campus.water.task; + +import com.campus.water.service.DeviceService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class DeviceStatusMonitorTask { + + private final DeviceService deviceService; + + /** + * 每5分钟检测一次离线设备 + */ + @Scheduled(fixedRate = 300000) // 5分钟 + public void monitorOfflineDevices() { + log.info("开始自动检测离线设备..."); + try { + // 检测30分钟无数据的设备 + deviceService.autoDetectOfflineDevices(30); + } catch (Exception e) { + log.error("离线设备检测任务执行失败: {}", e.getMessage(), e); + } + } + + /** + * 每小时统计一次设备状态 + */ + @Scheduled(cron = "0 0 * * * ?") // 每小时执行一次 + public void collectDeviceStatusStatistics() { + log.info("开始收集设备状态统计..."); + try { + // 这里可以保存统计结果到数据库 + // deviceService.getDeviceStatusCount(null, null); + log.info("设备状态统计收集完成"); + } catch (Exception e) { + log.error("设备状态统计收集失败: {}", e.getMessage(), e); + } + } +} \ No newline at end of file -- 2.34.1 From 4461d18845261e95eb178424020f725e8410c539 Mon Sep 17 00:00:00 2001 From: wanglei <3085637232@qq.com> Date: Fri, 5 Dec 2025 18:42:02 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E7=8E=8B=E7=A3=8A=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E6=95=B0=E6=8D=AE=E7=BB=9F=E8=AE=A1=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=88=E6=8C=89=E8=AE=BE=E5=A4=87=20/=20=E5=9C=B0?= =?UTF-8?q?=E5=8C=BA=20/=20=E6=97=B6=E9=97=B4=E7=BB=9F=E8=AE=A1=E7=94=A8?= =?UTF-8?q?=E6=B0=B4=E9=87=8F=E3=80=81=E5=91=8A=E8=AD=A6=E6=AC=A1=E6=95=B0?= =?UTF-8?q?=EF=BC=89=E3=80=81=E8=AE=BE=E5=A4=87=E7=8A=B6=E6=80=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=8E=A5=E5=8F=A3=EF=BC=88=E5=9C=A8=E7=BA=BF=20/=20?= =?UTF-8?q?=E7=A6=BB=E7=BA=BF=E6=A0=87=E8=AE=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../campus/water/config/MyBatisConfig.java | 214 +++++ .../campus/water/config/SwaggerConfig.java | 410 +++++++++ .../app/AppStatisticsController.java | 84 ++ .../web/DeviceStatusController.java | 153 ++++ .../controller/web/StatisticsController.java | 103 +++ .../request/DeviceStatusUpdateRequest.java | 27 + .../dto/request/StatisticsQueryRequest.java | 35 + .../water/entity/vo/AlarmStatisticsVO.java | 40 + .../campus/water/entity/vo/StatisticsVO.java | 45 + .../com/campus/water/mapper/DeviceMapper.java | 301 +++++++ .../com/campus/water/mapper/DeviceMapper.xml | 383 ++++++++ .../campus/water/mapper/StatisticsMapper.java | 102 +++ .../campus/water/mapper/StatisticsMapper.xml | 251 ++++++ .../campus/water/service/AlertService.java | 828 ++++++++++++++++++ .../campus/water/service/DeviceService.java | 219 +++++ .../water/service/StatisticsService.java | 247 ++++++ .../water/task/DeviceStatusMonitorTask.java | 54 ++ 17 files changed, 3496 insertions(+) create mode 100644 src/main/java/com/campus/water/config/MyBatisConfig.java create mode 100644 src/main/java/com/campus/water/config/SwaggerConfig.java create mode 100644 src/main/java/com/campus/water/controller/app/AppStatisticsController.java create mode 100644 src/main/java/com/campus/water/controller/web/DeviceStatusController.java create mode 100644 src/main/java/com/campus/water/controller/web/StatisticsController.java create mode 100644 src/main/java/com/campus/water/entity/dto/request/DeviceStatusUpdateRequest.java create mode 100644 src/main/java/com/campus/water/entity/dto/request/StatisticsQueryRequest.java create mode 100644 src/main/java/com/campus/water/entity/vo/AlarmStatisticsVO.java create mode 100644 src/main/java/com/campus/water/entity/vo/StatisticsVO.java create mode 100644 src/main/java/com/campus/water/mapper/DeviceMapper.java create mode 100644 src/main/java/com/campus/water/mapper/DeviceMapper.xml create mode 100644 src/main/java/com/campus/water/mapper/StatisticsMapper.java create mode 100644 src/main/java/com/campus/water/mapper/StatisticsMapper.xml create mode 100644 src/main/java/com/campus/water/service/AlertService.java create mode 100644 src/main/java/com/campus/water/service/DeviceService.java create mode 100644 src/main/java/com/campus/water/service/StatisticsService.java create mode 100644 src/main/java/com/campus/water/task/DeviceStatusMonitorTask.java diff --git a/src/main/java/com/campus/water/config/MyBatisConfig.java b/src/main/java/com/campus/water/config/MyBatisConfig.java new file mode 100644 index 0000000..b78aedb --- /dev/null +++ b/src/main/java/com/campus/water/config/MyBatisConfig.java @@ -0,0 +1,214 @@ +package com.campus.water.config; + +import com.zaxxer.hikari.HikariDataSource; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import javax.sql.DataSource; +import java.util.Properties; + +/** + * MyBatis配置类 + * 功能:配置MyBatis与Spring Boot的集成,包括数据源、事务管理、Mapper扫描等 + * 用途: + * 1. 数据源配置:连接池、连接参数 + * 2. MyBatis核心配置:SqlSessionFactory、事务管理器 + * 3. Mapper扫描:自动注册MyBatis映射接口 + * 4. 插件配置:分页插件、性能监控插件等 + * 核心注解: + * - @Configuration: 声明为配置类 + * - @MapperScan: 指定Mapper接口的扫描路径 + * - @EnableTransactionManagement: 启用声明式事务管理 + */ +@Configuration +@MapperScan(basePackages = "com.campus.water.mapper") // 扫描Mapper接口 +@EnableTransactionManagement // 启用事务管理 +public class MyBatisConfig { + + // ========== 数据源配置 ========== + + /** + * 创建HikariCP数据源 + * @return 数据源实例 + * 功能:配置数据库连接池 + * 优点: + * - HikariCP是目前性能最好的连接池 + * - 自动从application.yml读取配置 + * - 支持连接泄漏检测、空闲连接回收 + */ + @Bean + @ConfigurationProperties(prefix = "spring.datasource.hikari") // 绑定配置前缀 + public DataSource dataSource() { + HikariDataSource dataSource = new HikariDataSource(); + + // 设置连接池监控配置(可在application.yml中覆盖) + dataSource.setPoolName("CampusWaterHikariPool"); + dataSource.setMaximumPoolSize(20); // 最大连接数 + dataSource.setMinimumIdle(5); // 最小空闲连接 + dataSource.setIdleTimeout(600000); // 空闲连接超时时间(10分钟) + dataSource.setConnectionTimeout(30000); // 连接超时时间(30秒) + dataSource.setMaxLifetime(1800000); // 连接最大生命周期(30分钟) + + // 连接测试配置 + dataSource.setConnectionTestQuery("SELECT 1"); // MySQL测试语句 + dataSource.setValidationTimeout(5000); // 验证超时时间 + + // 连接泄漏检测 + dataSource.setLeakDetectionThreshold(60000); // 60秒泄漏检测阈值 + + return dataSource; + } + + // ========== MyBatis核心配置 ========== + + /** + * 创建SqlSessionFactory + * @param dataSource 数据源 + * @return SqlSessionFactory实例 + * 功能:MyBatis的核心工厂类,用于创建SqlSession + * 配置项: + * - 数据源绑定 + * - Mapper XML文件位置 + * - 类型别名包扫描 + * - 全局配置文件 + */ + @Bean + public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { + SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); + + // 1. 设置数据源 + sessionFactory.setDataSource(dataSource); + + // 2. 设置Mapper XML文件位置(支持Ant风格路径) + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + sessionFactory.setMapperLocations( + resolver.getResources("classpath:mapper/**/*.xml") + ); + + // 3. 设置类型别名包扫描路径(实体类包) + sessionFactory.setTypeAliasesPackage("com.campus.water.entity"); + + // 4. 创建Configuration对象,设置全局配置 + org.apache.ibatis.session.Configuration configuration = + new org.apache.ibatis.session.Configuration(); + configuration.setMapUnderscoreToCamelCase(true); // 下划线转驼峰 + configuration.setUseGeneratedKeys(true); // 使用JDBC的getGeneratedKeys获取主键 + configuration.setUseColumnLabel(true); // 使用列标签代替列名 + configuration.setCacheEnabled(true); // 启用二级缓存 + + // 5. 设置日志实现 + configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class); + + // 6. 设置默认的执行器类型 + configuration.setDefaultExecutorType(org.apache.ibatis.session.ExecutorType.SIMPLE); + + // 7. 设置配置 + sessionFactory.setConfiguration(configuration); + + // 8. 设置插件(分页插件、性能监控插件等) + sessionFactory.setPlugins( + // 分页插件 + pageInterceptor(), + // 性能分析插件(开发环境使用) + // performanceInterceptor() + ); + + return sessionFactory.getObject(); + } + + /** + * 分页插件配置 + * @return 分页拦截器 + * 功能:自动处理分页查询,支持MySQL、Oracle等多种数据库 + * 特性: + * - 自动识别数据库类型 + * - 支持多种分页方式 + * - 线程安全 + */ + @Bean + public com.github.pagehelper.PageInterceptor pageInterceptor() { + com.github.pagehelper.PageInterceptor pageInterceptor = + new com.github.pagehelper.PageInterceptor(); + + Properties properties = new Properties(); + + // 1. 分页合理化:pageNum<=0时查询第一页,pageNum>总页数时查询最后一页 + properties.setProperty("reasonable", "true"); + + // 2. 支持通过Mapper接口参数传递分页参数 + properties.setProperty("supportMethodsArguments", "true"); + + // 3. 自动检测数据库类型 + properties.setProperty("autoRuntimeDialect", "true"); + + // 4. 分页参数默认值 + properties.setProperty("pageSizeZero", "true"); // pageSize=0时返回全部结果 + properties.setProperty("params", "count=countSql"); + + // 5. 开启分页返回对象中的统计信息 + properties.setProperty("rowBoundsWithCount", "true"); + + // 6. 总是返回PageInfo对象 + properties.setProperty("returnPageInfo", "always"); + + pageInterceptor.setProperties(properties); + return pageInterceptor; + } + + /** + * 性能分析插件(开发环境使用) + * @return 性能分析拦截器 + * 功能:记录SQL执行时间,帮助优化慢查询 + * 注意:生产环境建议关闭或设置较高的阈值 + */ + /* + @Bean + @Profile({"dev", "test"}) // 只在开发测试环境启用 + public com.github.pagehelper.sql.SqlStatsInterceptor performanceInterceptor() { + com.github.pagehelper.sql.SqlStatsInterceptor interceptor = + new com.github.pagehelper.sql.SqlStatsInterceptor(); + + Properties properties = new Properties(); + properties.setProperty("maxTime", "1000"); // 最大执行时间阈值(毫秒) + properties.setProperty("format", "true"); // 格式化SQL输出 + + interceptor.setProperties(properties); + return interceptor; + } + */ + + // ========== 事务管理配置 ========== + + /** + * 创建事务管理器 + * @param dataSource 数据源 + * @return 平台事务管理器 + * 功能:管理数据库事务,支持声明式事务 + * 注解支持:@Transactional + */ + @Bean + public PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + + // ========== 其他配置 ========== + + /** + * MyBatis属性配置 + * @return MyBatis配置属性 + * 功能:集中管理MyBatis相关配置 + */ + @Bean + @ConfigurationProperties(prefix = "mybatis.configuration") + public org.apache.ibatis.session.Configuration mybatisConfiguration() { + return new org.apache.ibatis.session.Configuration(); + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/config/SwaggerConfig.java b/src/main/java/com/campus/water/config/SwaggerConfig.java new file mode 100644 index 0000000..5f4c669 --- /dev/null +++ b/src/main/java/com/campus/water/config/SwaggerConfig.java @@ -0,0 +1,410 @@ +package com.campus.water.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import java.util.Arrays; +import java.util.List; + +/** + * Swagger/OpenAPI配置类 + * 功能:配置API文档生成和展示 + * 用途: + * 1. 自动生成API文档:基于代码注解 + * 2. 在线API测试:提供交互式测试界面 + * 3. 接口规范定义:统一API响应格式和错误码 + * 4. 权限控制:JWT token验证配置 + * 访问地址: + * - Swagger UI: http://localhost:8080/swagger-ui/index.html + * - OpenAPI JSON: http://localhost:8080/v3/api-docs + * 核心注解: + * - @Configuration: 声明为配置类 + * - @Profile: 指定生效的环境(开发/测试环境) + */ +@Configuration +@Profile({"dev", "test"}) // 只在开发测试环境启用,生产环境关闭 +public class SwaggerConfig { + + @Value("${spring.application.name:校园直饮矿化水物联网运维平台}") + private String applicationName; + + @Value("${server.port:8080}") + private String serverPort; + + /** + * 创建OpenAPI配置 + * @return OpenAPI配置实例 + * 功能:定义API文档的基本信息、安全方案、服务器等 + * 结构: + * - info: API基本信息(标题、版本、描述等) + * - servers: API服务器地址 + * - components: 可重用的组件(安全方案、响应模型等) + * - security: 全局安全要求 + */ + @Bean + public OpenAPI campusWaterOpenAPI() { + return new OpenAPI() + // 1. API基本信息 + .info(buildApiInfo()) + + // 2. API服务器配置 + .servers(buildServers()) + + // 3. 安全方案配置(JWT Bearer Token) + .components(buildComponents()) + .addSecurityItem(buildSecurityRequirement()) + + // 4. 全局标签(可在此定义,也可以在Controller上使用@Tag) + // .tags(buildTags()) + + // 5. 外部文档链接 + .externalDocs(new io.swagger.v3.oas.models.ExternalDocumentation() + .description("项目GitHub仓库") + .url("https://github.com/campus-water/water-management-system")); + } + + /** + * 构建API基本信息 + * @return Info对象 + * 功能:定义API的标题、版本、描述、联系方式等 + */ + private Info buildApiInfo() { + return new Info() + .title("校园直饮矿化水物联网运维平台 API") + .version("v1.0.0") + .description(""" + ## 项目概述 + + 校园直饮矿化水物联网运维平台是一个集设备监控、数据统计、告警管理、运维调度于一体的智能化管理系统。 + + ## 功能模块 + + ### 1. 设备管理 + - 设备状态监控(在线/离线/故障) + - 设备信息维护 + - 设备区域分配 + + ### 2. 数据统计 + - 用水量统计(按设备/区域/时间) + - 告警统计分析 + - 设备使用率统计 + - 水质数据统计 + + ### 3. 告警管理 + - 实时告警监控 + - 告警处理流程 + - 告警统计分析 + + ### 4. 工单管理 + - 维修工单创建 + - 工单分配和跟踪 + - 维修结果反馈 + + ### 5. 用户管理 + - 多角色权限控制(学生/维修人员/管理员) + - 个人信息管理 + - 饮水记录查询 + + ## 接口规范 + + ### 响应格式 + ```json + { + "code": 200, // 状态码 + "msg": "success", // 消息 + "data": {} // 数据 + } + ``` + + ### 状态码说明 + - 200: 成功 + - 400: 请求参数错误 + - 401: 未授权 + - 403: 禁止访问 + - 404: 资源不存在 + - 500: 服务器内部错误 + """) + .termsOfService("https://campus-water.com/terms") + .contact(buildContact()) + .license(buildLicense()); + } + + /** + * 构建联系信息 + * @return Contact对象 + * 功能:提供项目联系人和联系方式 + */ + private Contact buildContact() { + return new Contact() + .name("开发团队") + .url("https://campus-water.com") + .email("dev@campus-water.com") + .extensions(new java.util.HashMap<>() {{ + put("技术支持", "support@campus-water.com"); + put("产品反馈", "feedback@campus-water.com"); + }}); + } + + /** + * 构建许可证信息 + * @return License对象 + * 功能:定义API的使用许可证 + */ + private License buildLicense() { + return new License() + .name("Apache 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0.html") + .identifier("Apache-2.0"); + } + + /** + * 构建服务器配置 + * @return 服务器列表 + * 功能:定义API的访问地址,支持多环境 + */ + private List buildServers() { + return Arrays.asList( + // 本地开发环境 + new Server() + .url("http://localhost:" + serverPort) + .description("本地开发环境") + .variables(new java.util.HashMap<>() {{ + put("port", new io.swagger.v3.oas.models.servers.ServerVariable() + ._default(serverPort) + .description("服务器端口")); + }}), + + // 测试环境 + new Server() + .url("https://test.campus-water.com") + .description("测试环境") + .variables(new java.util.HashMap<>() {{ + put("env", new io.swagger.v3.oas.models.servers.ServerVariable() + ._default("test") + .description("环境标识") + ._enum(Arrays.asList("test", "staging"))); + }}), + + // 生产环境 + new Server() + .url("https://api.campus-water.com") + .description("生产环境") + .variables(new java.util.HashMap<>() {{ + put("version", new io.swagger.v3.oas.models.servers.ServerVariable() + ._default("v1") + .description("API版本") + ._enum(Arrays.asList("v1", "v2"))); + }}) + ); + } + + /** + * 构建组件配置 + * @return Components对象 + * 功能:定义可重用的组件,如安全方案、响应模型等 + */ + private Components buildComponents() { + return new Components() + // 1. JWT Bearer Token安全方案 + .addSecuritySchemes("Bearer Token", buildJwtSecurityScheme()) + + // 2. API Key安全方案(备用) + .addSecuritySchemes("API Key", buildApiKeySecurityScheme()) + + // 3. 通用响应模型 + .addSchemas("ResultVO", buildResultVOSchema()) + .addSchemas("PageResult", buildPageResultSchema()) + + // 4. 通用请求头 + .addParameters("X-User-Id", buildUserIdHeader()) + .addParameters("X-User-Type", buildUserTypeHeader()); + } + + /** + * 构建JWT安全方案 + * @return SecurityScheme对象 + * 功能:定义JWT Bearer Token的认证方式 + */ + private SecurityScheme buildJwtSecurityScheme() { + return new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .description(""" + JWT Bearer Token认证 + + ### 获取Token + 1. 调用登录接口获取token + 2. 在请求头中添加:Authorization: Bearer {token} + + ### Token格式 + ```json + { + "sub": "用户ID", + "username": "用户名", + "userType": "用户类型", + "iat": 签发时间, + "exp": 过期时间 + } + ``` + """) + .in(SecurityScheme.In.HEADER) + .name("Authorization"); + } + + /** + * 构建API Key安全方案 + * @return SecurityScheme对象 + * 功能:备用认证方式,用于第三方系统集成 + */ + private SecurityScheme buildApiKeySecurityScheme() { + return new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .name("X-API-KEY") + .description("API Key认证,用于第三方系统集成"); + } + + /** + * 构建通用响应模型 + * @return Schema对象 + * 功能:定义统一的API响应格式 + */ + private io.swagger.v3.oas.models.media.Schema buildResultVOSchema() { + return new io.swagger.v3.oas.models.media.Schema<>() + .type("object") + .addProperties("code", new io.swagger.v3.oas.models.media.Schema<>() + .type("integer") + .description("状态码") + .example(200)) + .addProperties("msg", new io.swagger.v3.oas.models.media.Schema<>() + .type("string") + .description("消息") + .example("success")) + .addProperties("data", new io.swagger.v3.oas.models.media.Schema<>() + .type("object") + .description("数据") + .nullable(true)) + .addProperties("timestamp", new io.swagger.v3.oas.models.media.Schema<>() + .type("string") + .format("date-time") + .description("时间戳") + .example("2024-01-01T12:00:00Z")) + .description("通用响应格式") + .required(Arrays.asList("code", "msg", "timestamp")); + } + + /** + * 构建分页响应模型 + * @return Schema对象 + * 功能:定义统一的分页响应格式 + */ + private io.swagger.v3.oas.models.media.Schema buildPageResultSchema() { + return new io.swagger.v3.oas.models.media.Schema<>() + .type("object") + .addProperties("total", new io.swagger.v3.oas.models.media.Schema<>() + .type("integer") + .description("总记录数") + .example(100)) + .addProperties("pages", new io.swagger.v3.oas.models.media.Schema<>() + .type("integer") + .description("总页数") + .example(10)) + .addProperties("pageNum", new io.swagger.v3.oas.models.media.Schema<>() + .type("integer") + .description("当前页码") + .example(1)) + .addProperties("pageSize", new io.swagger.v3.oas.models.media.Schema<>() + .type("integer") + .description("每页大小") + .example(10)) + .addProperties("list", new io.swagger.v3.oas.models.media.Schema<>() + .type("array") + .description("数据列表") + .items(new io.swagger.v3.oas.models.media.Schema<>())) + .description("分页响应格式") + .required(Arrays.asList("total", "pages", "pageNum", "pageSize", "list")); + } + + /** + * 构建用户ID请求头 + * @return Parameter对象 + * 功能:定义用户ID请求头的规范 + */ + private io.swagger.v3.oas.models.parameters.Parameter buildUserIdHeader() { + return new io.swagger.v3.oas.models.parameters.Parameter() + .name("X-User-Id") + .in(io.swagger.v3.oas.models.parameters.Parameter.In.HEADER.toString()) + .description("用户ID(登录后由系统分配)") + .required(false) + .schema(new io.swagger.v3.oas.models.media.Schema<>() + .type("string") + .example("STU20240001")); + } + + /** + * 构建用户类型请求头 + * @return Parameter对象 + * 功能:定义用户类型请求头的规范 + */ + private io.swagger.v3.oas.models.parameters.Parameter buildUserTypeHeader() { + return new io.swagger.v3.oas.models.parameters.Parameter() + .name("X-User-Type") + .in(io.swagger.v3.oas.models.parameters.Parameter.In.HEADER.toString()) + .description("用户类型:student/repairer/admin") + .required(false) + .schema(new io.swagger.v3.oas.models.media.Schema<>() + .type("string") + .example("student")); + } + + /** + * 构建安全要求 + * @return SecurityRequirement对象 + * 功能:定义需要认证的接口范围 + */ + private SecurityRequirement buildSecurityRequirement() { + return new SecurityRequirement() + .addList("Bearer Token"); + } + + /** + * 构建API标签 + * @return 标签列表 + * 功能:组织API接口到不同的标签组 + */ + /* + private List buildTags() { + return Arrays.asList( + new io.swagger.v3.oas.models.tags.Tag() + .name("用户管理") + .description("用户认证、注册、个人信息等接口"), + new io.swagger.v3.oas.models.tags.Tag() + .name("设备管理") + .description("设备状态、信息、区域分配等接口"), + new io.swagger.v3.oas.models.tags.Tag() + .name("数据统计") + .description("用水量、告警、设备状态等统计接口"), + new io.swagger.v3.oas.models.tags.Tag() + .name("告警管理") + .description("告警创建、处理、查询等接口"), + new io.swagger.v3.oas.models.tags.Tag() + .name("工单管理") + .description("维修工单创建、分配、处理等接口"), + new io.swagger.v3.oas.models.tags.Tag() + .name("水质管理") + .description("水质数据查询、分析等接口") + ); + } + */ +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/app/AppStatisticsController.java b/src/main/java/com/campus/water/controller/app/AppStatisticsController.java new file mode 100644 index 0000000..a92a874 --- /dev/null +++ b/src/main/java/com/campus/water/controller/app/AppStatisticsController.java @@ -0,0 +1,84 @@ +/** + * 移动端(App)统计接口控制器 + * 功能:为移动端提供简化的统计查询接口 + * 用途:支持学生在手机上查看个人用水统计和设备状态 + * 接口特点: + * - 简化参数:减少查询维度,优化移动端体验 + * - 个人化:基于用户ID过滤数据 + * - 快速响应:返回核心数据,减少数据传输量 + * 接口列表: + * 1. GET /personal-water-usage: 个人用水统计(今日/本周/本月) + * 2. GET /device-status-overview: 设备状态概览 + * 技术:Spring MVC、Header参数验证、移动端优化 + */ +package com.campus.water.controller.app; + +import com.campus.water.entity.dto.request.StatisticsQueryRequest; +import com.campus.water.entity.vo.StatisticsVO; +import com.campus.water.service.StatisticsService; +import com.campus.water.util.ResultVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.Map; + +@RestController +@RequestMapping("/api/app/statistics") +@RequiredArgsConstructor +@Tag(name = "App统计接口", description = "移动端简化统计接口") +public class AppStatisticsController { + + private final StatisticsService statisticsService; + + @GetMapping("/personal-water-usage") + @Operation(summary = "个人用水统计", description = "获取当前用户的用水统计") + public ResponseEntity> getPersonalWaterUsage( + @RequestParam(required = false) String period, // today/week/month + @RequestHeader("X-User-Id") String userId) { + try { + StatisticsQueryRequest request = new StatisticsQueryRequest(); + request.setStatType("by_time"); + + LocalDate now = LocalDate.now(); + if ("today".equals(period)) { + request.setPeriod("day"); + request.setStartDate(now); + request.setEndDate(now); + } else if ("week".equals(period)) { + request.setPeriod("day"); + request.setStartDate(now.minusDays(7)); + request.setEndDate(now); + } else if ("month".equals(period)) { + request.setPeriod("day"); + request.setStartDate(now.withDayOfMonth(1)); + request.setEndDate(now); + } else { + request.setPeriod("day"); + request.setStartDate(now.minusDays(30)); + request.setEndDate(now); + } + + // 这里需要根据userId过滤数据,实际实现需要调整 + StatisticsVO result = statisticsService.getWaterUsageStatistics(request); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "获取个人用水统计失败: " + e.getMessage())); + } + } + + @GetMapping("/device-status-overview") + @Operation(summary = "设备状态概览", description = "获取设备状态概览信息") + public ResponseEntity>> getDeviceStatusOverview( + @RequestParam(required = false) String areaId) { + try { + Map result = statisticsService.getDeviceStatusStatistics(areaId, null); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "获取设备状态概览失败: " + e.getMessage())); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/web/DeviceStatusController.java b/src/main/java/com/campus/water/controller/web/DeviceStatusController.java new file mode 100644 index 0000000..05c71cf --- /dev/null +++ b/src/main/java/com/campus/water/controller/web/DeviceStatusController.java @@ -0,0 +1,153 @@ +/** + * Web端设备状态管理接口控制器 + * 功能:提供设备状态管理的RESTful API接口 + * 用途:支持Web管理端对设备状态的手动和自动管理 + * 接口列表: + * 1. 状态更新:单设备状态变更 + * 2. 状态标记:在线/离线/故障快捷操作 + * 3. 批量操作:批量更新设备状态 + * 4. 状态查询:按状态筛选设备列表 + * 5. 离线检测:查询超时离线设备 + * 6. 自动检测:触发离线设备检测任务 + * 安全:需要权限验证,记录操作日志 + */ +package com.campus.water.controller.web; + +import com.campus.water.entity.Device; +import com.campus.water.entity.dto.request.DeviceStatusUpdateRequest; +import com.campus.water.service.DeviceService; +import com.campus.water.util.ResultVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/web/device-status") +@RequiredArgsConstructor +@Tag(name = "设备状态管理接口", description = "Web管理端设备状态管理接口") +public class DeviceStatusController { + + private final DeviceService deviceService; + + @PostMapping("/update") + @Operation(summary = "更新设备状态", description = "手动更新设备状态(在线/离线/故障)") + public ResponseEntity> updateDeviceStatus( + @Valid @RequestBody DeviceStatusUpdateRequest request) { + try { + boolean result = deviceService.updateDeviceStatus(request); + return ResponseEntity.ok(ResultVO.success(result, "设备状态更新成功")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "设备状态更新失败: " + e.getMessage())); + } + } + + @PostMapping("/{deviceId}/online") + @Operation(summary = "标记设备在线", description = "将设备标记为在线状态") + public ResponseEntity> markDeviceOnline(@PathVariable String deviceId) { + try { + boolean result = deviceService.markDeviceOnline(deviceId); + return ResponseEntity.ok(ResultVO.success(result, "设备已标记为在线")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "标记设备在线失败: " + e.getMessage())); + } + } + + @PostMapping("/{deviceId}/offline") + @Operation(summary = "标记设备离线", description = "将设备标记为离线状态") + public ResponseEntity> markDeviceOffline( + @PathVariable String deviceId, + @RequestParam(required = false) String reason) { + try { + boolean result = deviceService.markDeviceOffline(deviceId, reason); + return ResponseEntity.ok(ResultVO.success(result, "设备已标记为离线")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "标记设备离线失败: " + e.getMessage())); + } + } + + @PostMapping("/{deviceId}/fault") + @Operation(summary = "标记设备故障", description = "将设备标记为故障状态") + public ResponseEntity> markDeviceFault( + @PathVariable String deviceId, + @RequestParam String faultType, + @RequestParam String description) { + try { + boolean result = deviceService.markDeviceFault(deviceId, faultType, description); + return ResponseEntity.ok(ResultVO.success(result, "设备已标记为故障")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "标记设备故障失败: " + e.getMessage())); + } + } + + @PostMapping("/batch-update") + @Operation(summary = "批量更新设备状态", description = "批量更新多个设备的状态") + public ResponseEntity> batchUpdateDeviceStatus( + @RequestParam List deviceIds, + @RequestParam String status, + @RequestParam(required = false) String remark) { + try { + boolean result = deviceService.batchUpdateDeviceStatus(deviceIds, status, remark); + return ResponseEntity.ok(ResultVO.success(result, "批量更新设备状态成功")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "批量更新设备状态失败: " + e.getMessage())); + } + } + + @GetMapping("/by-status") + @Operation(summary = "按状态查询设备", description = "根据状态查询设备列表") + public ResponseEntity>> getDevicesByStatus( + @RequestParam String status, + @RequestParam(required = false) String areaId, + @RequestParam(required = false) String deviceType) { + try { + List devices = deviceService.getDevicesByStatus(status, areaId, deviceType); + return ResponseEntity.ok(ResultVO.success(devices)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "查询设备失败: " + e.getMessage())); + } + } + + @GetMapping("/status-count") + @Operation(summary = "设备状态数量统计", description = "统计各状态设备数量") + public ResponseEntity>> getDeviceStatusCount( + @RequestParam(required = false) String areaId, + @RequestParam(required = false) String deviceType) { + try { + Map result = deviceService.getDeviceStatusCount(areaId, deviceType); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "设备状态统计失败: " + e.getMessage())); + } + } + + @GetMapping("/offline-detection") + @Operation(summary = "离线设备检测", description = "检测离线时间超过阈值的设备") + public ResponseEntity>> getOfflineDevices( + @RequestParam(defaultValue = "30") Integer thresholdMinutes, + @RequestParam(required = false) String areaId) { + try { + List devices = deviceService.getOfflineDevicesExceedThreshold(thresholdMinutes, areaId); + return ResponseEntity.ok(ResultVO.success(devices)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "离线设备检测失败: " + e.getMessage())); + } + } + + @PostMapping("/auto-detect-offline") + @Operation(summary = "自动检测离线设备", description = "自动检测并标记离线设备") + public ResponseEntity> autoDetectOfflineDevices( + @RequestParam(defaultValue = "30") Integer thresholdMinutes) { + try { + deviceService.autoDetectOfflineDevices(thresholdMinutes); + return ResponseEntity.ok(ResultVO.success("离线设备检测完成")); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "自动检测离线设备失败: " + e.getMessage())); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/controller/web/StatisticsController.java b/src/main/java/com/campus/water/controller/web/StatisticsController.java new file mode 100644 index 0000000..b008925 --- /dev/null +++ b/src/main/java/com/campus/water/controller/web/StatisticsController.java @@ -0,0 +1,103 @@ +/** + * Web端统计接口控制器 + * 功能:提供Web管理端的统计数据查询API接口 + * 用途:前后端分离架构中的后端API服务 + * 接口列表: + * 1. POST /water-usage: 用水量统计(支持多维度) + * 2. POST /alarm: 告警统计(次数、处理情况) + * 3. GET /device-status: 设备状态数量统计 + * 4. GET /dashboard: 仪表板综合数据 + * 5. GET /hot-devices: 热门设备用水量排名 + * 技术:Spring MVC、参数验证、统一响应格式 + */ +package com.campus.water.controller.web; + +import com.campus.water.entity.dto.request.StatisticsQueryRequest; +import com.campus.water.entity.vo.AlarmStatisticsVO; +import com.campus.water.entity.vo.StatisticsVO; +import com.campus.water.service.StatisticsService; +import com.campus.water.util.ResultVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/web/statistics") +@RequiredArgsConstructor +@Tag(name = "统计分析接口", description = "Web管理端统计分析接口") +public class StatisticsController { + + private final StatisticsService statisticsService; + + @PostMapping("/water-usage") + @Operation(summary = "用水量统计", description = "按设备/区域/时间统计用水量") + public ResponseEntity> getWaterUsageStatistics( + @Valid @RequestBody StatisticsQueryRequest request) { + try { + StatisticsVO result = statisticsService.getWaterUsageStatistics(request); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "统计失败: " + e.getMessage())); + } + } + + @PostMapping("/alarm") + @Operation(summary = "告警统计", description = "统计告警次数和处理情况") + public ResponseEntity> getAlarmStatistics( + @Valid @RequestBody StatisticsQueryRequest request) { + try { + AlarmStatisticsVO result = statisticsService.getAlarmStatistics(request); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "告警统计失败: " + e.getMessage())); + } + } + + @GetMapping("/device-status") + @Operation(summary = "设备状态统计", description = "统计各状态设备数量") + public ResponseEntity>> getDeviceStatusStatistics( + @RequestParam(required = false) String areaId, + @RequestParam(required = false) String deviceType) { + try { + Map result = statisticsService.getDeviceStatusStatistics(areaId, deviceType); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "设备状态统计失败: " + e.getMessage())); + } + } + + @GetMapping("/dashboard") + @Operation(summary = "仪表盘数据", description = "获取综合仪表盘统计数据") + public ResponseEntity>> getDashboardStatistics() { + try { + Map result = statisticsService.getDashboardStatistics(); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "获取仪表盘数据失败: " + e.getMessage())); + } + } + + @GetMapping("/hot-devices") + @Operation(summary = "热门设备统计", description = "获取用水量最高的设备") + public ResponseEntity> getHotDevices( + @RequestParam(defaultValue = "7") Integer days, + @RequestParam(defaultValue = "10") Integer limit) { + try { + StatisticsQueryRequest request = new StatisticsQueryRequest(); + request.setStatType("by_device"); + request.setStartDate(java.time.LocalDate.now().minusDays(days)); + request.setEndDate(java.time.LocalDate.now()); + request.setLimit(limit); + + StatisticsVO result = statisticsService.getWaterUsageStatistics(request); + return ResponseEntity.ok(ResultVO.success(result)); + } catch (Exception e) { + return ResponseEntity.ok(ResultVO.error(500, "获取热门设备失败: " + e.getMessage())); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/dto/request/DeviceStatusUpdateRequest.java b/src/main/java/com/campus/water/entity/dto/request/DeviceStatusUpdateRequest.java new file mode 100644 index 0000000..aa31b5f --- /dev/null +++ b/src/main/java/com/campus/water/entity/dto/request/DeviceStatusUpdateRequest.java @@ -0,0 +1,27 @@ +/** + * 设备状态更新请求数据传输对象(DTO) + * 功能:接收设备状态变更的请求参数 + * 用途:统一设备状态管理接口的入参规范 + * 参数: + * - deviceId: 设备唯一标识(必填) + * - status: 目标状态(online/offline/fault/maintenance) + * - remark: 状态变更备注(可选) + * 验证:非空验证、状态枚举验证 + */ +package com.campus.water.entity.dto.request; + +import lombok.Data; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Data +public class DeviceStatusUpdateRequest { + @NotBlank(message = "设备ID不能为空") + private String deviceId; + + @NotBlank(message = "设备状态不能为空") + private String status; // online/offline/fault/maintenance + + private String remark; // 状态变更备注 +} + diff --git a/src/main/java/com/campus/water/entity/dto/request/StatisticsQueryRequest.java b/src/main/java/com/campus/water/entity/dto/request/StatisticsQueryRequest.java new file mode 100644 index 0000000..9127415 --- /dev/null +++ b/src/main/java/com/campus/water/entity/dto/request/StatisticsQueryRequest.java @@ -0,0 +1,35 @@ +/** + * 统计查询请求数据传输对象(DTO) + * 功能:接收前端统计查询的参数,支持多种统计维度和过滤条件 + * 用途:统一统计查询接口的入参规范 + * 参数: + * - statType: 统计类型(water_usage/alarm/device_usage) + * - period: 统计周期(day/week/month/year/custom) + * - startDate/endDate: 时间范围 + * - deviceId/areaId: 设备/区域筛选 + */ +package com.campus.water.entity.dto.request; + +import lombok.Data; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDate; + +@Data +public class StatisticsQueryRequest { + @NotBlank(message = "统计类型不能为空") + private String statType; // water_usage/alarm/device_usage + + private String period; // day/week/month/year/custom + + private LocalDate startDate; + + private LocalDate endDate; + + private String deviceId; // 设备ID(可选) + + private String areaId; // 区域ID(可选) + + private String deviceType; // water_maker/water_supply + + private Integer limit = 10; // 返回条数限制 +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/vo/AlarmStatisticsVO.java b/src/main/java/com/campus/water/entity/vo/AlarmStatisticsVO.java new file mode 100644 index 0000000..183ff67 --- /dev/null +++ b/src/main/java/com/campus/water/entity/vo/AlarmStatisticsVO.java @@ -0,0 +1,40 @@ +/** + * 告警统计数据视图对象(VO) + * 功能:封装告警相关的统计数据,包括告警级别、类型、设备分布等 + * 用途:展示告警统计分析和处理情况 + * 结构: + * - AlarmStatisticsVO: 告警统计主类 + * - DeviceAlarmStatVO: 设备告警统计明细项 + */ +package com.campus.water.entity.vo; + +import lombok.Data; +import java.util.List; +import java.util.Map; + +@Data +public class AlarmStatisticsVO { + private Integer totalAlarms; // 总告警数 + private Integer pendingAlarms; // 未处理告警数 + private Integer resolvedAlarms; // 已处理告警数 + private Double averageResponseTime; // 平均响应时间(小时) + + // 按级别统计 + private Map alarmLevelCount; + + // 按类型统计 + private Map alarmTypeCount; + + // 按设备统计 + private List deviceAlarmStats; + + @Data + public static class DeviceAlarmStatVO { + private String deviceId; + private String deviceName; + private Integer totalAlarms; + private Integer pendingAlarms; + private Integer resolvedAlarms; + private String mostCommonAlarmType; // 最常见告警类型 + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/entity/vo/StatisticsVO.java b/src/main/java/com/campus/water/entity/vo/StatisticsVO.java new file mode 100644 index 0000000..7892510 --- /dev/null +++ b/src/main/java/com/campus/water/entity/vo/StatisticsVO.java @@ -0,0 +1,45 @@ +/** + * 统计数据显示视图对象(VO) + * 功能:封装统计数据查询的响应结果,包含总计、明细、时间序列等数据结构 + * 用途:用于前后端数据交互,展示用水量、告警等统计信息 + * 结构: + * - StatisticsVO: 主统计响应类 + * - StatItemVO: 统计明细项(按设备/区域/时间维度) + * - TimeSeriesVO: 时间序列数据(用于图表展示) + */ +package com.campus.water.entity.vo; + +import lombok.Data; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Data +public class StatisticsVO { + private Integer totalCount; // 总数量 + private Double totalAmount; // 总用水量/总金额 + private Double avgAmount; // 平均值 + private String period; // 统计周期:day/week/month/year + private LocalDate startDate; // 统计开始日期 + private LocalDate endDate; // 统计结束日期 + + // 明细数据 + private List items; + + @Data + public static class StatItemVO { + private String dimensionKey; // 维度键:设备ID/区域ID/时间 + private String dimensionValue; // 维度值:设备名称/区域名称/时间标签 + private Integer count; // 数量 + private Double amount; // 用水量/金额 + private Double percentage; // 占比百分比 + } + + // 时间序列数据 + @Data + public static class TimeSeriesVO { + private List timeLabels; // 时间标签 + private List values; // 对应数值 + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/DeviceMapper.java b/src/main/java/com/campus/water/mapper/DeviceMapper.java new file mode 100644 index 0000000..a52b773 --- /dev/null +++ b/src/main/java/com/campus/water/mapper/DeviceMapper.java @@ -0,0 +1,301 @@ +package com.campus.water.mapper; + +import com.campus.water.entity.Device; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 设备数据访问接口(MyBatis Mapper) + * 功能:定义设备相关的数据库操作方法,包括设备信息CRUD和状态管理 + * 用途:为设备管理提供数据访问支持,支持设备状态查询、更新、统计等操作 + * 包含方法: + * - 设备基本信息CRUD操作 + * - 设备状态管理相关操作 + * - 设备统计和查询操作 + * 对应XML:DeviceMapper.xml中的SQL实现 + */ +@Mapper +public interface DeviceMapper { + + // ========== 设备基本信息CRUD操作 ========== + + /** + * 根据设备ID查询设备信息 + * @param deviceId 设备ID + * @return 设备实体对象 + */ + Device findById(@Param("deviceId") String deviceId); + + /** + * 查询所有设备 + * @return 设备列表 + */ + List findAll(); + + /** + * 根据设备名称模糊查询 + * @param deviceName 设备名称(模糊匹配) + * @return 匹配的设备列表 + */ + List findByDeviceNameLike(@Param("deviceName") String deviceName); + + /** + * 新增设备 + * @param device 设备实体 + * @return 影响的行数 + */ + int insert(Device device); + + /** + * 更新设备信息 + * @param device 设备实体 + * @return 影响的行数 + */ + int update(Device device); + + /** + * 删除设备 + * @param deviceId 设备ID + * @return 影响的行数 + */ + int delete(@Param("deviceId") String deviceId); + + // ========== 设备状态管理相关操作(新增) ========== + + /** + * 更新设备状态 + * @param deviceId 设备ID + * @param status 目标状态(online/offline/fault/maintenance) + * @param remark 状态变更备注 + * @return 影响的行数 + */ + int updateDeviceStatus(@Param("deviceId") String deviceId, + @Param("status") String status, + @Param("remark") String remark); + + /** + * 标记设备为在线状态 + * @param deviceId 设备ID + * @return 影响的行数 + */ + int markDeviceOnline(@Param("deviceId") String deviceId); + + /** + * 标记设备为离线状态 + * @param deviceId 设备ID + * @param reason 离线原因 + * @return 影响的行数 + */ + int markDeviceOffline(@Param("deviceId") String deviceId, + @Param("reason") String reason); + + /** + * 标记设备为故障状态 + * @param deviceId 设备ID + * @param faultType 故障类型 + * @param description 故障描述 + * @return 影响的行数 + */ + int markDeviceFault(@Param("deviceId") String deviceId, + @Param("faultType") String faultType, + @Param("description") String description); + + /** + * 批量更新设备状态 + * @param deviceIds 设备ID列表 + * @param status 目标状态 + * @param remark 状态变更备注 + * @return 影响的行数 + */ + int batchUpdateDeviceStatus(@Param("deviceIds") List deviceIds, + @Param("status") String status, + @Param("remark") String remark); + + // ========== 设备统计和查询操作 ========== + + /** + * 根据状态查询设备 + * @param status 设备状态(online/offline/fault/maintenance) + * @param areaId 区域ID(可选) + * @param deviceType 设备类型(water_maker/water_supply)(可选) + * @return 设备列表 + */ + List findByStatus(@Param("status") String status, + @Param("areaId") String areaId, + @Param("deviceType") String deviceType); + + /** + * 统计各状态设备数量 + * @param areaId 区域ID(可选) + * @param deviceType 设备类型(可选) + * @return 状态统计列表,每个元素包含status和count + */ + List> countByStatus(@Param("areaId") String areaId, + @Param("deviceType") String deviceType); + + /** + * 根据区域ID查询设备 + * @param areaId 区域ID + * @return 设备列表 + */ + List findByAreaId(@Param("areaId") String areaId); + + /** + * 根据设备类型查询设备 + * @param deviceType 设备类型(water_maker/water_supply) + * @return 设备列表 + */ + List findByDeviceType(@Param("deviceType") String deviceType); + + /** + * 根据安装位置模糊查询设备 + * @param location 安装位置关键词 + * @return 设备列表 + */ + List findByInstallLocationContaining(@Param("location") String location); + + /** + * 查询安装日期在指定范围内的设备 + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 设备列表 + */ + List findByInstallDateBetween(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + // ========== 设备监控相关操作 ========== + + /** + * 获取设备最后在线时间 + * @param deviceId 设备ID + * @return 设备信息(包含updated_time) + */ + Device getDeviceLastOnlineTime(@Param("deviceId") String deviceId); + + /** + * 查询离线时间超过阈值的设备 + * @param thresholdMinutes 离线阈值(分钟) + * @param areaId 区域ID(可选) + * @return 离线设备列表 + */ + List findOfflineDevicesExceedThreshold(@Param("thresholdMinutes") Integer thresholdMinutes, + @Param("areaId") String areaId); + + /** + * 更新设备最后通信时间 + * @param deviceId 设备ID + * @return 影响的行数 + */ + int updateLastCommunicationTime(@Param("deviceId") String deviceId); + + /** + * 查询需要维护的设备(根据维护计划) + * @param currentDate 当前日期 + * @return 需要维护的设备列表 + */ + List findDevicesNeedMaintenance(@Param("currentDate") LocalDate currentDate); + + // ========== 设备统计报表相关操作 ========== + + /** + * 统计各区域设备数量 + * @return 区域设备统计列表,每个元素包含areaId, areaName, deviceCount + */ + List> countDevicesByArea(); + + /** + * 统计各类型设备数量 + * @return 类型设备统计列表,每个元素包含deviceType, deviceCount + */ + List> countDevicesByType(); + + /** + * 统计设备在线率(按区域) + * @return 在线率统计列表,每个元素包含areaId, areaName, totalDevices, onlineDevices, onlineRate + */ + List> getOnlineRateByArea(); + + /** + * 查询设备运行时长统计 + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return 设备运行时长列表,每个元素包含deviceId, deviceName, totalOnlineHours + */ + List> getDeviceRuntimeStats(@Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + // ========== 批量操作 ========== + + /** + * 批量插入设备 + * @param devices 设备列表 + * @return 影响的行数 + */ + int batchInsert(@Param("devices") List devices); + + /** + * 批量更新设备信息 + * @param devices 设备列表 + * @return 影响的行数 + */ + int batchUpdate(@Param("devices") List devices); + + // ========== 设备关联查询 ========== + + /** + * 根据设备ID查询关联的终端设备 + * @param deviceId 设备ID + * @return 终端映射列表 + */ + List> findTerminalsByDeviceId(@Param("deviceId") String deviceId); + + /** + * 根据设备ID查询最近的告警记录 + * @param deviceId 设备ID + * @param limit 限制条数 + * @return 告警记录列表 + */ + List> findRecentAlertsByDeviceId(@Param("deviceId") String deviceId, + @Param("limit") Integer limit); + + /** + * 根据设备ID查询最近的维修记录 + * @param deviceId 设备ID + * @param limit 限制条数 + * @return 工单记录列表 + */ + List> findRecentWorkOrdersByDeviceId(@Param("deviceId") String deviceId, + @Param("limit") Integer limit); + + // ========== 设备高级搜索 ========== + + /** + * 多条件组合查询设备 + * @param deviceName 设备名称(可选) + * @param deviceType 设备类型(可选) + * @param status 设备状态(可选) + * @param areaId 区域ID(可选) + * @param startDate 安装开始日期(可选) + * @param endDate 安装结束日期(可选) + * @return 设备列表 + */ + List searchDevices(@Param("deviceName") String deviceName, + @Param("deviceType") String deviceType, + @Param("status") String status, + @Param("areaId") String areaId, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); + + // ========== 设备导出相关 ========== + + /** + * 获取设备导出数据(用于Excel导出) + * @param conditions 查询条件 + * @return 设备导出数据列表 + */ + List> getDeviceExportData(@Param("conditions") Map conditions); +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/DeviceMapper.xml b/src/main/java/com/campus/water/mapper/DeviceMapper.xml new file mode 100644 index 0000000..9591e40 --- /dev/null +++ b/src/main/java/com/campus/water/mapper/DeviceMapper.xml @@ -0,0 +1,383 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO device ( + device_id, device_name, device_type, area_id, + install_location, install_date, status, + create_time, remark + ) VALUES ( + #{deviceId}, #{deviceName}, #{deviceType}, #{areaId}, + #{installLocation}, #{installDate}, #{status}, + #{createTime}, #{remark} + ) + + + + UPDATE device + SET device_name = #{deviceName}, + device_type = #{deviceType}, + area_id = #{areaId}, + install_location = #{installLocation}, + install_date = #{installDate}, + status = #{status}, + updated_time = NOW(), + remark = #{remark} + WHERE device_id = #{deviceId} + + + + DELETE FROM device WHERE device_id = #{deviceId} + + + + + + UPDATE device + SET status = #{status}, + updated_time = NOW(), + remark = CONCAT(IFNULL(remark, ''), ';', #{remark}) + WHERE device_id = #{deviceId} + + + + UPDATE device + SET status = 'online', + updated_time = NOW(), + remark = CONCAT(IFNULL(remark, ''), ';标记为在线:', DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')) + WHERE device_id = #{deviceId} + + + + UPDATE device + SET status = 'offline', + updated_time = NOW(), + remark = CONCAT(IFNULL(remark, ''), ';标记为离线:', DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'), ';原因:', #{reason}) + WHERE device_id = #{deviceId} + + + + UPDATE device + SET status = 'fault', + updated_time = NOW(), + remark = CONCAT(IFNULL(remark, ''), ';标记为故障:', DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'), + ';故障类型:', #{faultType}, ';描述:', #{description}) + WHERE device_id = #{deviceId} + + + + UPDATE device + SET status = #{status}, + updated_time = NOW(), + remark = CONCAT(IFNULL(remark, ''), ';批量更新:', #{remark}, ';时间:', DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')) + WHERE device_id IN + + #{deviceId} + + + + + + + + + + + + + + + + + + + + + + + + + UPDATE device + SET updated_time = NOW(), + status = 'online' + WHERE device_id = #{deviceId} + + + + + + + + + + + + + + + + + + INSERT INTO device ( + device_id, device_name, device_type, area_id, + install_location, install_date, status, + create_time, remark + ) VALUES + + ( + #{device.deviceId}, #{device.deviceName}, #{device.deviceType}, #{device.areaId}, + #{device.installLocation}, #{device.installDate}, #{device.status}, + #{device.createTime}, #{device.remark} + ) + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/StatisticsMapper.java b/src/main/java/com/campus/water/mapper/StatisticsMapper.java new file mode 100644 index 0000000..e00caca --- /dev/null +++ b/src/main/java/com/campus/water/mapper/StatisticsMapper.java @@ -0,0 +1,102 @@ +/** + * 统计数据处理映射接口(MyBatis Mapper) + * 功能:定义统计数据查询的数据库操作方法 + * 用途:与数据库交互,执行复杂的统计聚合查询 + * 核心方法: + * - 按设备/区域/时间维度统计用水量 + * - 按设备/区域统计告警次数 + * - 获取设备状态统计和告警处理统计 + * 备注:对应StatisticsMapper.xml中的SQL映射 + */ +package com.campus.water.mapper; + +import com.campus.water.entity.dto.request.StatisticsQueryRequest; +import com.campus.water.entity.vo.StatisticsVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Mapper +public interface StatisticsMapper { + + // 按设备统计用水量 + List> statWaterUsageByDevice( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("areaId") String areaId, + @Param("limit") Integer limit); + + // 按区域统计用水量 + List> statWaterUsageByArea( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("deviceType") String deviceType, + @Param("limit") Integer limit); + + // 按时间段统计用水量(日/周/月) + List> statWaterUsageByTime( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("period") String period, // day/week/month + @Param("deviceId") String deviceId, + @Param("areaId") String areaId); + + // 按设备统计告警次数 + List> statAlarmCountByDevice( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("areaId") String areaId, + @Param("alarmLevel") String alarmLevel, + @Param("limit") Integer limit); + + // 按区域统计告警次数 + List> statAlarmCountByArea( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("deviceType") String deviceType, + @Param("limit") Integer limit); + + // 按时间段统计告警趋势 + List> statAlarmTrendByTime( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("period") String period, + @Param("deviceId") String deviceId, + @Param("areaId") String areaId); + + // 统计设备使用情况(使用次数、总用水量) + List> statDeviceUsage( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("areaId") String areaId, + @Param("deviceType") String deviceType, + @Param("limit") Integer limit); + + // 统计终端使用情况 + List> statTerminalUsage( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("areaId") String areaId, + @Param("limit") Integer limit); + + // 统计水质达标率 + List> statWaterQualityRate( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("deviceId") String deviceId, + @Param("areaId") String areaId); + + // 获取设备状态统计 + Map getDeviceStatusStatistics( + @Param("areaId") String areaId, + @Param("deviceType") String deviceType); + + // 获取告警处理统计 + Map getAlarmHandleStatistics( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("areaId") String areaId); +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/mapper/StatisticsMapper.xml b/src/main/java/com/campus/water/mapper/StatisticsMapper.xml new file mode 100644 index 0000000..4883759 --- /dev/null +++ b/src/main/java/com/campus/water/mapper/StatisticsMapper.xml @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/AlertService.java b/src/main/java/com/campus/water/service/AlertService.java new file mode 100644 index 0000000..7f8995a --- /dev/null +++ b/src/main/java/com/campus/water/service/AlertService.java @@ -0,0 +1,828 @@ +package com.campus.water.service; + +import com.campus.water.entity.Alert; +import com.campus.water.entity.WorkOrder; +import com.campus.water.entity.Device; +import com.campus.water.entity.dto.request.AlarmStatisticsRequest; +import com.campus.water.entity.vo.AlarmStatisticsVO; +import com.campus.water.mapper.AlertRepository; +import com.campus.water.mapper.WorkOrderRepository; +import com.campus.water.mapper.DeviceRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import jakarta.persistence.criteria.Predicate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 告警服务层 + * 功能:处理告警相关的业务逻辑,包括告警创建、查询、统计、处理等 + * 用途: + * 1. 告警生命周期管理(创建、处理、关闭) + * 2. 告警统计和分析 + * 3. 告警与工单关联管理 + * 4. 告警通知和推送 + * 核心方法: + * - 告警创建:自动触发告警、手动创建告警 + * - 告警处理:更新告警状态、指派处理人 + * - 告警统计:多维度的告警数据分析 + * - 告警查询:支持多条件筛选和分页 + * 技术:Spring Data JPA、事务管理、复杂查询构建 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class AlertService { + + private final AlertRepository alertRepository; + private final WorkOrderRepository workOrderRepository; + private final DeviceRepository deviceRepository; + + // ========== 告警创建相关方法 ========== + + /** + * 自动创建告警(从传感器数据触发) + * @param deviceId 设备ID + * @param alertType 告警类型 + * @param alertLevel 告警级别 + * @param message 告警信息 + * @param areaId 区域ID + * @return 创建的告警对象 + */ + @Transactional + public Alert createAutoAlert(String deviceId, String alertType, + Alert.AlertLevel alertLevel, String message, + String areaId) { + try { + // 检查是否存在重复未处理告警 + if (hasDuplicatePendingAlert(deviceId, alertType)) { + log.info("存在重复未处理告警,跳过创建: deviceId={}, alertType={}", deviceId, alertType); + return null; + } + + Alert alert = new Alert(); + alert.setDeviceId(deviceId); + alert.setAlertType(alertType); + alert.setAlertLevel(alertLevel); + alert.setAlertMessage(message); + alert.setAreaId(areaId); + alert.setStatus(Alert.AlertStatus.pending); + alert.setTimestamp(LocalDateTime.now()); + alert.setCreatedTime(LocalDateTime.now()); + + Alert savedAlert = alertRepository.save(alert); + log.info("自动告警创建成功: alertId={}, deviceId={}, type={}, level={}", + savedAlert.getAlertId(), deviceId, alertType, alertLevel); + + // 根据告警类型自动创建工单 + if (shouldCreateWorkOrder(alertType, alertLevel)) { + createWorkOrderForAlert(savedAlert); + } + + return savedAlert; + } catch (Exception e) { + log.error("自动告警创建失败: deviceId={}, error={}", deviceId, e.getMessage(), e); + throw new RuntimeException("告警创建失败: " + e.getMessage()); + } + } + + /** + * 手动创建告警(由管理员或维修人员创建) + * @param deviceId 设备ID + * @param alertType 告警类型 + * @param alertLevel 告警级别字符串 + * @param message 告警信息 + * @param areaId 区域ID + * @return 创建的告警对象 + */ + @Transactional + public Alert createManualAlert(String deviceId, String alertType, + String alertLevel, String message, + String areaId) { + try { + Alert.AlertLevel level; + try { + level = Alert.AlertLevel.valueOf(alertLevel.toLowerCase()); + } catch (IllegalArgumentException e) { + level = Alert.AlertLevel.warning; // 默认级别 + } + + Alert alert = new Alert(); + alert.setDeviceId(deviceId); + alert.setAlertType(alertType); + alert.setAlertLevel(level); + alert.setAlertMessage(message); + alert.setAreaId(areaId); + alert.setStatus(Alert.AlertStatus.pending); + alert.setTimestamp(LocalDateTime.now()); + alert.setCreatedTime(LocalDateTime.now()); + + Alert savedAlert = alertRepository.save(alert); + log.info("手动告警创建成功: alertId={}, deviceId={}, type={}, level={}", + savedAlert.getAlertId(), deviceId, alertType, alertLevel); + + // 手动创建的告警默认创建工单 + createWorkOrderForAlert(savedAlert); + + return savedAlert; + } catch (Exception e) { + log.error("手动告警创建失败: deviceId={}, error={}", deviceId, e.getMessage(), e); + throw new RuntimeException("告警创建失败: " + e.getMessage()); + } + } + + /** + * 批量创建告警(用于批量导入或同步) + * @param alerts 告警列表 + * @return 成功创建的告警列表 + */ + @Transactional + public List batchCreateAlerts(List alerts) { + try { + List savedAlerts = alertRepository.saveAll(alerts); + log.info("批量创建告警成功: count={}", savedAlerts.size()); + + // 为每个告警创建工单 + for (Alert alert : savedAlerts) { + if (shouldCreateWorkOrder(alert.getAlertType(), alert.getAlertLevel())) { + createWorkOrderForAlert(alert); + } + } + + return savedAlerts; + } catch (Exception e) { + log.error("批量创建告警失败: error={}", e.getMessage(), e); + throw new RuntimeException("批量创建告警失败: " + e.getMessage()); + } + } + + // ========== 告警处理相关方法 ========== + + /** + * 处理告警(开始处理) + * @param alertId 告警ID + * @param repairmanId 维修人员ID + * @return 是否处理成功 + */ + @Transactional + public boolean processAlert(Long alertId, String repairmanId) { + try { + Optional alertOpt = alertRepository.findById(alertId); + if (alertOpt.isEmpty()) { + log.warn("告警不存在: alertId={}", alertId); + return false; + } + + Alert alert = alertOpt.get(); + if (alert.getStatus() != Alert.AlertStatus.pending) { + log.warn("告警状态不允许处理: alertId={}, currentStatus={}", alertId, alert.getStatus()); + return false; + } + + alert.setStatus(Alert.AlertStatus.processing); + alert.setResolvedBy(repairmanId); + alert.setUpdatedTime(LocalDateTime.now()); + + alertRepository.save(alert); + log.info("告警开始处理: alertId={}, repairmanId={}", alertId, repairmanId); + return true; + } catch (Exception e) { + log.error("处理告警失败: alertId={}, error={}", alertId, e.getMessage(), e); + throw new RuntimeException("处理告警失败: " + e.getMessage()); + } + } + + /** + * 解决告警(完成处理) + * @param alertId 告警ID + * @param resolvedBy 解决人 + * @param resolutionNotes 解决说明 + * @return 是否解决成功 + */ + @Transactional + public boolean resolveAlert(Long alertId, String resolvedBy, String resolutionNotes) { + try { + Optional alertOpt = alertRepository.findById(alertId); + if (alertOpt.isEmpty()) { + log.warn("告警不存在: alertId={}", alertId); + return false; + } + + Alert alert = alertOpt.get(); + if (alert.getStatus() != Alert.AlertStatus.processing && + alert.getStatus() != Alert.AlertStatus.pending) { + log.warn("告警状态不允许解决: alertId={}, currentStatus={}", alertId, alert.getStatus()); + return false; + } + + alert.setStatus(Alert.AlertStatus.resolved); + alert.setResolvedBy(resolvedBy); + alert.setResolvedTime(LocalDateTime.now()); + alert.setAlertMessage(alert.getAlertMessage() + " [解决方案: " + resolutionNotes + "]"); + alert.setUpdatedTime(LocalDateTime.now()); + + alertRepository.save(alert); + log.info("告警已解决: alertId={}, resolvedBy={}", alertId, resolvedBy); + return true; + } catch (Exception e) { + log.error("解决告警失败: alertId={}, error={}", alertId, e.getMessage(), e); + throw new RuntimeException("解决告警失败: " + e.getMessage()); + } + } + + /** + * 关闭告警(无需处理或误报) + * @param alertId 告警ID + * @param closedBy 关闭人 + * @param closeReason 关闭原因 + * @return 是否关闭成功 + */ + @Transactional + public boolean closeAlert(Long alertId, String closedBy, String closeReason) { + try { + Optional alertOpt = alertRepository.findById(alertId); + if (alertOpt.isEmpty()) { + log.warn("告警不存在: alertId={}", alertId); + return false; + } + + Alert alert = alertOpt.get(); + alert.setStatus(Alert.AlertStatus.closed); + alert.setResolvedBy(closedBy); + alert.setResolvedTime(LocalDateTime.now()); + alert.setAlertMessage(alert.getAlertMessage() + " [关闭原因: " + closeReason + "]"); + alert.setUpdatedTime(LocalDateTime.now()); + + alertRepository.save(alert); + log.info("告警已关闭: alertId={}, closedBy={}, reason={}", alertId, closedBy, closeReason); + return true; + } catch (Exception e) { + log.error("关闭告警失败: alertId={}, error={}", alertId, e.getMessage(), e); + throw new RuntimeException("关闭告警失败: " + e.getMessage()); + } + } + + /** + * 批量更新告警状态 + * @param alertIds 告警ID列表 + * @param status 目标状态 + * @param updatedBy 更新人 + * @return 成功更新的数量 + */ + @Transactional + public int batchUpdateAlertStatus(List alertIds, Alert.AlertStatus status, String updatedBy) { + try { + List alerts = alertRepository.findAllById(alertIds); + for (Alert alert : alerts) { + alert.setStatus(status); + alert.setResolvedBy(updatedBy); + if (status == Alert.AlertStatus.resolved || status == Alert.AlertStatus.closed) { + alert.setResolvedTime(LocalDateTime.now()); + } + alert.setUpdatedTime(LocalDateTime.now()); + } + + alertRepository.saveAll(alerts); + log.info("批量更新告警状态: count={}, status={}, updatedBy={}", + alerts.size(), status, updatedBy); + return alerts.size(); + } catch (Exception e) { + log.error("批量更新告警状态失败: error={}", e.getMessage(), e); + throw new RuntimeException("批量更新告警状态失败: " + e.getMessage()); + } + } + + // ========== 告警查询相关方法 ========== + + /** + * 根据ID查询告警 + * @param alertId 告警ID + * @return 告警对象(Optional) + */ + @Transactional(readOnly = true) + public Optional findById(Long alertId) { + return alertRepository.findById(alertId); + } + + /** + * 多条件分页查询告警 + * @param deviceId 设备ID(可选) + * @param alertType 告警类型(可选) + * @param alertLevel 告警级别(可选) + * @param status 告警状态(可选) + * @param areaId 区域ID(可选) + * @param startTime 开始时间(可选) + * @param endTime 结束时间(可选) + * @param pageable 分页参数 + * @return 分页告警列表 + */ + @Transactional(readOnly = true) + public Page findAlertsByConditions(String deviceId, String alertType, + Alert.AlertLevel alertLevel, Alert.AlertStatus status, + String areaId, LocalDateTime startTime, + LocalDateTime endTime, Pageable pageable) { + Specification spec = (root, query, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + + if (StringUtils.hasText(deviceId)) { + predicates.add(criteriaBuilder.equal(root.get("deviceId"), deviceId)); + } + + if (StringUtils.hasText(alertType)) { + predicates.add(criteriaBuilder.equal(root.get("alertType"), alertType)); + } + + if (alertLevel != null) { + predicates.add(criteriaBuilder.equal(root.get("alertLevel"), alertLevel)); + } + + if (status != null) { + predicates.add(criteriaBuilder.equal(root.get("status"), status)); + } + + if (StringUtils.hasText(areaId)) { + predicates.add(criteriaBuilder.equal(root.get("areaId"), areaId)); + } + + if (startTime != null) { + predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("timestamp"), startTime)); + } + + if (endTime != null) { + predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("timestamp"), endTime)); + } + + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + + return alertRepository.findAll(spec, pageable); + } + + /** + * 查询设备的最新告警 + * @param deviceId 设备ID + * @param limit 限制条数 + * @return 告警列表 + */ + @Transactional(readOnly = true) + public List findRecentAlertsByDeviceId(String deviceId, int limit) { + return alertRepository.findByDeviceIdOrderByTimestampDesc(deviceId) + .stream() + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * 查询未处理告警 + * @param areaId 区域ID(可选) + * @return 未处理告警列表 + */ + @Transactional(readOnly = true) + public List findPendingAlerts(String areaId) { + if (StringUtils.hasText(areaId)) { + return alertRepository.findByStatusAndAreaId(Alert.AlertStatus.pending, areaId); + } else { + return alertRepository.findByStatus(Alert.AlertStatus.pending); + } + } + + /** + * 查询正在处理的告警 + * @param repairmanId 维修人员ID(可选) + * @return 正在处理的告警列表 + */ + @Transactional(readOnly = true) + public List findProcessingAlerts(String repairmanId) { + if (StringUtils.hasText(repairmanId)) { + return alertRepository.findByStatusAndResolvedBy(Alert.AlertStatus.processing, repairmanId); + } else { + return alertRepository.findByStatus(Alert.AlertStatus.processing); + } + } + + // ========== 告警统计相关方法 ========== + + /** + * 获取告警统计概览 + * @param areaId 区域ID(可选) + * @param startTime 开始时间(可选) + * @param endTime 结束时间(可选) + * @return 告警统计概览 + */ + @Transactional(readOnly = true) + public Map getAlertOverview(String areaId, LocalDateTime startTime, LocalDateTime endTime) { + Map overview = new HashMap<>(); + + // 获取统计时间段内的告警 + List alerts; + if (startTime != null && endTime != null) { + alerts = alertRepository.findByTimestampBetween(startTime, endTime); + } else if (startTime != null) { + alerts = alertRepository.findByTimestampAfter(startTime); + } else { + alerts = alertRepository.findAll(); + } + + // 按区域过滤 + if (StringUtils.hasText(areaId)) { + alerts = alerts.stream() + .filter(alert -> areaId.equals(alert.getAreaId())) + .collect(Collectors.toList()); + } + + // 计算统计指标 + long totalAlerts = alerts.size(); + long pendingAlerts = alerts.stream() + .filter(a -> a.getStatus() == Alert.AlertStatus.pending) + .count(); + long processingAlerts = alerts.stream() + .filter(a -> a.getStatus() == Alert.AlertStatus.processing) + .count(); + long resolvedAlerts = alerts.stream() + .filter(a -> a.getStatus() == Alert.AlertStatus.resolved) + .count(); + + // 计算平均响应时间(小时) + double avgResponseHours = alerts.stream() + .filter(a -> a.getResolvedTime() != null && a.getTimestamp() != null) + .mapToDouble(a -> ChronoUnit.HOURS.between(a.getTimestamp(), a.getResolvedTime())) + .average() + .orElse(0.0); + + // 按级别统计 + Map levelCount = alerts.stream() + .collect(Collectors.groupingBy(Alert::getAlertLevel, Collectors.counting())); + + // 按类型统计 + Map typeCount = alerts.stream() + .collect(Collectors.groupingBy(Alert::getAlertType, Collectors.counting())); + + overview.put("totalAlerts", totalAlerts); + overview.put("pendingAlerts", pendingAlerts); + overview.put("processingAlerts", processingAlerts); + overview.put("resolvedAlerts", resolvedAlerts); + overview.put("resolvedRate", totalAlerts > 0 ? (double) resolvedAlerts / totalAlerts * 100 : 0); + overview.put("avgResponseHours", avgResponseHours); + overview.put("alertLevelCount", levelCount); + overview.put("alertTypeCount", typeCount); + + return overview; + } + + /** + * 获取告警趋势统计(按时间分组) + * @param period 统计周期(day/week/month) + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param areaId 区域ID(可选) + * @return 时间序列告警数据 + */ + @Transactional(readOnly = true) + public List> getAlertTrend(String period, + LocalDateTime startTime, + LocalDateTime endTime, + String areaId) { + List alerts = alertRepository.findByTimestampBetween(startTime, endTime); + + if (StringUtils.hasText(areaId)) { + alerts = alerts.stream() + .filter(alert -> areaId.equals(alert.getAreaId())) + .collect(Collectors.toList()); + } + + // 按时间分组 + Map> groupedAlerts = new TreeMap<>(); + + for (Alert alert : alerts) { + String timeKey = formatTimeKey(alert.getTimestamp(), period); + groupedAlerts.computeIfAbsent(timeKey, k -> new ArrayList<>()).add(alert); + } + + // 构建结果 + List> result = new ArrayList<>(); + for (Map.Entry> entry : groupedAlerts.entrySet()) { + Map item = new HashMap<>(); + item.put("timeLabel", entry.getKey()); + item.put("totalAlerts", entry.getValue().size()); + + // 按级别统计 + Map levelCount = entry.getValue().stream() + .collect(Collectors.groupingBy(Alert::getAlertLevel, Collectors.counting())); + item.put("alertLevelCount", levelCount); + + result.add(item); + } + + return result; + } + + /** + * 获取设备告警排名 + * @param topN 前N名 + * @param startTime 开始时间(可选) + * @param endTime 结束时间(可选) + * @return 设备告警排名 + */ + @Transactional(readOnly = true) + public List> getDeviceAlertRanking(int topN, + LocalDateTime startTime, + LocalDateTime endTime) { + List alerts; + if (startTime != null && endTime != null) { + alerts = alertRepository.findByTimestampBetween(startTime, endTime); + } else { + alerts = alertRepository.findAll(); + } + + // 按设备分组统计 + Map> deviceAlerts = alerts.stream() + .collect(Collectors.groupingBy(Alert::getDeviceId)); + + // 获取设备信息 + List> ranking = new ArrayList<>(); + for (Map.Entry> entry : deviceAlerts.entrySet()) { + String deviceId = entry.getKey(); + List deviceAlertList = entry.getValue(); + + Optional deviceOpt = deviceRepository.findById(deviceId); + String deviceName = deviceOpt.map(Device::getDeviceName).orElse("未知设备"); + + Map item = new HashMap<>(); + item.put("deviceId", deviceId); + item.put("deviceName", deviceName); + item.put("totalAlerts", deviceAlertList.size()); + item.put("pendingAlerts", deviceAlertList.stream() + .filter(a -> a.getStatus() == Alert.AlertStatus.pending) + .count()); + item.put("resolvedAlerts", deviceAlertList.stream() + .filter(a -> a.getStatus() == Alert.AlertStatus.resolved) + .count()); + + // 计算最常见的告警类型 + String mostCommonType = deviceAlertList.stream() + .collect(Collectors.groupingBy(Alert::getAlertType, Collectors.counting())) + .entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse("未知"); + item.put("mostCommonAlertType", mostCommonType); + + ranking.add(item); + } + + // 按告警总数排序并限制数量 + return ranking.stream() + .sorted((a, b) -> Integer.compare( + (Integer) b.get("totalAlerts"), + (Integer) a.get("totalAlerts"))) + .limit(topN) + .collect(Collectors.toList()); + } + + // ========== 告警分析相关方法 ========== + + /** + * 分析告警模式(发现频繁出现的告警组合) + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param areaId 区域ID(可选) + * @return 告警模式分析结果 + */ + @Transactional(readOnly = true) + public List> analyzeAlertPatterns(LocalDateTime startTime, + LocalDateTime endTime, + String areaId) { + List alerts = alertRepository.findByTimestampBetween(startTime, endTime); + + if (StringUtils.hasText(areaId)) { + alerts = alerts.stream() + .filter(alert -> areaId.equals(alert.getAreaId())) + .collect(Collectors.toList()); + } + + // 按设备和时间窗口分组 + Map>> deviceTimeAlerts = new HashMap<>(); + + for (Alert alert : alerts) { + String deviceId = alert.getDeviceId(); + String timeWindow = formatTimeWindow(alert.getTimestamp()); + + deviceTimeAlerts.computeIfAbsent(deviceId, k -> new HashMap<>()) + .computeIfAbsent(timeWindow, k -> new ArrayList<>()) + .add(alert); + } + + // 分析同一时间窗口内的告警组合 + List> patterns = new ArrayList<>(); + for (Map> timeAlerts : deviceTimeAlerts.values()) { + for (List windowAlerts : timeAlerts.values()) { + if (windowAlerts.size() >= 2) { + // 发现多个告警同时出现 + Set alertTypes = windowAlerts.stream() + .map(Alert::getAlertType) + .collect(Collectors.toSet()); + + if (alertTypes.size() > 1) { + Map pattern = new HashMap<>(); + pattern.put("alertTypes", alertTypes); + pattern.put("count", windowAlerts.size()); + pattern.put("deviceIds", deviceTimeAlerts.keySet()); + pattern.put("timestamp", windowAlerts.get(0).getTimestamp()); + patterns.add(pattern); + } + } + } + } + + return patterns; + } + + /** + * 计算告警预测指标 + * @param deviceId 设备ID + * @return 告警预测结果 + */ + @Transactional(readOnly = true) + public Map predictAlertRisk(String deviceId) { + Optional deviceOpt = deviceRepository.findById(deviceId); + if (deviceOpt.isEmpty()) { + return Collections.emptyMap(); + } + + Device device = deviceOpt.get(); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime oneMonthAgo = now.minusMonths(1); + + // 获取最近一个月的告警 + List recentAlerts = alertRepository.findByDeviceIdAndTimestampAfter(deviceId, oneMonthAgo); + + Map prediction = new HashMap<>(); + prediction.put("deviceId", deviceId); + prediction.put("deviceName", device.getDeviceName()); + prediction.put("deviceStatus", device.getStatus()); + + // 计算告警频率 + long alertCount = recentAlerts.size(); + double alertsPerDay = alertCount / 30.0; + prediction.put("alertFrequency", alertsPerDay); + + // 风险等级评估 + String riskLevel; + if (alertsPerDay >= 1.0) { + riskLevel = "高风险"; + } else if (alertsPerDay >= 0.5) { + riskLevel = "中风险"; + } else if (alertsPerDay >= 0.1) { + riskLevel = "低风险"; + } else { + riskLevel = "无风险"; + } + prediction.put("riskLevel", riskLevel); + + // 最常见的告警类型 + String mostCommonType = recentAlerts.stream() + .collect(Collectors.groupingBy(Alert::getAlertType, Collectors.counting())) + .entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse("无"); + prediction.put("mostCommonAlertType", mostCommonType); + + // 平均解决时间 + double avgResolveHours = recentAlerts.stream() + .filter(a -> a.getResolvedTime() != null) + .mapToDouble(a -> ChronoUnit.HOURS.between(a.getTimestamp(), a.getResolvedTime())) + .average() + .orElse(0.0); + prediction.put("avgResolveHours", avgResolveHours); + + return prediction; + } + + // ========== 私有辅助方法 ========== + + /** + * 检查是否存在重复的未处理告警 + */ + private boolean hasDuplicatePendingAlert(String deviceId, String alertType) { + List pendingAlerts = alertRepository.findByDeviceIdAndAlertTypeAndStatus( + deviceId, alertType, Alert.AlertStatus.pending); + + if (!pendingAlerts.isEmpty()) { + // 检查是否有30分钟内的重复告警 + LocalDateTime thirtyMinutesAgo = LocalDateTime.now().minusMinutes(30); + return pendingAlerts.stream() + .anyMatch(alert -> alert.getTimestamp().isAfter(thirtyMinutesAgo)); + } + + return false; + } + + /** + * 判断是否需要创建工单 + */ + private boolean shouldCreateWorkOrder(String alertType, Alert.AlertLevel alertLevel) { + // 严重级别以上的告警都需要创建工单 + if (alertLevel == Alert.AlertLevel.critical || alertLevel == Alert.AlertLevel.error) { + return true; + } + + // 特定类型的告警需要工单 + Set workOrderRequiredTypes = Set.of( + "WATER_MAKER_FAULT", + "WATER_SUPPLY_FAULT", + "DEVICE_FAULT", + "SAFETY_ALERT" + ); + + return workOrderRequiredTypes.contains(alertType); + } + + /** + * 为告警创建工单 + */ + private void createWorkOrderForAlert(Alert alert) { + try { + WorkOrder workOrder = new WorkOrder(); + workOrder.setOrderId(generateOrderId()); + workOrder.setAlertId(alert.getAlertId()); + workOrder.setDeviceId(alert.getDeviceId()); + workOrder.setAreaId(alert.getAreaId()); + workOrder.setOrderType(WorkOrder.OrderType.repair); + workOrder.setDescription("告警处理工单: " + alert.getAlertMessage()); + workOrder.setPriority(convertAlertLevelToPriority(alert.getAlertLevel())); + workOrder.setStatus(WorkOrder.OrderStatus.pending); + workOrder.setCreatedTime(LocalDateTime.now()); + + workOrderRepository.save(workOrder); + log.info("为告警创建工单成功: alertId={}, orderId={}", + alert.getAlertId(), workOrder.getOrderId()); + } catch (Exception e) { + log.error("为告警创建工单失败: alertId={}, error={}", + alert.getAlertId(), e.getMessage(), e); + } + } + + /** + * 生成工单ID + */ + private String generateOrderId() { + return String.format("WO%s%03d", + System.currentTimeMillis(), + (int)(Math.random() * 1000)); + } + + /** + * 将告警级别转换为工单优先级 + */ + private WorkOrder.OrderPriority convertAlertLevelToPriority(Alert.AlertLevel alertLevel) { + switch (alertLevel) { + case critical: + return WorkOrder.OrderPriority.urgent; + case error: + return WorkOrder.OrderPriority.high; + case warning: + return WorkOrder.OrderPriority.medium; + default: + return WorkOrder.OrderPriority.low; + } + } + + /** + * 格式化时间键(用于分组) + */ + private String formatTimeKey(LocalDateTime time, String period) { + switch (period) { + case "day": + return time.toLocalDate().toString(); + case "week": + return String.format("%d-W%d", + time.getYear(), + time.get(java.time.temporal.WeekFields.ISO.weekOfYear())); + case "month": + return String.format("%d-%02d", + time.getYear(), time.getMonthValue()); + default: + return time.toLocalDate().toString(); + } + } + + /** + * 格式化时间窗口(用于模式分析) + */ + private String formatTimeWindow(LocalDateTime time) { + // 按小时窗口分组 + return String.format("%s-%02d", + time.toLocalDate().toString(), + time.getHour()); + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/DeviceService.java b/src/main/java/com/campus/water/service/DeviceService.java new file mode 100644 index 0000000..2fc2e74 --- /dev/null +++ b/src/main/java/com/campus/water/service/DeviceService.java @@ -0,0 +1,219 @@ +/** + * 设备状态管理业务服务层 + * 功能:处理设备状态相关的业务逻辑 + * 用途:为设备状态控制器提供业务处理服务 + * 核心方法: + * - 状态更新:单设备状态变更 + * - 状态标记:在线/离线/故障标记 + * - 批量操作:批量状态更新 + * - 自动检测:定时检测离线设备 + * - 故障告警:设备故障时自动创建告警 + * 业务逻辑:状态验证、事务管理、日志记录 + */ +package com.campus.water.service; + +import com.campus.water.entity.Device; +import com.campus.water.entity.dto.request.DeviceStatusUpdateRequest; +import com.campus.water.mapper.DeviceMapper; +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; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DeviceService { + + private final DeviceMapper deviceMapper; + private final AlertService alertService; + + /** + * 更新设备状态 + */ + @Transactional + public boolean updateDeviceStatus(DeviceStatusUpdateRequest request) { + try { + int rows = deviceMapper.updateDeviceStatus( + request.getDeviceId(), + request.getStatus(), + request.getRemark() + ); + + if (rows > 0) { + log.info("设备状态更新成功: deviceId={}, status={}", + request.getDeviceId(), request.getStatus()); + + // 如果是故障状态,自动创建告警 + if ("fault".equals(request.getStatus())) { + createFaultAlert(request.getDeviceId(), request.getRemark()); + } + + return true; + } + return false; + } catch (Exception e) { + log.error("设备状态更新失败: deviceId={}, error={}", + request.getDeviceId(), e.getMessage(), e); + throw new RuntimeException("设备状态更新失败: " + e.getMessage()); + } + } + + /** + * 标记设备在线 + */ + @Transactional + public boolean markDeviceOnline(String deviceId) { + try { + int rows = deviceMapper.markDeviceOnline(deviceId); + if (rows > 0) { + log.info("设备标记为在线: deviceId={}", deviceId); + return true; + } + return false; + } catch (Exception e) { + log.error("标记设备在线失败: deviceId={}, error={}", deviceId, e.getMessage()); + throw new RuntimeException("标记设备在线失败: " + e.getMessage()); + } + } + + /** + * 标记设备离线 + */ + @Transactional + public boolean markDeviceOffline(String deviceId, String reason) { + try { + int rows = deviceMapper.markDeviceOffline(deviceId, reason); + if (rows > 0) { + log.info("设备标记为离线: deviceId={}, reason={}", deviceId, reason); + return true; + } + return false; + } catch (Exception e) { + log.error("标记设备离线失败: deviceId={}, error={}", deviceId, e.getMessage()); + throw new RuntimeException("标记设备离线失败: " + e.getMessage()); + } + } + + /** + * 标记设备故障 + */ + @Transactional + public boolean markDeviceFault(String deviceId, String faultType, String description) { + try { + int rows = deviceMapper.markDeviceFault(deviceId, faultType, description); + if (rows > 0) { + log.info("设备标记为故障: deviceId={}, type={}, desc={}", + deviceId, faultType, description); + + // 创建故障告警 + createFaultAlert(deviceId, String.format("故障类型: %s, 描述: %s", faultType, description)); + + return true; + } + return false; + } catch (Exception e) { + log.error("标记设备故障失败: deviceId={}, error={}", deviceId, e.getMessage()); + throw new RuntimeException("标记设备故障失败: " + e.getMessage()); + } + } + + /** + * 批量更新设备状态 + */ + @Transactional + public boolean batchUpdateDeviceStatus(List deviceIds, String status, String remark) { + try { + if (deviceIds == null || deviceIds.isEmpty()) { + return false; + } + + int rows = deviceMapper.batchUpdateDeviceStatus(deviceIds, status, remark); + log.info("批量更新设备状态: count={}, status={}, updated={}", + deviceIds.size(), status, rows); + + // 如果是故障状态,为每个设备创建告警 + if ("fault".equals(status)) { + for (String deviceId : deviceIds) { + createFaultAlert(deviceId, remark); + } + } + + return rows > 0; + } catch (Exception e) { + log.error("批量更新设备状态失败: error={}", e.getMessage(), e); + throw new RuntimeException("批量更新设备状态失败: " + e.getMessage()); + } + } + + /** + * 获取设备状态统计 + */ + @Transactional(readOnly = true) + public Map getDeviceStatusCount(String areaId, String deviceType) { + return deviceMapper.countByStatus(areaId, deviceType); + } + + /** + * 查询离线设备(超过阈值) + */ + @Transactional(readOnly = true) + public List getOfflineDevicesExceedThreshold(Integer thresholdMinutes, String areaId) { + return deviceMapper.findOfflineDevicesExceedThreshold(thresholdMinutes, areaId); + } + + /** + * 获取设备最后在线时间 + */ + @Transactional(readOnly = true) + public LocalDateTime getDeviceLastOnlineTime(String deviceId) { + Device device = deviceMapper.getDeviceLastOnlineTime(deviceId); + return device != null ? device.getUpdatedTime() : null; + } + + /** + * 根据状态查询设备 + */ + @Transactional(readOnly = true) + public List getDevicesByStatus(String status, String areaId, String deviceType) { + return deviceMapper.findByStatus(status, areaId, deviceType); + } + + /** + * 自动检测并标记离线设备 + */ + @Transactional + public void autoDetectOfflineDevices(Integer thresholdMinutes) { + List offlineDevices = getOfflineDevicesExceedThreshold(thresholdMinutes, null); + + for (Device device : offlineDevices) { + markDeviceOffline(device.getDeviceId(), + String.format("自动检测离线,超过%d分钟无数据", thresholdMinutes)); + } + + if (!offlineDevices.isEmpty()) { + log.warn("自动标记离线设备完成: count={}", offlineDevices.size()); + } + } + + /** + * 创建故障告警 + */ + private void createFaultAlert(String deviceId, String description) { + try { + alertService.createManualAlert( + deviceId, + "DEVICE_FAULT", + "设备故障", + String.format("设备故障告警 - 设备ID: %s, 描述: %s", deviceId, description), + "fault" + ); + } catch (Exception e) { + log.error("创建故障告警失败: deviceId={}, error={}", deviceId, e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/service/StatisticsService.java b/src/main/java/com/campus/water/service/StatisticsService.java new file mode 100644 index 0000000..d5e31e8 --- /dev/null +++ b/src/main/java/com/campus/water/service/StatisticsService.java @@ -0,0 +1,247 @@ +/** + * 统计业务逻辑服务层 + * 功能:处理统计数据的业务逻辑,包括数据转换、计算、聚合 + * 用途:为控制器提供统计数据处理服务 + * 核心方法: + * - getWaterUsageStatistics(): 用水量统计逻辑处理 + * - getAlarmStatistics(): 告警统计逻辑处理 + * - getDeviceStatusStatistics(): 设备状态统计 + * - getDashboardStatistics(): 仪表板综合数据 + * 业务逻辑:数据验证、结果格式化、异常处理 + */ +package com.campus.water.service; + +import com.campus.water.entity.dto.request.StatisticsQueryRequest; +import com.campus.water.entity.vo.AlarmStatisticsVO; +import com.campus.water.entity.vo.StatisticsVO; +import com.campus.water.mapper.StatisticsMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class StatisticsService { + + private final StatisticsMapper statisticsMapper; + private final DeviceService deviceService; + + /** + * 获取用水量统计 + */ + @Transactional(readOnly = true) + public StatisticsVO getWaterUsageStatistics(StatisticsQueryRequest request) { + StatisticsVO result = new StatisticsVO(); + result.setPeriod(request.getPeriod()); + result.setStartDate(request.getStartDate()); + result.setEndDate(request.getEndDate()); + + List> data; + + switch (request.getStatType()) { + case "by_device": + data = statisticsMapper.statWaterUsageByDevice( + request.getStartDate(), request.getEndDate(), + request.getAreaId(), request.getLimit()); + break; + case "by_area": + data = statisticsMapper.statWaterUsageByArea( + request.getStartDate(), request.getEndDate(), + request.getDeviceType(), request.getLimit()); + break; + case "by_time": + data = statisticsMapper.statWaterUsageByTime( + request.getStartDate(), request.getEndDate(), + request.getPeriod(), request.getDeviceId(), + request.getAreaId()); + break; + default: + throw new IllegalArgumentException("不支持的统计类型: " + request.getStatType()); + } + + // 计算总计 + double totalAmount = data.stream() + .mapToDouble(item -> item.get("totalWaterOutput") != null ? + Double.parseDouble(item.get("totalWaterOutput").toString()) : 0) + .sum(); + + int totalCount = data.stream() + .mapToInt(item -> item.get("usageCount") != null ? + Integer.parseInt(item.get("usageCount").toString()) : 0) + .sum(); + + result.setTotalCount(totalCount); + result.setTotalAmount(totalAmount); + result.setAvgAmount(totalCount > 0 ? totalAmount / totalCount : 0); + + // 构建明细项 + List items = new ArrayList<>(); + for (Map item : data) { + StatisticsVO.StatItemVO statItem = new StatisticsVO.StatItemVO(); + + if (request.getStatType().equals("by_time")) { + statItem.setDimensionKey(item.get("timeLabel").toString()); + statItem.setDimensionValue(item.get("timeLabel").toString()); + } else if (request.getStatType().equals("by_device")) { + statItem.setDimensionKey(item.get("deviceId").toString()); + statItem.setDimensionValue(item.get("deviceName") != null ? + item.get("deviceName").toString() : item.get("deviceId").toString()); + } else { + statItem.setDimensionKey(item.get("areaId").toString()); + statItem.setDimensionValue(item.get("areaName") != null ? + item.get("areaName").toString() : item.get("areaId").toString()); + } + + Integer count = item.get("usageCount") != null ? + Integer.parseInt(item.get("usageCount").toString()) : 0; + Double amount = item.get("totalWaterOutput") != null ? + Double.parseDouble(item.get("totalWaterOutput").toString()) : 0; + + statItem.setCount(count); + statItem.setAmount(amount); + statItem.setPercentage(totalAmount > 0 ? (amount / totalAmount) * 100 : 0); + + items.add(statItem); + } + + result.setItems(items); + return result; + } + + /** + * 获取告警统计 + */ + @Transactional(readOnly = true) + public AlarmStatisticsVO getAlarmStatistics(StatisticsQueryRequest request) { + AlarmStatisticsVO result = new AlarmStatisticsVO(); + + // 获取告警统计 + List> alarmStats; + + if ("by_device".equals(request.getStatType())) { + alarmStats = statisticsMapper.statAlarmCountByDevice( + request.getStartDate(), request.getEndDate(), + request.getAreaId(), null, request.getLimit()); + } else if ("by_area".equals(request.getStatType())) { + alarmStats = statisticsMapper.statAlarmCountByArea( + request.getStartDate(), request.getEndDate(), + request.getDeviceType(), request.getLimit()); + } else { + throw new IllegalArgumentException("不支持的告警统计类型: " + request.getStatType()); + } + + // 构建设备告警统计 + List deviceStats = alarmStats.stream() + .map(item -> { + AlarmStatisticsVO.DeviceAlarmStatVO deviceStat = new AlarmStatisticsVO.DeviceAlarmStatVO(); + deviceStat.setDeviceId(item.get("deviceId").toString()); + deviceStat.setDeviceName(item.get("deviceName") != null ? + item.get("deviceName").toString() : ""); + deviceStat.setTotalAlarms(Integer.parseInt(item.get("totalAlarms").toString())); + deviceStat.setPendingAlarms(Integer.parseInt(item.get("pendingAlarms").toString())); + deviceStat.setResolvedAlarms(Integer.parseInt(item.get("resolvedAlarms").toString())); + return deviceStat; + }) + .collect(Collectors.toList()); + + // 计算总数 + int totalAlarms = deviceStats.stream().mapToInt(AlarmStatisticsVO.DeviceAlarmStatVO::getTotalAlarms).sum(); + int pendingAlarms = deviceStats.stream().mapToInt(AlarmStatisticsVO.DeviceAlarmStatVO::getPendingAlarms).sum(); + int resolvedAlarms = deviceStats.stream().mapToInt(AlarmStatisticsVO.DeviceAlarmStatVO::getResolvedAlarms).sum(); + + result.setTotalAlarms(totalAlarms); + result.setPendingAlarms(pendingAlarms); + result.setResolvedAlarms(resolvedAlarms); + result.setDeviceAlarmStats(deviceStats); + + // 获取告警处理统计 + Map handleStats = statisticsMapper.getAlarmHandleStatistics( + request.getStartDate(), request.getEndDate(), request.getAreaId()); + + if (handleStats != null && handleStats.get("avgResponseHours") != null) { + result.setAverageResponseTime(Double.parseDouble(handleStats.get("avgResponseHours").toString())); + } + + return result; + } + + /** + * 获取设备状态统计 + */ + @Transactional(readOnly = true) + public Map getDeviceStatusStatistics(String areaId, String deviceType) { + return statisticsMapper.getDeviceStatusStatistics(areaId, deviceType); + } + + /** + * 获取综合仪表盘数据 + */ + @Transactional(readOnly = true) + public Map getDashboardStatistics() { + Map result = new HashMap<>(); + + // 今日数据 + LocalDate today = LocalDate.now(); + StatisticsVO todayWaterUsage = getWaterUsageStatistics(createTodayQuery()); + result.put("todayWaterUsage", todayWaterUsage); + + // 本月数据 + StatisticsVO monthWaterUsage = getWaterUsageStatistics(createMonthQuery()); + result.put("monthWaterUsage", monthWaterUsage); + + // 设备状态统计 + Map deviceStats = getDeviceStatusStatistics(null, null); + result.put("deviceStatus", deviceStats); + + // 告警统计 + AlarmStatisticsVO alarmStats = getAlarmStatistics(createAlarmQuery()); + result.put("alarmStatistics", alarmStats); + + // 热门设备(按用水量) + StatisticsQueryRequest hotDevicesQuery = new StatisticsQueryRequest(); + hotDevicesQuery.setStatType("by_device"); + hotDevicesQuery.setStartDate(today.minusDays(7)); + hotDevicesQuery.setEndDate(today); + hotDevicesQuery.setLimit(5); + StatisticsVO hotDevices = getWaterUsageStatistics(hotDevicesQuery); + result.put("hotDevices", hotDevices); + + return result; + } + + private StatisticsQueryRequest createTodayQuery() { + StatisticsQueryRequest request = new StatisticsQueryRequest(); + request.setStatType("by_time"); + request.setPeriod("day"); + request.setStartDate(LocalDate.now()); + request.setEndDate(LocalDate.now()); + return request; + } + + private StatisticsQueryRequest createMonthQuery() { + StatisticsQueryRequest request = new StatisticsQueryRequest(); + request.setStatType("by_time"); + request.setPeriod("month"); + request.setStartDate(LocalDate.now().withDayOfMonth(1)); + request.setEndDate(LocalDate.now()); + return request; + } + + private StatisticsQueryRequest createAlarmQuery() { + StatisticsQueryRequest request = new StatisticsQueryRequest(); + request.setStatType("by_device"); + request.setStartDate(LocalDate.now().minusDays(30)); + request.setEndDate(LocalDate.now()); + request.setLimit(10); + return request; + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/water/task/DeviceStatusMonitorTask.java b/src/main/java/com/campus/water/task/DeviceStatusMonitorTask.java new file mode 100644 index 0000000..dafd726 --- /dev/null +++ b/src/main/java/com/campus/water/task/DeviceStatusMonitorTask.java @@ -0,0 +1,54 @@ +/** + * 设备状态监控定时任务 + * 功能:定时执行设备状态检测和统计收集任务 + * 用途:自动化设备状态管理,减少人工干预 + * 核心任务: + * 1. 离线设备检测:每5分钟检测超时离线设备 + * 2. 状态统计收集:每小时收集设备状态统计数据 + * 技术:Spring @Scheduled定时任务、异常处理、日志记录 + * 配置:定时频率可在application.yml中配置 + */ +package com.campus.water.task; + +import com.campus.water.service.DeviceService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class DeviceStatusMonitorTask { + + private final DeviceService deviceService; + + /** + * 每5分钟检测一次离线设备 + */ + @Scheduled(fixedRate = 300000) // 5分钟 + public void monitorOfflineDevices() { + log.info("开始自动检测离线设备..."); + try { + // 检测30分钟无数据的设备 + deviceService.autoDetectOfflineDevices(30); + } catch (Exception e) { + log.error("离线设备检测任务执行失败: {}", e.getMessage(), e); + } + } + + /** + * 每小时统计一次设备状态 + */ + @Scheduled(cron = "0 0 * * * ?") // 每小时执行一次 + public void collectDeviceStatusStatistics() { + log.info("开始收集设备状态统计..."); + try { + // 这里可以保存统计结果到数据库 + // deviceService.getDeviceStatusCount(null, null); + log.info("设备状态统计收集完成"); + } catch (Exception e) { + log.error("设备状态统计收集失败: {}", e.getMessage(), e); + } + } +} \ No newline at end of file -- 2.34.1