任务8:接 ClickHouse —— 实现 3 个大屏数据接口
目标: 实现 3 个接口,为大屏提供历史与预测数据:
| 接口 | 路径 | 用途 |
|---|---|---|
| ① 历史 7 天温度 | GET /api/history/temp7d | 柱形图 |
| ② 风险占比 | GET /api/predict/riskPie | 环形图 |
| ③ 高风险 Top3 | GET /api/predict/topRisk?limit=3 | 表格 |
ClickHouse 现有 2 张表:
iot_report.dws_temp_trend_day(历史趋势)iot_report.dws_device_pred_detail(预测明细)
步骤1:检查数据库
1)确认 ClickHouse 两张表存在
# 在 master 节点进入 ClickHouse 客户端
clickhouse-client -m
-- 查看表
SHOW TABLES FROM iot_report;
-- 查看表结构
DESC iot_report.dws_temp_trend_day;
DESC iot_report.dws_device_pred_detail;
2)确认表中有数据
-- 检查历史表
SELECT * FROM iot_report.dws_temp_trend_day ORDER BY dt DESC LIMIT 5;
-- 检查预测表
SELECT * FROM iot_report.dws_device_pred_detail ORDER BY created_at DESC LIMIT 5;
步骤2:数据库配置
1)检查 application.properties
确认 src/main/resources/application.properties 中已有:
# ClickHouse 自定义配置
clickhouse.url=jdbc:clickhouse://master:8123/iot_report?jdbcCompliant=false
clickhouse.username=default
clickhouse.password=
2)配置 ClickHouse 读取类
📁 config/ClickHouseProps.java
package com.demo.smartscreenbackend.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* ClickHouse 配置属性类
* 从 application.properties 读取连接信息
*/
@Component
public class ClickHouseProps {
// @Value:Spring 启动时自动把配置文件的值塞进变量
@Value("${clickhouse.url}")
private String url;
@Value("${clickhouse.username}")
private String username;
@Value("${clickhouse.password}")
private String password;
public String getUrl() { return url; }
public String getUsername() { return username; }
public String getPassword() { return password; }
}
3)新增 ClickHouse 工具类
📁 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 数据库连接工具类
* 给 DAO 层提供数据库连接
*/
@Component
public class ClickHouseJdbc {
private final ClickHouseProps props;
// 构造方法注入
public ClickHouseJdbc(ClickHouseProps props) {
this.props = props;
}
/**
* 获取 ClickHouse 数据库连接
*/
public Connection getConn() throws Exception {
return DriverManager.getConnection(
props.getUrl(),
props.getUsername(),
props.getPassword()
);
}
}
步骤3:创建 DTO 数据结构
3.1 TempDayItem(历史 7 天)
📁 dto/TempDayItem.java
package com.demo.smartscreenbackend.dto;
/**
* 按天统计的温度数据
*/
public class TempDayItem {
public String dt; // 日期
public Double avgTemp; // 当日平均温度
public TempDayItem(String dt, Double avgTemp) {
this.dt = dt;
this.avgTemp = avgTemp;
}
}
3.2 RiskPieItem(风险环形图)
📁 dto/RiskPieItem.java
package com.demo.smartscreenbackend.dto;
/**
* 风险统计饼图数据项
*/
public class RiskPieItem {
public String name; // 风险类型:正常 / 预警 / 高危
public Long value; // 该类型设备数量
public RiskPieItem(String name, Long value) {
this.name = name;
this.value = value;
}
}
3.3 TopRiskItem(高风险 Top)
📁 dto/TopRiskItem.java
package com.demo.smartscreenbackend.dto;
/**
* 高风险设备排名项
*/
public class TopRiskItem {
public String deviceId; // 设备编号
public Double prob1; // 风险概率
public String eventTime; // 事件时间
public TopRiskItem(String deviceId, Double prob1, String eventTime) {
this.deviceId = deviceId;
this.prob1 = prob1;
this.eventTime = eventTime;
}
}
步骤4:编写 DAO
4.1 HistoryDao
📁 dao/HistoryDao.java
package com.demo.smartscreenbackend.dao;
import com.demo.smartscreenbackend.dto.TempDayItem;
import com.demo.smartscreenbackend.util.ClickHouseJdbc;
import org.springframework.stereotype.Repository;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
/**
* 历史数据 DAO
* 从 ClickHouse 查询历史统计数据
*/
@Repository
public class HistoryDao {
private final ClickHouseJdbc ck;
public HistoryDao(ClickHouseJdbc ck) {
this.ck = ck;
}
/**
* 查询最近 7 天的每日平均温度
*/
public List<TempDayItem> last7Days() {
String sql = "SELECT toString(dt) AS dt, avg_temp " +
"FROM iot_report.dws_temp_trend_day " +
"ORDER BY dt DESC LIMIT 7";
List<TempDayItem> list = new ArrayList<>();
// try-with-resources:自动关闭连接、防资源泄漏
try (Connection conn = ck.getConn();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
// 遍历结果,封装成 TempDayItem
while (rs.next()) {
list.add(new TempDayItem(
rs.getString("dt"),
rs.getDouble("avg_temp")
));
}
} catch (Exception e) {
throw new RuntimeException("查询历史7天失败:" + e.getMessage(), e);
}
return list;
}
}4.2 PredictDao
📁 dao/PredictDao.java
package com.demo.smartscreenbackend.dao;
// 导入用到的类
import com.demo.smartscreenbackend.dto.RiskPieItem; // 风险占比的数据封装对象(饼图用)
import com.demo.smartscreenbackend.dto.TopRiskItem; // 高风险设备的数据封装对象(排行榜用)
import com.demo.smartscreenbackend.util.ClickHouseJdbc; // 自己写的工具类,负责连接 ClickHouse 数据库
import org.springframework.stereotype.Repository; // Spring 注解,标记这是一个“数据访问层”组件
import java.sql.Connection; // 数据库连接
import java.sql.PreparedStatement; // 预编译 SQL 语句(能防 SQL 注入)
import java.sql.ResultSet; // 查询结果集(一行行读数据)
import java.util.*; // List、ArrayList 等集合类
/**
* DAO = Data Access Object,数据访问对象。
* 这个类专门负责“查数据库”,业务逻辑不写在这里。
* @Repository:告诉 Spring 这是一个数据库操作类,Spring 会自动帮我们管理它(创建对象、注入等)。
*/
@Repository
public class PredictDao {
// ck 就是数据库连接工具,final 表示一旦赋值就不再改变
private final ClickHouseJdbc ck;
/**
* 构造方法。
* Spring 启动时会自动把 ClickHouseJdbc 对象“送进来”(这叫依赖注入),
* 我们不用自己 new,直接拿来用就行。
*/
public PredictDao(ClickHouseJdbc ck) {
this.ck = ck;
}
/**
* 功能一:风险占比统计。
* 把最近 10 分钟的设备,按预测概率 prob1 分成“正常”和“预警”两类,
* 分别数一数各有多少台,给前端画饼图用。
*/
public List<RiskPieItem> riskPie() {
// 拼接 SQL 语句(用 + 把多行字符串连起来,方便阅读)
String sql =
"SELECT " +
// multiIf:类似 if-else。prob1 小于 0.5 算“正常”,否则算“预警”
" multiIf(prob1 < 0.5, '正常', '预警') AS level, " +
" count() AS cnt " + // count() 统计每一类有多少条记录
"FROM iot_report.dws_device_pred_detail " + // 要查询的数据表
"WHERE event_time >= now() - INTERVAL 10 MINUTE " + // 只看最近10分钟的数据
"GROUP BY level " + // 按 level(正常/预警)分组统计
"ORDER BY cnt DESC"; // 数量从多到少排序
// 准备一个列表,用来装查询结果
List<RiskPieItem> list = new ArrayList<>();
// try(...) 这种写法叫“try-with-resources”:括号里的连接、语句、结果集
// 用完后会自动关闭,不用自己写 close(),能防止资源泄漏
try (Connection conn = ck.getConn(); // 获取数据库连接
PreparedStatement ps = conn.prepareStatement(sql); // 把 SQL 交给数据库准备执行
ResultSet rs = ps.executeQuery()) { // 执行查询,拿到结果集
// rs.next() 把“游标”往下移动一行,有数据返回 true,没有就结束循环
while (rs.next()) {
// 从这一行里取出 level 和 cnt,封装成对象放进列表
list.add(new RiskPieItem(
rs.getString("level"), // 取字符串类型的字段:正常/预警
rs.getLong("cnt") // 取长整型的字段:数量
));
}
} catch (Exception e) {
// 查询出错时,抛出运行时异常,并带上原始错误信息,方便排查问题
throw new RuntimeException("查询风险占比失败:" + e.getMessage(), e);
}
// 把结果返回给调用者(一般是 Service 层)
return list;
}
/**
* 功能二:高风险设备 TopN 排行榜。
* 找出最近 10 分钟内预测概率 prob1 最高的前几台设备。
* 如果 prob1 一样,就取事件时间更新的那条。
*
* @param limit 要取多少条(比如传 5 就是 Top5)
*/
public List<TopRiskItem> topRisk(int limit) {
String sql =
"SELECT device_id, prob1, event_time " + // 查设备编号、风险概率、事件时间
"FROM iot_report.dws_device_pred_detail " +
"WHERE event_time >= now() - INTERVAL 10 MINUTE " + // 最近10分钟
// 先按 prob1 从大到小排,prob1 相同时再按时间从新到旧排
"ORDER BY prob1 DESC, event_time DESC " +
"LIMIT ?"; // ? 是占位符,下面再填具体数字
List<TopRiskItem> list = new ArrayList<>();
try (Connection conn = ck.getConn();
PreparedStatement ps = conn.prepareStatement(sql)) {
// 给 SQL 里第 1 个 ? 赋值,填入 limit。
// 用占位符而不是直接拼字符串,能防止 SQL 注入,更安全
ps.setInt(1, limit);
// 执行查询,结果集也用 try-with-resources 自动关闭
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
list.add(new TopRiskItem(
rs.getString("device_id"), // 设备编号
rs.getDouble("prob1"), // 风险概率(小数)
// 时间类型转成字符串,方便前端直接显示
rs.getTimestamp("event_time").toString()
));
}
}
} catch (Exception e) {
throw new RuntimeException("查询高风险Top失败:" + e.getMessage(), e);
}
return list;
}
}步骤5:编写 Service
5.1 HistoryService
📁 service/HistoryService.java
package com.demo.smartscreenbackend.service;
import com.demo.smartscreenbackend.dao.HistoryDao;
import com.demo.smartscreenbackend.dto.TempDayItem;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 历史数据业务层
*/
@Service
public class HistoryService {
private final HistoryDao historyDao;
public HistoryService(HistoryDao historyDao) {
this.historyDao = historyDao;
}
/**
* 查询最近 7 天温度
*/
public List<TempDayItem> last7Days() {
return historyDao.last7Days();
}
}
5.2 PredictService
📁 service/PredictService.java
package com.demo.smartscreenbackend.service;
import com.demo.smartscreenbackend.dao.PredictDao;
import com.demo.smartscreenbackend.dto.RiskPieItem;
import com.demo.smartscreenbackend.dto.TopRiskItem;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 风险预测业务层
*/
@Service
public class PredictService {
private final PredictDao predictDao;
public PredictService(PredictDao predictDao) {
this.predictDao = predictDao;
}
/**
* 风险等级占比
*/
public List<RiskPieItem> riskPie() {
return predictDao.riskPie();
}
/**
* 高风险设备 TopN
*/
public List<TopRiskItem> topRisk(int limit) {
// 参数防御
if (limit <= 0) limit = 3;
if (limit > 50) limit = 50;
return predictDao.topRisk(limit);
}
}
步骤6:编写 Controller
6.1 HistoryController
📁 controller/HistoryController.java
package com.demo.smartscreenbackend.controller;
import com.demo.smartscreenbackend.dto.ApiResp;
import com.demo.smartscreenbackend.service.HistoryService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 历史数据接口
*/
@RestController
@RequestMapping("/api/history")
public class HistoryController {
private final HistoryService historyService;
public HistoryController(HistoryService historyService) {
this.historyService = historyService;
}
/**
* GET /api/history/temp7d
*/
@GetMapping("/temp7d")
public ApiResp<?> temp7d() {
return ApiResp.ok(historyService.last7Days());
}
}6.2 PredictController
📁 controller/PredictController.java
package com.demo.smartscreenbackend.controller;
import com.demo.smartscreenbackend.dto.ApiResp;
import com.demo.smartscreenbackend.service.PredictService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 风险预测接口
*/
@RestController
@RequestMapping("/api/predict")
public class PredictController {
private final PredictService predictService;
public PredictController(PredictService predictService) {
this.predictService = predictService;
}
/**
* GET /api/predict/riskPie
*/
@GetMapping("/riskPie")
public ApiResp<?> riskPie() {
return ApiResp.ok(predictService.riskPie());
}
/**
* GET /api/predict/topRisk?limit=3
*/
@GetMapping("/topRisk")
public ApiResp<?> topRisk(@RequestParam(defaultValue = "3") int limit) {
return ApiResp.ok(predictService.topRisk(limit));
}
}
步骤7:启动后端
运行 SmartScreenBackendApplication,看到 Tomcat started on port(s): 8080
步骤8:测试接口
历史 7 天: http://localhost:8080/api/history/temp7d
风险环形图: http://localhost:8080/api/predict/riskPie
高风险 Top3: http://localhost:8080/api/predict/topRisk?limit=3
✅ 验收清单
[ ]
temp7d返回 7 条数据(dt + avgTemp)[ ]
riskPie返回 3 段(正常/预警/高危)[ ]
topRisk返回 Top3(deviceId + prob1 + eventTime)
任务9:后端配置跨域(给前端对接做准备)
什么是"跨域"?
浏览器规定,前端和后端的地址或端口不一样时,默认不允许互相访问。
| 角色 | 地址 | 端口 |
|---|---|---|
| 前端(Vite) | http://localhost | 5173 |
| 后端(Spring Boot) | http://localhost | 8080 |
端口不同 → 跨域 → 前端调接口会被浏览器拦截。
解决办法: 在后端加白名单,允许 http://localhost:5173 访问。
9.1 统一异常返回
📁 exception/GlobalExceptionHandler.java
package com.demo.smartscreenbackend.exception;
import com.demo.smartscreenbackend.dto.ApiResp;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理:把异常包装成 {code, msg, data} 格式
* 避免 500 错误返回白页
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ApiResp<?> handle(Exception e) {
// 教学:先把异常信息返回前端,便于排查
// 生产:不建议直接暴露 e.getMessage()
return ApiResp.fail("服务器异常:" + e.getMessage());
}
}9.2 配置跨域
📁 config/WebConfig.java
package com.demo.smartscreenbackend.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 跨域配置:允许前端 5173 访问后端 8080
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
// 对所有以 /api/ 开头的接口生效
.allowedOrigins("http://localhost:5173")
// 只允许 Vite 前端地址访问
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// 允许的请求方式(OPTIONS 是浏览器跨域预检请求)
.allowedHeaders("*")
// 允许所有请求头
.allowCredentials(true)
// 允许携带 cookie
.maxAge(3600);
// 预检请求缓存 1 小时
}
}9.3 验收
启动后端(8080)和前端(5173)
打开
http://localhost:5173/,按F12进入Console控制台中执行:
fetch("http://localhost:8080/api/history/temp7d")
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err))验收结果:
✅ 成功拿到
{ code, msg, data }→ 跨域配置成功❌ 报错
CORS policy→ 检查allowedOrigins配置❌ 报错
404→ 检查接口路径❌ 报错
Failed to fetch→ 检查后端是否启动
任务10:打包与部署(服务器长期运行)
1)打包后端项目
IDEA → Maven → smart-screen-backend → Lifecycle → clean → package
打包成功后,在 target/ 目录生成 smart-screen-backend-1.0.0-SNAPSHOT.jar
2)上传 JAR 到服务器
将 smart-screen-backend-1.0.0-SNAPSHOT.jar 上传到 master:/opt/jars/
3)启动后端服务
# 方法1:前台启动(临时测试,关闭终端服务停止)
java -jar /opt/jars/smart-screen-backend-1.0.0-SNAPSHOT.jar
# 方法2:后台启动(推荐,日志输出到文件)
nohup java -jar /opt/jars/smart-screen-backend-1.0.0-SNAPSHOT.jar \
> /opt/jars/smart-screen-backend.log \
2>&1 &
启动成功后会看到:
Tomcat started on port(s): 8080
Started SmartScreenBackendApplication
4)检查服务状态
# 查看进程
jps -l
# 查看端口
ss -tunlp | grep 8080
# 查看日志
tail -f /opt/jars/smart-screen-backend.log
5)停止服务
kill -9 进程号
任务11:测试接口
访问地址说明:
浏览器与后端在同一台电脑 →
http://localhost:8080浏览器与后端不在同一台电脑(jar 部署在 master) →
http://master:8080
所有接口统一返回格式:
{
"code": 200,
"msg": "ok",
"data": ...
}1. KPI 汇总接口
GET http://master:8080/api/kpi/summary
{
"code": 200,
"msg": "ok",
"data": {
"onlineCount": 50,
"alarmCount": 10,
"avgTemp": 26.35,
"highRiskCount": 8
}
}
字段说明:在线设备数、告警数、平均温度、高风险设备数。
2. 温度趋势接口
GET http://master: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.8192, "maxTemp": 45.27},
{"ts": "2026-02-16 14:30:10", "avgTemp": 27.0959, "maxTemp": 46.65}
]
}
字段说明:limit=60 查最近 60 条;每条含时间、平均温度、最高温度。
3. 告警 Top 接口
GET http://master: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}
]
}
字段说明:limit=3 查告警排名前 3 的设备。
4. 历史 7 天温度接口
GET http://master:8080/api/history/temp7d
{
"code": 200,
"msg": "ok",
"data": [
{"dt": "2026-04-23", "avgTemp": 26.1039},
{"dt": "2026-04-22", "avgTemp": 26.2274},
{"dt": "2026-04-21", "avgTemp": 26.1832}
]
}
字段说明:来源 ClickHouse dws_temp_trend_day,按日期倒序取 7 条。
5. 风险环形图接口
GET http://master:8080/api/predict/riskPie
{
"code": 200,
"msg": "ok",
"data": [
{"name": "正常", "value": 1433183},
{"name": "预警", "value": 1425},
{"name": "高危", "value": 681827}
]
}
字段说明:来源 ClickHouse dws_device_pred_detail,按 prob1 分段统计。
6. 高风险 Top3 接口
GET http://master:8080/api/predict/topRisk?limit=3
{
"code": 200,
"msg": "ok",
"data": [
{"deviceId": "device-044", "prob1": 1.0, "eventTime": "2026-01-29 07:41:00"},
{"deviceId": "device-022", "prob1": 1.0, "eventTime": "2026-01-28 17:46:00"},
{"deviceId": "device-013", "prob1": 1.0, "eventTime": "2026-01-01 17:39:00"}
]
}
字段说明:按 prob1 DESC 排序的前 3 条高风险设备。
✅ 验收清单
[ ]
/api/kpi/summary返回 KPI 汇总[ ]
/api/kpi/tempTrend返回温度趋势列表[ ]
/api/kpi/alarmTop返回告警 Top 排名[ ]
/api/history/temp7d返回历史温度[ ]
/api/predict/riskPie返回风险占比统计[ ]
/api/predict/topRisk返回高风险设备 Top[ ] 所有接口返回格式为
code + msg + data,msg为