李翔-大数据技术

Big data technology!

8-1 数据可视化 实验一:后端部署(1)

八、数据可视化 实验一:后端部署

目标: 编写后端程序,让前端大屏从数据库获取数据。

前端大屏 →【发请求】→ 后端 →【查数据库】→ 返回数据给大屏

整体路线图

任务1:创建 Spring Boot 项目
  ↓
任务2:配置数据库连接
  ↓
任务3:完善 pom.xml
  ↓
任务4:验证后端启动
  ↓
任务5:建立分层目录
  ↓
任务6:写第一个接口 - 健康检查
  ↓
任务7:写 3 个真实接口(连 MySQL 查数据)

任务1:创建后端项目(Spring Boot 2.7.x)

目标: 用 IDEA 创建 Spring Boot 项目,访问 http://localhost:8080 能打开页面。

步骤 1:新建项目

打开 IntelliJ IDEA → File → New → Project

步骤 2:选择 Spring Initializr

左侧选择 Spring Initializr,右侧设置:

  • JDK:1.8

  • Java:8

  • Service URLhttps://start.aliyun.com/(阿里云镜像,下载快)

⚠️ 确认 Service URL 右侧没有未连接的黄色叹号,点 Next

步骤 3:填写项目信息


项目填写
Namesmart-screen-backend
Groupcom.demo
Artifactsmart-screen-backend
TypeMaven
PackagingJar
JDK1.8
Java8


步骤 4:选择版本和依赖

  • Spring Boot 版本2.7.6(必须 2.7.x,3.x 要求 JDK 17)

勾选 5 个依赖:


路径依赖名称
WebSpring Web
SQLJDBC API
SQLMySQL Driver
Developer ToolsLombok
Developer ToolsSpring Boot DevTools


Create,等待 IDEA 自动初始化。

生成的项目结构:

smart-screen-backend/
├─ src/main/java/com/demo/
│   └─ SmartScreenBackendApplication.java   ← 启动入口
├─ src/main/resources/
│   └─ application.properties               ← 配置文件
└─ pom.xml                                  ← 依赖清单

任务2:配置 application.properties

目标: 让项目知道数据库在哪里、用什么账号连,启动时不再报错。

编辑 src/main/resources/application.properties

# ===== 服务器配置 =====
# 允许任何 IP 访问(不只是 localhost)
server.address=0.0.0.0
# 配置 Spring Boot 监听 8080 端口
server.port=8080

# ===== MySQL 配置 =====
spring.datasource.url=jdbc:mysql://master:3306/rt?useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# ===== ClickHouse 自定义配置 =====
clickhouse.url=jdbc:clickhouse://master:8123/iot_report
clickhouse.username=default
clickhouse.password=

注意:com.mysql.cj.jdbc.Driver 暂时报红,完成任务 3 添加驱动依赖后即可解决。


任务3:编写 Pom 文件

