基于 Hadoop MapReduce + Spring Boot + Vue 3 的每日饮水数据分析平台

基于 Hadoop MapReduce + Spring Boot + Vue 3 的每日饮水数据分析平台

本文详细介绍一个完整的大数据分析项目,从数据采集、MapReduce 分析处理到前端可视化展示的全流程实现。项目采用 Hadoop MapReduce 进行大数据分析,Spring Boot 提供 RESTful API,Vue 3 + ECharts 实现数据可视化。

一、项目简介

每日饮水数据分析平台是一个基于大数据技术栈的健康数据分析系统,旨在通过分析个体每日饮水量与补水模式,探索年龄、性别、体重、身体活动水平及天气状况等多重因素对饮水行为的影响。

1.1 项目背景

本数据集聚焦于个体每日饮水量与水分摄入水平的研究,综合了人口统计学特征、生活方式及环境因素。每条记录代表一个独立个体,系统展示了年龄、性别、体重、身体活动水平及天气状况等多重变量如何共同影响其每日饮水量与整体水分状态。

1.2 数据字段说明

英文字段 中文名称 类型 说明
Age 年龄 数值 个体的年龄
Gender 性别 字符串 Male/Female
Weight (Kg) 体重 数值 体重(千克)
Daily Water Intake (liters) 每日饮水量 数值 每日饮水量(升)
Physical Activity Level 身体活动水平 字符串 活动强度等级
Weather 天气状况 字符串 天气类型
Hydration Level 饮水水平 字符串 良好/不良

二、技术架构

2.1 整体架构

本项目采用前后端分离的架构设计,结合大数据处理技术,实现从数据采集、分析处理到可视化展示的完整技术链路。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        前端层 (Vue 3)                            │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │ Vue Router   │  │ Element Plus │  │   ECharts    │          │
│  └──────────────┘  └──────────────┘  └──────────────┘          │
└─────────────────────────────────────────────────────────────────┘
                              │ HTTP/REST
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    后端层 (Spring Boot)                           │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │  Controller  │  │   Service    │  │   Config     │          │
│  └──────────────┘  └──────────────┘  └──────────────┘          │
└─────────────────────────────────────────────────────────────────┘
                              │ 读取文件
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                  数据处理层 (MapReduce)                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │    Mapper    │  │   Reducer    │  │    Driver    │          │
│  └──────────────┘  └──────────────┘  └──────────────┘          │
└─────────────────────────────────────────────────────────────────┘
                              │ 处理数据
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                       数据存储层                                 │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
│  │  CSV 数据文件 │  │  输出结果文件 │  │  本地文件系统 │          │
│  └──────────────┘  └──────────────┘  └──────────────┘          │
└─────────────────────────────────────────────────────────────────┘

2.2 技术栈

后端技术:

  • Java 1.8
  • Spring Boot 2.7.18
  • Hadoop 3.3.4
  • Maven

前端技术:

  • Vue.js 3.5.26
  • Vue Router 4.4.5
  • Element Plus 2.13.1
  • ECharts 5.5.1
  • Axios 1.13.2
  • Vite 7.3.0

三、核心功能

项目实现了以下 6 种数据分析维度:

功能 说明
按性别统计平均饮水量 分析不同性别的平均饮水量差异
按活动水平统计平均饮水量 分析不同活动水平下的饮水量
按天气统计平均饮水量 分析不同天气对饮水量的影响
按性别统计饮水水平分布 统计不同性别的饮水水平分布情况
按年龄段统计平均饮水量 分析不同年龄段的饮水量趋势
按活动水平和天气统计平均饮水量 分析活动水平和天气的综合影响

四、源码分析

4.1 MapReduce 模块

4.1.1 数据解析工具类

WaterIntakeParser.java 负责解析 CSV 数据行,提取各个字段。

java 复制代码
public class WaterIntakeParser {
    private int age;
    private String gender;
    private double weight;
    private double dailyWaterIntake;
    private String physicalActivityLevel;
    private String weather;
    private String hydrationLevel;

