李翔-大数据技术

Big data technology!

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

八、数据可视化 实验二:后端部署(2)


任务8:接 ClickHouse —— 实现 3 个大屏数据接口

目标: 实现 3 个接口,为大屏提供历史与预测数据:


接口路径用途
① 历史 7 天温度GET /api/history/temp7d柱形图
② 风险占比GET /api/predict/riskPie环形图
③ 高风险 Top3GET /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://localhost5173
后端(Spring Boot)http://localhost8080


端口不同 → 跨域 → 前端调接口会被浏览器拦截。

解决办法: 在后端加白名单,允许 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 验收

  1. 启动后端(8080)和前端(5173)

  2. 打开 http://localhost:5173/,按 F12 进入 Console

  3. 控制台中执行:

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 + datamsg"ok"


发表评论:

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

Powered By Z-BlogPHP 1.7.3

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