
简介这是一份基于PythonDjangoMySQL构建的农业生产可视化系统项目主要面向Python Web初学者以及需要完成课程设计、期末大作业或毕业设计的学生。系统围绕农业指标数据和气象数据两条主线通过爬虫采集数据、清洗入库再在Django后端进行统计分析并以可视化图表呈现覆盖了数据获取、存储、接口和前端展示的完整链路。资源包共85个文件以27个Python源码文件为核心包含数据采集与存储脚本、Django项目配置和应用代码配套HTML/CSS/JS页面展示可视化结果SQL文件用于初始化MySQL数据库另有XML配置、字体图标资源以及requirements依赖清单和README说明文档压缩包整体仅1.35MB轻量易部署。项目基于Python 3.9.16与MySQL 8.0.26开发下载后按文档配置环境、导入SQL即可直接运行无需修改。目前已有316人学习使用作为导师指导并通过的高分毕业设计系统功能完善、界面美观非常适合直接参考或提交作业。1. 为什么农业生产一定要上可视化系统手头二十个地块、六七种作物、三年的采收记录放 Excel 里还能翻一旦掺进天气、施肥和用工数据每次按条件筛选都想放弃。这也是基于 pythondjangomysql 的农业生产可视化系统最常见的建设起点。做法是把散落的种植档案收进 MySQL用 Django 写聚合接口浏览器端用图表呈现产量趋势和区域分布解决的是数据有了却用不起来的问题。这套组合适合农业信息化团队、农场数字化负责人也适合快速交付数据项目的 Python 开发者。下面按开发顺序推进先定表结构再写聚合接口然后接 ECharts最后处理部署与性能。每步都有可复制的代码和参数说明新手能按步骤跑通熟手可以直接跳去接口和调优部分拿参数。2. 数据建模把农业指标拆成 Django 模型和 MySQL 表2.1 先分维度表和事实表再动手写模型农业数据的核心事实是某天在某地块采收了多少公斤某作物。围绕它有两类维度地块的面积、位置、土壤类型作物的品类、生长周期、参考单价。设计时把维度单独建表采收记录作为事实表用外键关联。后续做按年、按地块、按品类的聚合时Django ORM 的 annotate 与 MySQL 的 GROUP BY 才有稳定的落点。常见误区是图省事把地块名和作物名直接写成字符串字段塞进一张宽表。短期写着简单地块一旦改名或换品种历史记录跟着错。外键把变更收敛成一次 update代价是多一次 joinMySQL 只要索引建对这个代价可以忽略。真正要警惕的是过度拆表地块面积、作物单价这类极少变化的属性没必要拆到第三范式冗余存一份能让聚合查询少两张关联表。2.2 三张核心表的 Django 模型定义前提是用 django-admin startproject 建好项目、python manage.py startapp farm 创建应用。下面这段是 farm/models.py 的完整内容# farm/models.py from django.db import models class Field(models.Model): 地块维度表一个农场名下的地块 name models.CharField(地块名称, max_length64) area models.DecimalField(面积(亩), max_digits10, decimal_places2) location models.CharField(所在区域, max_length128, db_indexTrue) soil_type models.CharField(土壤类型, max_length32, blankTrue) class Meta: db_table farm_field def __str__(self): return self.name class Crop(models.Model): 作物维度表品类、周期、单价 name models.CharField(作物名称, max_length64, uniqueTrue) growth_cycle models.IntegerField(生长周期(天), default90) unit_price models.DecimalField(参考单价(元/斤), max_digits8, decimal_places2) class Meta: db_table farm_crop class HarvestRecord(models.Model): 采收事实表一次采收一行数据 field models.ForeignKey(Field, on_deletemodels.PROTECT, related_nameharvests) crop models.ForeignKey(Crop, on_deletemodels.PROTECT, related_nameharvests) harvest_date models.DateField(采收日期) yield_amount models.DecimalField(产量(kg), max_digits12, decimal_places2) labor_hours models.DecimalField(用工(小时), max_digits8, decimal_places1, default0) created_at models.DateTimeField(记录创建时间, auto_now_addTrue) class Meta: db_table farm_harvest_record indexes [ models.Index(fields[harvest_date, crop], nameidx_harvest_date_crop), ]这段代码有三个细节值得展开。on_delete 用了 PROTECT 而不是 CASCADE防止误删地块或作物时连带清空历史产量农业数据一旦丢失很难补录。产量和面积都用 DecimalField 而不是 FloatField浮点在累加时会产生精度误差做月度汇总差几公斤看不出来年度结算就对不上账。联合索引 idx_harvest_date_crop 按等值条件在前、范围条件在后排列crop 的相等判断在前、harvest_date 的范围查询在后这是 MySQL 组合索引最有效的字段顺序。2.3 Django 字段到 MySQL 类型的对应关系执行 makemigrations 和 migrate 前先确认字段最终落在哪种 MySQL 类型上。下面是开发中常用的对应表Django 字段MySQL 类型农业场景示例IntegerFieldint生长周期天数DecimalField(max_digits12, decimal_places2)decimal(12,2)产量、面积、单价DateFielddate采收日期DateTimeFielddatetime数据录入时间CharField(max_length64)varchar(64)地块名、区域TextFieldlongtext农事备注迁移完成后用 mysql workbench 或命令行执行 SHOW CREATE TABLE farm_harvest_record核对字符集是否为 utf8mb4、引擎是否为 InnoDB。utf8mb4 存中文和生僻字没有问题InnoDB 是事务和行级锁的基础这两项在 Django 2.2 之后的默认配置里已经就绪改动前先确认现有库的实际值。2.4 历史数据导入从 CSV 到 MySQL 的三个方案新系统上线前总有历史 Excel 要搬。简单说有三种路径小数据量用 admin 后台手动录几万行的中等数据量写脚本调 bulk_create几十万行以上用 pandas 读 CSV 后直接 to_sql绕过 ORM 的开销。农业项目最常见的是第二种代码可以复用下面的模板# scripts/import_harvest.py import csv from datetime import datetime from django.utils.timezone import now from farm.models import HarvestRecord, Field, Crop def import_csv(filepath): rows [] with open(filepath, encodingutf-8-sig) as f: for line in csv.DictReader(f): field Field.objects.filter(nameline[地块]).first() crop Crop.objects.filter(nameline[作物]).first() if not field or not crop: continue rows.append(HarvestRecord( fieldfield, cropcrop, harvest_datedatetime.strptime(line[日期], %Y-%m-%d).date(), yield_amountline[产量(kg)], labor_hoursline[用工(小时)] or 0, created_atnow(), )) HarvestRecord.objects.bulk_create(rows, batch_size2000)这段脚本有两个容易翻车的地方。csv.DictReader 读到的所有字段都是字符串yield_amount 存进 DecimalField 前 Django 会做转换但如果源文件里有空值或暂无这类文本转换失败会导致整批回滚稳妥做法是在循环里加 try/except 记录日志跳过脏行而不是中断整个导入。另一处是 bulk_create 不会触发模型的 save() 信号也不会写 auto_now_add 字段所以 created_at 必须在循环里用 now() 显式赋值否则入库后时间全是空。3. 聚合查询与 Django API让前端拿到可直接出图的数据3.1 聚合放在数据库而不是 Python 内存里可视化接口要做的事很固定给时间范围返回按月产量合计给区域返回各作物占比给年份返回月度趋势。这些问题用 Django ORM 的 aggregate 和 annotate 能直接翻译成 SQL 的 SUM 和 GROUP BY计算由 MySQL 在索引上完成。最常见的反面案例是先 .all() 把数据倒进 Python再用循环累加。两三万行看不出问题跨到三四个年份、几十个地块时接口响应会从 100ms 涨到 2 秒以上图表加载时的空白足够用户关掉页面。判断聚合放哪层有个简单标准结果行数远小于源数据行数时必须交给数据库。按月聚合 10 万行得到 24 行等于把 10 万行的遍历让给 MySQL 存储引擎Django 只负责把 24 行结果序列化成 JSON。有人习惯把这类聚合写成 MySQL 存储过程Django 侧用 raw query 调用但农业项目的统计口径经常变加减一个筛选条件就得改存储过程改起来比 Python 代码慢不推荐这条路。3.2 按月产量接口TruncMonth 与 annotate 的组合下面是 farm/views.py 里可直接用的接口输出格式专门为 ECharts 折线图设计# farm/views.py from django.db.models import Sum, Avg from django.db.models.functions import TruncMonth from django.http import JsonResponse, HttpResponse from django.views.decorators.http import require_GET from .models import HarvestRecord def _build_trend_data(year, crop_id): 按年/作物过滤后按月聚合产量返回 ECharts 可直接使用的结构 qs HarvestRecord.objects.all() if year: qs qs.filter(harvest_date__yearyear) if crop_id: qs qs.filter(crop_idcrop_id) rows ( qs.annotate(monthTruncMonth(harvest_date)) .values(month, crop__name) .annotate(totalSum(yield_amount), avg_yieldAvg(yield_amount)) .order_by(month) ) result {months: [], series: {}} for row in rows: month row[month].strftime(%Y-%m) crop row[crop__name] if month not in result[months]: result[months].append(month) result[series].setdefault(crop, []).append(float(row[total])) return result require_GET def yield_trend(request): data _build_trend_data( yearrequest.GET.get(year, ), crop_idrequest.GET.get(crop_id, ), ) if not data[months]: return HttpResponse(status204) return JsonResponse(data)配套的路由挂在工程根 urls.py 的 include 下前端就能通过 /api/yield-trend/ 访问# farm/urls.py from django.urls import path from . import views urlpatterns [ path(api/yield-trend/, views.yield_trend, nameyield_trend), ]TruncMonth 是 Django 提供的日期截断函数对应 MySQL 的 DATE_FORMAT 聚合逻辑把 2024-03-15 归到 2024-03-01 这一档。values(month, crop__name) 决定分组粒度即按月、按作物各出一行。annotate 里的 total 和 avg_yield 会追加为每行的新字段前端不用再做任何二次计算avg_yield 后续做单产对比报表时也不用改接口。最后用 strftime 把 date 对象转成 YYYY-MM 字符串避免 JSON 序列化时把日期变成数组。3.3 接口参数校验与边界处理year 和 crop_id 都来自 GET 参数拼进 filter 前要做两层校验。第一层判断空字符串上面代码已处理第二层做类型校验避免 crop_idabc 这类值传到 MySQL 后抛异常。简单项目可以在函数开头加一层转换try: crop_id int(crop_id) except (TypeError, ValueError): crop_id None注意crop_id 转 int 时抛出的 TypeError 和 ValueError 要分开处理前者是 None 传入后者是字符串无法转换统一置 None 再走不过滤分支比直接返回 400 对前端更友好。另一个容易忽略的点是空数据响应。某年完全没有采收记录时result 的 months 和 series 都是空对象前端 setOption 会渲染成空白。接口侧约定 months 为空时返回 HTTP 204前端收到 204 就显示暂无数据占位图避免 ECharts 抛 canvas 相关的难懂报错。这个约定写进接口文档前后端联调能少吵两次架。接口的参数约定汇总如下参数类型必填说明示例yearstr否年份过滤格式 YYYY空返回全部年份2024crop_idint否作物主键空返回所有作物34. 可视化落地ECharts 图表与 Django 的两种集成路线4.1 模板渲染 Ajax 与 Vue 前后端分离怎么选农业生产可视化通常有两种形态内部管理后台的图表页和接待参观用的可视化大屏。对应到技术选型前者用 Django 模板 Ajax 拉 JSON 就够后者一般是 Vue 前后端分离。分界线不在技术新鲜度而在页面数量和交互复杂度。只有三五个图表页面、且由同一团队维护时模板渲染是短路径少一套 Node 构建流程部署少一个静态资源目录。指标超过十个、需要联动筛选、路由切换和组件复用时再上 Vue 不迟。两种路线的取舍关系大致如下维度Django 模板 AjaxVue 前后端分离图表页面数量3-5 个够用10 个以上交互复杂度简单筛选、定时刷新多图表联动、多维下钻开发链路Python HTML JS需 Node 构建流程部署方式Django 模板目录Nginx 托管 dist 静态目录4.2 最小可用的 ECharts 折线图模板下面是一个完整的 Django 模板页面直接从第 3 章的接口拉数据渲染折线图!-- templates/dashboard.html -- {% load static %} !DOCTYPE html html langzh-CN head meta charsetUTF-8 title农业生产可视化平台/title script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script /head body div idyield-chart stylewidth:100%; height:460px;/div script const chart echarts.init(document.getElementById(yield-chart)); async function loadTrend() { const resp await fetch(/api/yield-trend/?year2024); if (resp.status 204) { chart.clear(); return; } const data await resp.json(); const series Object.keys(data.series).map(name ({ name: name, type: line, smooth: true, showSymbol: false, data: data.series[name], areaStyle: { opacity: 0.12 }, })); chart.setOption({ title: { text: 2024 年各作物月度产量(kg) }, tooltip: { trigger: axis }, legend: { bottom: 0 }, grid: { left: 60, right: 20, top: 60, bottom: 60 }, xAxis: { type: category, data: data.months }, yAxis: { type: value, name: 产量 }, series: series, }, true); } loadTrend(); setInterval(loadTrend, 60000); /script /body /htmlsetOption 的第二个参数传 true 表示 notMerge每次加载用新配置整体替换旧配置比默认的 merge 模式更适合动态刷新否则上次渲染的 series 会残留出现图例越来越多、线条叠加的灵异现象。yAxis 的 name 标注单位tooltip 的 trigger 设为 axis鼠标悬停时才能看到同一月份所有作物的对比。showSymbol 设为 false数据点多时只保留线条避免圆点挤成一团。提示内网农场环境访问外网 CDN 可能很慢上线前把 echarts.min.js 下载到 Django 的 static 目录用 {% static js/echarts.min.js %} 引用。4.2.1 折线变柱状一个参数切换图表类型ECharts 的系列类型由 series 的 type 字段声明同样一组数据把 type 从 line 换成 bar 就是柱状图。生产环境常见做法是页面放两个按钮让用户在图型之间切换后端接口完全不用动。注意 bar 图不要配 areaStyle柱子和面积的视觉冲突会很难看。折线图适合看趋势柱状图适合同月份不同作物的横向比较这套切换逻辑留在模板里即可。4.3 大屏实时刷新的轮询实现可视化大屏最常见的需求是数据每五分钟更新一次。最省事的做法是 setInterval 定时重拉接口上面代码里的 60000 毫秒就是轮询间隔。这个值按业务容忍度调气象数据半小时一次足够采收进度可以压到 10 秒但低于 5 秒就要同时考虑接口压力和数据库负载。每次刷新都重新查 MySQL 聚合QPS 会随大屏数量和轮询密度线性上涨。如果只是单机几块大屏轮询给聚合结果加一层 Django cache 就够不必上消息队列。常见做法是把结果按年份做缓存键缓存 5 分钟# farm/cache_utils.py from django.core.cache import cache from .views import _build_trend_data def get_trend_cached(year): key ftrend_{year} data cache.get(key) if data is None: data _build_trend_data(yearyear) cache.set(key, data, 300) return data缓存后端可以是数据库、文件或 redis生产上用 redis 存这类聚合结果读写都在内存里比文件缓存少一次磁盘 IO。300 秒过期时间和 60 秒轮询配合同一分钟内 5 块大屏只有第 1 个请求真正打到 MySQL。缓存失效瞬间同时涌入大量请求会造成击穿常见做法是用互斥锁包住重建逻辑保证同一时刻只有一个请求去查 MySQL其余请求短等后重试读缓存。区域分布大屏同理ECharts 的 map 系列加 GeoJSON 就能画县级产量分布图聚合接口只把分组字段从 crop 换成 location 即可。5. 部署与性能调优MySQL 连接参数、索引和缓存策略5.1 生产环境 Django 连接 MySQL 的推荐参数开发时默认配置跑得欢上生产第一波问题几乎都出在数据库连接上。settings.py 里的 MySQL 配置推荐如下DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: farm_visual, USER: farm_user, PASSWORD: xxxxx, HOST: 127.0.0.1, PORT: 3306, CONN_MAX_AGE: 60, OPTIONS: { charset: utf8mb4, connect_timeout: 5, }, } }CONN_MAX_AGE 设为 60 秒让 Django 复用同一个 MySQL 连接省去每次请求的 TCP 握手和认证开销。注意这个参数必须小于 MySQL 的 wait_timeout否则连接被服务端回收后 Django 还在复用下一次请求直接报 MySQL server has gone away。宝塔部署 django 项目时在 MySQL 配置面板把 wait_timeout 改成 300 左右再配合 CONN_MAX_AGE60 就足够安全。同一次配置里顺手把 max_allowed_packet 从默认 4M 提到 64M历史数据导入走 CSV 大文件时会省掉 packet 超限的报错。5.2 慢查询定位与索引补建大屏接口变慢时先确认慢查询日志并设置阈值SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;再看对应 SQL 的 EXPLAIN 执行计划。聚合查询能走联合索引 idx_harvest_date_crop 是关键如果按区域统计各作物产量查询会转去扫 farm_field.location 的单列索引。低于五万行的表全表扫描也就几十毫秒索引不是越多越好只为真实出现在 WHERE 和 GROUP BY 里的字段建索引。5.3 一个容易被忽略的验证技巧调试可视化接口时打开 Django Debug 工具栏或直接打印 connection.queries把 ORM 生成的 SQL 复制到 mysql workbench 里手动执行。对比前后两条 EXPLAIN 的 type 列type 从 ALL 变成 ref 或 range 说明索引生效rows 列明显收缩说明查询代价下降。照这个流程能在一分钟内定位大多数图表接口的卡顿点。本文还有配套的精品资源点击获取