    public WaterIntakeParser(String line) {
        String[] fields = line.split(",");
        if (fields.length >= 7) {
            try {
                this.age = Integer.parseInt(fields[0].trim());
                this.gender = fields[1].trim();
                this.weight = Double.parseDouble(fields[2].trim());
                this.dailyWaterIntake = Double.parseDouble(fields[3].trim());
                this.physicalActivityLevel = fields[4].trim();
                this.weather = fields[5].trim();
                this.hydrationLevel = fields[6].trim();
            } catch (NumberFormatException e) {
                this.age = 0;
                this.weight = 0.0;
                this.dailyWaterIntake = 0.0;
            }
        }
    }

    public String getAgeGroup() {
        if (age < 20) return "0-19";
        if (age < 30) return "20-29";
        if (age < 40) return "30-39";
        if (age < 50) return "40-49";
        if (age < 60) return "50-59";
        return "60+";
    }
}

技术要点:

  • 使用 split(",") 分割 CSV 行
  • 添加异常处理,防止数据格式错误
  • 提供 getAgeGroup() 方法进行年龄段分组
4.1.2 Mapper 类设计

性别平均饮水量 Mapper:

java 复制代码
public class GenderAvgWaterIntakeMapper extends Mapper<LongWritable, Text, Text, DoubleWritable> {
    private Text gender = new Text();
    private DoubleWritable waterIntake = new DoubleWritable();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        if (key.get() == 0) {
            return; // 跳过 CSV 头部
        }

        WaterIntakeParser parser = new WaterIntakeParser(value.toString());
        String genderStr = parser.getGender();

        if (genderStr != null && !genderStr.isEmpty()) {
            gender.set(genderStr);
            waterIntake.set(parser.getDailyWaterIntake());
            context.write(gender, waterIntake);
        }
    }
}

性别饮水水平分布 Mapper:

java 复制代码
public class GenderHydrationLevelMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    private Text genderHydration = new Text();
    private IntWritable count = new IntWritable(1);

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        if (key.get() == 0) {
            return; // 跳过 CSV 头部
        }

        WaterIntakeParser parser = new WaterIntakeParser(value.toString());
        String gender = parser.getGender();
        String hydrationLevel = parser.getHydrationLevel();

        if (gender != null && !gender.isEmpty() && hydrationLevel != null && !hydrationLevel.isEmpty()) {
            String keyStr = gender + "-" + hydrationLevel;
            genderHydration.set(keyStr);
            context.write(genderHydration, count);
        }
    }
}

技术要点:

  • 继承 Mapper<LongWritable, Text, Text, DoubleWritable>Mapper<LongWritable, Text, Text, IntWritable>
  • 使用 key.get() == 0 判断是否为 CSV 头部
  • 使用 WaterIntakeParser 解析数据行
  • 输出键值对:<分类键, 值>
4.1.3 Reducer 类设计

平均饮水量 Reducer:

java 复制代码
public class GenderAvgWaterIntakeReducer extends Reducer<Text, DoubleWritable, Text, DoubleWritable> {
    private DoubleWritable avgWaterIntake = new DoubleWritable();

    @Override
    protected void reduce(Text key, Iterable<DoubleWritable> values, Context context) throws IOException, InterruptedException {
        double sum = 0;
        int count = 0;

        for (DoubleWritable value : values) {
            sum += value.get();
            count++;
        }

        double average = sum / count;
        DecimalFormat df = new DecimalFormat("0.00");
        String formattedAvg = df.format(average);
        double roundedAvg = Double.parseDouble(formattedAvg);

        avgWaterIntake.set(roundedAvg);
        context.write(key, avgWaterIntake);
    }
}

计数 Reducer:

java 复制代码
public class GenderHydrationLevelReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    private IntWritable totalCount = new IntWritable();

    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int count = 0;

        for (IntWritable value : values) {
            count += value.get();
        }

        totalCount.set(count);
        context.write(key, totalCount);
    }
}