替换 pom.xml 内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                            https://maven.apache.org/xsd/maven-4.0.0.xsd">

   <modelVersion>4.0.0</modelVersion>

   <!-- 项目坐标 -->
   <groupId>com.demo</groupId>
   <artifactId>smart-screen-backend</artifactId>
   <version>1.0.0-SNAPSHOT</version>

   <!-- 全局属性:统一管理版本与编码 -->
   <properties>
       <java.version>1.8</java.version>
       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
       <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
       <spring-boot.version>2.7.6</spring-boot.version>
       <clickhouse-jdbc.version>0.6.0</clickhouse-jdbc.version>
       <lombok.version>1.18.32</lombok.version>
   </properties>

   <dependencies>
       <!-- 1) Spring Web:提供 REST 接口、内置 Tomcat -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
       </dependency>

       <!-- 2) JDBC 能力 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-data-jdbc</artifactId>
       </dependency>

       <!-- 3) MySQL 驱动 -->
       <dependency>
           <groupId>com.mysql</groupId>
           <artifactId>mysql-connector-j</artifactId>
           <scope>runtime</scope>
       </dependency>

       <!-- 4) ClickHouse JDBC -->
       <dependency>
           <groupId>com.clickhouse</groupId>
           <artifactId>clickhouse-jdbc</artifactId>
           <version>${clickhouse-jdbc.version}</version>
       </dependency>

       <!-- 5) ClickHouse 依赖的 LZ4 压缩 -->
       <dependency>
           <groupId>org.lz4</groupId>
           <artifactId>lz4-java</artifactId>
           <version>1.8.0</version>
       </dependency>

       <!-- 6) ClickHouse JDBC 0.6.x 需要的 HttpClient5 -->
       <dependency>
           <groupId>org.apache.httpcomponents.client5</groupId>
           <artifactId>httpclient5</artifactId>
           <version>5.2.1</version>
       </dependency>
       <dependency>
           <groupId>org.apache.httpcomponents.core5</groupId>
           <artifactId>httpcore5</artifactId>
           <version>5.2.1</version>
       </dependency>

       <!-- 7) Lombok:自动生成 getter/setter -->
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <version>${lombok.version}</version>
           <scope>provided</scope>
       </dependency>

       <!-- 8) DevTools:开发阶段热部署 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-devtools</artifactId>
           <scope>runtime</scope>
           <optional>true</optional>
       </dependency>

       <!-- 9) 测试依赖 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
   </dependencies>

   <!-- 依赖版本统一管理 -->
   <dependencyManagement>
       <dependencies>
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-dependencies</artifactId>
               <version>${spring-boot.version}</version>
               <type>pom</type>
               <scope>import</scope>
           </dependency>
       </dependencies>
   </dependencyManagement>

   <!-- 构建配置 -->
   <build>
       <plugins>
           <!-- 编译插件:指定 Java8 -->
           <plugin>
               <groupId>org.apache.maven.plugins</groupId>
               <artifactId>maven-compiler-plugin</artifactId>
               <version>3.8.1</version>
               <configuration>
                   <source>1.8</source>
                   <target>1.8</target>
                   <encoding>UTF-8</encoding>
                   <annotationProcessorPaths>
                       <path>
                           <groupId>org.projectlombok</groupId>
                           <artifactId>lombok</artifactId>
                           <version>${lombok.version}</version>
                       </path>
                   </annotationProcessorPaths>
               </configuration>
           </plugin>

           <!-- Spring Boot 打包插件 -->
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
               <version>${spring-boot.version}</version>
               <executions>
                   <execution>
                       <id>repackage</id>
                       <goals>
                           <goal>repackage</goal>
                       </goals>
                   </execution>
               </executions>
           </plugin>
       </plugins>
   </build>
</project>

保存后:右键项目 → MavenReload Project

✅ 验收清单

  • [ ] pom.xml 无红色报错

  • [ ] External Libraries 中能看到 clickhouse-jdbc-0.6.0


任务4:验证后端是否成功

右键 SmartScreenBackendApplication.javaRun,控制台看到:

Tomcat started on port(s): 8080

浏览器访问 http://localhost:8080

✅ 验收清单

  • [ ] 控制台看到 Tomcat started on port(s): 8080

  • [ ] 没有 Failed to configure a DataSource 报错

  • [ ] 浏览器访问 localhost:8080 能打开(白页或 404 都算成功)


任务5:创建项目目录结构(三层架构)

目标: 按"三层架构"创建 6 个包。

右键主包 com.demo.smartscreenbackendNewPackage,依次创建:

com.demo.smartscreenbackend
├── controller   接口层:接收前端请求,返回结果
├── service      业务层:处理业务逻辑
├── dao          数据访问层:写 SQL,操作数据库
├── dto          数据传输对象:定义数据结构
├── config       配置类:系统配置
└── util         工具类:通用工具方法

调用流程:

前端请求 → controller → service → dao → 数据库
             ↑           ↓        ↓
             └─────── dto 数据封装 ──┘

各层职责与注解:


包名职责常用注解
controller接收请求,返回响应@RestController
service处理业务逻辑@Service
dao查询和操作数据库@Repository
dto定义数据结构
config管理系统配置@Configuration
util通用工具方法


分层调用原则:

  1. 调用方向只能向下:controller → service → dao

  2. 不可跨层调用:controller 不能直接调用 dao

  3. dto 可在任意层之间传递

  4. config 和 util 任何层都可调用


任务6:写第一个接口 - 健康检查

目标: 写一个 /api/health 接口,访问返回 {"code":200,"msg":"ok"},验证三层架构走通。

请求流程:

浏览器访问 /api/health
    ↓
[Controller] HealthController 接收请求
    ↓ 调用
[Service]    HealthService 处理业务
    ↓ 返回
[Controller] 自动转 JSON
    ↓
