news 2026/9/4 5:09:45

Django+MySQL+ECharts全栈实践:空气质量数据可视化项目全流程拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django+MySQL+ECharts全栈实践:空气质量数据可视化项目全流程拆解

简介:本资源是一套完整的Python高分课程设计项目,面向数据分析初学者、Web开发入门者及环境科学相关专业学生,聚焦城市PM2.5空气质量数据的采集、存储、分析与可视化全流程实践。项目基于Django框架构建Web应用,后端采用MySQL持久化存储,集成多维度CSV实测数据(覆盖北京、上海、广州等五城六年PM2.5时序与气象关联数据),并提供配套HTML模板、模型定义、视图逻辑及SQL建库脚本,结构清晰、模块解耦,便于理解MVC架构与数据驱动开发范式。压缩包共66个文件,含17个核心Python源码、24个地域/维度CSV数据集、3个HTML前端页面、2个Markdown文档(含详细部署指南)、1个SQL数据库初始化脚本及requirements依赖清单,整体大小12.38MB。已有143人学习下载,资源经实测可运行,附带数据替换说明与常见问题提示,小白可依文档快速部署,亦可作为课程设计、毕业设计或环保类数据分析实战参考。

1. 项目概述与核心价值

最近在整理过往项目资料时,翻出了一个几年前做的关于城市空气质量数据可视化的老项目。这个项目用Python的Django框架搭了个后端,MySQL存数据,前端做了些图表来展示PM2.5的变化趋势。虽然技术栈现在看来不算新颖,但整个项目的设计思路、从数据获取到前端展示的完整链路,以及其中踩过的坑和积累的经验,对于想入门全栈开发或者数据可视化方向的朋友来说,依然有很高的参考价值。它不是一个简单的“玩具”项目,而是具备了数据采集、清洗、存储、分析、展示和用户交互等环节的“准生产级”应用雏形。如果你正在学习Django,想了解如何将一个数据分析想法落地成一个可交互的Web应用,或者对如何构建一个数据驱动的业务系统感到好奇,那么这个项目的拆解可能会给你不少启发。

这个项目的核心,是解决“如何让静态的、枯燥的空气质量数据‘活’起来”的问题。单纯看Excel表格里成列的数字,很难直观感受到污染的变化规律、时空分布特征。通过Web可视化的方式,将时间序列数据转化为折线图、将地理数据转化为热力图或分级统计地图,用户就能一目了然地看到哪个区域污染严重、哪个时段需要减少户外活动。技术实现上,它涵盖了Web开发的经典三层架构:Django负责业务逻辑和路由控制,MySQL作为关系型数据库持久化存储结构化数据,前端则利用ECharts或Highcharts这类成熟的图表库进行渲染。接下来,我会把这个项目从设计到部署的每一个环节掰开揉碎,结合我当时的思考和一些“事后诸葛亮”式的优化建议,分享给大家。

2. 项目整体架构与设计思路

2.1 为什么选择Django + MySQL这个技术栈?

当时选择Django,首要考虑的是其“开箱即用”的特性和强大的ORM(对象关系映射)。对于一个数据驱动的分析型项目,核心模型(如监测站点、空气质量记录、城市信息)的结构是相对稳定和清晰的。Django的models.py可以让我用Python类的方式定义这些模型,然后通过简单的命令行操作就能同步到MySQL数据库生成数据表,这极大地提升了开发效率,也减少了直接手写SQL可能带来的错误。其次,Django自带的后台管理界面(Admin)在项目初期简直是“神器”。录入测试数据、手动修改某些记录、管理用户权限,都可以通过这个现成的界面完成,让我能快速聚焦在核心的业务逻辑和数据可视化上,而不是花大量时间去搭建一个基础的数据管理后台。

MySQL的选择则更多是出于可靠性和生态的考虑。空气质量数据是典型的时序数据,虽然总量可能很大(全国数百个城市,多年每日数据),但结构非常规整(时间、地点、污染物浓度)。MySQL对于这类结构化数据的存储、索引和关联查询已经非常成熟,配合Django ORM,进行如“查询北京市2023年所有PM2.5日均值”这样的操作会非常方便。当然,如果纯粹从时序数据优化的角度,现在可能会考虑InfluxDB或TDengine,但在项目当时,MySQL的通用性和团队熟悉度是更重要的因素。这个组合确保了项目在数据持久化层的稳定和高效。