技术要点:

  • 遍历 Iterable<DoubleWritable>Iterable<IntWritable> 聚合数据
  • 使用 DecimalFormat 格式化输出,保留两位小数
  • 输出最终聚合结果
4.1.4 Driver 类设计

WaterIntakeAnalysisDriver.java 是主驱动类,负责启动所有 MapReduce 作业。

java 复制代码
public class WaterIntakeAnalysisDriver {
    public static void main(String[] args) throws Exception {
        String inputPath;
        String genderAvgWaterIntakeOutputPath;
        String activityAvgWaterIntakeOutputPath;
        // ... 其他输出路径

        if (args.length < 1) {
            System.err.println("Using default paths...");
            inputPath = "F:\\AAproject\\dailyWaterAnalysis\\data\\Daily_Water_Intake.csv";
        } else {
            inputPath = args[0];
        }

        String baseOutputDir = "F:\\AAproject\\dailyWaterAnalysis\\output\\";
        genderAvgWaterIntakeOutputPath = baseOutputDir + "gender_avg_water_intake_output";
        // ... 其他输出路径

        boolean genderAvgWaterIntakeJobSuccess = runGenderAvgWaterIntakeJob(inputPath, genderAvgWaterIntakeOutputPath);
        if (!genderAvgWaterIntakeJobSuccess) {
            System.err.println("按性别统计平均饮水量作业失败!");
            System.exit(-1);
        }

        // ... 运行其他作业
    }

    private static boolean runGenderAvgWaterIntakeJob(String inputPath, String outputPath) throws IOException, InterruptedException, ClassNotFoundException {
        Configuration conf = new Configuration();
        Path outputDir = new Path(outputPath);
        FileSystem fs = FileSystem.get(conf);
        if (fs.exists(outputDir)) {
            fs.delete(outputDir, true);
        }

        Job job = Job.getInstance(conf, "Gender Average Water Intake");
        job.setJarByClass(WaterIntakeAnalysisDriver.class);
        job.setMapperClass(GenderAvgWaterIntakeMapper.class);
        job.setReducerClass(GenderAvgWaterIntakeReducer.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(DoubleWritable.class);
        FileInputFormat.addInputPath(job, new Path(inputPath));
        FileOutputFormat.setOutputPath(job, outputDir);

        return job.waitForCompletion(true);
    }
}

技术要点:

  • 支持命令行参数指定输入输出路径
  • 提供默认路径配置
  • 删除已存在的输出目录
  • 依次运行多个 MapReduce 作业
  • 使用 job.waitForCompletion(true) 等待作业完成

4.2 Spring Boot 后端模块

4.2.1 Controller 层

AnalysisResultController.java 提供 RESTful API 接口。

java 复制代码
@RestController
@RequestMapping("/analysis")
public class AnalysisResultController {
    @Autowired
    private AnalysisResultService analysisResultService;

    @GetMapping("/gender-avg-water-intake")
    public List<AnalysisResultService.ResultItem> getGenderAvgWaterIntake() {
        return analysisResultService.getGenderAvgWaterIntakeResult();
    }

    @GetMapping("/gender-hydration-level")
    public List<AnalysisResultService.TwoFieldResultItem> getGenderHydrationLevel() {
        return analysisResultService.getGenderHydrationLevelResult();
    }

    // ... 其他接口
}

技术要点:

  • 使用 @RestController 注解声明为 REST 控制器
  • 使用 @RequestMapping 定义基础路径
  • 使用 @GetMapping 定义 GET 请求接口
  • 使用 @Autowired 注入 Service 层
4.2.2 Service 层

AnalysisResultService.java 负责读取和解析 MapReduce 输出文件。

java 复制代码
@Service
public class AnalysisResultService {
    private static final String OUTPUT_BASE_PATH = "F:/AAproject/dailyWaterAnalysis/output/";

