目标: 编写后端程序,让前端大屏从数据库获取数据。
前端大屏 →【发请求】→ 后端 →【查数据库】→ 返回数据给大屏
整体路线图
任务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 URL:
https://start.aliyun.com/(阿里云镜像,下载快)
⚠️ 确认 Service URL 右侧没有未连接的黄色叹号,点 Next
步骤 3:填写项目信息
| 项目 | 填写 |
|---|---|
| Name | smart-screen-backend |
| Group | com.demo |
| Artifact | smart-screen-backend |
| Type | Maven |
| Packaging | Jar |
| JDK | 1.8 |
| Java | 8 |
步骤 4:选择版本和依赖
Spring Boot 版本:
2.7.6(必须 2.7.x,3.x 要求 JDK 17)
勾选 5 个依赖:
| 路径 | 依赖名称 |
|---|---|
| Web | Spring Web |
| SQL | JDBC API |
| SQL | MySQL Driver |
| Developer Tools | Lombok |
| Developer Tools | Spring 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>
保存后:右键项目 → Maven → Reload Project
✅ 验收清单
[ ]
pom.xml无红色报错[ ]
External Libraries中能看到clickhouse-jdbc-0.6.0
任务4:验证后端是否成功
右键 SmartScreenBackendApplication.java → Run,控制台看到:
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.smartscreenbackend → New → Package,依次创建:
com.demo.smartscreenbackend
├── controller 接口层:接收前端请求,返回结果
├── service 业务层:处理业务逻辑
├── dao 数据访问层:写 SQL,操作数据库
├── dto 数据传输对象:定义数据结构
├── config 配置类:系统配置
└── util 工具类:通用工具方法
调用流程:
前端请求 → controller → service → dao → 数据库
↑ ↓ ↓
└─────── dto 数据封装 ──┘
各层职责与注解:
| 包名 | 职责 | 常用注解 |
|---|---|---|
| controller | 接收请求,返回响应 | @RestController |
| service | 处理业务逻辑 | @Service |
| dao | 查询和操作数据库 | @Repository |
| dto | 定义数据结构 | 无 |
| config | 管理系统配置 | @Configuration |
| util | 通用工具方法 | 无 |
分层调用原则:
调用方向只能向下:
controller → service → dao不可跨层调用:controller 不能直接调用 dao
dto 可在任意层之间传递
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 条温度趋势 |
| ③ 告警 Top | GET /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:数据库配置
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 字段有数据库的真实数据