基于MapReduce+Spring Boot+Vue的奶茶销售数据分析平台、hadoop大数据分析系统、mapreduce实战、源码
基于MapReduce+Spring Boot+Vue的奶茶销售数据分析平台
前言
随着奶茶行业的快速发展,对销售数据进行深入分析变得越来越重要。本文将详细介绍一个完整的奶茶销售数据分析平台,该平台采用大数据处理技术(MapReduce)、后端开发(Spring Boot)和前端可视化(Vue.js + ECharts)实现,从数据处理到可视化展示的完整流程。
一、项目概述
1.1 项目背景
本项目基于31895条奶茶销售数据,通过多维度分析揭示不同年龄段、城市、品牌和产品类型之间的消费规律,为奶茶行业的市场策略制定、产品创新和品牌定位提供数据支持。
1.2 技术架构
原始数据(CSV) → MapReduce处理 → 结果文件存储
↓
数据可视化页面 ← Spring Boot API ← 结果数据读取
1.3 技术栈
| 模块 | 技术 | 版本 |
|---|---|---|
| 数据处理 | Hadoop MapReduce | 3.3.4 |
| 后端开发 | Spring Boot | 2.7.18 |
| 前端开发 | Vue.js | 3.x |
| 图表库 | ECharts | 5.5.1 |
| UI组件库 | Element Plus | 2.13.1 |
| 构建工具 | Maven + Vite | - |
二、数据源说明
2.1 数据结构
原始数据存储在data/teaMilkDate.csv文件中,包含18个字段:
| 字段名 | 描述 | 示例值 |
|---|---|---|
| order_id | 订单ID | 1 |
| user_id | 用户ID | 100000 |
| city | 城市 | 武汉 |
| city_level | 城市水平 | 二线 |
| brand | 品牌 | 蜜雪冰城 |
| product_type | 产品类型 | 低糖茶饮 |
| price | 单价 | 28.42 |
| quantity | 数量 | 1 |
| order_amount | 订单金额 | 28.42 |
| age_group | 年龄段 | 18-24岁 |
| year | 年份 | 2019 |
三、MapReduce数据处理实现
3.1 项目结构
teaMilk-mapreduce/
├── driver/ # 驱动类
├── mapper/ # Mapper类
├── reducer/ # Reducer类
└── util/ # 工具类
3.2 核心代码实现
3.2.1 数据解析工具类
[TeaMilkParser.java](file:///f:\AAproject\teaMilkAnalysis\teaMilk-mapreduce\src\main\java\org\lipstick\util\TeaMilkParser.java)负责解析CSV行数据:
public class TeaMilkParser {
private String[] fields;
public TeaMilkParser(String line) {
if (line.startsWith("\uFEFF")) {
line = line.substring(1);
}
this.fields = parseCsvLine(line);
}
private String[] parseCsvLine(String line) {
return line.split(",", -1);
}
public String getBrand() {
if (fields.length >= 6) {
return fields[4].trim();
}
return "";
}
public String getAgeGroup() {
if (fields.length >= 14) {
return fields[12].trim();
}
return "";
}
public String getUserId() {
if (fields.length >= 3) {
return fields[1].trim();
}
return "";
}
}
3.2.2 Mapper实现
以年龄段品牌购买人数统计为例,[AgeBrandCountMapper.java](file:///f:\AAproject\teaMilkAnalysis\teaMilk-mapreduce\src\main\java\org\lipstick\mapper\AgeBrandCountMapper.java)实现如下:
public class AgeBrandCountMapper extends Mapper<LongWritable, Text, Text, Text> {
private static final String SEPARATOR = "\u001F";
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = new String(value.getBytes(), 0, value.getLength(), "UTF-8").trim();
if (line.startsWith("order_id")) {
return;
}
TeaMilkParser parser = new TeaMilkParser(line);
String ageGroup = parser.getAgeGroup();
String brand = parser.getBrand();
String userId = parser.getUserId();
if (ageGroup.isEmpty() || brand.isEmpty() || userId.isEmpty()) {
return;
}
String outputKey = ageGroup + SEPARATOR + brand;
String outputValue = userId;
context.write(new Text(outputKey), new Text(outputValue));
}
}
核心逻辑:
- 跳过表头行
- 解析CSV数据,提取年龄段、品牌和用户ID
- 使用单元分隔符(
\u001F)组合年龄段和品牌作为Key - 用户ID作为Value输出
3.2.3 Reducer实现
[AgeBrandCountReducer.java](file:///f:\AAproject\teaMilkAnalysis\teaMilk-mapreduce\src\main\java\org\lipstick\reducer\AgeBrandCountReducer.java)实现如下:
public class AgeBrandCountReducer extends Reducer<Text, Text, Text, Text> {
private static final String SEPARATOR = "\u001F";
private static final String OUTPUT_SEPARATOR = "\t";
@Override
protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
String keyStr = new String(key.getBytes(), 0, key.getLength(), "UTF-8");
String[] dataParts = keyStr.split(SEPARATOR);
if (dataParts.length < 2) {
return;
}
String ageGroup = dataParts[0];
String brand = dataParts[1];
Set<String> userSet = new HashSet<>();
for (Text value : values) {
String valueStr = new String(value.getBytes(), 0, value.getLength(), "UTF-8");
userSet.add(valueStr.trim());
}
int userCount = userSet.size();
String outputKey = ageGroup + OUTPUT_SEPARATOR + brand;
String outputValue = String.valueOf(userCount);
context.write(new Text(outputKey), new Text(outputValue));
}
}
核心逻辑:
- 从Key中提取年龄段和品牌
- 使用Set去重,统计唯一用户数量
- 输出格式:
年龄段\t品牌\t用户数量
3.2.4 Driver实现
[TeaMilkAnalysisDriver.java](file:///f:\AAproject\teaMilkAnalysis\teaMilk-mapreduce\src\main\java\org\lipstick\driver\TeaMilkAnalysisDriver.java)负责配置和启动MapReduce作业:
public class TeaMilkAnalysisDriver implements Tool {
@Override
public int run(String[] args) throws Exception {
String inputPath = args.length > 0 ? args[0] : "F:/AAproject/teaMilkAnalysis/data/teaMilkDate.csv";
String outputPath = args.length > 1 ? args[1] : "F:/AAproject/teaMilkAnalysis/output";
Configuration conf = new Configuration();
conf.set("mapreduce.framework.name", "local");
conf.set("fs.defaultFS", "file:///");
conf.set("textoutputformat.separator", ",");
conf.set("mapreduce.output.textoutputformat.encoding", "UTF-8");
conf.set("mapreduce.input.textinputformat.encoding", "UTF-8");
Job job = Job.getInstance(conf, "Tea Milk Analysis");
job.setJarByClass(TeaMilkAnalysisDriver.class);
job.setInputFormatClass(TextInputFormat.class);
LazyOutputFormat.setOutputFormatClass(job, TextOutputFormat.class);
job.setMapperClass(TeaMilkMapper.class);
job.setReducerClass(TeaMilkReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
FileInputFormat.addInputPath(job, new Path(inputPath));
FileOutputFormat.setOutputPath(job, new Path(outputPath));
return job.waitForCompletion(true) ? 0 : 1;
}
}
3.3 MapReduce作业列表
项目包含7个MapReduce作业:
| 作业名称 | 输出目录 | 功能描述 |
|---|---|---|
| age_brand_count | age_brand_count/ | 年龄段品牌购买人数统计 |
| age_product_count | age_product_count/ | 年龄段产品类型购买人数统计 |
| brand_city_popularity | brand_city_popularity/ | 品牌在城市受欢迎程度统计 |
| brand_year_orders | brand_year_orders/ | 品牌每年订单数量统计 |
| brand_year_sales | brand_year_sales/ | 品牌每年消费总额统计 |
| city_level_brand_sales | city_level_brand_sales/ | 城市水平品牌消费总额统计 |
| city_level_purchase | city_level_purchase/ | 城市水平购买数量统计 |
四、Spring Boot后端API实现
4.1 项目结构
teaMilk-api/
├── controller/ # 控制器
├── service/ # 服务层
└── config/ # 配置类
4.2 核心代码实现
4.2.1 服务层
[AnalysisResultService.java](file:///f:\AAproject\teaMilkAnalysis\teaMilk-api\src\main\java\org\lipstick\api\service\AnalysisResultService.java)负责读取MapReduce处理结果:
@Service
public class AnalysisResultService {
private static final String OUTPUT_BASE_PATH = "F:/AAproject/teaMilkAnalysis/output/";
public static class ResultItem {
private String field1;
private String field2;
private Object value;
public ResultItem(String field1, String field2, Object value) {
this.field1 = field1;
this.field2 = field2;
this.value = value;
}
public String getField1() { return field1; }
public String getField2() { return field2; }
public Object getValue() { return value; }
}
public List<ResultItem> getAgeBrandCountResult() {
return readResultFileToList("age_brand_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 >= 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 ResultItem(field1, field2, value));
}
}
} catch (IOException e) {
e.printStackTrace();
System.err.println("文件不存在: " + filePath);
}
return result;
}
}
核心逻辑:
- 定义ResultItem内部类封装结果数据
- 读取MapReduce输出文件(UTF-8编码)
- 按制表符分割行数据
- 自动识别数值类型并转换
4.2.2 控制器
[AnalysisResultController.java](file:///f:\AAproject\teaMilkAnalysis\teaMilk-api\src\main\java\org\lipstick\api\controller\AnalysisResultController.java)提供RESTful API接口:
@RestController
@RequestMapping("/analysis")
public class AnalysisResultController {
@Autowired
private AnalysisResultService analysisResultService;
@GetMapping("/age-brand-count")
public List<AnalysisResultService.ResultItem> getAgeBrandCount() {
return analysisResultService.getAgeBrandCountResult();
}
@GetMapping("/age-product-count")
public List<AnalysisResultService.ResultItem> getAgeProductCount() {
return analysisResultService.getAgeProductCountResult();
}
@GetMapping("/brand-city-popularity")
public List<AnalysisResultService.ResultItem> getBrandCityPopularity() {
return analysisResultService.getBrandCityPopularityResult();
}
@GetMapping("/brand-year-orders")
public List<AnalysisResultService.ResultItem> getBrandYearOrders() {
return analysisResultService.getBrandYearOrdersResult();
}
@GetMapping("/brand-year-sales")
public List<AnalysisResultService.ResultItem> getBrandYearSales() {
return analysisResultService.getBrandYearSalesResult();
}
@GetMapping("/city-level-brand-sales")
public List<AnalysisResultService.ResultItem> getCityLevelBrandSales() {
return analysisResultService.getCityLevelBrandSalesResult();
}
@GetMapping("/city-level-purchase")
public List<AnalysisResultService.ResultItem> getCityLevelPurchase() {
return analysisResultService.getCityLevelPurchaseResult();
}
}
4.3 API接口列表
| 接口路径 | 方法 | 描述 |
|---|---|---|
/analysis/age-brand-count |
GET | 年龄段品牌购买人数统计 |
/analysis/age-product-count |
GET | 年龄段产品类型购买人数统计 |
/analysis/brand-city-popularity |
GET | 品牌在城市受欢迎程度统计 |
/analysis/brand-year-orders |
GET | 品牌每年订单数量统计 |
/analysis/brand-year-sales |
GET | 品牌每年消费总额统计 |
/analysis/city-level-brand-sales |
GET | 城市水平品牌消费总额统计 |
/analysis/city-level-purchase |
GET | 城市水平购买数量统计 |
五、Vue.js前端可视化实现
5.1 项目结构
teaMilk-dashboard/
├── src/
│ ├── views/ # 页面组件
│ ├── api/ # API调用
│ ├── router/ # 路由配置
│ └── App.vue # 根组件
5.2 核心代码实现
5.2.1 API调用封装
[index.js](file:///f:\AAproject\teaMilkAnalysis\teaMilk-dashboard\src\api\index.js)封装了所有API调用:
import axios from 'axios'
const api = axios.create({
baseURL: 'http://localhost:8080/analysis',
timeout: 10000
})
export const analysisApi = {
getAgeBrandCount: () => api.get('/age-brand-count'),
getAgeProductCount: () => api.get('/age-product-count'),
getBrandCityPopularity: () => api.get('/brand-city-popularity'),
getBrandYearOrders: () => api.get('/brand-year-orders'),
getBrandYearSales: () => api.get('/brand-year-sales'),
getCityLevelBrandSales: () => api.get('/city-level-brand-sales'),
getCityLevelPurchase: () => api.get('/city-level-purchase')
}
5.2.2 年龄段分析页面
[AgeAnalysisView.vue](file:///f:\AAproject\teaMilkAnalysis\teaMilk-dashboard\src\views\AgeAnalysisView.vue)展示年龄段品牌和产品类型购买偏好:
const renderAgeBrandChart = (data) => {
if (!ageBrandChart.value || !data || data.length === 0) {
return
}
const ageGroups = [...new Set(data.map(item => item.field1))].sort()
const brands = [...new Set(data.map(item => item.field2))]
const series = []
const legendData = []
brands.forEach(brand => {
const brandData = ageGroups.map(age => {
const item = data.find(d => d.field1 === age && d.field2 === brand)
return item ? item.value : 0
})
series.push({
name: brand,
type: 'bar',
data: brandData
})
legendData.push(brand)
})
const option = {
title: {
text: '各年龄段品牌购买人数分布',
left: 'center'
},
tooltip: {
trigger: 'item',
axisPointer: {
type: 'shadow'
},
formatter: function(params) {
return `${params.name}年龄段<br/>${params.seriesName}: ${params.value}人`
}
},
legend: {
data: legendData,
bottom: 0,
selectedMode: 'multiple'
},
xAxis: {
type: 'category',
data: ageGroups,
name: '年龄段'
},
yAxis: {
type: 'value',
name: '购买人数'
},
series: series
}
ageBrandChart.value.clear()
ageBrandChart.value.setOption(option)
}
5.2.3 品牌分析页面
[BrandAnalysisView.vue](file:///f:\AAproject\teaMilkAnalysis\teaMilk-dashboard\src\views\BrandAnalysisView.vue)展示品牌市场表现和趋势:
const renderBrandYearSalesChart = (data) => {
if (!brandYearSalesChart.value || !data || data.length === 0) {
return
}
const years = [...new Set(data.map(item => item.field1))].sort()
const brands = [...new Set(data.map(item => item.field2))]
const series = []
const legendData = []
brands.forEach(brand => {
const brandData = years.map(year => {
const item = data.find(d => d.field1 === year && d.field2 === brand)
return item ? item.value : 0
})
series.push({
name: brand,
type: 'bar',
data: brandData
})
legendData.push(brand)
})
const option = {
title: {
text: '品牌年度销售总额对比',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: function(params) {
return params.name + '<br/>' + params.marker + params.seriesName + ': ' + params.value.toFixed(2) + ' 元'
}
},
legend: {
data: legendData,
bottom: 0,
selectedMode: 'multiple'
},
xAxis: {
type: 'category',
data: years,
name: '年份'
},
yAxis: {
type: 'value',
name: '销售总额(元)'
},
series: series
}
brandYearSalesChart.value.clear()
brandYearSalesChart.value.setOption(option, true)
}
六、最终效果展示
6.1 年龄段分析
年龄段品牌购买人数统计:
- 图表类型:分组柱状图
- X轴:年龄段(18-24岁、25-30岁、31-40岁等)
- Y轴:购买人数
- 系列:不同品牌(蜜雪冰城、喜茶、奈雪的茶等)
- 交互:支持图例筛选、数据提示框
年龄段产品类型购买人数统计:
- 图表类型:折线图
- X轴:年龄段
- Y轴:购买人数
- 系列:不同产品类型(低糖茶饮、果茶、奶茶等)
- 交互:支持图例筛选、数据提示框
6.2 品牌分析
品牌年度销售总额统计:
- 图表类型:分组柱状图
- X轴:年份(2019、2020、2021等)
- Y轴:销售总额(元)
- 系列:不同品牌
- 交互:支持图例筛选、数据提示框
品牌年度订单数量统计:
- 图表类型:折线图
- X轴:年份
- Y轴:订单数量
- 系列:不同品牌
- 交互:支持图例筛选、数据提示框
品牌城市受欢迎程度统计:
- 图表类型:分组柱状图
- X轴:城市(武汉、北京、上海等)
- Y轴:受欢迎程度指数
- 系列:不同品牌
- 交互:支持图例筛选、数据提示框
6.3 城市分析
城市水平品牌消费总额统计:
- 图表类型:分组柱状图
- X轴:城市水平(一线、二线、三线等)
- Y轴:消费总额(元)
- 系列:不同品牌
- 交互:支持图例筛选、数据提示框
城市水平购买数量统计:
- 图表类型:饼图
- 数据:各城市水平购买数量占比
- 交互:支持扇区高亮、数据提示框
七、项目特点与创新点
7.1 技术特点
- 完整的大数据处理流程:从原始数据收集、MapReduce处理到前端可视化的完整流程
- 多维度数据分析:从年龄段、品牌、产品类型、城市等多个维度进行分析
- 直观的数据可视化:使用ECharts实现丰富的图表展示
- 前后端分离架构:采用现代前后端分离架构,提高开发效率和系统可维护性
- 可扩展的API设计:API接口设计简洁统一,便于扩展和集成
7.2 创新点
- MapReduce本地模式运行:支持在本地环境运行MapReduce作业,无需搭建Hadoop集群
- 中文编码处理:解决了MapReduce处理中文数据的编码问题
- 数据去重统计:使用Set数据结构实现用户去重,确保统计准确性
- 灵活的图表配置:支持图例筛选、数据提示框等多种交互方式
八、部署与运行
8.1 环境要求
- JDK 1.8+
- Maven 3.6.0+
- Node.js ^20.19.0 || >=22.12.0
- Hadoop 3.3.4(可选,用于运行MapReduce作业)
8.2 运行步骤
8.2.1 后端服务
cd teaMilk-api
mvn clean package
mvn spring-boot:run
8.2.2 前端服务
cd teaMilk-dashboard
npm install
npm run dev
8.2.3 访问应用
- 前端应用:http://localhost:5173
- 后端API:http://localhost:8080/analysis
九、总结
本项目通过对奶茶销售数据的深入分析,展示了大数据技术在消费行为分析中的应用。项目实现了从原始数据处理到可视化展示的完整流程,为奶茶行业的市场分析和决策提供了有力支持。
项目采用了现代的技术架构和开发方法,具有良好的可扩展性和可维护性。通过多维度的数据分析和直观的可视化展示,为用户提供了丰富的数据洞察。
未来,项目可以进一步扩展,引入实时数据处理、机器学习等技术,实现更高级的数据分析和预测功能。
十、源码地址
https://m.tb.cn/h.7rssKpB?tk=HBWsUR7v1B3
作者:[大数据基础]
日期:2026-02-01
技术栈:MapReduce + Spring Boot + Vue.js + ECharts
更多推荐




所有评论(0)