    public static class ResultItem {
        private String key;
        private Object value;

        public ResultItem(String key, Object value) {
            this.key = key;
            this.value = value;
        }

        public String getKey() { return key; }
        public void setKey(String key) { this.key = key; }
        public Object getValue() { return value; }
        public void setValue(Object value) { this.value = value; }
    }

    public static class TwoFieldResultItem {
        private String field1;
        private String field2;
        private Object value;

        public TwoFieldResultItem(String field1, String field2, Object value) {
            this.field1 = field1;
            this.field2 = field2;
            this.value = value;
        }

        public String getField1() { return field1; }
        public void setField1(String field1) { this.field1 = field1; }
        public String getField2() { return field2; }
        public void setField2(String field2) { this.field2 = field2; }
        public Object getValue() { return value; }
        public void setValue(Object value) { this.value = value; }
    }

    public List<ResultItem> getGenderAvgWaterIntakeResult() {
        return readResultFileToList("gender_avg_water_intake_output/part-r-00000");
    }

    public List<TwoFieldResultItem> getGenderHydrationLevelResult() {
        return readTwoFieldResultFile("gender_hydration_level_output/part-r-00000");
    }

    private List<ResultItem> readResultFileToList(String fileName) {
        List<ResultItem> result = new ArrayList<>();
        String filePath = OUTPUT_BASE_PATH + fileName;

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF-8"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (line.isEmpty()) continue;

                String[] parts = line.split("\\t");
                if (parts.length >= 2) {
                    String key = parts[0];
                    String valueStr = parts[1];

                    Object value;
                    try {
                        value = Double.parseDouble(valueStr);
                    } catch (NumberFormatException e) {
                        value = valueStr;
                    }

                    result.add(new ResultItem(key, value));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            System.err.println("文件不存在: " + filePath);
        }

        return result;
    }

    private List<TwoFieldResultItem> readTwoFieldResultFile(String fileName) {
        List<TwoFieldResultItem> result = new ArrayList<>();
        String filePath = OUTPUT_BASE_PATH + fileName;

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF-8"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (line.isEmpty()) continue;

                String[] parts = line.split("\t");
                if (parts.length >= 2) {
                    String key = parts[0];
                    String valueStr = parts[1];

                    String[] keyParts = key.split("-");
                    String field1 = keyParts[0];
                    String field2 = keyParts.length > 1 ? keyParts[1] : "";

                    Object value;
                    try {
                        value = Double.parseDouble(valueStr);
                    } catch (NumberFormatException e) {
                        value = valueStr;
                    }

                    result.add(new TwoFieldResultItem(field1, field2, value));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            System.err.println("文件读取错误: " + filePath + ", " + e.getMessage());
        }

        return result;
    }
}

技术要点:

  • 使用 @Service 注解声明为服务类
  • 定义内部类 ResultItemTwoFieldResultItem 封装返回数据
  • 使用 BufferedReader 读取文件,指定 UTF-8 编码
  • 使用 split("\\t") 分割制表符分隔的数据
  • 使用 try-catch 处理文件读取异常
4.2.3 配置类

WebConfig.java 处理跨域请求和响应编码。

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);

        registry.addMapping("/analysis/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new ResponseEncodingInterceptor())
                .addPathPatterns("/**");
    }
}

技术要点:

  • 使用 @Configuration 注解声明为配置类
  • 实现 WebMvcConfigurer 接口
  • 配置 CORS 允许跨域请求
  • 添加响应编码拦截器确保 UTF-8 编码

4.3 Vue 3 前端模块

4.3.1 API 封装

api/index.js 基于 Axios 封装 HTTP 请求。

javascript 复制代码
import axios from 'axios'

const api = axios.create({
  baseURL: 'http://localhost:8080',
  timeout: 10000
})

api.interceptors.request.use(
  config => {
    return config
  },
  error => {
    console.error('请求错误:', error)
    return Promise.reject(error)
  }
)