2.2 核心数据流与功能模块设计

整个系统的数据流可以概括为“采集 -> 入库 -> 处理 -> 展示”四个核心环节。虽然项目压缩包里可能只包含了处理后的示例数据和展示部分,但一个完整的系统必须考虑数据从何而来。

  1. 数据采集模块:这是项目的“源头活水”。当时我采用了混合方式。一部分历史数据来源于公开的数据平台(如环保部门官网、数据集市),通过编写Python爬虫(使用requestsBeautifulSoup)定期抓取。另一部分则需要模拟实时数据,我写了一个脚本,基于历史数据的统计特征(如季节性、日变化规律)生成模拟的实时监测数据,并按照固定频率(如每小时)通过Django的API接口或直接写入数据库。在设计时,这个模块被设计成独立于主Web应用的服务,通过计划任务(如Linux的cron或Celery)调度运行,确保数据源的可持续性。

  2. 数据存储与模型设计:这是Django ORM发挥核心作用的地方。我设计了几个关键模型:

    • City: 存储城市信息,如城市代码、名称、所属省份、经纬度等。经纬度字段是为后续地图可视化准备的。
    • MonitoringSite: 监测站点信息,关联到某个City,包含站点名称、编码、具体位置经纬度。一个城市可能有多个监测站点。
    • AirQualityData: 核心数据表。字段包括:关联的MonitoringSite(或City,取决于数据粒度)、数据日期(date)、数据时间点(time,如果是小时数据)、以及各种污染物浓度字段,如pm25,pm10,so2,no2,co,o3,还有通用的空气质量指数aqi和首要污染物primary_pollutant。这里有一个关键设计点:为了优化查询速度,特别是在按城市、按时间范围筛选时,我对datecity_id(如果直接关联城市)等字段建立了数据库索引。
  3. 业务逻辑与API层:Django的视图(Views)负责处理Web请求。我主要设计了两类视图:一类是渲染HTML模板的视图,用于返回包含图表容器的主页面;另一类是返回JSON数据的API视图,这是前后端分离的关键。例如,一个名为/api/pm25_trend/的API,接收city_idstart_dateend_date参数,后端视图函数中便使用Django ORM查询AirQualityData表,按时间排序,将PM2.5浓度和对应日期序列化成JSON格式返回给前端。Django REST framework (DRF) 是构建这类API的绝佳工具,它能自动处理序列化、验证和文档生成,但我最初为了保持项目简洁,直接使用了Django内置的JsonResponse

  4. 前端可视化层:前端页面使用基础的HTML/CSS/JavaScript,并引入了ECharts库。页面上有一个城市选择下拉框和一个日期范围选择器。当用户选择后,JavaScript会调用上述的Django API,获取到JSON格式的数据,然后调用ECharts的API,动态地绘制出PM2.5浓度时间趋势折线图。除了趋势图,项目还包含了其他视图,比如用散点图展示PM2.5与PM10的相关性,用日历热力图展示全年每日的污染情况,以及利用城市经纬度在地图上用不同颜色圆点标记污染等级的地理分布图。

注意:在模型设计时,关于数据粒度需要仔细权衡。如果数据源是城市级别的日均值,那么直接关联City表即可。如果数据源是站点级别的小时值,则关联MonitoringSite表更合理。这决定了后续查询的复杂度和效率。我的建议是,尽可能保留最细粒度的原始数据,在需要聚合时(如求城市日均值)通过数据库查询实时计算或定期预计算,这样灵活性最高。

3. 核心细节解析与实操要点

3.1 Django模型定义与数据库优化实践

让我们深入看看models.py里的关键部分。以核心的AirQualityData模型为例,其定义远不止是简单的字段罗列。