浏览器收到 {"code":200,"msg":"ok"}

1. 创建 DTO

📁 dto/CommonResp.java

package com.demo.smartscreenbackend.dto;

/**
 * 通用返回对象
 * 所有接口统一返回格式(code + msg)
 */
public class CommonResp {

    private Integer code;   // 状态码:200=成功,500=失败
    private String msg;     // 提示信息

    public CommonResp() {}

    public CommonResp(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    // getter / setter
    public Integer getCode() { return code; }
    public void setCode(Integer code) { this.code = code; }

    public String getMsg() { return msg; }
    public void setMsg(String msg) { this.msg = msg; }
}

2. 创建 Service

📁 service/HealthService.java

package com.demo.smartscreenbackend.service;

import com.demo.smartscreenbackend.dto.CommonResp;
import org.springframework.stereotype.Service;

/**
 * 健康检查业务类
 */
@Service // 标记为业务层组件,交给 Spring 管理
public class HealthService {

    public CommonResp check() {
        // 返回成功状态
        return new CommonResp(200, "ok");
    }
}

3. 创建 Controller

📁 controller/HealthController.java

package com.demo.smartscreenbackend.controller;

import com.demo.smartscreenbackend.dto.CommonResp;
import com.demo.smartscreenbackend.service.HealthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 健康检查接口
 */
@RestController          // 标记为接口控制器,返回值自动转 JSON
@RequestMapping("/api")  // 类级路径前缀
public class HealthController {

    // 自动注入 Service
    @Autowired
    private HealthService healthService;

    /**
     * 访问地址:http://localhost:8080/api/health
     */
    @GetMapping("/health")
    public CommonResp health() {
        return healthService.check();
    }
}

关键注解说明:


注解作用
@RestController标记为接口控制器,返回值自动转 JSON
@RequestMapping("/api")类下所有接口的路径前缀
@GetMapping("/health")GET 请求 /health 路径
@Autowired自动注入,无需手动 new


4. 启动并测试

浏览器访问 http://localhost:8080/api/health,返回:

{ "code": 200, "msg": "ok" }

报错处理:若启动时出现 Connection timed out 数据库连接错误,可在启动类排除自动配置:

@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class,
    JdbcRepositoriesAutoConfiguration.class
})

任务7:编写 3 个真实接口(连 MySQL)

目标: 把 MySQL 实时表数据通过 Spring Boot 接口返回 JSON,供大屏调用。

4 张表 → 3 个接口:


表名存什么给哪个接口用
rt_kpi_online_10s实时在线设备数summary
rt_kpi_alarm_10s实时告警数summary
rt_temp_trend_10s温度趋势点tempTrend
rt_alarm_top5_1m告警 TOP 榜alarmTop


接口清单:


接口路径说明
① KPI 汇总GET /api/kpi/summary在线数+告警数+平均温度+高危数
② 温度趋势GET /api/kpi/tempTrend?limit=60最近 N 条温度趋势
③ 告警 TopGET /api/kpi/alarmTop?limit=3最新窗口告警 TopN



步骤1:检查数据库

mysql -uroot -p123456
use rt;

select * from rt.rt_kpi_online_10s order by ts desc limit 3;
select * from rt.rt_kpi_alarm_10s order by ts desc limit 3;
select * from rt.rt_temp_trend_10s order by ts desc limit 3;
select * from rt.rt_alarm_top5_1m order by dt desc limit 3;

步骤2:数据库配置

1)检查数据库

查看Mysql中的数据表的数据


2) 配置 ClickHouse 读取类

作用:集中读取 application.yml 中的 ClickHouse 连接配置,包括 url、username、password,供其他类统一使用。

路径建议:

src/main/java/com/demo/smartscreenbackend/config/ClickHouseProps.java

完整代码:

package com.demo.smartscreenbackend.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
* ClickHouse 数据库配置属性类
* 作用:从 application.yml 读取 ClickHouse 连接信息(url、账号、密码)
* 给 ClickHouseJdbc 工具类使用
*/
@Component
public class ClickHouseProps {

   /**
    * @Value("${xxx}") 是 Spring 提供的注解
    * Spring 启动时,会:
    *   1) 读 application.properties 里的 clickhouse.url
    *   2) 把值"塞进"下面这个 url 变量
    */
   @Value("${clickhouse.url}")
   private String url;