api.interceptors.response.use(
  response => {
    return response.data
  },
  error => {
    console.error('响应错误:', error)
    return Promise.reject(error)
  }
)

export const analysisApi = {
  getGenderAvgWaterIntake: () => api.get('/analysis/gender-avg-water-intake'),
  getActivityAvgWaterIntake: () => api.get('/analysis/activity-avg-water-intake'),
  getWeatherAvgWaterIntake: () => api.get('/analysis/weather-avg-water-intake'),
  getGenderHydrationLevel: () => api.get('/analysis/gender-hydration-level'),
  getAgeGroupAvgWaterIntake: () => api.get('/analysis/age-group-avg-water-intake'),
  getActivityWeatherAvgWaterIntake: () => api.get('/analysis/activity-weather-avg-water-intake')
}

export default analysisApi

技术要点:

  • 使用 axios.create() 创建实例
  • 配置 baseURLtimeout
  • 使用请求拦截器和响应拦截器统一处理
  • 导出 API 方法供组件调用
4.3.2 页面组件

性别平均饮水量页面:

vue 复制代码
<template>
  <div class="analysis-view">
    <h2>按性别统计平均饮水量</h2>

    <el-card class="chart-card">
      <template #header>
        <div class="card-header">
          <span>性别平均饮水量对比</span>
        </div>
      </template>
      <div ref="chartRef" class="chart-container"></div>
    </el-card>

    <el-card class="data-table-card">
      <template #header>
        <div class="card-header">
          <span>详细数据</span>
        </div>
      </template>
      <el-table :data="chartData" style="width: 100%">
        <el-table-column prop="key" label="性别" min-width="180" />
        <el-table-column prop="value" label="平均饮水量(升)" min-width="150" />
      </el-table>
    </el-card>
  </div>
</template>

<script>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import * as echarts from 'echarts'
import { analysisApi } from '../api'
import { ElMessage } from 'element-plus'

export default {
  name: 'GenderAvgWaterIntakeView',
  setup() {
    const chartRef = ref(null)
    let chart = null
    const chartData = ref([])

    const initChart = () => {
      chart = echarts.init(chartRef.value)
      window.addEventListener('resize', () => chart.resize())
    }

    const updateChart = () => {
      const option = {
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          }
        },
        xAxis: {
          type: 'category',
          data: chartData.value.map(item => item.key),
          axisLabel: {
            rotate: 0
          }
        },
        yAxis: {
          type: 'value',
          name: '平均饮水量(升)'
        },
        series: [{
          data: chartData.value.map(item => parseFloat(item.value)),
          type: 'bar',
          itemStyle: {
            color: '#67C23A'
          }
        }]
      }

      chart.setOption(option)
    }

    const loadData = async () => {
      try {
        chartData.value = await analysisApi.getGenderAvgWaterIntake()
        updateChart()
      } catch (error) {
        console.error('加载数据失败:', error)
        ElMessage.error('加载数据失败,请稍后重试')
      }
    }

    onMounted(() => {
      initChart()
      loadData()
    })

    onBeforeUnmount(() => {
      if (chart) {
        chart.dispose()
      }
      window.removeEventListener('resize', () => chart.resize())
    })

    return {
      chartRef,
      chartData
    }
  }
}
</script>

性别饮水水平分布页面(饼图):

vue 复制代码
<template>
  <div class="analysis-view">
    <h2>按性别统计饮水水平分布</h2>

    <div class="charts-row">
      <el-card class="chart-card">
        <template #header>
          <div class="card-header">
            <span>女性饮水水平分布</span>
          </div>
        </template>
        <div ref="femaleChartRef" class="chart-container"></div>
      </el-card>

      <el-card class="chart-card">
        <template #header>
          <div class="card-header">
            <span>男性饮水水平分布</span>
          </div>
        </template>
        <div ref="maleChartRef" class="chart-container"></div>
      </el-card>
    </div>

    <el-card class="data-table-card">
      <template #header>
        <div class="card-header">
          <span>详细数据</span>
        </div>
      </template>
      <el-table :data="chartData" style="width: 100%">
        <el-table-column prop="field1" label="性别" min-width="100" />
        <el-table-column prop="field2" label="饮水水平" min-width="100" />
        <el-table-column prop="value" label="人数" min-width="100" />
      </el-table>
    </el-card>
  </div>