from django.db import models class City(models.Model): """城市模型""" code = models.CharField(max_length=10, unique=True, verbose_name='城市代码') name = models.CharField(max_length=50, verbose_name='城市名称') province = models.CharField(max_length=50, verbose_name='省份') longitude = models.FloatField(verbose_name='经度') # 用于地图可视化 latitude = models.FloatField(verbose_name='纬度') # 用于地图可视化 class Meta: db_table = 'city' # 自定义表名 verbose_name = '城市' verbose_name_plural = verbose_name indexes = [ models.Index(fields=['province']), # 为省份字段建立索引,方便按省筛选 ] def __str__(self): return f'{self.name} ({self.province})' class AirQualityData(models.Model): """空气质量数据模型""" city = models.ForeignKey(City, on_delete=models.CASCADE, related_name='air_quality_data', verbose_name='城市') date = models.DateField(verbose_name='日期', db_index=True) # 关键索引字段 pm25 = models.FloatField(null=True, blank=True, verbose_name='PM2.5浓度(μg/m³)') pm10 = models.FloatField(null=True, blank=True, verbose_name='PM10浓度(μg/m³)') aqi = models.IntegerField(null=True, blank=True, verbose_name='空气质量指数') # ... 其他污染物字段 created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: db_table = 'air_quality_data' verbose_name = '空气质量数据' verbose_name_plural = verbose_name # 复合索引:对于经常按城市和日期查询的场景,复合索引效率极高 indexes = [ models.Index(fields=['city', 'date']), models.Index(fields=['date']), # 单日期索引也保留,用于全局时间范围查询 ] # 唯一性约束:防止同一城市同一日期的数据被重复插入 unique_together = ['city', 'date'] def __str__(self): return f'{self.city.name} - {self.date} - AQI: {self.aqi}'

实操要点与避坑指南:

  1. 索引是双刃剑:为datecity以及它们的组合创建索引,能极大提升filter(city=some_city, date__range=(start, end))这类查询的速度。但是,索引会降低数据插入和更新的速度,并占用额外磁盘空间。对于这个以查询为主、批量插入(如每日凌晨导入前一天数据)的应用,利远大于弊。切记不要为每一个字段都建索引,通常只为高频查询条件排序字段建立。

  2. 使用db_index=True与在Meta中定义indexes:两者效果相同。对于单字段索引,直接在字段定义中使用db_index=True更简洁。对于复合索引(多字段联合索引),必须在Meta.indexes中定义。复合索引的顺序很重要,应遵循“最左前缀匹配”原则。例如索引['city', 'date'],对filter(city=xx)filter(city=xx, date__range=...)查询都有效,但对filter(date__range=...)单独查询则无效。

  3. unique_together(唯一约束):这个设置至关重要。它能从数据库层面保证不会出现同一城市同一日期的重复数据,避免了数据混乱。在数据采集脚本中,应采用“插入或更新”(UPSERT)的逻辑,比如使用Django的update_or_create方法。

  4. 关于空值:污染物浓度字段设置为null=True, blank=True,因为原始数据可能存在缺失。在后续的数据处理和可视化中,需要处理这些None值,比如在前端用虚线断开折线,或者在聚合计算时忽略。

3.2 高效数据查询与序列化

API视图的核心是从数据库高效查询并序列化数据。以下是一个获取PM2.5趋势数据的视图函数示例:

# views.py from django.http import JsonResponse from django.views.decorators.http import require_GET from django.core.exceptions import ObjectDoesNotExist from .models import City, AirQualityData import json @require_GET def get_pm25_trend(request): """获取指定城市、时间范围内的PM2.5趋势数据API""" city_code = request.GET.get('city_code') start_date = request.GET.get('start_date') end_date = request.GET.get('end_date') # 1. 参数验证 if not all([city_code, start_date, end_date]): return JsonResponse({'error': '缺少必要参数: city_code, start_date, end_date'}, status=400) try: city = City.objects.get(code=city_code) except ObjectDoesNotExist: return JsonResponse({'error': '城市代码不存在'}, status=404) # 2. 数据库查询 - 使用select_related优化,并利用索引 # filter条件顺序与复合索引(city, date)匹配,数据库能高效利用索引 queryset = AirQualityData.objects.filter( city=city, date__gte=start_date, date__lte=end_date ).select_related('city') # 一次性连表获取城市信息,避免N+1查询 # 只取需要的字段,减少数据传输量 queryset = queryset.values('date', 'pm25').order_by('date') # 3. 数据序列化 trend_data = list(queryset) # 将QuerySet转换为列表 # 处理可能存在的空值,为前端提供统一格式 processed_data = [] for item in trend_data: processed_data.append({ 'date': item['date'].strftime('%Y-%m-%d'), # 日期格式化为字符串 'pm25': item['pm25'] if item['pm25'] is not None else None, }) # 4. 返回JSON响应 return JsonResponse({ 'city_name': city.name, 'trend': processed_data })