   /** 用户名,对应 yml 中的 clickhouse.username */
   @Value("${clickhouse.username}")
   private String username;

   /** 密码,对应 yml 中的 clickhouse.password */
   @Value("${clickhouse.password}")
   private String password;

   public String getUrl() { return url; }
   public String getUsername() { return username; }
   public String getPassword() { return password; }
}

3)新增 ClickHouse 工具类(用 JDBC 直连)

作用:根据 ClickHouseProps 中读取到的连接信息,创建 ClickHouse 数据库连接 Connection,供 DAO 层执行 SQL 查询使用。

路径建议:

src/main/java/com/demo/smartscreenbackend/util/ClickHouseJdbc.java

完整代码:

package com.demo.smartscreenbackend.util;

import com.demo.smartscreenbackend.config.ClickHouseProps;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.DriverManager;

/**
* ClickHouse 数据库连接工具类
* 作用:获取 ClickHouse 数据库连接(Connection)
* 给 DAO 层使用,用于查询高风险设备数据
*/
@Component // 交给Spring管理,可被自动注入
public class ClickHouseJdbc {

   // ===================== 依赖注入 =====================
   // 成员变量:存放【配置工具】(从配置文件读取的地址、账号、密码)
   private final ClickHouseProps props;

   /**
    * 构造方法注入
    * Spring 自动把 ClickHouseProps 配置对象传进来
    */
   public ClickHouseJdbc(ClickHouseProps props) {
       // 把传入的配置工具,保存到成员变量中
       this.props = props;
   }

   /**
    * 获取 ClickHouse 数据库连接
    * @return Connection 数据库连接对象(可用来执行SQL)
    * @throws Exception 连接失败抛出异常
    */
   public Connection getConn() throws Exception {

       // 通过驱动管理器,获取数据库连接
       // 使用配置中的:地址、用户名、密码
       return DriverManager.getConnection(
               props.getUrl(),        // 数据库连接地址
               props.getUsername(),   // 用户名
               props.getPassword()    // 密码
       );
   }
}




步骤3:编写 DTO

3.1 统一返回对象 ApiResp

📁 dto/ApiResp.java

package com.demo.smartscreenbackend.dto;

/**
 * 统一返回结果
 * 所有接口都用这个格式:{ code, msg, data }
 * <T> 是泛型占位符,data 类型不固定
 */
public class ApiResp<T> {

    private Integer code;   // 状态码:200=成功,500=失败
    private String  msg;    // 提示信息
    private T       data;   // 业务数据(类型不固定)

    public ApiResp() {}

    public ApiResp(Integer code, String msg) {
        this.code = code;
        this.msg  = msg;
    }

    public ApiResp(Integer code, String msg, T data) {
        this.code = code;
        this.msg  = msg;
        this.data = data;
    }

    // 快捷方法:返回成功结果
    public static <T> ApiResp<T> ok(T data) {
        return new ApiResp<>(200, "ok", data);
    }

    // 快捷方法:返回失败结果
    public static <T> ApiResp<T> fail(String msg) {
        return new ApiResp<>(500, msg, null);
    }

    // getter / setter
    public Integer getCode()             { return code; }
    public void    setCode(Integer code) { this.code = code; }
    public String  getMsg()              { return msg; }
    public void    setMsg(String msg)    { this.msg = msg; }
    public T       getData()             { return data; }
    public void    setData(T data)       { this.data = data; }
}

3.2 KpiSummary

📁 dto/KpiSummary.java

package com.demo.smartscreenbackend.dto;

/**
 * KPI 汇总数据:大屏顶部 4 个统计卡片
 */
public class KpiSummary {

    private Long onlineCount;      // 在线设备数
    private Long alarmCount;       // 告警总数
    private Double avgTemp;        // 平均温度
    private Long highRiskCount;    // 高风险数量

    public KpiSummary() {}

    public KpiSummary(Long onlineCount, Long alarmCount, Double avgTemp, Long highRiskCount) {
        this.onlineCount = onlineCount;
        this.alarmCount = alarmCount;
        this.avgTemp = avgTemp;
        this.highRiskCount = highRiskCount;
    }