</template>

<script>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import * as echarts from 'echarts'
import { analysisApi } from '../api'
import { ElMessage } from 'element-plus'

export default {
  name: 'GenderHydrationLevelView',
  setup() {
    const femaleChartRef = ref(null)
    const maleChartRef = ref(null)
    let femaleChart = null
    let maleChart = null
    const chartData = ref([])

    const initCharts = () => {
      if (femaleChartRef.value && maleChartRef.value) {
        femaleChart = echarts.init(femaleChartRef.value)
        maleChart = echarts.init(maleChartRef.value)
        window.addEventListener('resize', () => {
          femaleChart.resize()
          maleChart.resize()
        })
      }
    }

    const updateCharts = () => {
      const femaleData = chartData.value.filter(item => item.field1 === 'Female')
      const maleData = chartData.value.filter(item => item.field1 === 'Male')

      const femaleSeriesData = femaleData.map(item => ({
        name: item.field2,
        value: parseInt(item.value)
      }))

      const maleSeriesData = maleData.map(item => ({
        name: item.field2,
        value: parseInt(item.value)
      }))

      if (femaleChart && femaleSeriesData.length > 0) {
        const femaleOption = {
          tooltip: {
            trigger: 'item',
            formatter: '{a} <br/>{b}: {c}人 ({d}%)'
          },
          legend: {
            orient: 'vertical',
            right: 10,
            top: 'center'
          },
          series: [
            {
              name: '女性饮水水平',
              type: 'pie',
              radius: ['40%', '70%'],
              avoidLabelOverlap: false,
              itemStyle: {
                borderRadius: 10,
                borderColor: '#fff',
                borderWidth: 2
              },
              label: {
                show: false,
                position: 'center'
              },
              emphasis: {
                label: {
                  show: true,
                  fontSize: 20,
                  fontWeight: 'bold'
                }
              },
              labelLine: {
                show: false
              },
              data: femaleSeriesData
            }
          ]
        }

        femaleChart.setOption(femaleOption)
      }

      if (maleChart && maleSeriesData.length > 0) {
        const maleOption = {
          tooltip: {
            trigger: 'item',
            formatter: '{a} <br/>{b}: {c}人 ({d}%)'
          },
          legend: {
            orient: 'vertical',
            right: 10,
            top: 'center'
          },
          series: [
            {
              name: '男性饮水水平',
              type: 'pie',
              radius: ['40%', '70%'],
              avoidLabelOverlap: false,
              itemStyle: {
                borderRadius: 10,
                borderColor: '#fff',
                borderWidth: 2
              },
              label: {
                show: false,
                position: 'center'
              },
              emphasis: {
                label: {
                  show: true,
                  fontSize: 20,
                  fontWeight: 'bold'
                }
              },
              labelLine: {
                show: false
              },
              data: maleSeriesData
            }
          ]
        }

        maleChart.setOption(maleOption)
      }
    }

    const loadData = async () => {
      try {
        chartData.value = await analysisApi.getGenderHydrationLevel()
        await nextTick()
        updateCharts()
      } catch (error) {
        console.error('加载数据失败:', error)
        ElMessage.error('加载数据失败,请稍后重试')
      }
    }

    onMounted(() => {
      initCharts()
      loadData()
    })

    onBeforeUnmount(() => {
      if (femaleChart) {
        femaleChart.dispose()
      }
      if (maleChart) {
        maleChart.dispose()
      }
      window.removeEventListener('resize', () => {
        femaleChart.resize()
        maleChart.resize()
      })
    })

    return {
      femaleChartRef,
      maleChartRef,
      chartData
    }
  }
}
</script>