经验分享:

  • select_relatedprefetch_related:这是Django ORM性能优化的关键。select_related用于一对一或一对多关系(外键),通过SQL JOIN一次性取出关联对象的数据。在这个例子里,虽然我们查询的是AirQualityData,但返回结果中需要城市名,使用select_related('city')能避免在循环中为每一条数据再次查询数据库获取城市信息(即N+1查询问题)。如果是多对多关系,则使用prefetch_related
  • values()values_list():当只需要模型的部分字段时,使用values()来指定字段,QuerySet会返回字典列表,而不是完整的模型对象实例,这能减少内存占用和序列化开销。如果只需要元组列表,values_list()更高效。
  • 查询集惰性求值与缓存:Django的QuerySet是惰性的,直到被求值(如迭代、切片、序列化)时才会真正执行数据库查询。同时,对同一个已求值的QuerySet再次访问时会使用缓存。但要注意,像len(queryset)list(queryset)会导致求值,而queryset.count()会执行COUNT查询,不会缓存结果。

4. 前端可视化实现与交互设计

4.1 ECharts集成与动态图表绘制

前端页面我们使用Bootstrap进行简单布局,并引入ECharts。核心在于JavaScript如何与Django API交互并更新图表。

<!-- trend.html 关键部分 --> <div class="row"> <div class="col-md-3"> <select id="citySelect" class="form-select"> <!-- 选项由后端动态渲染或通过另一个API加载 --> <option value="beijing">北京市</option> <option value="shanghai">上海市</option> <!-- ... --> </select> <input type="date" id="startDate" class="form-control mt-2"> <input type="date" id="endDate" class="form-control mt-2"> <button onclick="loadChartData()" class="btn btn-primary mt-2">查询</button> </div> <div class="col-md-9"> <div id="pm25Chart" style="width: 100%; height: 500px;"></div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> <script> // 初始化ECharts实例 const chartDom = document.getElementById('pm25Chart'); const myChart = echarts.init(chartDom); // 默认加载最近30天的数据 function loadChartData() { const cityCode = document.getElementById('citySelect').value; const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; // 构建API请求URL const apiUrl = `/api/pm25_trend/?city_code=${cityCode}&start_date=${startDate}&end_date=${endDate}`; // 使用Fetch API发起请求 fetch(apiUrl) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { if (data.error) { alert(data.error); return; } // 成功获取数据后,更新图表 updateChart(data); }) .catch(error => { console.error('获取数据失败:', error); alert('数据加载失败,请检查网络或参数。'); }); } function updateChart(apiData) { const dates = apiData.trend.map(item => item.date); const pm25Values = apiData.trend.map(item => item.pm25); // ECharts配置项 const option = { title: { text: `${apiData.city_name} PM2.5浓度趋势`, left: 'center' }, tooltip: { trigger: 'axis', formatter: function (params) { // 自定义提示框内容,处理空值显示 const date = params[0].axisValue; const value = params[0].data; const displayValue = (value === null || value === undefined) ? '数据缺失' : value.toFixed(1); return `日期:${date}<br/>PM2.5:${displayValue} μg/m³`; } }, xAxis: { type: 'category', data: dates, axisLabel: { rotate: 45 // 日期标签倾斜,避免重叠 } }, yAxis: { type: 'value', name: 'PM2.5 (μg/m³)', // 可以添加分割线区域,直观显示污染等级(如0-35优,35-75良...) splitArea: { show: true, areaStyle: { color: ['rgba(144, 238, 144, 0.1)', 'rgba(255, 255, 224, 0.1)', 'rgba(255, 165, 0, 0.1)', 'rgba(255, 99, 71, 0.1)'] } } }, series: [{ name: 'PM2.5浓度', type: 'line', data: pm25Values, smooth: true, // 平滑曲线 markPoint: { // 标记最高最低点 data: [ { type: 'max', name: '最大值' }, { type: 'min', name: '最小值' } ] }, lineStyle: { width: 3 }, itemStyle: { color: '#5470c6' } }], // 数据区域缩放,适合查看长时间序列的细节 dataZoom: [{ type: 'inside', start: 0, end: 100 }, { start: 0, end: 100, handleSize: '80%' }] }; myChart.setOption(option); // 响应窗口大小变化 window.addEventListener('resize', function() { myChart.resize(); }); } // 页面加载时初始化默认数据 window.onload = function() { const end = new Date(); const start = new Date(); start.setDate(start.getDate() - 30); // 默认查最近30天 document.getElementById('startDate').valueAsDate = start; document.getElementById('endDate').valueAsDate = end; loadChartData(); // 自动加载一次 }; </script>

