李翔-大数据技术

Big data technology!

数据可视化01-后端

analyse.zip

数据可视化展示步骤:

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中启动后端analyse

12.在IDEA中启动前端analyse-ui

# 在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

image-20250521224105423

image-20250521224256692

Spring Initializr → 依赖选择界面

image-20250518210603204

依赖的功能

依赖作用
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下创建包:controllerservicemappermodelutils

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 的代码自动生成工具】

image-20250501215254348

选择Getter and Setter;【自动为属性生成 getXxx()setXxx() 方法】

image-20250501215427205

在弹出框内选择需生成get和set方法的属性,点击确定

image-20250501215620130


同样的方法再生成构造函数空构造函数

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

image-20250520092944274

完整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实体类,该实体为用户消费类,用于接收用户消费数据

创建属性userIdtotalSpent(总支出)和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)调用常用注解Slf4jRestControllerRequestMapping

@注解 给代码“贴标签”,告诉Java程序:遇到这个标签,要做什么特别的事。

// Lombok库提供的注解,自动注入日志对象 log,可用于输出日志(如 log.info、log.error)
@Slf4j
// 声明这是一个 REST 风格的控制器,方法返回的对象会自动转换为 JSON 并响应给前端
@RestController
// 为当前控制器定义统一的请求路径前缀(可省略)
@RequestMapping()


接口说明:

接口路径功能说明数据来源数据来自的表名(或视图)
/hive支付方式与区域销售统计Hivepay_type_analysearea_sale_analyse
/hbase商品 ID 数量统计 & 商品价格总额统计HBaseOrderItemsHB
/redis各城市的实时统计数据Redis各城市作为 key(如 "广州市""北京市"
/clickhouse每日订单量 / 用户消费分析 / 商品销售 Top10ClickHouseOrdersOrderItemsProductInfo

image-20250518224540763

图表位置图表功能说明接口路径数据来源数据表名(或 Redis Key)图表类型
左上(图1)各种支付方式的使用情况/hiveHivepay_type_analyse柱状图
左中(图2)不同地区的销售情况/hiveHivearea_sale_analyse柱状图
左下(图3)订单销售排行(按商品 ID)/hbaseHBaseOrderItemsHB(按商品 ID 分组)表格(排行)
中间(图4)城市订单量(分布在地图)/redisRedis各城市名为 key(如 "广州市"热力图 / 地图
右上(图5)每天订单数量(趋势图)/clickhouseClickHouseOrders(字段:purchase)折线图
右中(图6)销售额占比(按商品 ID)/hbaseHBaseOrderItemsHB(统计价格总额)多色柱状图
中下(图7)用户购买排行(消费金额 Top10)/clickhouseClickHouseOrders + OrderItems表格(金额排行)
右下(图8)商品销售额排行(按分类)/clickhouseClickHouseOrderItems + 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 查询:

    1. 查询各种支付方式的使用次数【Hive中的pay_type_analyse表】

    2. 查询不同地区的销售数据【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 菜单:ViewTool WindowsDatabase

点击左上角的 ➕ 添加数据源 → 选择 Apache Hive

配置连接信息:

  • URL: jdbc:hive2://192.168.36.100:10000/default

  • 用户名: root

  • 密码: 123456

测试连接成功后,点击 Apply/OK

等待同步数据库结构,表名报红会自动消失


跳转到Hive接口 resultMap 数据格式示例

启动步骤:

  1. 用 IntelliJ IDEA 打开该项目目录analyse

  2. 确保 HiveServer2 正在运行

  3. 运行Spring Boot 启动类 AnalyseApplication ( Web 应用程序入口)。启动 Spring Boot 项目,开启 Java 后端服务

  4. 打开浏览器访问接口:

    http://localhost:8080/index
  5. 返回的数据类似:

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 中读取订单明细,然后分别统计:

    1. 每个商品(productid)被购买了多少次(频率)

    2. 每个商品的总销售额(价格总和)

  • 把结果封装成 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 中获取多个城市的订单数量数据,然后封装成一个列表结构,,返回给前端页面图表使用:

    1. 遍历城市列表,依次从 Redis 查询该城市的订单数量(如果没有查到则默认为 0),数据来源"实时数据分析"的“实验任务4”

    2. 将每个城市和对应的订单数量保存为 Model 对象(含 keyvalue 字段)

    3. 把所有城市数据放到一个列表中,通过 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.用户消费排行榜(条形图)

  • OrdersOrderItems 表关联

  • 统计每个用户的:

    • 总消费金额(SUM(price)

    • 平均每单消费金额(AVG(price)

  • 从高到低取前 10 名用户

  • 展示在条形图中(横向柱状图)


3.商品销售额排行榜(柱状图)

  • OrderItemsProductInfo 表关联

  • 按商品分类(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},
......
 ]
}




发表评论:

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

Powered By Z-BlogPHP 1.7.3

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