Hadoop MapReduce + Vue3 实现二手房数据分析平台完整开发流程

一、项目需求分析

1.1 业务需求

对二手房交易数据进行分析,实现以下功能:

  • 按市区统计房源数量和平均价格
  • 按户型统计房源数量
  • 按面积范围统计平均价格
  • 按朝向、楼层、年份等多维度统计分析
  • 数据可视化展示

1.2 技术选型

层次技术方案说明
数据处理Hadoop MapReduce 3.3.4处理大规模CSV数据
后端APISpring Boot 2.7.18提供RESTful接口
前端展示Vue 3.5.26 + ECharts 5.5.1数据可视化

二、项目搭建

2.1 创建Maven父项目

pom.xml

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>secondHouseAnalysis</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>secondHouse-mapreduce</module>
        <module>secondHouse-api</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <hadoop.version>3.3.4</hadoop.version>
        <spring-boot.version>2.7.18</spring-boot.version>
    </properties>
</project>

2.2 创建MapReduce模块

secondHouse-mapreduce/pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.example</groupId>
        <artifactId>secondHouseAnalysis</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>secondHouse-mapreduce</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-common</artifactId>
            <version>${hadoop.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-mapreduce-client-core</artifactId>
            <version>${hadoop.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

2.3 创建Spring Boot模块

secondHouse-api/pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.example</groupId>
        <artifactId>secondHouseAnalysis</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>secondHouse-api</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
            </plugin>
        </plugins>
    </build>
</project>

2.4 创建Vue3前端项目

npm create vite@latest secondHouse-dashboard -- --template vue
cd secondHouse-dashboard
npm install
npm install axios echarts element-plus vue-router

三、MapReduce数据处理实现

3.1 数据解析工具类

SecondHouseParser.java

package org.secondHouse.util;

import java.util.HashMap;
import java.util.Map;

public class SecondHouseParser {

    public static Map<String, String> parseLine(String line) {
        Map<String, String> result = new HashMap<>();

        String[] fields = line.split(",");
        if (fields.length >= 7) {
            result.put("district", fields[0].trim());
            result.put("houseType", fields[1].trim());
            result.put("area", fields[2].trim());
            result.put("price", fields[3].trim());
            result.put("orientation", fields[4].trim());
            result.put("floor", fields[5].trim());
            result.put("year", fields[6].trim());
        }

        return result;
    }

    public static String getAreaRange(String area) {
        try {
            double areaValue = Double.parseDouble(area);
            if (areaValue < 50) return "0-50";
            else if (areaValue < 70) return "50-70";
            else if (areaValue < 90) return "70-90";
            else if (areaValue < 110) return "90-110";
            else if (areaValue < 130) return "130-150";
            else return "150+";
        } catch (NumberFormatException e) {
            return "未知";
        }
    }
}

3.2 户型统计Mapper和Reducer

HouseTypeCountMapper.java

package org.secondHouse.mapper;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.secondHouse.util.SecondHouseParser;

import java.io.IOException;

public class HouseTypeCountMapper extends Mapper<LongWritable, Text, Text, LongWritable> {

    private Text outKey = new Text();
    private LongWritable outValue = new LongWritable(1);

    @Override
    protected void map(LongWritable key, Text value, Context context) 
            throws IOException, InterruptedException {
        
        String line = value.toString();
        Map<String, String> data = SecondHouseParser.parseLine(line);
        
        String houseType = data.get("houseType");
        if (houseType != null && !houseType.isEmpty()) {
            outKey.set(houseType);
            context.write(outKey, outValue);
        }
    }
}

HouseTypeCountReducer.java

package org.secondHouse.reducer;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

public class HouseTypeCountReducer extends Reducer<Text, LongWritable, Text, LongWritable> {

    private LongWritable result = new LongWritable();

    @Override
    protected void reduce(Text key, Iterable<LongWritable> values, Context context) 
            throws IOException, InterruptedException {
        
        long sum = 0;
        for (LongWritable val : values) {
            sum += val.get();
        }
        
        result.set(sum);
        context.write(key, result);
    }
}

3.3 市区统计Mapper和Reducer

DistrictStatsMapper.java

package org.secondHouse.mapper;

import org.apache.hadoop.io.DoubleWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.secondHouse.util.SecondHouseParser;

import java.io.IOException;

public class DistrictStatsMapper extends Mapper<LongWritable, Text, Text, Text> {

    private Text outKey = new Text();
    private Text outValue = new Text();

    @Override
    protected void map(LongWritable key, Text value, Context context) 
            throws IOException, InterruptedException {
        
        String line = value.toString();
        Map<String, String> data = SecondHouseParser.parseLine(line);
        
        String district = data.get("district");
        String price = data.get("price");
        
        if (district != null && !district.isEmpty() && price != null && !price.isEmpty()) {
            outKey.set(district);
            outValue.set(price);
            context.write(outKey, outValue);
        }
    }
}

DistrictStatsReducer.java

package org.secondHouse.reducer;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

public class DistrictStatsReducer extends Reducer<Text, Text, Text, Text> {

    private Text result = new Text();

    @Override
    protected void reduce(Text key, Iterable<Text> values, Context context) 
            throws IOException, InterruptedException {
        
        long count = 0;
        double totalPrice = 0;
        
        for (Text val : values) {
            try {
                double price = Double.parseDouble(val.toString());
                count++;
                totalPrice += price;
            } catch (NumberFormatException e) {
                // 忽略格式错误的数据
            }
        }
        
        if (count > 0) {
            double avgPrice = totalPrice / count;
            result.set(count + "," + avgPrice);
            context.write(key, result);
        }
    }
}

3.4 市区户型统计Mapper和Reducer

DistrictHouseTypeCountMapper.java

package org.secondHouse.mapper;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.secondHouse.util.SecondHouseParser;

import java.io.IOException;

public class DistrictHouseTypeCountMapper extends Mapper<LongWritable, Text, Text, LongWritable> {

    private Text outKey = new Text();
    private LongWritable outValue = new LongWritable(1);

    @Override
    protected void map(LongWritable key, Text value, Context context) 
            throws IOException, InterruptedException {
        
        String line = value.toString();
        Map<String, String> data = SecondHouseParser.parseLine(line);
        
        String district = data.get("district");
        String houseType = data.get("houseType");
        
        if (district != null && !district.isEmpty() && houseType != null && !houseType.isEmpty()) {
            String compositeKey = district + "\t" + houseType;
            outKey.set(compositeKey);
            context.write(outKey, outValue);
        }
    }
}

DistrictHouseTypeCountReducer.java

package org.secondHouse.reducer;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

public class DistrictHouseTypeCountReducer extends Reducer<Text, LongWritable, Text, LongWritable> {

    private Text outKey = new Text();
    private Text outField2 = new Text();
    private LongWritable result = new LongWritable();

    @Override
    protected void reduce(Text key, Iterable<LongWritable> values, Context context) 
            throws IOException, InterruptedException {
        
        String[] parts = key.toString().split("\t");
        if (parts.length >= 2) {
            String district = parts[0];
            String houseType = parts[1];
            
            long sum = 0;
            for (LongWritable val : values) {
                sum += val.get();
            }
            
            outKey.set(district);
            outField2.set(houseType);
            result.set(sum);
            
            context.write(outKey, outField2);
            context.write(outField2, result);
        }
    }
}

3.5 驱动类

SecondHouseDriver.java

package org.secondHouse.driver;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.secondHouse.mapper.*;
import org.secondHouse.reducer.*;

public class SecondHouseDriver {

    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        
        String inputPath = args[0];
        String outputPath = args[1];
        
        // 运行户型统计
        runHouseTypeCountJob(conf, inputPath, outputPath + "/house_type_count");
        
        // 运行市区统计
        runDistrictStatsJob(conf, inputPath, outputPath + "/district_stats");
        
        // 运行市区户型统计
        runDistrictHouseTypeCountJob(conf, inputPath, outputPath + "/district_house_type_count");
        
        // 其他统计任务...
    }
    
    private static void runHouseTypeCountJob(Configuration conf, String inputPath, String outputPath) 
            throws Exception {
        Job job = Job.getInstance(conf, "House Type Count");
        job.setJarByClass(SecondHouseDriver.class);
        
        job.setMapperClass(HouseTypeCountMapper.class);
        job.setReducerClass(HouseTypeCountReducer.class);
        
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(LongWritable.class);
        
        FileInputFormat.addInputPath(job, new Path(inputPath));
        FileOutputFormat.setOutputPath(job, new Path(outputPath));
        
        job.waitForCompletion(true);
    }
    
    private static void runDistrictStatsJob(Configuration conf, String inputPath, String outputPath) 
            throws Exception {
        Job job = Job.getInstance(conf, "District Stats");
        job.setJarByClass(SecondHouseDriver.class);
        
        job.setMapperClass(DistrictStatsMapper.class);
        job.setReducerClass(DistrictStatsReducer.class);
        
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(Text.class);
        
        FileInputFormat.addInputPath(job, new Path(inputPath));
        FileOutputFormat.setOutputPath(job, new Path(outputPath));
        
        job.waitForCompletion(true);
    }
    
    private static void runDistrictHouseTypeCountJob(Configuration conf, String inputPath, String outputPath) 
            throws Exception {
        Job job = Job.getInstance(conf, "District House Type Count");
        job.setJarByClass(SecondHouseDriver.class);
        
        job.setMapperClass(DistrictHouseTypeCountMapper.class);
        job.setReducerClass(DistrictHouseTypeCountReducer.class);
        
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(LongWritable.class);
        
        FileInputFormat.addInputPath(job, new Path(inputPath));
        FileOutputFormat.setOutputPath(job, new Path(outputPath));
        
        job.waitForCompletion(true);
    }
}

3.6 运行MapReduce任务

cd secondHouse-mapreduce
mvn clean package
hadoop jar target/secondHouse-mapreduce-1.0-SNAPSHOT.jar org.secondHouse.driver.SecondHouseDriver data/二手房数据.csv output/

四、Spring Boot API实现

4.1 应用主类

CustomerAnalysisApiApplication.java

package org.secondHouse.api;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CustomerAnalysisApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(CustomerAnalysisApiApplication.class, args);
    }
}