前端开发心得:

  1. 异步加载与用户体验:使用fetch进行异步数据请求,页面不会刷新,体验更好。一定要做好加载状态提示(比如在按钮上显示“加载中...”)和错误处理(catch部分),不要让用户面对一个空白或卡死的图表。
  2. 图表配置的灵活性:ECharts的配置项option非常庞大。上述代码只实现了基础折线图。你可以轻松扩展,比如添加一个series数组来同时绘制PM2.5和PM10的对比线,或者利用visualMap组件将折线颜色与污染等级关联。多查阅ECharts官方文档和示例,是提升可视化效果的最佳途径。
  3. 处理缺失数据:真实数据常有缺失。在formatter函数中处理null值,避免图表出错。也可以考虑使用ECharts的connectNulls: true选项让折线在缺失点处断开,这比直接插值更诚实。
  4. 性能考虑:如果时间范围拉得很长(比如一年以上的日数据),返回的数据点可能过多,导致前端渲染卡顿。此时,后端API应该考虑数据聚合(如返回月均值)或分页,前端也可以使用dataZoom组件让用户先看概览,再缩放查看细节。

4.2 多视图联动与高级可视化

一个完整的分析系统不应只有趋势图。我们可以增加更多视图,并实现联动。

  • 日历热力图:使用echarts-calendar扩展,可以直观展示一年中每天PM2.5的污染情况,颜色深浅代表浓度高低。这对于发现污染的周期性(如冬季更严重)非常有效。
  • 地理分布图(散点/热力图):利用每个城市的经纬度,在ECharts地图上绘制散点。点的颜色和大小可以映射PM2.5浓度,一眼就能看出全国哪些区域是“重灾区”。这需要引入中国地图的GeoJSON数据。
  • 相关性散点图:绘制PM2.5和PM10(或其他污染物)的散点图,并计算相关系数,可以分析污染物之间的同源性。
  • 仪表盘集成:使用Bootstrap的栅格系统,将上述多个图表组织在一个仪表盘页面中。通过全局的筛选控件(城市、时间),让所有图表联动更新。这需要前端状态管理,可以将筛选参数保存在一个全局对象或Vue/React的状态中,每个图表组件监听其变化并重新请求数据。

提示:在实现多图表联动时,要警惕“瀑布流”请求问题。即用户点击查询后,每个图表独立发起一个API请求。如果图表多,会对服务器造成压力,且加载完成时间不一致。一个优化方案是,后端提供一个“聚合API”,一次请求返回所有图表所需的数据;或者前端使用Promise.all()并发请求,统一管理加载状态。

5. 项目部署与生产环境考量

开发完成只是第一步,让应用在服务器上稳定运行才是终点。这里分享从开发机(如Windows/Mac)部署到Linux生产环境(以Ubuntu + Nginx为例)的关键步骤和坑点。

5.1 部署准备与依赖管理

首先,确保你的项目根目录有一个requirements.txt文件,它记录了所有Python依赖包及其版本。使用pip freeze > requirements.txt生成。在生产服务器上,建议使用虚拟环境。