    // getter / setter
    public Long getOnlineCount() { return onlineCount; }
    public void setOnlineCount(Long onlineCount) { this.onlineCount = onlineCount; }
    public Long getAlarmCount() { return alarmCount; }
    public void setAlarmCount(Long alarmCount) { this.alarmCount = alarmCount; }
    public Double getAvgTemp() { return avgTemp; }
    public void setAvgTemp(Double avgTemp) { this.avgTemp = avgTemp; }
    public Long getHighRiskCount() { return highRiskCount; }
    public void setHighRiskCount(Long highRiskCount) { this.highRiskCount = highRiskCount; }
}

3.3 TempTrendPoint

📁 dto/TempTrendPoint.java

package com.demo.smartscreenbackend.dto;

/**
 * 温度趋势点:折线图每个数据点
 */
public class TempTrendPoint {

    private String ts;        // 时间点
    private Double avgTemp;   // 平均温度
    private Double maxTemp;   // 最高温度

    public TempTrendPoint() {}

    public TempTrendPoint(String ts, Double avgTemp, Double maxTemp) {
        this.ts = ts;
        this.avgTemp = avgTemp;
        this.maxTemp = maxTemp;
    }

    // getter / setter
    public String getTs() { return ts; }
    public void setTs(String ts) { this.ts = ts; }
    public Double getAvgTemp() { return avgTemp; }
    public void setAvgTemp(Double avgTemp) { this.avgTemp = avgTemp; }
    public Double getMaxTemp() { return maxTemp; }
    public void setMaxTemp(Double maxTemp) { this.maxTemp = maxTemp; }
}

3.4 AlarmTopItem

📁 dto/AlarmTopItem.java

package com.demo.smartscreenbackend.dto;

/**
 * 告警 Top 排名项:告警设备排行榜
 */
public class AlarmTopItem {

    private String deviceId;    // 设备编号
    private Long cnt;           // 告警次数

    public AlarmTopItem() {}

    public AlarmTopItem(String deviceId, Long cnt) {
        this.deviceId = deviceId;
        this.cnt = cnt;
    }

    // getter / setter
    public String getDeviceId() { return deviceId; }
    public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
    public Long getCnt() { return cnt; }
    public void setCnt(Long cnt) { this.cnt = cnt; }
}

步骤4:编写 DAO

4.1 KpiSummaryDao

📁 dao/KpiSummaryDao.java

package com.demo.smartscreenbackend.dao;

import com.demo.smartscreenbackend.util.ClickHouseJdbc;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

/**
 * KPI 汇总数据访问层
 * 只负责和数据库打交道,不做业务判断
 */
@Repository
public class KpiSummaryDao {

    // MySQL 操作工具
    private final JdbcTemplate jdbcTemplate;
    // ClickHouse 操作工具(自定义)
    private final ClickHouseJdbc clickHouseJdbc;

    // 构造方法注入
    public KpiSummaryDao(JdbcTemplate jdbcTemplate, ClickHouseJdbc clickHouseJdbc) {
        this.jdbcTemplate   = jdbcTemplate;
        this.clickHouseJdbc = clickHouseJdbc;
    }

    /**
     * 查最新在线设备数(MySQL)
     */
    public Long latestOnlineCount() {
        String sql = "SELECT online_count FROM rt.rt_kpi_online_10s ORDER BY ts DESC LIMIT 1";
        return jdbcTemplate.query(sql,
                rs -> rs.next() ? rs.getLong("online_count") : 0L);
    }

    /**
     * 查最新告警数(MySQL)
     */
    public Long latestAlarmCount() {
        String sql = "SELECT alarm_count FROM rt.rt_kpi_alarm_10s ORDER BY ts DESC LIMIT 1";
        return jdbcTemplate.query(sql,
                rs -> rs.next() ? rs.getLong("alarm_count") : 0L);
    }

    /**
     * 查最新平均温度(MySQL)
     */
    public Double latestAvgTemp() {
        String sql = "SELECT avg_temp FROM rt.rt_temp_trend_10s ORDER BY ts DESC LIMIT 1";
        return jdbcTemplate.query(sql,
                rs -> rs.next() ? rs.getDouble("avg_temp") : 0.0);
    }