4.2 配置类

WebConfig.java

package org.secondHouse.api.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*");
    }
}

application.properties

server.port=8080
spring.application.name=secondHouse-api
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true

4.3 服务层实现

AnalysisResultService.java

package org.secondHouse.api.service;

import org.springframework.stereotype.Service;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

@Service
public class AnalysisResultService {

    private static final String OUTPUT_BASE_PATH = "F:/AAproject/secondHouseAnalysis/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 static class DistrictStatsItem {
        private String district;
        private int count;
        private double avgPrice;

        public DistrictStatsItem(String district, int count, double avgPrice) {
            this.district = district;
            this.count = count;
            this.avgPrice = avgPrice;
        }

        public String getDistrict() { return district; }
        public void setDistrict(String district) { this.district = district; }
        public int getCount() { return count; }
        public void setCount(int count) { this.count = count; }
        public double getAvgPrice() { return avgPrice; }
        public void setAvgPrice(double avgPrice) { this.avgPrice = avgPrice; }
    }

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

    public List<DistrictStatsItem> getDistrictStatsResult() {
        return readDistrictStatsResultFile("district_stats/part-r-00000");
    }

    public List<TwoFieldResultItem> getDistrictHouseTypeCountResult() {
        return readTwoFieldResultFile("district_house_type_count/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();
        }

        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 >= 3) {
                    String field1 = parts[0];
                    String field2 = parts[1];
                    String valueStr = parts[2];

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

                    result.add(new TwoFieldResultItem(field1, field2, value));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return result;
    }

    private List<DistrictStatsItem> readDistrictStatsResultFile(String fileName) {
        List<DistrictStatsItem> 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 district = parts[0];
                    String[] stats = parts[1].split(",");

                    if (stats.length >= 2) {
                        try {
                            int count = Integer.parseInt(stats[0]);
                            double avgPrice = Double.parseDouble(stats[1]);
                            result.add(new DistrictStatsItem(district, count, avgPrice));
                        } catch (NumberFormatException e) {
                            // 忽略格式错误的行
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return result;
    }
}

4.4 控制器实现

AnalysisResultController.java

package org.secondHouse.api.controller;

import org.secondHouse.api.service.AnalysisResultService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/analysis")
public class AnalysisResultController {

    @Autowired
    private AnalysisResultService analysisResultService;

    @GetMapping("/house-type-count")
    public List<AnalysisResultService.ResultItem> getHouseTypeCount() {
        return analysisResultService.getHouseTypeCountResult();
    }

    @GetMapping("/district-stats")
    public List<AnalysisResultService.DistrictStatsItem> getDistrictStats() {
        return analysisResultService.getDistrictStatsResult();
    }

    @GetMapping("/district-house-type-count")
    public List<AnalysisResultService.TwoFieldResultItem> getDistrictHouseTypeCount() {
        return analysisResultService.getDistrictHouseTypeCountResult();
    }
}

4.5 运行Spring Boot应用

cd secondHouse-api
mvn clean package
java -jar target/secondHouse-api-1.0-SNAPSHOT.jar

五、Vue3前端实现

5.1 项目配置

package.json

{
  "name": "secondHouse-dashboard",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "axios": "^1.13.2",
    "echarts": "^5.5.1",
    "element-plus": "^2.13.1",
    "vue": "^3.5.26",
    "vue-router": "^4.4.5"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^6.0.3",
    "vite": "^7.3.0"
  }
}