# 在Ubuntu服务器上 # 1. 更新系统并安装基础软件 sudo apt update && sudo apt upgrade -y sudo apt install python3-pip python3-venv nginx git -y # 2. 克隆你的项目代码 cd /var/www sudo git clone <你的项目Git仓库地址> air_quality_app cd air_quality_app # 3. 创建并激活虚拟环境 python3 -m venv venv source venv/bin/activate # 4. 安装依赖(建议使用国内镜像加速) pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple # 关键依赖通常包括:Django, mysqlclient, gunicorn, gevent等

部署心得:

  • 永远不要在生产环境用pip安装全局包:虚拟环境是隔离的,避免污染系统Python环境,也便于管理不同项目的依赖。
  • 固定版本requirements.txt里最好使用Django==4.2.10这样的固定版本号,而不是Django>=4.2,确保生产环境和开发环境完全一致,避免因包版本升级导致的不兼容问题。
  • MySQL客户端问题:在Linux上安装mysqlclient可能需要系统级的MySQL开发库。如果pip install失败,先运行sudo apt install libmysqlclient-devsudo apt install default-libmysqlclient-dev

5.2 数据库迁移与静态文件收集

在服务器上配置好MySQL数据库(创建数据库、用户并授权)后,需要修改Django项目的settings.py中的DATABASES配置,然后进行迁移和创建超级用户。

# 在虚拟环境中操作 # 1. 执行数据库迁移,创建数据表 python manage.py migrate # 2. 创建超级管理员(用于Django Admin) python manage.py createsuperuser # 3. 收集静态文件(CSS, JS, 图片等)到指定目录,供Nginx直接服务 python manage.py collectstatic

关键配置(settings.py片段):

# 生产环境设置 DEBUG = False # 必须关闭调试模式! ALLOWED_HOSTS = ['your_domain.com', 'your_server_ip'] # 允许访问的域名/IP # 数据库配置 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'air_quality_db', # 数据库名 'USER': 'db_user', # 数据库用户 'PASSWORD': 'strong_password', # 强密码 'HOST': 'localhost', 'PORT': '3306', } } # 静态文件配置 STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') # collectstatic后文件存放目录

警告DEBUG = False时,Django不再处理静态文件。这就是为什么必须运行collectstatic并将目录交给Nginx等Web服务器处理。同时,需要正确配置ALLOWED_HOSTS,否则会收到“Invalid HTTP_HOST header”错误。

5.3 使用Gunicorn作为应用服务器

Django自带的runserver仅用于开发。生产环境需要一个更强大、稳定的WSGI服务器,Gunicorn是主流选择。

# 在虚拟环境中安装 pip install gunicorn gevent # 使用gevent worker提高并发性能,在项目根目录下启动 gunicorn --workers 3 --worker-class gevent --bind 0.0.0.0:8000 your_project_name.wsgi:application
  • --workers 3: 启动3个工作进程。一个经验法则是CPU核心数 * 2 + 1
  • --worker-class gevent: 使用gevent协程worker,对于I/O密集型应用(如数据库查询、API调用)能有效提升并发能力。
  • --bind 0.0.0.0:8000: 绑定到所有网络接口的8000端口。

但这样运行会在前台阻塞。更好的方式是用Systemd服务来管理:创建文件/etc/systemd/system/gunicorn.service

[Unit] Description=Gunicorn daemon for Air Quality Django App After=network.target mysql.service [Service] User=www-data # 运行用户,根据情况调整 Group=www-data WorkingDirectory=/var/www/air_quality_app ExecStart=/var/www/air_quality_app/venv/bin/gunicorn --workers 3 --worker-class gevent --bind unix:/var/www/air_quality_app/gunicorn.sock your_project_name.wsgi:application Restart=on-failure [Install] WantedBy=multi-user.target

然后启用并启动服务:

sudo systemctl daemon-reload sudo systemctl enable gunicorn sudo systemctl start gunicorn sudo systemctl status gunicorn # 检查状态

使用Unix Socket(gunicorn.sock)比TCP端口(127.0.0.1:8000)通信效率更高、更安全。

5.4 配置Nginx作为反向代理

Nginx负责处理静态文件、将动态请求转发给Gunicorn,并提供HTTPS、负载均衡等能力。

创建配置文件/etc/nginx/sites-available/air_quality

