Hadoop+Spark+Django这套组合,加上ECharts可视化大屏,基本就是大数据方向毕业设计里最高频的一套工程框架。我最近刚好在调试一个基于大数据的足球数据分析与可视化项目,顺着源码、文档、调试记录一路盘下来,发现很多环节的坑其实都集中在工程衔接上,而不是算法本身。
这个项目到底解决了什么问题?说白了就是三件事:用分布式存储接下多维度、大体量的足球比赛数据;用分布式计算完成球队胜率、进球趋势、主客场表现等统计指标;再通过Web接口和大屏把结果直观地展示出来。对读者来说,无论你是准备毕业设计,还是想积累一套完整的离线数据工程经验,这套链路都是很典型的练手对象。下面我按实际开发顺序,把架构、数据、计算、后端、可视化、调试部署这些环节一一拆开讲。
1. 项目架构与核心链路拆解
1.1 这个项目解决的痛点
先说说为什么要用一套看起来挺重的架构去做足球分析。足球比赛数据有个特点:字段不算特别多,但历史积累后量不小。一场比赛至少包含主客队、比分、控球率、射门数、射正数、角球、犯规、红黄牌等三四十个字段,一个赛季几千场,如果再加上球员、转会、历史交锋这些维度,记录数很快到百万级。这时候单机MySQL直接跑多表关联和跨赛季统计,慢查询会非常明显。更麻烦的是,很多项目把采集、清洗、计算、展示全都塞在一个脚本里,后面想改一个指标,牵一发而动全身。
我接到这类项目时,第一步都是先做流程拆分。把项目分成数据采集/清洗、分布式存储、离线计算、Web后端、可视化大屏五层,每层职责单一,接口清晰。这样做的直接好处是:哪个环节出问题能快速定位,答辩或汇报时也讲得清楚每一层做了什么、为什么要拆。
1.2 为什么是Hadoop+Spark+Django这套组合
这套组合不是唯一选择,但在目前的毕业设计和中小型数据项目中算是很主流的一个搭配。拆开看每个组件都有明确的不可替代性。
HDFS负责原始数据和中间结果的存储。足球数据虽然远达不到真正意义上的海量,但使用分布式文件系统的好处是:数据文件可以按赛季、联赛分目录组织,NameNode维护元数据,DataNode负责实际存储,后面Spark可以从多个节点并行读取,读取速度比单机磁盘快很多。Hive在这个架构里更多是数仓层的逻辑表工具,如果你后续要写复杂的SQL分析,可以让Spark直接读Hive表。
Spark负责离线计算。相比MapReduce,Spark的核心优势是内存计算和丰富的DataFrame/SQL API。写足球指标统计时,用SQL表达“每支球队的主场胜率、客场胜率、场均进球”这类需求,代码量比MapReduce少一个量级,调试效率也高。另一个实际原因是:MapReduce在迭代计算场景下频繁落盘,跑一次全量分析可能要几十分钟,Spark把中间结果放在内存里,同样的任务通常几分钟内能跑完。
Django负责业务后端。为什么不选Flask?因为Flask虽然轻,但ORM、Admin后台、DRF这些东西都要自己拼。Django自带Admin和数据模型,非常适合“分析结果表到只读查询接口”这种场景,而且社区资料多,遇到问题容易搜到解决方案。本项目的后端其实不复杂,但用Django会让整个项目更完整,也更像正规工程。
三者分工可以这样看:
| 层级 | 技术 | 核心职责 | 选择理由 |
|---|---|---|---|
| 数据存储 | HDFS + Hive | 保存原始清洗数据和数仓表 | 分布式存储、批量读写稳定 |
| 离线计算 | Spark Core / Spark SQL | 计算球队和联赛统计指标 | 内存计算快、SQL表达友好 |
| 业务后端 | Django + DRF | 对外提供JSON数据接口 | ORM成熟、接口开发效率高 |
| 数据缓存 | Redis | 缓存大屏高频查询结果 | 降低MySQL压力、提升响应速度 |
| 可视化 | ECharts + 原生Web | 大屏图表展示 | 配置灵活、前端无负担 |
1.3 完整数据流转链路
项目的数据流向是这样的:
- 采集脚本从公开数据源或接口拿到原始比赛数据,存成CSV/JSON;
- Python脚本做缺失值、重复值、字段格式处理,得到一份干净的明细表;
- 把清洗后的文件上传到HDFS指定目录,或者先建Hive外部表;
- Spark读取HDFS上的文件,按分析需求计算各类指标,结果写回MySQL;
- Django从MySQL读取指标数据,设计成RESTful API;
- 大屏页面通过AJAX/fetch请求Django接口,用ECharts渲染图表。
这里要强调一个设计原则:大屏和Django不直接读HDFS或Hive。原因很简单,Hive查询的平均延迟在几十秒甚至分钟级,大屏需要的接口响应是毫秒级。所以离线计算的结果必须落到MySQL这样的关系型数据库里,Web层只负责读结果表。这个“离线算好、在线查询”的思路,本身就是大数据离线数仓架构的简化版。
2. 数据获取、清洗与HDFS入库
2.1 足球数据源怎么选
在做这类项目时,数据来源一般有三条路:公开数据集平台下载整理好的历史比赛数据、官方开放数据接口、自己写爬虫抓取。我的建议是:除非项目明确要求展示爬虫能力,否则优先使用公开数据集。原因很现实:自己爬数据要处理反爬、字段解析、赛季更新,周期很长,而且爬下来的数据质量不一定好;公开数据集通常已经整理过字段,拿到手可以直接进入清洗环节。
但要注意,公开数据集也有坑。比如同一文件里球队名的写法可能不统一,有时是中文名,有时是英文名,有时带空格;又比如某些数据集的日期格式是字符串,需要转成标准日期。所以即便是“干净数据”,清洗脚本仍然不能省。
一个合理的字段设计至少应该包含这些:
| 字段 | 含义 | 示例 |
|---|---|---|
| match_id | 比赛唯一编号 | M20240001 |
| season | 赛季 | 2024-2025 |
| round | 轮次 | 18 |
| match_date | 比赛日期 | 2024-01-15 |
| home_team | 主队编号 | T001 |
| away_team | 客队编号 | T002 |
| home_goals | 主队进球 | 2 |
| away_goals | 客队进球 | 1 |
| possession_home | 主队控球率(百分比) | 61.5 |
| shots_home / shots_away | 射门数 | 15 / 7 |
| shots_on_target | 射正数 | 6 / 2 |
| corners | 角球数 | 8 / 3 |
| fouls | 犯规数 | 12 / 15 |
| cards | 红黄牌 | 2 / 4 |
字段不贪多,够做分析和大屏展示就行。如果你一开始就把三四十个字段全部纳入,清洗工作量会翻倍,但最后大屏上可能用不到一半。
2.2 数据清洗:决定后续计算质量的一步
清洗这一步看起来不起眼,但Spark算出来的指标准不准,完全取决于这里。我在实际项目里通常会执行四类操作:去重、缺失值处理、类型统一、派生字段生成。
去重主要是按match_id去重,很有必要。有些数据集会出现同一场比赛被重复记录两次的情况,如果不去重,后面的总场次和总进球数都会翻倍。缺失值方面,控球率、射门数这类连续字段偶有缺失,可以用同类字段的中位数填充;日期缺失则直接丢弃该行,因为时间维度是后续趋势分析的基础。类型统一要特别注意:如果把比分字段读成了字符串,Spark里SUM就会直接报错或得到错误结果。
派生字段也建议在清洗阶段就生成好。比如“比赛结果”字段,可以根据主客队进球数生成win/lose/draw;“总进球数”可以生成home_goals + away_goals;“净胜球”可以生成主队视角的正负值。把这些计算放在清洗脚本里,Spark分析时就不用一遍遍重复CASE WHEN,代码会清爽很多。
一个参考的清洗脚本如下:
import pandas as pd df = pd.read_csv("raw_football_matches.csv") df = df.drop_duplicates(subset=["match_id"]) df = df.dropna(subset=["match_date"]) df["match_date"] = pd.to_datetime(df["match_date"]) df["home_goals"] = df["home_goals"].astype(int) df["away_goals"] = df["away_goals"].astype(int) df["possession_home"] = df["possession_home"].astype(float) df["home_win"] = (df["home_goals"] > df["away_goals"]).astype(int) df["draw"] = (df["home_goals"] == df["away_goals"]).astype(int) df["total_goals"] = df["home_goals"] + df["away_goals"] df["possession_home"] = df["possession_home"].fillna( df["possession_home"].median() ) df.to_parquet("clean_football_matches.parquet", index=False)这里特意输出成Parquet格式而不是CSV,原因是:Parquet是列式存储,后续Spark读取时可以只扫描需要的字段,而且自带压缩,同样数据量占的HDFS空间更小。如果环境不支持Parquet,输出CSV也能跑通,只是性能和存储上会差一些。
2.3 数据上传HDFS的几种落地姿势
清洗完的数据要进HDFS。最常用的方式是命令行上传:
hdfs dfs -mkdir -p /football/ods/match hdfs dfs -put /data/football/clean_football_matches.parquet /football/ods/match/ hdfs dfs -ls /football/ods/match如果希望直接在Spark里读取,可以写成:
df = spark.read.parquet("hdfs:///football/ods/match")除了命令行,还有两种常见方式:一是用Python的pyhdfs库在清洗脚本里直接put文件;二是如果数据原本在MySQL里,可以用Sqoop全量导入。命令行适合一次性测试,pyhdfs适合把清洗和上传打成一条自动化流程,Sqoop则适合从业务库同步。实际项目里我通常先命令行确认数据能正常读取,再考虑自动化。
上传时有三个细节一定要检查。第一,HDFS默认副本数是3,如果机器磁盘紧张,可以在集群配置里调成2,但不要在生产环境乱调;第二,上传前用hdfs dfs -du -h /football查一下目录空间,避免写到一半磁盘满;第三,测试数据要和正式数据目录分开,比如/football/test和/football/ods,避免误删污染。
3. Spark离线分析:从明细到指标
3.1 分析主题怎么设计才有层次
很多人在分析阶段容易犯一个错:把所有能算的平均值都算一遍,最后大屏上全是平均数,没有任何故事感。我的建议是围绕几个“可解释的结论”来设计指标,比如:
- 联赛总览:总场次、总进球数、场均进球、主场胜率、平局率;
- 球队攻防:每队进球数、失球数、场均射门、射门转化率、主场/客场净胜球差异;
- 排名模拟:按积分(胜3平1)生成赛季排名,观察主客场表现波动;
- 相关性探索:控球率与胜率之间的关系,射门次数与进球数之间的关系。
这些主题覆盖了“整体到局部、事实到结论”的层次,大屏上既有核心指标卡,也有排行和关联分析,不会显得单薄。更重要的是,每个指标背后都能讲出业务意义,汇报时不会只回答“我算了个均值”。
3.2 用Spark SQL计算球队统计指标
前面说过,主客队在同一行记录里。如果直接按home_team分组,就只能看到主场视角,按away_team分组又只能看到客场视角。正确的做法是把一行拆成两行,再统一分组。这个思想很简单,但很关键。
先创建SparkSession并读取数据:
from pyspark.sql import SparkSession spark = SparkSession.builder \ .appName("football_analysis") \ .config("spark.sql.shuffle.partitions", "10") \ .getOrCreate() match_df = spark.read.parquet("hdfs:///football/ods/match") match_df.createOrReplaceTempView("match_info")然后用UNION ALL展开成球队视角:
SELECT team_id, COUNT(*) AS total_matches, SUM(CASE WHEN result = 'win' THEN 1 ELSE 0 END) AS wins, SUM(CASE WHEN result = 'draw' THEN 1 ELSE 0 END) AS draws, SUM(goals_for) AS goals_for, SUM(goals_against) AS goals_against, ROUND(AVG(possession), 2) AS avg_possession, ROUND(AVG(shots), 2) AS avg_shots FROM ( SELECT home_team_id AS team_id, home_goals AS goals_for, away_goals AS goals_against, CASE WHEN home_goals > away_goals THEN 'win' WHEN home_goals = away_goals THEN 'draw' ELSE 'lose' END AS result, possession_home AS possession, shots_home AS shots FROM match_info UNION ALL SELECT away_team_id AS team_id, away_goals AS goals_for, home_goals AS goals_against, CASE WHEN away_goals > home_goals THEN 'win' WHEN away_goals = home_goals THEN 'draw' ELSE 'lose' END AS result, 100 - possession_home AS possession, shots_away AS shots FROM match_info ) team_match GROUP BY team_id这个SQL执行后,每支球队的总比赛场次、胜场、平场、进失球、控球率、射门数就都出来了。后面要算胜率,只需要在结果基础上做wins / total_matches。如果要对主客场分开看,把GROUP BY改成team_id, home_flag即可,思路完全一样。
实际运行时,需要注意一个现象:UNION ALL展开后数据量会变成原表的两倍,Spark默认的shuffle分区数是200,如果只有几千场比赛,200个分区反而浪费资源。所以我习惯把spark.sql.shuffle.partitions设成和集群核数相关的较小值,比如10或者24。这个参数不是越大越好,后面调试部分会再展开。
3.3 把分析结果写回MySQL
分析结果只有落到MySQL,Django才能高效读取。Spark写MySQL的常用方式是JDBC:
result_df.write.format("jdbc") \ .mode("overwrite") \ .option("url", "jdbc:mysql://localhost:3306/football_bi?useUnicode=true&characterEncoding=utf8") \ .option("driver", "com.mysql.cj.jdbc.Driver") \ .option("dbtable", "team_stat") \ .option("user", "root") \ .option("password", "your_password") \ .option("batchsize", "1000") \ .save()写之前,先在MySQL里建好表结构,字段类型要和Spark的DataFrame对应。比如整数字段用INT,比率字段用DOUBLE或DECIMAL,主键用team_id。如果不提前建表,让Spark自动建表,类型推断常常会给出不太合理的结果,后面Django读出来还要再转换。
mode("overwrite")会先清空整张表再写,开发环境没问题,但如果以后按赛季跑批,建议改成mode("append")并加上统计日期字段,查询时再按日期过滤。还有一个经验:如果结果集很小,比如几百行,直接用foreach按行写入也行,但用JDBC的batchsize会更稳定,不会频繁创建连接。
4. Django后端与API设计
4.1 从零搭建Django项目
后端部分相对简单,但配置上有几个容易忽略的点。先创建虚拟环境并安装依赖:
python -m venv venv source venv/bin/activate pip install django djangorestframework django-cors-headers mysqlclient django-admin startproject football_project cd football_project python manage.py startapp stats然后修改settings.py,把应用加进INSTALLED_APPS里:
INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", "corsheaders", "stats", ]数据库配置要改成MySQL,不能使用默认的SQLite,因为我们希望Django直接读取Spark写进去的统计分析表:
DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "football_bi", "USER": "root", "PASSWORD": "your_password", "HOST": "127.0.0.1", "PORT": "3306", "OPTIONS": {"charset": "utf8mb4"}, } }跨域配置也要加上,否则大屏页面从另一个端口访问接口时,浏览器会直接拦截响应:
MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", ... ] CORS_ALLOW_ALL_ORIGINS = True这里要说明,CORS_ALLOW_ALL_ORIGINS在生产环境不应该直接打开,正确做法是配置具体的允许来源。开发阶段为了方便可以先全部放开,但上线前一定要收紧。
4.2 数据模型与序列化器
在stats/models.py里定义一个模型来对应Spark写入的team_stat表:
class TeamStat(models.Model): team_id = models.IntegerField(primary_key=True) season = models.CharField(max_length=20) total_matches = models.IntegerField() wins = models.IntegerField() draws = models.IntegerField() losses = models.IntegerField() goals_for = models.IntegerField() goals_against = models.IntegerField() avg_possession = models.FloatField() avg_shots = models.FloatField() win_rate = models.FloatField() class Meta: db_table = "team_stat" ordering = ["-win_rate"]注意db_table要和Spark写入的表名完全一致,大小写也要一致,否则Django会默认找stats_teamstat这张不存在的表。
序列化器用DRF的ModelSerializer写,几行就够:
from rest_framework import serializers from .models import TeamStat class TeamStatSerializer(serializers.ModelSerializer): class Meta: model = TeamStat fields = "__all__"4.3 接口设计:既要好用,也要扛得住大屏轮询
大屏页面一般会定时刷新,如果每次请求都去MySQL里跑一遍全表排序,接口压力不小。我的做法是:接口层保留常用参数的灵活度,再用Redis缓存高频结果。
先定义一个排名接口:
from rest_framework.views import APIView from rest_framework.response import Response from django.core.cache import cache from .models import TeamStat from .serializers import TeamStatSerializer class TeamRankView(APIView): def get(self, request, format=None): ordering = request.query_params.get("ordering", "-win_rate") limit = int(request.query_params.get("limit", 20)) cache_key = f"team_rank_{ordering}_{limit}" data = cache.get(cache_key) if data is None: qs = TeamStat.objects.all().order_by(ordering)[:limit] data = TeamStatSerializer(qs, many=True).data cache.set(cache_key, data, 300) return Response({"code": 0, "data": data})关于接口风格,我认为不用强行上复杂的路由注册。项目就几个接口,APIView最直观;等以后接口多了,再重构成ViewSet也不迟。返回格式统一为{"code": 0, "data": ...},前端处理起来简单,排查问题也方便。时间字段统一用ISO格式,避免前端在“2024-01-15”和“2024/01/15”之间反复踩坑。
接口层面还要考虑一件事:数据权限和字段暴露。本项目没有敏感数据,直接返回全部字段没问题。但如果以后做了球员薪资这类数据,建议用Serializer明确指定要暴露的字段,不要fields = "__all__"一把梭。
5. 可视化大屏的设计与开发
5.1 大屏布局不靠堆图
可视化大屏是最直观的交付物,也是最容易做花的一个环节。很多初学者会把十几个图表全部堆进一屏,结果信息密度太高,观众反而什么都记不住。我常用的布局思路是:1920x1080的固定画布,分上、中、下三个信息区。
顶部是标题和核心指标卡,展示总场次、总进球、场均进球、主场胜率,这是大屏的“结论层”;中部是大屏的主体,左边放球队积分或胜率排行,中间放联赛进球趋势或主客场对比,右边放关联分析散点或球员数据;底部放次要信息,比如近期赛果、红黄牌统计。
配色方面,深色背景配高亮色是通用做法。背景色用深蓝黑(比如#0f1c2e),主数据色用青蓝色(比如#00e5ff),辅助色用黄橙色做对比。整套页面用一套CSS变量统一管理色板,不要每张图单独配色,否则视觉会很乱。
5.2 ECharts图表配置的实战案例
先通过fetch请求我们上面写好的接口,再渲染球队胜率排行柱状图:
async function loadTeamRank() { const resp = await fetch("http://127.0.0.1:8000/api/team/rank/?ordering=-win_rate&limit=10"); const json = await resp.json(); const teams = json.data.map(item => `球队${item.team_id}`); const rates = json.data.map(item => (item.win_rate * 100).toFixed(1)); chart.setOption({ tooltip: { trigger: "axis" }, grid: { left: 40, right: 20, bottom: 30, top: 30 }, xAxis: { type: "category", data: teams, axisLabel: { color: "#ccc" } }, yAxis: { type: "value", max: 100, axisLabel: { color: "#ccc" } }, series: [{ name: "胜率", type: "bar", data: rates, itemStyle: { color: "#00e5ff" }, barWidth: 18 }] }); }这里的几点经验:柱状图的barWidth要固定,不要依赖自动宽度,否则榜单球队数量变化时图表比例会崩;yAxis的max要根据指标含义设置,胜率是百分比所以设定100,如果是进球数就不应该写死;工具提示用axis而不是item,鼠标滑过时能看到整行数据,大屏演示更友好。
折线图、饼图、散点图的配置思路完全一致,无非是把series里的type换成line、pie、scatter。唯一要注意的是饼图的扇区数量,超过6个扇区建议合并为“其他”,否则标签全叠在一起,观感很差。
5.3 大屏的刷新、适配与缓存
大屏通常要求自动刷新数据。最简单的方式是轮询:
setInterval(() => { loadTeamRank(); loadOverview(); }, 60000);轮询间隔建议60秒以上,不要每5秒刷一次。分析任务是离线跑批,结果短期内不会变化,频繁请求只会增加服务器压力。如果以后要接入实时数据,可以再换WebSocket,但现在这个阶段setInterval完全够用。
适配方面,大屏设计稿一般是1920x1080,但演示电脑可能是1366x768或者更大屏,直接使用固定像素会在非设计分辨率下出现错位。我习惯用scale方案:外面套一个固定宽高的容器,按视口尺寸计算缩放比例:
function resizeScreen() { const screen = document.getElementById("screen"); const scale = Math.min(window.innerWidth / 1920, window.innerHeight / 1080); screen.style.transform = `scale(${scale})`; screen.style.transformOrigin = "left top"; } window.addEventListener("resize", resizeScreen); resizeScreen();这个方案虽然简单,但能解决大部分演示场景。要注意的是,缩放后外层容器的高度一定要手动设置为scale后的高度,否则会出现滚动条。
最后,如果大屏静态文件和Django接口不在同一个服务器,开发阶段必须配CORS;上线后我更推荐把编译好的静态文件放到Django/Nginx同一域下,用Nginx直接托管,再用反向代理转发API,这样既省去跨域,又能让Nginx做gzip压缩,大屏加载速度会明显提升。
6. 调试排坑与部署交付经验
6.1 高频问题速查表
把我在调试类似项目过程中遇到的高频问题整理成一张速查表,基本上覆盖了从Hadoop到Django的各个环节:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| Spark任务报OOM | executor内存不足或shuffle分区过多 | 调大spark.executor.memory,降低spark.sql.shuffle.partitions |
| HDFS put时报磁盘空间不足 | 副本数为3导致占用翻倍 | 检查dfs.replication,测试集群可改为2 |
| Spark读不到HDFS文件 | 路径写错或没有权限 | 用hdfs dfs -ls确认路径,检查目录权限 |
| MySQL连接报错 | JDBC驱动缺失或时区问题 | 确认mysql-connector-java已放classpath,URL加useSSL=false&serverTimezone=Asia/Shanghai |
| Django接口返回慢 | 大屏高频查MySQL且无缓存 | 接口加Redis缓存,列表查询加limit |
| 大屏请求接口跨域失败 | 未配置CORS或前后端端口不一致 | 安装django-cors-headers并配置允许来源 |
| 大屏图表不显示 | 接口字段和前端取字段不一致 | 用浏览器Network看返回结构,逐个对照字段名 |
| MySQL中文乱码 | 连接未指定utf8 | JDBC连接串和Django连接都加characterEncoding=utf8/charset=utf8mb4 |
这张表看起来零散,但每一个我都实际遇到或帮别人排查过,属于典型的“不踩不知道”的问题。
6.2 Spark内存参数调试实例
OOM是Spark任务里最常遇到的问题。假设集群是4个节点、每个节点16GB内存,你写了200个shuffle分区,每根executor只分到2GB,跑UNION ALL加GROUP BY时非常容易栈溢出或磁盘溢出。我一般这样调整:
spark-submit \ --master yarn \ --deploy-mode cluster \ --num-executors 4 \ --executor-memory 4g \ --executor-cores 2 \ --conf spark.sql.shuffle.partitions=24 \ --conf spark.default.parallelism=24 \ football_analysis.py注意,--num-executors乘以--executor-cores最好不要超过集群总核心数;executor-memory乘以num-executors不要超过总内存减去系统预留。对于几千场比赛的数据量来说,4个executor、每个4GB已经非常宽裕。如果数据量再大,优先考虑增加executor数量而不是盲目把单个executor调大到16GB,因为单个executor过大会导致资源碎片化。
另外,如果发现任务运行很快但写MySQL很慢,大概率是JDBC连接串没加rewriteBatchedStatements=true,批量写入效率会差很多。这条参数加上之后,通常能明显感受到写入速度提升。
6.3 Django部署与大屏演示的细节
部署时,我不推荐只用python manage.py runserver,它不够稳定。最小可用方案是:Gunicorn启动Django应用,Nginx负责静态文件和反向代理。大致步骤是:
- 在服务器上创建venv并安装依赖;
- 用gunicorn启动服务:
gunicorn football_project.wsgi:application -b 127.0.0.1:8000 --workers 2; - 配置Nginx把80端口转发到8000,同时把大屏静态文件放到Nginx的root目录;
- 用systemd管理Gunicorn进程,保证服务器重启后自动拉起。
文档和调试记录也一样重要。交付项目时,除了源码,至少应该包含:一份数据字典(每个字段的含义和来源)、一份架构和流程说明、一份接口文档(路径、参数、返回示例)、一份部署文档(环境要求、启动命令、常见问题),再加一份调试记录。这会让整个项目的完整度提升一个层级。答辩演示时,我强烈建议准备一份种子数据生成脚本,把分析结果提前算好并写入MySQL。这样即使现场网络不稳定或者集群没开,大屏依然能正常展示。
最后再分享一个经验:做这种全链路项目,一定要先把“HDFS → Spark → MySQL → Django → 大屏”这条主干跑通,再回头优化图表样式和指标维度。主干不通时做出来的大屏再多也只是静态图;主干通了,后面加指标、加图表都是水到渠成的事。我就是按这个顺序做了很多次之后,才慢慢养成了先跑通主链路、再精修细节的习惯。