1.Zookeeper启动命令
# 在三个节点都要启动
zkServer.sh start
2.Hadoop启动命令
start-dfs.sh
start-yarn.sh
3.Hive服务启动命令
nohup hive --service metastore &
nohup hive --service hiveserver2 &
4.HBase服务启动命令
start-hbase.sh
5.redis服务启动命令
cd /opt/apps/redis/src
./redis-server /opt/apps/redis/redis.conf
6.Kafka服务启动命令
# 在三个节点都要启动
kafka-server-start.sh -daemon /opt/apps/kafka/config/server.properties
7.启动实时模拟数据
# 在master上操作
cd /
python3 main.py
8.Flume启动命令
# 新开第2个master窗口运行
flume-ng agent --conf /opt/apps/flume/conf/dataCleanLog --conf-file \
/opt/apps/flume/conf/dataCleanLog/flume_To_Kafka_dataClean.properties --name a1 \
-Dflume.root.logger=info,console
9.启动kafka消费者
# 新开第3个master窗口运行
kafka-console-consumer.sh --bootstrap-server master:9092 --topic my-topic
10.在IDEA中启动Flink实时任务
# 在IDEA中运行FlinkCityToRedis.scala
11.在IDEA中启动后端
# 在IDEA的终端[永久切换到Cmd]运行下面的命令【如下图】 # Settings → Tools → Terminal → Shell path npm run dev
一、基本要求
(1)熟悉Vue CLI、Vue Router等Vue生态系统中的重要工具;
(2)能够设计和实现可复用性强、性能优化的Vue组件;
(3)熟悉Echarts的基本配置和常用图表类型,如折线图、柱状图、饼图等;
(4)具备数据可视化设计的能力,能够根据数据的特性和展示需求,设计美观、易读的图表;
(5)在使用Echarts进行大规模数据可视化时,能够进行性能优化,提高图表的渲染速度和响应性。
二、前置环境设置
1.三台集群服务器 配置 Hadoop、Hive、Spark、HBase、Clickhouse 等大数据组件的运行环境,包括主节点和两个从节点。
2.Java 运行环境 安装 JDK8,用于支持后端 Spring Boot 项目的编译与运行。
3.离线分析环境 包括 Hadoop 分布式文件系统、Hive 数据仓库、Spark 分析引擎,用于处理历史数据分析任务。
4.实时分析环境 集成 Flume、Kafka、Flink 和 Redis,实现实时数据采集、处理与结果缓存。
5.IntelliJ IDEA 开发工具 用于编写 Java 后端(Spring Boot)和前端 Vue 项目,支持项目管理、调试与代码提示。
6.Windows 与集群的 IP 映射 编辑本地 hosts 文件,实现 Windows 与集群服务器的域名访问映射,如: 192.168.36.100 master
7.Vue(前端框架) 用于构建网页用户界面,实现页面显示与用户交互逻辑,适合构建可视化数据展示平台。
8.Node.js(前端运行环境) 用于运行前端开发服务器(如 Vite),支持执行 npm run dev 启动本地 Vue 页面预览与热更新。
⚠ 本项目中 Node.js 不用于后端接口开发,后端由 Spring Boot 提供。
三、Java后端(Spring boot)
1. 项目准备
项目搭建
(1)在IDEA创建springboot maven项目analyse
Spring Boot 是用来快速创建和运行 Java Web 应用的框架
Spring Boot 编写后端接口、连数据库、处理业务逻辑、返回数据给前端的一整套工具。
参见视频教程
1.1 创建SpringBoot2X项目
注意图中的以下设置:
JDK 是“工具箱-开发工具包”,Java 语言版本是“使用的语法”,两者要匹配,JDK 版本必须 ≥ Java 语言版本。
很多三方依赖和工具插件都是围绕 Java 8 编写的,兼容性好,但如果IDEA2022的JDK只有1.8,JAVA里选择不了1.8,解决方案:
方案一:【推荐方法】
替换创建项目的服务器网址:
https://start.aliyun.com,替换后,就可以选择Java8
Spring Initializr → 依赖选择界面