server { listen 80; server_name your_domain.com your_server_ip; # 你的域名或IP location = /favicon.ico { access_log off; log_not_found off; } # 静态文件由Nginx直接处理,高效 location /static/ { alias /var/www/air_quality_app/staticfiles/; expires 30d; # 设置缓存时间 } # 动态请求转发给Gunicorn Unix Socket location / { include proxy_params; proxy_pass http://unix:/var/www/air_quality_app/gunicorn.sock; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 如果应用需要处理长时间轮询或WebSocket,可能需要调整超时时间 # proxy_read_timeout 300s; } # 限制客户端上传大小,防止攻击 client_max_body_size 10M; }

创建软链接并测试、重启Nginx:

sudo ln -s /etc/nginx/sites-available/air_quality /etc/nginx/sites-enabled/ sudo nginx -t # 测试配置语法 sudo systemctl reload nginx

Nginx配置要点:

  • location /static/:务必指向collectstatic生成的STATIC_ROOT目录。这样Nginx可以直接发送CSS/JS/图片文件,无需经过Django/Python,性能极佳。
  • proxy_pass:指向Gunicorn的Unix Socket文件。proxy_set_header系列指令确保将原始客户端的IP等信息传递给Django应用(否则Django里看到的request.META['REMOTE_ADDR']会是Nginx服务器的本地IP)。
  • 安全加固:考虑配置HTTPS(使用Let‘s Encrypt免费证书)、设置安全的HTTP头(如HSTS)、限制请求速率等。

6. 常见问题排查与性能优化实录

即使按照步骤部署,也难免会遇到问题。这里记录几个我踩过的坑和解决方案。

6.1 部署后访问出现“DisallowedHost”或静态文件404

  • 症状:浏览器访问显示“Invalid HTTP_HOST header”错误,或者CSS/JS文件无法加载(404)。
  • 排查
    1. 检查settings.py中的DEBUG是否为FalseALLOWED_HOSTS是否包含了你的域名或IP地址(['*']可用于测试但不安全)。
    2. 检查STATIC_ROOT配置的路径,以及Nginx配置中location /static/alias路径是否正确指向该目录。确保运行了python manage.py collectstatic且Nginx进程用户(如www-data)对该目录有读取权限。
    3. 检查Nginx错误日志:sudo tail -f /var/log/nginx/error.log
  • 解决
    • 正确设置ALLOWED_HOSTS = ['your_domain.com']
    • 确保静态文件目录权限:sudo chmod -R 755 /var/www/air_quality_app/staticfilessudo chown -R www-data:www-data /var/www/air_quality_app/staticfiles(根据你的Nginx运行用户调整)。
    • 重启相关服务:sudo systemctl restart gunicornsudo systemctl reload nginx

6.2 数据库连接错误或查询缓慢

  • 症状:应用报错django.db.utils.OperationalError,或页面加载特别慢,尤其是查询大量数据时。
  • 排查
    1. 检查settings.py中的数据库配置(用户名、密码、主机、端口、数据库名)是否与服务器上的MySQL实例一致。
    2. 登录MySQL,确认数据库和用户已创建,且用户有足够权限:GRANT ALL PRIVILEGES ON air_quality_db.* TO 'db_user'@'localhost'; FLUSH PRIVILEGES;
    3. 对于慢查询,打开Django的SQL日志。在settings.py中临时添加:
      LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.db.backends': { 'level': 'DEBUG', 'handlers': ['console'], } } }
      重启服务后,在日志中查看生成的SQL语句和执行时间。重点关注没有用到索引的查询。
  • 解决
    • 修正数据库连接配置。
    • 使用Django的select_relatedprefetch_relatedonlydefer等方法优化ORM查询。
    • 为高频查询条件字段添加数据库索引。可以使用Django的@register.simple_tag装饰器创建一个自定义模板标签来检查索引,或在MySQL中使用EXPLAIN命令分析慢查询。
    • 考虑对历史数据(如一年前的数据)进行归档或分表,减少单表数据量。

6.3 并发访问下Gunicorn Worker崩溃或响应慢

  • 症状:当多个用户同时访问时,应用无响应或返回502 Bad Gateway错误。
  • 排查:查看Gunicorn日志sudo journalctl -u gunicorn,可能看到worker超时或被杀死的信息。
  • 解决
    • 增加Worker数量:根据服务器CPU核心数调整--workers参数。但注意,Worker数不是越多越好,每个Worker都是一个独立的Python进程,会消耗内存。对于内存有限的服务器,需要权衡。
    • 调整Worker类型:对于I/O密集型应用(大量数据库、API调用),使用异步Worker如geventeventlet比同步Worker(默认)能处理更多并发连接。使用前需要用pip install gevent安装,并用--worker-class gevent启动。
    • 调整超时时间:默认情况下,Gunicorn worker处理一个请求超过30秒会被重启。如果某些查询或操作确实很耗时,可以适当增加超时时间:--timeout 120。但这只是权宜之计,根本还是要优化慢请求。
    • 使用进程监控:确保systemd服务配置中的Restart=on-failure生效,这样worker崩溃后会自动重启。

6.4 数据更新与后台任务

项目运行后,需要定期更新空气质量数据。这通常通过后台任务(如Celery)或系统的计划任务(cron)来实现。

使用Cron的简单方案:编写一个Django管理命令update_air_quality.py,放在yourapp/management/commands/目录下。这个命令内部调用你的数据采集脚本。

# 编辑crontab crontab -e # 添加一行,每天凌晨2点执行数据更新,并记录日志 0 2 * * * cd /var/www/air_quality_app && /var/www/air_quality_app/venv/bin/python manage.py update_air_quality >> /var/log/air_quality_update.log 2>&1

使用Celery的进阶方案(更适合复杂、耗时的任务):Celery是一个分布式任务队列。你可以定义一个fetch_data_task,然后让Celery Worker在后台执行。结合Celery Beat可以替代cron做定时任务。部署Celery需要额外的Redis或RabbitMQ作为消息代理,配置相对复杂,但功能更强大、可控性更高,适合生产环境。

我个人在项目后期就切换到了Celery,因为它能更好地管理任务状态、重试失败任务,并且与Django集成度很高。但对于初学者或小型应用,从cron开始是完全可行的。

从一行命令启动开发服务器,到最终在Linux生产环境中稳定运行一个包含数据库、后台任务、Web服务的完整应用,这个过程本身就是一次宝贵的全栈实践。这个PM2.5可视化项目麻雀虽小,五脏俱全,它涉及的技术点和决策思路,在很多中后台管理系统、数据报表平台中都是相通的。希望这份超详细的拆解,能帮你少走些弯路,更顺畅地完成自己的项目。如果在复现过程中遇到新的问题,不妨多看看日志,那通常是寻找答案的第一步。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 5:09:33

5分钟在Dify工作流接入数据库,实现自然语言查询自动化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 5:08:43

AI叙事退潮后,模型工程化如何承接能力?从Anthropic上市谈起

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 5:06:15

31个QT工业上位机实战源码解析:串口通讯、运动控制与数据采集

简介&#xff1a;本资源为面向嵌入式开发、工业自动化与Qt初/中级学习者的31个实用上位机源码合集&#xff0c;覆盖步进电机控制、温湿度监测、触摸屏交互、串口通信、汽车仪表界面模拟及多轴运动控制等典型工控场景。压缩包共77个文件&#xff0c;含7个头文件&#xff08;.h&a…

作者头像 李华
网站建设 2026/9/4 5:04:44

MATLAB弧齿锥齿轮参数化建模与齿面网格生成

简介&#xff1a;本资源是一套面向机械设计工程师与高校机械类专业学生的弧齿锥齿轮MATLAB辅助设计工具&#xff0c;聚焦于动力传动系统中关键部件——弧齿锥齿轮的几何建模、参数计算与强度评估等核心工程问题。压缩包共3个文件&#xff08;2个txt说明文档 1个gear.m主计算脚…

作者头像 李华
网站建设 2026/9/4 5:04:01

ORB-SLAM3 Tracking::MonocularInitialization()

函数流程: 如果 !mbReadyToInitializate,表示还没有设置初始参考帧。此时需要判断当前帧的特征点数量是否大于100,如果满足则: 保存当前帧为 mInitialFrame 和 mLastFrame(都是副本)。 初始化 mvbPrevMatched 为当前帧中关键点的坐标(pt),用于下一帧的匹配。 将 mvI…

作者头像 李华