    /**
     * 查高风险设备数(ClickHouse)
     * 规则:每台设备取最新预测的 prob1,prob1 >= 0.9 算高风险
     */
    public Long latestHighRiskCount() {
        String sql = "SELECT count() AS cnt " +
                     "FROM ( " +
                     "    SELECT device_id, argMax(prob1, event_time) AS last_prob " +
                     "    FROM iot_report.dws_device_pred_detail " +
                     "    GROUP BY device_id " +
                     ") t " +
                     "WHERE t.last_prob >= 0.9";

        // try-with-resources:自动关闭连接,防资源泄漏
        try (
            Connection conn = clickHouseJdbc.getConn();
            PreparedStatement ps = conn.prepareStatement(sql);
            ResultSet rs = ps.executeQuery()
        ) {
            return rs.next() ? rs.getLong("cnt") : 0L;
        } catch (Exception e) {
            e.printStackTrace();
            return 0L; // 出错返回 0,避免大屏崩掉
        }
    }
}

4.2 TempTrendDao

📁 dao/TempTrendDao.java

package com.demo.smartscreenbackend.dao;

import com.demo.smartscreenbackend.dto.TempTrendPoint;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * 温度趋势 DAO:折线图数据
 */
@Repository
public class TempTrendDao {

    private final JdbcTemplate jdbcTemplate;

    public TempTrendDao(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * 查最近 N 条温度趋势数据
     * @param limit 查询条数
     */
    public List<TempTrendPoint> latestTrend(int limit) {
        String sql = "SELECT ts, avg_temp, max_temp " +
                     "FROM rt.rt_temp_trend_10s " +
                     "ORDER BY ts DESC LIMIT ?";

        // 把每行结果封装成 TempTrendPoint 对象
        return jdbcTemplate.query(sql, new Object[]{limit}, (rs, rowNum) ->
                new TempTrendPoint(
                        rs.getString("ts"),
                        rs.getDouble("avg_temp"),
                        rs.getDouble("max_temp")
                )
        );
    }
}

4.3 AlarmTopDao

📁 dao/AlarmTopDao.java

package com.demo.smartscreenbackend.dao;

import com.demo.smartscreenbackend.dto.AlarmTopItem;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.Collections;
import java.util.List;

/**
 * 告警 Top 排名 DAO
 */
@Repository
public class AlarmTopDao {

    private final JdbcTemplate jdbcTemplate;

    public AlarmTopDao(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * 查最新窗口的告警 TopN
     * @param limit 取前几名
     */
    public List<AlarmTopItem> latestTop(int limit) {
        // 1. 先查最新的时间窗口
        String latestWindowSql = "SELECT MAX(window_end) AS window_end FROM rt.rt_alarm_top5_1m";
        String windowEnd = jdbcTemplate.query(latestWindowSql,
                rs -> rs.next() ? rs.getString("window_end") : null);

        // 没有数据则返回空列表
        if (windowEnd == null) {
            return Collections.emptyList();
        }

        // 2. 按最新时间窗口查 TopN
        String sql = "SELECT device_id, COALESCE(cnt,0) AS cnt " +
                     "FROM rt.rt_alarm_top5_1m " +
                     "WHERE window_end = ? AND rn <= ? " +
                     "ORDER BY rn ASC";

        return jdbcTemplate.query(sql, new Object[]{windowEnd, limit}, (rs, rowNum) ->
                new AlarmTopItem(
                        rs.getString("device_id"),
                        rs.getLong("cnt")
                )
        );
    }
}

步骤5:编写 Service

📁 service/KpiService.java

package com.demo.smartscreenbackend.service;

import com.demo.smartscreenbackend.dao.AlarmTopDao;
import com.demo.smartscreenbackend.dao.KpiSummaryDao;
import com.demo.smartscreenbackend.dao.TempTrendDao;
import com.demo.smartscreenbackend.dto.AlarmTopItem;
import com.demo.smartscreenbackend.dto.KpiSummary;
import com.demo.smartscreenbackend.dto.TempTrendPoint;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * KPI 业务服务层
 * 调用 DAO 拿数据 → 组装成 DTO → 返回给 Controller
 */
@Service
public class KpiService {

    private final KpiSummaryDao kpiSummaryDao;
    private final TempTrendDao  tempTrendDao;
    private final AlarmTopDao   alarmTopDao;

    // 构造方法注入 3 个 DAO
    public KpiService(KpiSummaryDao kpiSummaryDao,
                      TempTrendDao tempTrendDao,
                      AlarmTopDao alarmTopDao) {
        this.kpiSummaryDao = kpiSummaryDao;
        this.tempTrendDao  = tempTrendDao;
        this.alarmTopDao   = alarmTopDao;
    }