5.2 API封装

src/api/index.js

import axios from 'axios'

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

export const analysisApi = {
  getHouseTypeCount: () => api.get('/analysis/house-type-count'),
  getDistrictStats: () => api.get('/analysis/district-stats'),
  getDistrictHouseTypeCount: () => api.get('/analysis/district-house-type-count')
}

5.3 路由配置

src/router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    }
  ]
})

export default router

5.4 主应用组件

src/App.vue

<template>
  <div id="app">
    <el-container>
      <el-header>
        <h1>二手房数据分析平台</h1>
      </el-header>
      <el-main>
        <router-view />
      </el-main>
    </el-container>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
#app {
  font-family: Arial, sans-serif;
}
.el-header {
  background-color: #409eff;
  color: white;
  text-align: center;
  line-height: 60px;
}
</style>

5.5 首页组件

src/views/HomeView.vue

<template>
  <div class="home-view">
    <h2>二手房数据分析</h2>
    
    <!-- 统计卡片 -->
    <div class="stats-cards">
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-title">总房源数</div>
          <div class="stat-value">{{ totalHouses }}</div>
        </div>
      </el-card>
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-title">平均价格</div>
          <div class="stat-value">{{ averagePrice.toFixed(2) }}</div>
        </div>
      </el-card>
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-title">市区数量</div>
          <div class="stat-value">{{ districtCount }}</div>
        </div>
      </el-card>
      <el-card class="stat-card">
        <div class="stat-content">
          <div class="stat-title">户型种类</div>
          <div class="stat-value">{{ houseTypeCount }}</div>
        </div>
      </el-card>
    </div>
    
    <!-- 户型统计柱状图 -->
    <el-card class="chart-card">
      <template #header>
        <span>户型数量统计</span>
      </template>
      <div ref="houseTypeChartRef" class="chart-container"></div>
    </el-card>
    
    <!-- 市区平均价格柱状图 -->
    <el-card class="chart-card">
      <template #header>
        <span>市区平均价格</span>
      </template>
      <div ref="districtPriceChartRef" class="chart-container"></div>
    </el-card>
  </div>