技术要点:

  • 使用 Vue 3 Composition API 的 setup() 语法
  • 使用 ref 创建响应式引用
  • 使用 onMountedonBeforeUnmount 生命周期钩子
  • 使用 echarts.init() 初始化图表实例
  • 使用 chart.setOption() 更新图表配置
  • 使用 chart.dispose() 销毁图表实例
  • 使用 window.addEventListener('resize') 监听窗口大小变化
  • 使用 nextTick() 确保 DOM 更新后再操作图表
  • 使用 try-catch 处理异步请求错误
  • 使用 ElMessage.error() 显示错误提示

五、技术亮点

5.1 完整的大数据处理流程

项目实现了从数据采集、MapReduce 分析处理到前端可视化展示的完整大数据处理流程,适合学习大数据技术的全栈应用。

5.2 模块化设计

项目采用模块化设计,MapReduce、Spring Boot、Vue 3 三个模块独立开发、独立部署,便于维护和扩展。

5.3 统一的代码规范

所有 Mapper 和 Reducer 类遵循统一的设计模式,代码结构清晰,易于理解和扩展。

5.4 前后端分离架构

采用前后端分离架构,前端使用 Vue 3 + ECharts 实现数据可视化,后端使用 Spring Boot 提供 RESTful API,职责清晰。

5.5 响应式设计

前端页面采用响应式设计,适配不同屏幕尺寸,提供良好的用户体验。

5.6 丰富的可视化效果

使用 ECharts 实现多种图表类型,包括柱状图、饼图等,数据展示直观清晰。

六、项目运行

6.1 运行 MapReduce 分析

bash 复制代码
cd dailyWater-mapreduce
mvn clean package
java -jar target/dailyWater-mapreduce-1.0-SNAPSHOT.jar

6.2 启动后端服务

bash 复制代码
cd dailyWater-api
mvn clean package
mvn spring-boot:run

6.3 启动前端服务

bash 复制代码
cd dailyWater-dashboard
npm install
npm run dev

七、总结

本文详细介绍了一个基于 Hadoop MapReduce + Spring Boot + Vue 3 的每日饮水数据分析平台,从技术架构、核心功能到源码实现进行了全面的分析。项目展示了大数据分析的全流程实现,包括数据解析、MapReduce 处理、RESTful API 设计和前端可视化展示,适合作为学习大数据技术和全栈开发的实战项目。

通过本项目,可以学习到:

  • Hadoop MapReduce 编程模型
  • Spring Boot RESTful API 开发
  • Vue 3 Composition API 使用
  • ECharts 数据可视化
  • 前后端分离架构设计

希望本文对大家学习大数据技术有所帮助!


项目地址: https://m.tb.cn/h.7vtU7Al?tk=ebhzUO7sLRi

作者: 大数据基础

日期: 2026-02-11

相关推荐
才聚PMP2 小时前
基于易经思维的组织级项目管理测评体系
大数据·人工智能
Elastic 中国社区官方博客10 小时前
DevRel 通讯 — 2026 年 2 月
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·jina
workflower11 小时前
多变量时间序列预测
java·hadoop·nosql·需求分析·big data·结对编程
caoz12 小时前
AI的春节档
大数据·人工智能·深度学习·机器学习·计算机视觉
Volunteer Technology12 小时前
DynamicTP动态线程池(四)
java·spring boot·后端·spring
samFuB12 小时前
面板数据-人力资源和社会保障事业发展统计核心指标数据(2000-2024)
大数据
前路不黑暗@12 小时前
Java项目:Java脚手架项目的统一模块的封装(四)
java·开发语言·spring boot·笔记·学习·spring cloud·maven
Lalolander13 小时前
工厂手工统计耗时耗力怎么办?
大数据·制造执行系统·工厂管理系统·工厂工艺管理·工厂生产进度管理