    /**
     * 顶部统计汇总:查 4 个指标,打包成一个对象
     */
    public KpiSummary summary() {
        Long   online   = kpiSummaryDao.latestOnlineCount();
        Long   alarm    = kpiSummaryDao.latestAlarmCount();
        Double avgTemp  = kpiSummaryDao.latestAvgTemp();
        Long   highRisk = kpiSummaryDao.latestHighRiskCount();
        return new KpiSummary(online, alarm, avgTemp, highRisk);
    }

    /**
     * 温度趋势:直接返回 DAO 查询结果
     */
    public List<TempTrendPoint> tempTrend(int limit) {
        return tempTrendDao.latestTrend(limit);
    }

    /**
     * 告警 Top 排名:直接返回 DAO 查询结果
     */
    public List<AlarmTopItem> alarmTop(int limit) {
        return alarmTopDao.latestTop(limit);
    }
}

步骤6:编写 Controller

📁 controller/KpiController.java

package com.demo.smartscreenbackend.controller;

import com.demo.smartscreenbackend.dto.*;
import com.demo.smartscreenbackend.service.KpiService;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * KPI 接口控制层
 * 接收请求 → 调用 Service → 用 ApiResp 包装 JSON 返回
 */
@RestController
@RequestMapping("/api/kpi")  // 类前缀:所有接口以 /api/kpi 开头
public class KpiController {

    private final KpiService kpiService;

    public KpiController(KpiService kpiService) {
        this.kpiService = kpiService;
    }

    /**
     * 接口1:大屏顶部汇总
     * GET /api/kpi/summary
     */
    @GetMapping("/summary")
    public ApiResp<KpiSummary> summary() {
        return ApiResp.ok(kpiService.summary());
    }

    /**
     * 接口2:温度趋势
     * GET /api/kpi/tempTrend?limit=60
     * defaultValue:前端不传时默认 60
     */
    @GetMapping("/tempTrend")
    public ApiResp<List<TempTrendPoint>> tempTrend(
            @RequestParam(defaultValue = "60") int limit
    ) {
        // 参数防御:避免前端传非法值压垮数据库
        if (limit <= 0)   limit = 60;
        if (limit > 500)  limit = 500;
        return ApiResp.ok(kpiService.tempTrend(limit));
    }

    /**
     * 接口3:告警 Top 排名
     * GET /api/kpi/alarmTop?limit=3
     */
    @GetMapping("/alarmTop")
    public ApiResp<List<AlarmTopItem>> alarmTop(
            @RequestParam(defaultValue = "3") int limit
    ) {
        if (limit <= 0)  limit = 3;
        if (limit > 20)  limit = 20;
        return ApiResp.ok(kpiService.alarmTop(limit));
    }
}

步骤7:启动后端

运行 SmartScreenBackendApplication,看到:

Tomcat started on port(s): 8080

步骤8:测试接口

接口①:KPI 汇总

http://localhost:8080/api/kpi/summary

返回


{
 "code": 200,
 "msg": "ok",
 "data": {
   "onlineCount": 50,
   "alarmCount": 10,
   "avgTemp": 26.35,
   "highRiskCount": 0
 }
}

接口②:温度趋势

http://localhost:8080/api/kpi/tempTrend?limit=60

返回

{
 "code": 200,
 "msg": "ok",
 "data": [
   {"ts": "2026-02-16 14:30:30", "avgTemp": 26.3543,             "maxTemp": 45.27},
   {"ts": "2026-02-16 14:30:20", "avgTemp": 26.819199999999995,  "maxTemp": 45.27},
   {"ts": "2026-02-16 14:30:10", "avgTemp": 27.0959,             "maxTemp": 46.65}
 ]
}

接口③:告警 Top

http://localhost:8080/api/kpi/alarmTop?limit=3

返回

{
 "code": 200,
 "msg": "ok",
 "data": [
   {"deviceId":"device-003","cnt":3},
   {"deviceId":"device-033","cnt":3},
   {"deviceId":"device-046","cnt":3}
 ]
}

✅ 验收清单

  • [ ] 3 个接口都能返回 code: 200

  • [ ] data 字段有数据库的真实数据

  • [ ] 浏览器或 Postman 访问无报错


发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

Powered By Z-BlogPHP 1.7.3

版权:李翔
备案/许可证编号为:新ICP备2024006115号-1