</template>

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

export default {
  name: 'HomeView',
  setup() {
    const houseTypeChartRef = ref(null)
    const districtPriceChartRef = ref(null)
    
    let houseTypeChart = null
    let districtPriceChart = null
    
    const houseTypeData = ref([])
    const districtStatsData = ref([])
    
    const totalHouses = computed(() => {
      return districtStatsData.value.reduce((sum, item) => sum + item.count, 0)
    })
    
    const averagePrice = computed(() => {
      const total = districtStatsData.value.reduce((sum, item) => sum + item.avgPrice * item.count, 0)
      return totalHouses.value > 0 ? total / totalHouses.value : 0
    })
    
    const districtCount = computed(() => {
      return districtStatsData.value.length
    })
    
    const houseTypeCount = computed(() => {
      return houseTypeData.value.length
    })
    
    const loadData = async () => {
      try {
        const [houseTypeRes, districtStatsRes] = await Promise.all([
          analysisApi.getHouseTypeCount(),
          analysisApi.getDistrictStats()
        ])
        
        houseTypeData.value = houseTypeRes
        districtStatsData.value = districtStatsRes
        
        initHouseTypeChart()
        initDistrictPriceChart()
      } catch (error) {
        console.error('数据加载失败:', error)
      }
    }
    
    const initHouseTypeChart = () => {
      if (houseTypeChartRef.value) {
        houseTypeChart = echarts.init(houseTypeChartRef.value)
        const option = {
          title: {
            text: '户型数量统计',
            left: 'center'
          },
          tooltip: {
            trigger: 'axis'
          },
          xAxis: {
            type: 'category',
            data: houseTypeData.value.map(item => item.key),
            axisLabel: {
              rotate: 45
            }
          },
          yAxis: {
            type: 'value',
            name: '数量'
          },
          series: [{
            data: houseTypeData.value.map(item => item.value),
            type: 'bar',
            itemStyle: {
              color: '#5470c6'
            }
          }]
        }
        houseTypeChart.setOption(option)
      }
    }
    
    const initDistrictPriceChart = () => {
      if (districtPriceChartRef.value) {
        districtPriceChart = echarts.init(districtPriceChartRef.value)
        const option = {
          title: {
            text: '市区平均价格',
            left: 'center'
          },
          tooltip: {
            trigger: 'axis'
          },
          xAxis: {
            type: 'category',
            data: districtStatsData.value.map(item => item.district),
            axisLabel: {
              rotate: 45
            }
          },
          yAxis: {
            type: 'value',
            name: '平均价格'
          },
          series: [{
            data: districtStatsData.value.map(item => item.avgPrice),
            type: 'bar',
            itemStyle: {
              color: '#91cc75'
            }
          }]
        }
        districtPriceChart.setOption(option)
      }
    }
    
    const handleResize = () => {
      houseTypeChart?.resize()
      districtPriceChart?.resize()
    }
    
    onMounted(() => {
      loadData()
      window.addEventListener('resize', handleResize)
    })
    
    onBeforeUnmount(() => {
      window.removeEventListener('resize', handleResize)
      houseTypeChart?.dispose()
      districtPriceChart?.dispose()
    })
    
    return {
      houseTypeChartRef,
      districtPriceChartRef,
      totalHouses,
      averagePrice,
      districtCount,
      houseTypeCount
    }
  }
}
</script>