依赖的功能
依赖 作用 Spring Boot DevTools 开发工具,支持代码热部署,改代码不需要重启 Lombok 自动帮写代码的工具,比如自动生成 getter/setter/toString Spring Web 支持构建 Web 服务(Spring MVC),可写 REST 接口。Spring MVC 的作用是用来处理网页请求,把数据传给前端页面展示。 MySQL Driver 添加 MySQL 数据库的驱动,可以操作 MySQL
(2)在pom.xml文件中添加项目所需依赖,主要包括Spring boot、Hbase、ClickHouse、Redis、Lombok等相关依赖。
<?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">
<!-- Maven 项目的版本,不用动,所有 Maven 工程都是 4.0.0 -->
<modelVersion>4.0.0</modelVersion>
<!-- ========== 继承 Spring Boot 的默认父项目 ========= -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/> <!-- 不写路径,自动从 Maven 仓库下载 -->
</parent>
<!-- ========== 当前项目的基本信息 ========= -->
<groupId>com.hhrz</groupId> <!-- 组织或公司名(一般是公司或学校域名反写) -->
<artifactId>analyse</artifactId> <!-- 项目名称(决定 jar 包叫什么) -->
<version>0.0.1-SNAPSHOT</version> <!-- 项目版本 -->
<name>analyse</name> <!-- 项目名称(项目描述,起备注的作用) -->
<description>analyse</description> <!-- 项目描述(可选) -->
<url/> <!-- 项目的官网地址,可以不写 -->
<!-- ========== 项目用的 Java 版本 ========= -->
<properties>
<java.version>8</java.version> <!-- 使用 Java 8,因为很多大数据框架兼容最好 -->
</properties>
<!-- ========== 依赖(项目要用的各种库) ========= -->
<dependencies>
<!-- 1.Spring Boot Web 启动器(用来开发 Web 接口) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 2.Hive 的 JDBC 驱动 -->
<dependency>
<groupId>org.apache.hive</groupId>
<artifactId>hive-jdbc</artifactId>
<version>3.1.3</version>
<exclusions>
<!-- 排除 Hive 自带的 Jetty,不然和 Spring Boot 的 Web 组件冲突 -->
<exclusion>
<groupId>org.eclipse.jetty</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 3.HBase 的客户端依赖(用来操作 HBase 数据库) -->
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-server</artifactId>
<version>2.2.7</version>
<!-- 注意:hbase-server 包含了 HBase 的所有功能,包含客户端 -->
</dependency>
<!-- 4.ClickHouse 的 JDBC 驱动 -->
<dependency>
<groupId>com.clickhouse</groupId>
<artifactId>clickhouse-jdbc</artifactId>
<version>0.4.6</version>
</dependency>
<!-- 5.Redis 的 Java 客户端(Jedis) -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.1.5</version>
</dependency>
<!-- 6.Spring Boot 开发工具 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
<!--
作用:
- 开发阶段支持热部署(代码改了不用重启服务器)
- 生产环境不会生效(因为 scope=runtime)
-->
</dependency>
<!-- 7.Lombok(简化 Java 代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<!--
作用:
- 自动生成 get/set/toString 等方法
- 写实体类(Model、UserCast)时非常方便
- 注意:运行时不需要 Lombok,所以 optional=true
-->
</dependency>
<!-- 8.Spring Boot 的单元测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<!--
作用:
- 用来做 JUnit 单元测试
- 只有在写测试类或执行测试时才用
-->
</dependency>
</dependencies>
<!-- ========== Maven 构建配置 ========= -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<!-- 打包时不包含 Lombok(因为 Lombok 只在开发时用,运行时不需要) -->
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
创建 Spring Boot 项目结构
依次在项目的com.hhrz.analyse下创建包:controller、service、mapper、model、utils
Spring Boot 三层架构(项目的包结构)示意图
com.hhrz.analyse ├──
controller---> 控制器层:对接前端,接收请求,返回数据,放接口文件 │ └── XxxController.java ├── service ---> 业务逻辑层:处理具体的业务规则 │ └── impl ---> 业务实现类 │ └── XxxServiceImpl.java ├── mapper ---> 数据访问层(DAO):写数据库操作 │ └── XxxMapper.java ├──model---> 模型层:和数据库表一一对应的实体类 │ └── Xxx.java ├──utils---> 工具类:写项目通用的小工具 │ └── XxxUtil.java └──AnalyseApplication.java---> 启动类(带 @SpringBootApplication 注解)
总结:
controller 收请求 → service 做事 → mapper 找数据 → model 装数据 → utils 帮忙。
2. 实体编写
(1)在model文件夹内创建Model实体类,该实体为通用实体,用于接收key-value类型数据。
创建私有字段(属性)key和value,两者均为Sting类型
实体类就是用来表示一个“对象”或“一行数据”的类,里面只包含“构造函数+属性 + Getter/Setter” 方法。
private表示:外部不能直接访问,必须通过方法来访问(这是封装的原则)
内容 解释 Model是一个数据类,用来封装一组键值对(key / value) key类中的一个属性,通常代表“字段名” value类中的另一个属性,通常代表“字段值” get / set 方法 是外部读写属性的接口
package com.hhrz.analyse.model;
public class Model {
private String key;
private String value;
}
在属性字段的下方右击,选择Generate【 “生成” 是 IntelliJ IDEA 的代码自动生成工具】
选择Getter and Setter;【自动为属性生成 getXxx() 和 setXxx() 方法】
在弹出框内选择需生成get和set方法的属性,点击确定

同样的方法再生成构造函数和空构造函数
注意:在弹出的对话框中不勾选任何字段,直接点击 OK,即可创建空构造函数

完整Model实体类如下所示
package com.hhrz.analyse.model;
public class Model {
private String key;
private String value;
public Model() {
// 空构造函数
}
// 构造函数
public Model(String key, String value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
get 方法返回属性的值(要有返回类型)
set 方法修改属性的值(要有参数,但不返回任何东西)
(2)在model文件夹内创建UserCast实体类,该实体为用户消费类,用于接收用户消费数据
创建属性userId、totalSpent(总支出)和avgSpentPerOrder(每单平均支出),其中userId为String类型,totalSpent和avgSpentPerOrder为Integer类型
private String userId;
private Integer totalSpent;
private Integer avgSpentPerOrder;
使用IDEA软件自动生成get和set方法,完整UserCast实体类如下所示
package com.hhrz.analyse.model;
public class UserCast {
private String userId;
private Integer totalSpent;
private Integer avgSpentPerOrder;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public Integer getTotalSpent() {
return totalSpent;
}
public void setTotalSpent(Integer totalSpent) {
this.totalSpent = totalSpent;
}
public Integer getAvgSpentPerOrder() {
return avgSpentPerOrder;
}
public void setAvgSpentPerOrder(Integer avgSpentPerOrder) {
this.avgSpentPerOrder = avgSpentPerOrder;
}
}
3. 接口编写
(1)在utils下编写loadDriver方法,用于加载特定JDBC驱动
loadDriver(driverName)方法的作用1.检查指定的 JDBC 驱动类是否已经被注册到 Java 的 DriverManager 中(即:是否已经可以用来建立数据库连接);
2.如果未注册,则通过
Class.forName(driverName)使用反射机制手动加载该驱动类;3.驱动类在加载时,会在其静态代码块中自动将自身注册到 DriverManager,从而确保后续可以通过
DriverManager.getConnection(...)成功连接数据库。总结:
loadDriver()方法是为了确保项目中用的数据库驱动被 Java 正确加载,否则数据库没法连接。
pom.xml中添加了驱动依赖(JAR 包)只是“准备好了类文件”,Java 并不会自动加载驱动类,仍需调用loadDriver()或其他方式手动触发加载。
package com.hhrz.analyse.utils;
import java.sql.Driver;
import java.sql.DriverManager;
import java.util.Enumeration;
/**
* JDBC 工具类:提供数据库驱动加载方法(支持 Hive、MySQL 等)
*/
public class JdbcUtil {
/**
* 加载指定的数据库驱动(如 Hive JDBC 驱动)
* 该方法是线程安全的,只在第一次加载时执行,后续调用不会重复加载。
*
* @param driverName 要加载的驱动类全名,例如:org.apache.hive.jdbc.HiveDriver
* @throws Exception 如果加载失败,将抛出异常
*/
public static synchronized void loadDriver(String driverName) throws Exception {
// 获取当前 JVM 中已注册的所有 JDBC 驱动
Enumeration<Driver> drivers = DriverManager.getDrivers();
// 遍历所有已注册的驱动
while (drivers.hasMoreElements()) {
Driver driver = drivers.nextElement();
// 如果已经加载了指定的驱动类,则无需再次加载
if (driver.getClass().getName().equals(driverName)) {
return; // 已存在,直接返回
}
}
// 如果未找到指定驱动类,则通过反射方式加载它
Class.forName(driverName); // 会自动注册到 DriverManager 中
}
}
(2)在controller文件夹内创建各***Controller接口类,该类用于前后端数据交互
(3)调用常用注解Slf4j、RestController和RequestMapping
@注解给代码“贴标签”,告诉Java程序:遇到这个标签,要做什么特别的事。
// Lombok库提供的注解,自动注入日志对象 log,可用于输出日志(如 log.info、log.error)
@Slf4j
// 声明这是一个 REST 风格的控制器,方法返回的对象会自动转换为 JSON 并响应给前端
@RestController
// 为当前控制器定义统一的请求路径前缀(可省略)
@RequestMapping()
接口说明:
| 接口路径 | 功能说明 | 数据来源 | 数据来自的表名(或视图) |
|---|---|---|---|
/hive | 支付方式与区域销售统计 | Hive | pay_type_analyse、area_sale_analyse |
/hbase | 商品 ID 数量统计 & 商品价格总额统计 | HBase | OrderItemsHB |
/redis | 各城市的实时统计数据 | Redis | 各城市作为 key(如 "广州市"、"北京市") |
/clickhouse | 每日订单量 / 用户消费分析 / 商品销售 Top10 | ClickHouse | Orders、OrderItems、ProductInfo |

| 图表位置 | 图表功能说明 | 接口路径 | 数据来源 | 数据表名(或 Redis Key) | 图表类型 |
|---|---|---|---|---|---|
| 左上(图1) | 各种支付方式的使用情况 | /hive | Hive | pay_type_analyse | 柱状图 |
| 左中(图2) | 不同地区的销售情况 | /hive | Hive | area_sale_analyse | 柱状图 |
| 左下(图3) | 订单销售排行(按商品 ID) | /hbase | HBase | OrderItemsHB(按商品 ID 分组) | 表格(排行) |
| 中间(图4) | 城市订单量(分布在地图) | /redis | Redis | 各城市名为 key(如 "广州市") | 热力图 / 地图 |
| 右上(图5) | 每天订单数量(趋势图) | /clickhouse | ClickHouse | Orders(字段:purchase) | 折线图 |
| 右中(图6) | 销售额占比(按商品 ID) | /hbase | HBase | OrderItemsHB(统计价格总额) | 多色柱状图 |
| 中下(图7) | 用户购买排行(消费金额 Top10) | /clickhouse | ClickHouse | Orders + OrderItems | 表格(金额排行) |
| 右下(图8) | 商品销售额排行(按分类) | /clickhouse | ClickHouse | OrderItems + ProductInfo | 柱状图 |
Hive 数据交互接口
(4)调用GetMapping注解,并编写index方法,当HTTP GET请求发送到/index路径时,Spring MVC(请求接线员)将调用该方法获取hive数据。
解释:
Spring MVC是 Java Web 项目的“请求接线员”,把前端的请求自动分配给后台代码处理,简化开发。调用
@GetMapping("/hive")注解后,当前方法就会变成一个 接口入口。当浏览器或前端向地址
http://localhost:8080/hive发起 GET 请求 时,Spring Boot 会自动调用这个index()方法,去获取 Hive 数据,并把结果返回给前端。作用:
它是一个 接口方法,路径是
/index,前端可以访问它会连接 Hive 数据库,执行两个 SQL 查询:
查询各种支付方式的使用次数【Hive中的
pay_type_analyse表】查询不同地区的销售数据【Hive中的
area_sale_analyse表】把两个查询结果封装到
resultMap中,以 JSON 形式返回前端
package com.hhrz.analyse.controller;
import com.hhrz.analyse.utils.JdbcUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.sql.*;
import java.util.*;
/**
* 控制器:处理 Hive 数据查询接口
* 接口路径:http://localhost:8080/hive
*/
// 自动注入日志对象 log,可用于输出日志(如 log.info、log.error)
@Slf4j
// 标识这是一个 REST 控制器,自动将方法返回值转换为 JSON,供前端访问
@RestController
// 设置该控制器的统一请求路径前缀为 /hive:
// 这表示,类中的所有接口方法访问路径都会以 /hive 开头,
// 如果方法上写的是 @GetMapping("/pay"),实际访问地址就是 /hive/pay。
@RequestMapping("/hive")
public class HiveController {
// 处理 GET 请求:当用户访问 http://localhost:8080/hive 时,会执行此方法
@GetMapping
public Map<String, Object> hive() {
// 创建一个结果映射,用于存储返回给接口的数据
Map<String, Object> resultMap = new HashMap<>();
// 先放一个成功状态码200,表示成功
resultMap.put("code", 200);
try {
// 加载Hive JDBC驱动
JdbcUtil.loadDriver("org.apache.hive.jdbc.HiveDriver");
// 加载Hive JDBC驱动
// loadDriver("org.apache.hive.jdbc.HiveDriver");
// 创建一个列表,用于存储支付类型分析数据
List<Map<String, Object>> payTypeData = new ArrayList<>();
// 定义Hive数据库的JDBC URL、用户名和密码
String url = "jdbc:hive2://192.168.36.100:10000/default";
java.sql.Connection conn = java.sql.DriverManager.getConnection(url, "root", "123456");
// 创建Statement对象,用于执行SQL查询
Statement statement = conn.createStatement();
// 执行查询,获取支付类型分析数据
ResultSet resultSet = statement.executeQuery("SELECT * from pay_type_analyse");
// 一条条读取结果中的数据
while (resultSet.next()) {
// 从结果集中提取数据,并存储到临时映射中
String payType = resultSet.getString("pay_type"); // 支付方式(如:微信、支付宝)
int total = resultSet.getInt("total"); // 使用次数
Map<String, Object> row = new HashMap<>(); // 把每条数据存成一个小Map
row.put("payType", payType);
row.put("total", total); // 格式如:{"payType":"微信","total":108}
payTypeData.add(row); // 把小Map放到列表中,[{ "payType": "微信", "total": 108 } ,{"payType": "支付宝", "total": 95 }]
}
// 关闭Statement对象,释放资源
statement.close();
// 将支付类型分析数据添加到结果映射中
resultMap.put("payTypeData", payTypeData);
/* 返回的resultMap数据格式如下:
{
"payTypeData": [
{"payType":"微信", "total":108 },
{"payType":"支付宝", "total":95 },
......
]
}
*/
// 重新创建Statement对象,用于执行下一个SQL查询
statement = conn.createStatement();
// 执行查询,获取区域销售分析数据
ResultSet resultSet2 = statement.executeQuery("SELECT * from area_sale_analyse");
List<Map<String, Object>> areaData = new ArrayList<>();
while (resultSet2.next()) {
// 从结果集中提取数据,并存储到临时映射中
Map<String, Object> row = new HashMap<>();
String statename = resultSet2.getString("Statename"); // 区域名
int total = resultSet2.getInt("total"); // 销售数量
row.put("statename", statename);
row.put("total", total); // 格式如:{"statename":"广州","total":100}
areaData.add(row); // 把小Map放到列表中,[{"statename":"广州","total":100} ,{"statename":"北京","total":158}]
}
// 关闭Statement对象,释放资源
statement.close();
// 将区域销售分析数据添加到结果映射中
resultMap.put("areaData", areaData);
/* 返回的resultMap数据格式如下:
{
"areaData": [
{"statename":"广州","total":100},
{"statename":"北京","total":158},
......
]
}
*/
// 关闭数据库连接,释放资源
conn.close();
} catch (Exception e) {
// 如果发生异常,设置状态码为500,并存储异常信息
resultMap.put("code", 500);
// 返回错误信息
resultMap.put("message", e.getMessage());
// 把错误也写到控制台(方便排查问题)
log.error("index error", e);
}
// 7.Spring Boot 会根据 @RestController 注解,自动将返回的 Java 对象(如 Map)序列化为 JSON,并作为 HTTP 响应发送给前端
// 如果返回值中包含集合(如 List<Map<String, Object>>),每个元素也会被转换为对应的 JSON 对象,其中 Map 的键会变为 JSON 的字段名
return resultMap;
}
}
如果代码报红:
❗ Unable to resolve table 'pay_type_analyse' ❗ Unable to resolve table 'area_sale_analyse'
原因:IntelliJ IDEA 识别不了这两个表(在代码中执行了 Hive 查询),说明没有为 IDEA 配置 Data Source(数据源)连接 Hive,或者 这些表在实际 Hive 中并不存在。如果Hive中有此有表,此报错可忽略!
解决方法:也配置 Hive 数据源,使项目识别SQL
步骤:
打开 IntelliJ IDEA 菜单:
View→Tool Windows→Database点击左上角的 ➕ 添加数据源 → 选择 Apache Hive
配置连接信息:
URL:
jdbc:hive2://192.168.36.100:10000/default用户名:
root密码:
123456测试连接成功后,点击 Apply/OK
等待同步数据库结构,表名报红会自动消失
启动步骤:
用 IntelliJ IDEA 打开该项目目录
analyse确保
HiveServer2正在运行运行Spring Boot 启动类 AnalyseApplication ( Web 应用程序入口)。启动 Spring Boot 项目,开启 Java 后端服务
打开浏览器访问接口:
http://localhost:8080/index
返回的数据类似:
resultMap = {
"code" : 200,
"payTypeData" : [
{ "payType": "boleto", "total": 19784 },
{ "payType": "credit_card", "total": 76505 },
{ "payType": "debit_card", "total": 1528 },
......
],
"areaData" : [
{ "statename": "Acre", "total": 16108 },
{ "statename": "Alagoas", "total": 79022 },
{ "statename": "Bahia", "total": 489701 },
......
]
}
hbase数据交互接口
(5)调用GetMapping注解,并编写hbase方法,当HTTP GET请求发送到/hbase路径时,Spring MVC将调用该方法获取hbase数据。
解释:
调用
@GetMapping("/hbase")注解后,当前方法就会变成一个 接口入口。当浏览器或前端向地址
http://localhost:8080/hbase发起 GET 请求 时,Spring Boot 会自动调用这个hbase()方法,去获取 Hbase 数据,并把结果返回给前端。作用:
从 HBase 表
OrderItemsHB中读取订单明细,然后分别统计:
每个商品(productid)被购买了多少次(频率)
每个商品的总销售额(价格总和)
把结果封装成 JSON 返回给前端图表用。
package com.hhrz.analyse.controller;
import com.hhrz.analyse.model.Model;
import lombok.extern.slf4j.Slf4j;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
/**
* 控制器:处理 HBase 数据查询接口
* 接口路径:http://localhost:8080/hbase
*/
// 自动注入日志对象 log,可用于输出日志(如 log.info、log.error)
@Slf4j
// 标识这是一个 REST 控制器,自动将方法返回值转换为 JSON,供前端访问
@RestController
// 设置该控制器的统一请求路径前缀为 /hbase,所有接口路径都将从 /hbase 开始
@RequestMapping("/hbase")
public class HBaseController {
// 处理 GET 请求:当用户访问 http://localhost:8080/hbase 时,会执行此方法
@GetMapping
public Map<String, Object> queryHBase() {
Map<String, Object> resultMap = new HashMap<>();
try {
// 1. 配置 HBase 连接参数
Configuration config = new Configuration();
config.set("hbase.zookeeper.quorum", "master,slave1,slave2");
config.set("hbase.zookeeper.property.clientPort", "2181");
config.set("hbase.master", "192.168.36.100:6000");
// 2. 创建连接对象,用于操作 HBase 表
Connection connection = ConnectionFactory.createConnection(config);
Table table = connection.getTable(TableName.valueOf("OrderItemsHB")); // 表名:OrderItemsHB
// 3. 全表扫描(取出所有数据)
Scan scan = new Scan();
ResultScanner scanner = table.getScanner(scan);
// 4. 创建一个列表,用于存储每一行的数据
List<Map<String, Object>> rowList = new ArrayList<>();
// 遍历每一行 Result,每一行是 HBase 的一条记录
for (Result result : scanner) {
Map<String, Object> rowMap = new HashMap<>();
rowMap.put("rowKey", Bytes.toString(result.getRow())); //提取rowKey,放入rowMap
// 遍历每一列(字段) → 把列族、字段名和值拼成 key-value
for (Cell cell : result.rawCells()) {
String family = Bytes.toString(cell.getFamilyArray(), cell.getFamilyOffset(), cell.getFamilyLength());
String qualifier = Bytes.toString(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength());
String value = Bytes.toString(cell.getValueArray(), cell.getValueOffset(), cell.getValueLength());
rowMap.put(family + ":" + qualifier, value);
}
rowList.add(rowMap); // 每一行是一个 Map,加入列表中
}
/* 🔸示例 rowList:
[
{rowKey:r001, goodsinfo:productid=P001, goodsinfo:price=100.0},
{rowKey:r002, goodsinfo:productid=P002, goodsinfo:price=50.0},
{rowKey:r003, goodsinfo:productid=P001, goodsinfo:price=80.0}
]
*/
// 5. 统计每个 productid 的订单数量(即出现的次数)
Map<String, Integer> countMap = new HashMap<>();
for (Map<String, Object> row : rowList) {
// 从每行数据中提取字段 "goodsinfo:productid",并强制转换为 String 类型。
String productId = (String) row.get("goodsinfo:productid");
if (productId != null) {
// productId 是要插入或更新的键,1 是默认值,Integer::sum 表示如果键已存在就将旧值与新值相加。
countMap.merge(productId, 1, Integer::sum);
}
}
// 🔸示例 countMap:
// {"P001"=2, "P002"=1}
// 6. 把统计结果封装成 Model 列表(用于前端展示)
/*
遍历 Map<K, V> 的常用 for 循环方式的通用语法格式
for (Map.Entry<String, Double> entry : map.entrySet()) {
String key = entry.getKey();
Double value = entry.getValue();
// 处理 key 和 value
}
*/
// orderCountList的格式:[Model("P001", "2"), Model("P002", "5"), ...]
List<Model> orderCountList = new ArrayList<>();
for (Map.Entry<String, Integer> entry : countMap.entrySet()) {
orderCountList.add(new Model(entry.getKey(), entry.getValue().toString()));
}
// 7. 按订单数量降序排序,最多取前100条
orderCountList.sort((a, b) -> Integer.parseInt(b.getValue()) - Integer.parseInt(a.getValue()));
//topOrderList的格式:[Model("P002", "5"), Model("P003", "3"), ...]
List<Model> topOrderList = orderCountList.subList(0, Math.min(100, orderCountList.size()));
// 8. 放入最终返回的数据中(订单数分析结果)
// 执行return resultMap时,Spring Boot根据@RestController自动调用Jackson把List<Model>转成 JSON 数组
// 由 Java 类的字段名(如 key、value)自动映射成 JSON 的 key 名称
// resultMap的格式: { "orderData": [ { "key": ..., "value": ... } ] }
resultMap.put("orderData", topOrderList);
// 9. 统计每个 productid 的销售总额(price 字段求和)
// 创建一个 Map,用于统计每个商品 productId 的销售总额
// key 是商品编号(如 "P001"),Value 是总销售金额(如 180.0)
Map<String, Double> priceMap = new HashMap<>();
// 遍历 rowList(之前从 HBase 中取出的每一行数据)
for (Map<String, Object> row : rowList) {
// 从当前行中取出商品编号 productId
String productId = (String) row.get("goodsinfo:productid");
// 从当前行中取出价格字段 price,对应的是 Object 类型(可能是字符串)
Object priceObj = row.get("goodsinfo:price");
// 如果两个字段都不为空,才继续处理
if (productId != null && priceObj != null) {
// 价格转成字符串再转为 double(用于计算)
double price = Double.parseDouble(priceObj.toString());
// productId 是要插入或更新的键,1 是默认值,Integer::sum 表示如果键已存在就将旧值与新值相加。
priceMap.merge(productId, price, Double::sum);
}
}
// 🔸示例 priceMap:
// {P001=180.0, P002=50.0}
// 10. 把销售总额封装为 Model 列表
List<Model> salesList = new ArrayList<>();
/*
遍历 Map<K, V> 的常用 for 循环方式的通用语法格式
for (Map.Entry<String, Double> entry : map.entrySet()) {
String key = entry.getKey();
Double value = entry.getValue();
// 处理 key 和 value
}
*/
for (Map.Entry<String, Double> entry : priceMap.entrySet()) {
salesList.add(new Model(entry.getKey(), String.format("%.2f", entry.getValue())));
}
// salesList数据格式:[Model("P001","50.00"),Model("P002", "180.00"),...]
// 11. 按销售额降序排序,最多取前100条
salesList.sort((a, b) -> Double.compare(Double.parseDouble(b.getValue()), Double.parseDouble(a.getValue())));
// topSalesList数据格式:[Model("P002","180.00"),Model("P002", "50.00"),...]
List<Model> topSalesList = salesList.subList(0, Math.min(100, salesList.size()));
// 12. 放入最终返回的数据中(销售额分析结果)
/*
{
"orderData": [ ... ],
"salesData": [
{ "key": "P002", "value": "180.00" },
{ "key": "P001", "value": "50.00" }
]
}
*/
resultMap.put("salesData", topSalesList);
// 13. 关闭资源
scanner.close();
table.close();
connection.close();
// 返回成功状态码
resultMap.put("code", 200);
} catch (Exception e) {
// 出错时,返回错误码和异常信息
resultMap.put("code", 500);
resultMap.put("message", e.getMessage());
log.error("HBase 查询失败", e);
}
return resultMap;
}
}
返回的数据格式样例
{
"code": 200,
"orderData": [
{
"key": "P001",
"value": "2"
},
{
"key": "P002",
"value": "1"
}
......
],
"salesData": [
{
"key": "P001",
"value": "180.00"
},
{
"key": "P002",
"value": "50.00"
}
......
]
}
redis数据交互接口(实时数据)
(6)调用GetMapping注解,并编写redis方法,当HTTP GET请求发送到/redis路径时,Spring MVC将调用该方法获取redis数据。
任务:循环获取 Redis 中所有城市的 value,把 key 和 value 封装成 Model,最终放入 resultMap 返给前端。
解释:
调用
@GetMapping("/redis")注解后,当前方法就会变成一个 接口入口。当浏览器或前端向地址
http://localhost:8080/redis发起 GET 请求 时,Spring Boot 会自动调用这个redis()方法,去获取 redis 数据,并把结果返回给前端。作用:
从 Redis 中获取多个城市的订单数量数据,然后封装成一个列表结构,,返回给前端页面图表使用:
遍历城市列表,依次从 Redis 查询该城市的订单数量(如果没有查到则默认为 0),数据来源"实时数据分析"的“实验任务4”
将每个城市和对应的订单数量保存为
Model对象(含key、value字段)把所有城市数据放到一个列表中,通过
Map封装返回给前端最终作用:用于实时可视化城市订单热力图、地图展示等。
修改Redis的配置
# 修改配置文件
vim /opt/apps/redis/redis.conf
# 修改下面配置项,允许所有 IP 访问 Redis(适合局域网部署或测试环境)
bind 0.0.0.0
# 关闭保护模式,允许任何机器连接到 Redis【默认保护模式是开启的,只允许本机的客户端访问】。
protected-mode no
完整代码:
package com.hhrz.analyse.controller;
import com.hhrz.analyse.model.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;
import java.util.*;
/**
* 控制器:处理 Redis 查询城市数据的接口
* 接口路径:http://localhost:8080/redis
*/
// 自动注入日志对象 log,可用于输出日志(如 log.info、log.error)
@Slf4j
// 标识这是一个 REST 控制器,自动将方法返回值转换为 JSON,供前端访问
@RestController
// 设置该控制器的统一请求路径前缀为 /redis,所有接口路径都将从 /redis 开始
@RequestMapping("/redis")
public class RedisController {
// 处理 GET 请求:当用户访问 http://localhost:8080/redis 时,会执行此方法
@GetMapping()
public Map<String, Object> redis() {
// 创建一个用于返回结果的 Map(返回给前端)
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", 200); // 设置状态码为 200 表示成功
// 1. 创建 Jedis 实例,连接到 Redis 服务器 master 节点,端口 6379
Jedis jedis = new Jedis("master", 6379);
// 2. 定义要查询的城市名列表(作为 Redis 的 key)
List<String> keys = Arrays.asList("北京市", "上海市", "天津市", "重庆市", "广州市", "深圳市", "石家庄市", "唐山市", "秦皇岛市", "邯郸市", "邢台市", "保定市", "张家口市", "承德市", "沧州市", "廊坊市", "衡水市", "太原市", "大同市", "阳泉市", "长治市", "晋城市", "朔州市", "晋中市", "运城市", "忻州市", "临汾市", "吕梁市", "呼和浩特市", "包头市", "乌海市", "赤峰市", "通辽市", "鄂尔多斯市", "呼伦贝尔市", "巴彦淖尔市", "乌兰察布市", "沈阳市", "大连市", "鞍山市", "抚顺市", "本溪市", "丹东市", "锦州市", "营口市", "阜新市", "辽阳市", "盘锦市", "铁岭市", "朝阳市", "葫芦岛市", "长春市", "吉林市", "四平市", "辽源市", "通化市", "白山市", "松原市", "白城市", "哈尔滨市", "齐齐哈尔市", "鸡西市", "鹤岗市", "双鸭山市", "大庆市", "伊春市", "佳木斯市", "七台河市", "牡丹江市", "黑河市", "绥化市", "南京市", "无锡市", "徐州市", "常州市", "苏州市", "南通市", "连云港市", "淮安市", "盐城市", "扬州市", "镇江市", "泰州市", "宿迁市", "杭州市", "宁波市", "温州市", "嘉兴市", "湖州市", "绍兴市", "金华市", "衢州市", "舟山市", "台州市", "丽水市", "合肥市", "芜湖市", "蚌埠市", "淮南市", "马鞍山市", "淮北市", "铜陵市", "安庆市", "黄山市", "阜阳市", "宿州市", "滁州市", "六安市", "宣城市", "池州市", "亳州市", "福州市", "厦门市", "莆田市", "三明市", "泉州市", "漳州市", "南平市", "龙岩市", "宁德市", "南昌市", "景德镇市", "萍乡市", "九江市", "抚州市", "鹰潭市", "赣州市", "吉安市", "宜春市", "新余市", "上饶市", "济南市", "青岛市", "淄博市", "枣庄市", "东营市", "烟台市", "潍坊市", "济宁市", "泰安市", "威海市", "日照市", "临沂市", "德州市", "聊城市", "滨州市", "菏泽市", "武汉市", "黄石市", "十堰市", "宜昌市", "襄阳市", "鄂州市", "荆门市", "孝感市", "荆州市", "黄冈市", "咸宁市", "随州市", "长沙市", "株洲市", "湘潭市", "衡阳市", "邵阳市", "岳阳市", "常德市", "张家界市", "益阳市", "郴州市", "永州市", "怀化市", "娄底市", "南宁市", "柳州市", "桂林市", "梧州市", "北海市", "防城港市", "钦州市", "贵港市", "玉林市", "百色市", "贺州市", "河池市", "来宾市", "崇左市", "海口市", "三亚市", "三沙市", "儋州市", "成都市", "自贡市", "攀枝花市", "泸州市", "德阳市", "绵阳市", "广元市", "遂宁市", "内江市", "乐山市", "南充市", "眉山市", "宜宾市", "广安市", "达州市", "雅安市", "巴中市", "资阳市", "贵阳市", "六盘水市", "遵义市", "安顺市", "毕节市", "铜仁市", "昆明市", "曲靖市", "玉溪市", "保山市", "昭通市", "丽江市", "普洱市", "临沧市", "拉萨市", "日喀则市", "昌都市", "林芝市", "山南市", "那曲市", "西安市", "铜川市", "宝鸡市", "咸阳市", "渭南市", "延安市", "汉中市", "榆林市", "安康市", "商洛市", "兰州市", "嘉峪关市", "金昌市", "白银市", "天水市", "武威市", "张掖市", "平凉市", "酒泉市", "庆阳市", "定西市", "陇南市", "西宁市", "海东市", "银川市", "石嘴山市", "吴忠市", "固原市", "中卫市", "乌鲁木齐市", "克拉玛依市", "吐鲁番市", "哈密市");
// 3. 创建一个 List<Model> 用于封装每个查询到的城市数据
List<Model> list = new ArrayList<>();
// 4. 遍历每个城市名称,从 Redis 中读取其对应的值
for (String key : keys) {
String value = jedis.get(key); // 读取 Redis 中该城市的值(订单数)
Model model = new Model(); // 创建一个对象来封装这个 key-value 对
model.setKey(key); // 设置 key 为当前城市名
if (value != null) {
model.setValue(value); // 如果 Redis 有值,则设置为真实值
} else {
model.setValue("0"); // 如果 Redis 没有值,则设置默认值 "0"
}
list.add(model); // 把封装好的对象加入列表
}
// 5. 关闭 Redis 连接
jedis.close();
// 6. 将最终封装的数据列表加入返回结果 Map 中
resultMap.put("data", list);
// 7. 返回结果 Map,Spring Boot 会根据 @RestController 注解自动将其序列化为 JSON 格式,作为接口响应返回给前端
// 数据结构中的对象(如 List<Model>)也会被逐个转成 JSON 对象,字段变成 JSON 的 key
return resultMap;
}
}
返回的 resultMap Java 结构可能是:
java复制编辑{
"code" → 200,
"data" → List[
Model(key="北京市", value="12"),
Model(key="上海市", value="9")
]
}
最终被转成前端能接收的 JSON:
{
"code": 200,
"data": [
{ "key": "乌鲁木齐市", "value": "1" },
{ "key": "北京", "value": "2" },
{ "key": "上海", "value": "3" },
{ "key": "广州", "value": "4" },
{ "key": "深圳", "value": "5" }
]
}
clickhouse数据交互接口
(7)调用GetMapping注解,并编写clickhouse方法,当HTTP GET请求发送到/clickhouse路径时,Spring MVC将调用该方法获取clickhouse数据。
实验任务:基于 ClickHouse 的 Orders、OrderItems、ProductInfo 三张表,完成每日订单、用户消费、商品销售额的统计,并将结果封装到 resultMap,返回给前端。
解释:
调用
@GetMapping("/clickhouse")注解后,当前方法就会变成一个 接口入口。当浏览器或前端向地址
http://localhost:8080/clickhouse发起 GET 请求 时,Spring Boot 会自动调用这个clickhouse()方法,去获取clickhouse数据,并把结果返回给前端。作用:从 ClickHouse 数据库中读取订单和消费相关的统计信息,封装为 JSON 返回前端图表使用
1.每日订单数量统计(折线图)
从
Orders表中查询每天的订单数用于展示每日交易趋势(X轴是日期,Y轴是订单数)
2.用户消费排行榜(条形图)
把
Orders和OrderItems表关联统计每个用户的:
总消费金额(
SUM(price))平均每单消费金额(
AVG(price))从高到低取前 10 名用户
展示在条形图中(横向柱状图)
3.商品销售额排行榜(柱状图)
把
OrderItems和ProductInfo表关联按商品分类(
categoryName)统计总销售额取前 10 个分类,展示在柱状图中(Y轴是金额)
接口最终返回的是一个 JSON 格式的
Map<String, Object>,结构如下:{
"code": 200,
"dayData": [...], // 每日订单量数据(List<Map>)
"userCastData": [...], // 用户消费数据(List<UserCast>)
"saleData": [...] // 商品分类销售数据(List<Map>)
}
完整的代码:
package com.hhrz.analyse.controller;
import com.hhrz.analyse.model.UserCast;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.*;
/**
* 控制器类:用于处理 ClickHouse 数据查询接口
* 接口路径:http://localhost:8080/clickhouse
*/
// 自动注入日志对象 log,可用于输出日志(如 log.info、log.error)
@Slf4j
// 标识这是一个 REST 控制器,自动将方法返回值转换为 JSON,供前端访问
@RestController
// 设置该控制器的统一请求路径前缀为 /clickhouse,所有接口路径都将从 /clickhouse 开始
@RequestMapping("/clickhouse")
public class ClickHouseController {
// 处理 GET 请求:当用户访问 http://localhost:8080/redis 时,会执行此方法
@GetMapping
public Map<String, Object> clickhouse() {
// 创建一个 Map 封装返回数据(包含多组结果)
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", 200); // 默认设置状态码为 200(成功)
try {
// ===============================
// 【步骤一】连接 ClickHouse 数据库
// ===============================
// Class.forName("com.clickhouse.jdbc.ClickHouseDriver");
// 加载JDBC驱动
JdbcUtil.loadDriver("com.clickhouse.jdbc.ClickHouseDriver");
String url = "jdbc:clickhouse://192.168.36.100:8123/analysisdb?compress_algorithm=gzip";
Connection conn = DriverManager.getConnection(url, "default", "");
// ===============================
// 【步骤二】查询每日订单量(按 purchase 日期分组)
// ===============================
Statement statement = conn.createStatement();
ResultSet resultSet = statement.executeQuery(
"SELECT purchase AS order_date, COUNT(*) AS order_count " +
"FROM Orders GROUP BY purchase ORDER BY purchase");
List<Map<String, Object>> dayData = new ArrayList<>();
while (resultSet.next()) {
Map<String, Object> row = new HashMap<>();
row.put("orderDate", resultSet.getString("order_date"));
row.put("orderCount", resultSet.getInt("order_count")); //row的数据格式:{ "orderDate": "2024-05-01", "orderCount": 18 }
dayData.add(row);
}
/* dayData 数据格式
[
{ "orderDate": "2024-05-01", "orderCount": 18 },
{ "orderDate": "2024-05-02", "orderCount": 21 },
......
]
*/
resultSet.close();
statement.close();
resultMap.put("dayData", dayData); // 将订单量数据放入结果中
// ===============================
// 【步骤三】查询用户消费总额与平均值(前10名)
// ===============================
statement = conn.createStatement();
ResultSet resultSet2 = statement.executeQuery(
"SELECT o.cid AS user_id, SUM(oi.price) AS total_spent, " +
"AVG(oi.price) AS avg_spent_per_order " +
"FROM Orders AS o " +
"INNER JOIN OrderItems AS oi ON o.oid = oi.oid " +
"GROUP BY o.cid ORDER BY total_spent DESC LIMIT 10");
List<UserCast> userCastList = new ArrayList<>();
while (resultSet2.next()) {
UserCast userCast = new UserCast(); // {"userId": "u001", "totalSpent": 880, "avgSpentPerOrder": 146}
userCast.setUserId(resultSet2.getString("user_id"));
userCast.setTotalSpent(resultSet2.getInt("total_spent"));
userCast.setAvgSpentPerOrder(resultSet2.getInt("avg_spent_per_order"));
userCastList.add(userCast);
}
/* userCastList 数据格式
[
{"userId": "u001", "totalSpent": 880, "avgSpentPerOrder": 146},
{"userId": "u005", "totalSpent": 750, "avgSpentPerOrder": 125},
......
]
*/
resultSet2.close();
statement.close();
resultMap.put("userCastData", userCastList);
// ===============================
// 【步骤四】查询商品分类销售额(Top 10)
// ===============================
statement = conn.createStatement();
ResultSet resultSet3 = statement.executeQuery(
"SELECT p.pid AS product_id, p.categoryName AS product_category, " +
"SUM(oi.price) AS total_sales " +
"FROM OrderItems AS oi " +
"INNER JOIN ProductInfo AS p ON oi.pid = p.pid " +
"GROUP BY p.pid, p.categoryName " +
"ORDER BY total_sales DESC LIMIT 10");
List<Map<String, Object>> saleData = new ArrayList<>();
while (resultSet3.next()) {
Map<String, Object> row = new HashMap<>();
row.put("productId", resultSet3.getString("product_id"));
row.put("productCategory", resultSet3.getString("product_category"));
row.put("totalSales", resultSet3.getInt("total_sales"));
saleData.add(row);
}
/* saleData 数据格式
[
{"productId": "p001", "productCategory": "手机", "totalSales": 1300},
{"productId": "p004", "productCategory": "平板电脑", "totalSales": 980},
......
]
*/
resultSet3.close();
statement.close();
conn.close(); // 关闭数据库连接
resultMap.put("saleData", saleData); // 将分类销售额放入结果中
} catch (Exception e) {
// 异常处理:记录错误并设置失败信息
resultMap.put("code", 500);
resultMap.put("message", e.getMessage());
log.error("ClickHouse 查询失败", e);
}
// 返回所有分析结果数据(包含:dayData、userCastData、saleData)
return resultMap;
}
}
返回的数据格式样例
{
"code": 200,
"dayData": [
{ "orderDate": "2024-05-01", "orderCount": 18 },
{ "orderDate": "2024-05-02", "orderCount": 21 },
......
],
"userCastData": [
{"userId": "u001", "totalSpent": 880, "avgSpentPerOrder": 146},
{"userId": "u005", "totalSpent": 750, "avgSpentPerOrder": 125},
......
],
"saleData": [
{"productId": "p001", "productCategory": "手机", "totalSales": 1300},
{"productId": "p004", "productCategory": "平板电脑", "totalSales": 980},
......
]
}