<style scoped>
.home-view {
  padding: 20px;
}

.stats-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 20px;
  margin-bottom: 30px;
}

.stat-card {
  transition: transform 0.3s ease;
}

.stat-card:hover {
  transform: translateY(-5px);
}

.stat-content {
  text-align: center;
  padding: 20px 0;
}

.stat-title {
  font-size: 16px;
  color: #606266;
  margin-bottom: 10px;
}

.stat-value {
  font-size: 32px;
  font-weight: bold;
  color: #409eff;
}

.chart-card {
  margin-bottom: 20px;
}

.chart-container {
  width: 100%;
  height: 400px;
}
</style>

5.6 入口文件

src/main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

const app = createApp(App)

app.use(router)
app.use(ElementPlus)

app.mount('#app')

5.7 运行前端项目

cd secondHouse-dashboard
npm install
npm run dev

六、项目总结

6.1 技术要点

  1. MapReduce编程模型

    • Mapper负责数据解析和初步聚合
    • Reducer负责最终统计计算
    • 自定义数据类型和序列化
  2. Spring Boot RESTful API

    • 文件读取和数据封装
    • 跨域配置
    • 统一的数据返回格式
  3. Vue3 Composition API

    • ref和computed响应式数据
    • 生命周期钩子
    • ECharts图表集成

6.2 开发流程

  1. 数据准备和格式分析
  2. MapReduce任务开发和测试
  3. Spring Boot API接口开发
  4. Vue3前端页面开发
  5. 前后端联调测试
  6. 项目部署上线

6.3 扩展方向

  • 增加数据库存储,提高数据读取效率
  • 添加Redis缓存,减少重复计算
  • 实现数据实时更新机制
  • 增加用户认证和权限管理
  • 优化前端性能和用户体验

更多推荐