我第一次做这个平台,是为了学院的人工智能科普展示需求。当时的要求挺朴素:把 AI 的概念、发展脉络和应用场景讲清楚,再用动态图表把那些冷冰冰的数据变得直观。我试过直接用现成的 CMS 配自定义页面,也考虑过前后端分离方案,最后折腾下来,还是决定用 Django 做服务端、ECharts 做可视化,整套系统跑起来之后发现,这类"内容管理 + 数据展示"型的项目,这套组合几乎是最省力的底座。这篇文章不聊虚的,就把从需求拆解、数据建模、图表落地到部署上线的全过程完整复盘一遍,适合正在做 Django 项目、或者准备做可视化科普类网站的同学参考。
1. 项目定位与技术选型:为什么是 Django 配 ECharts 而不是别的
1.1 先拆清楚科普平台的真实需求
做项目之前,我习惯先把"这到底是个什么东西"想明白。科普平台不是企业官网,也不是传统博客,它同时承担三件完全不同的事。
第一是内容传递。要把机器学习、深度学习、自然语言处理这些概念组织成普通人能读懂的条目和文章,这意味着需要一个可靠的内容管理能力,包括分类、标签、编辑、发布、修改。
第二是数据形象化。科普不只是写文字,更要把"近十年 AI 论文数量变化""各行业 AI 应用占比""算法模型演进时间线"这类数据变成图表。好的可视化比大段文字说明更有说服力。
第三是运营和维护要简单。内容是持续的,今天这篇数据过时了,明天可能要加一个新概念,如果每次改内容都要动代码,后患无穷。
这三件事决定了技术选型的方向:开发效率要高、后台管理要省事、图表渲染要灵活、部署维护要轻。这个需求画出来之后,技术栈的思路就非常清晰了。
1.2 Django 在内容型项目上的几个优势
我之前也用过 Flask 做类似的事情,但对比下来,Django 在内容管理这个领域确实省心太多。
首先是自带完整后台。Django Admin 几乎是开箱即用的,注册好模型之后,文章增删改查、分类管理、数据录入这些活全都能在后台完成。科普平台面向的运营者往往不写代码,能把后台直接交给他们使用,这个价值非常大。我实际开发时,给运营同学配置好后台前后花了不到半天时间。
其次是 ORM 非常顺手。统计各分类下的文章数量、查最近一个月发布的内容、按标签过滤数据,这些操作用 Python 写查询非常自然,不用拼 SQL,也不用担心注入问题。对于科普平台这种中等数据量的场景,ORM 的性能完全够用。
第三是 Django 的目录结构和 MTV 架构非常规整。models、views、templates、urls 分工明确,多人协作时不容易乱。虽然项目结构比 Flask 重,但科普平台是要长期迭代的,重一点反而踏实。它还把数据库迁移、表单校验、认证授权这些重复劳动都内置了,省下的时间足够把可视化做精细。
1.3 可视化方案对比后我选了 ECharts
可视化部分其实可以选的路不少,我做了个简单对比。
| 方案 | 上手难度 | 图表丰富度 | 可定制性 | 适合场景 |
|---|---|---|---|---|
| ECharts | 低 | 高 | 中 | Dashboard、大屏、快速出图 |
| Chart.js | 低 | 中 | 低 | 简单统计图 |
| D3.js | 高 | 高 | 高 | 深度定制可视化 |
| AntV G2 | 中 | 高 | 中 | 数据驱动分析型图表 |
我最终选 ECharts,核心原因是它的图表类型覆盖最全。折线、柱状、饼图、雷达图、桑基图、关系图、热力图全部自带,文档示例非常丰富,而且是纯前端库,不依赖框架。科普平台里我需要展示时间趋势、分布占比、概念关系等多种形式,ECharts 基本一套通吃。
实际体验下来,ECharts 的学习成本很低。它的核心逻辑就是给定一个 option 对象,里面配置数据、坐标轴、系列类型,然后 init 一个容器渲染出来。相比 D3 那种"自己从零构建图形"的模式,ECharts 更适合项目周期紧、又要保证效果的情况。
1.4 模块划分让代码不被内容拖垮
规划的时候我坚持把平台拆成几个独立应用,而不是把所有代码堆在一个 app 里。整个平台按职责分成三块。
- content 模块:文章、知识卡片、分类、术语表,负责全部科普内容。
- charts 模块:图表数据、统计指标、趋势数据、概念关系,负责可视化数据模型。
- api 模块:对外提供 JSON 数据的 REST 接口,负责前后端数据传递。
每个模块在 Django 里对应一个 app,models 的边界非常清楚。这样做的好处是后续扩展不会互相影响。比如后面想加一个 AI 问答模块,直接新建一个 app,跟原有内容模块互不干扰。这个组织方式看似简单,但我见过太多项目因为一开始没规划好,最后代码全挤在一个 models.py 里,改一个字段牵一发动全身。
2. 数据模型设计:让内容与图表都"有据可依"
2.1 内容类模型的字段设计思路
科普内容不能只靠一张 article 表硬扛,我设计了三个核心模型:分类、文章、知识卡片。分类解决内容的目录结构,文章解决深度阅读,知识卡片解决碎片化知识点速览。
from django.db import models class Category(models.Model): name = models.CharField(max_length=50, unique=True) slug = models.SlugField(max_length=50, unique=True) description = models.TextField(blank=True) class Meta: ordering = ['name'] def __str__(self): return self.name class Article(models.Model): title = models.CharField(max_length=200) content = models.TextField() category = models.ForeignKey( Category, on_delete=models.CASCADE, related_name='articles' ) cover_image = models.ImageField(upload_to='covers/', blank=True) views = models.PositiveIntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['-created_at'] def __str__(self): return self.title class KnowledgeCard(models.Model): title = models.CharField(max_length=100) summary = models.TextField() category = models.ForeignKey( Category, on_delete=models.CASCADE, related_name='cards' ) related_models = models.CharField(max_length=200, blank=True, help_text='关联模型名,多个用逗号分隔') created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.title字段设计上有一个容易被忽略的点:updated_at和created_at一定要分开。科普内容会反复修改,运营需要知道某个知识点最近有没有更新。Views 字段用来记录文章浏览量,后续在首页做"热门内容"排序就直接用这个字段,不用额外统计表。
我用的 content 是 TextField 存储富文本。Django 的 TextField 不限制长度,存几万字的科普长文没有问题。不过要注意,如果未来考虑用富文本编辑器,尽量把富文本内容以 HTML 格式存,前端直接渲染即可,不要再做一层 Markdown 转换,省掉很多麻烦。如果要支持 Markdown,则需要额外引入转换库并注意 XSS 过滤。
知识卡片的设计是科普平台比较有特色的一块。它解决的是"快速认识一个概念"的需求,比如什么是神经网络、什么是过拟合。每张卡片做成一个独立条目,放到列表页以卡片网格展示,比长文更有亲和力。
2.2 时序统计数据的独立建模
科普平台经常要做趋势图,比如"AI 论文年度发表数量""某领域融资规模变化"。这种数据很有规律:一条记录就是一个数据点,字段是分类、年份、数值。
class TrendData(models.Model): category = models.CharField(max_length=50) year = models.IntegerField() value = models.FloatField() class Meta: unique_together = ('category', 'year') ordering = ['year'] def __str__(self): return f'{self.category} {self.year}: {self.value}'模型设计思路很直接。unique_together 保证同一个分类同一年的数据只有一条,避免重复录入导致图表出现毛刺。后端查询时按照 category 过滤,再按照 year 排序,就能得到一组平滑的数据序列。
实际使用时,我建议把 ECharts 需要的 x 轴和 y 轴数据在后端就组装好,返回给前端的 JSON 直接是下面这种结构。
{ "category": "ai_papers", "years": [2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024], "values": [1240, 1620, 2200, 3120, 4100, 5300, 6900, 8200, 9600, 11300] }这样前端拿到的数据就是"可以立刻 setOption"的格式,不需要在前端再做数组转换。
关于数据库选型,科普平台这个数据量级,SQLite 完全能扛住。我本地开发和生产初期都用的 SQLite,几百篇内容、几万条趋势记录,查询毫无压力。但当并发上来之后,SQLite 会逐渐吃力,这时候建议切换到 PostgreSQL。数据模型设计上从一开始就避免使用 SQLite 不适配的字段,后续迁移成本就会很小,这属于"现在省事,未来不痛苦"的典型操作。
2.3 后台管理与运营效率的平衡
光有模型不行,得能用起来顺手。Django Admin 的注册代码虽然简单,但配置细节很影响日常使用效率。
from django.contrib import admin from .models import Category, Article, KnowledgeCard, TrendData @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): prepopulated_fields = {'slug': ('name',)} @admin.register(Article) class ArticleAdmin(admin.ModelAdmin): list_display = ('title', 'category', 'views', 'created_at') list_filter = ('category',) search_fields = ('title', 'content') raw_id_fields = ('category',) @admin.register(KnowledgeCard) class KnowledgeCardAdmin(admin.ModelAdmin): list_display = ('title', 'category', 'created_at') @admin.register(TrendData) class TrendDataAdmin(admin.ModelAdmin): list_display = ('category', 'year', 'value') list_filter = ('category',)prepopulated_fields 会在后台输入分类名时自动生成 slug,避免手动维护 URL 别名。search_fields 支持内容全文检索式搜索标题和正文,对编辑非常友好。list_filter 把分类做成侧边栏筛选,运营同学找某一分类下的文章只需点一次。raw_id_fields 在关联数据多时比下拉框高效,当然文章量少的时候用默认的下拉框也没问题。
实际交付之后,运营同学在后台录入文章和数据完全不需要开发介入。这个体验很重要,因为它意味着内容更新不会成为开发瓶颈,科普平台能真正"活"起来。
3. 可视化接口与图表落地:从 JSON 到大屏
3.1 接口返回结构要"为图表而生"
可视化数据接口我用的是 Django REST Framework。它最大的价值是把 Python 数据结构序列化成 JSON 非常方便,而且视图编写简单,权限控制也内置。
接口设计上我有一条核心原则:后端返回的数据结构应该直接匹配前端图表的输入格式。不要让前端拿到数据还要自己组装。
以趋势图接口为例。
from rest_framework.views import APIView from rest_framework.response import Response from .models import TrendData class TrendChartView(APIView): def get(self, request, category): queryset = TrendData.objects.filter(category=category).order_by('year') data = { 'category': category, 'years': [item.year for item in queryset], 'values': [item.value for item in queryset], } return Response(data)URL 路由配置上,我习惯把 API 统一挂在 /api/ 前缀下。
# api/urls.py from django.urls import path from .views import TrendChartView urlpatterns = [ path('trend/<str:category>/', TrendChartView.as_view(), name='trend-chart'), ]这种设计让前端非常轻松。拿到 JSON 后,直接把 years 数组给 xAxis.data,values 数组给 series.data,图表就出来了。前后端联调时不需要反复对齐字段格式,因为格式在接口设计阶段就已经固化好了。
3.2 ECharts 在 Django 模板里的接入方式
可视化页面我用 Django 模板直接渲染,图表部分用 ECharts 的 CDN 引入。配合 fetch 请求接口数据,再 setOption 渲染图表。
<!-- templates/charts/trend.html --> <div id="trendChart" style="width: 100%; height: 420px;"></div> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <script> fetch('/api/trend/ai_papers/') .then(res => res.json()) .then(data => { var chart = echarts.init(document.getElementById('trendChart')); chart.setOption({ title: { text: 'AI 论文年度发表趋势', left: 'center' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.years }, yAxis: { type: 'value' }, series: [{ name: '论文数量', type: 'line', smooth: true, data: data.values }] }); // 窗口变化时自动重绘 window.addEventListener('resize', function () { chart.resize(); }); }); </script>这里有三个特别容易被新手忽略的坑。
第一,图表容器必须设置明确高度。ECharts 渲染时如果容器 height 是 0,最终图表就是一个什么都看不见的白板。最稳的做法是直接在 div 上用内联样式写死 height。
第二,ECharts 的 init 必须在 DOM 渲染完成之后调用。如果页面是动态加载的,等数据返回再 init 也不迟。我见过有人把 init 写在数据请求之前,结果图表初始化时容器还没准备就绪。
第三,图表数据格式要严格匹配。series.data 如果传的是普通对象数组,需要在 series 里额外配置 encode,否则图表不知道怎么取字段。我通常直接用平铺的字符串数组或者数值数组,绕开这个坑。
3.3 不止折线图:科普展示里我用过的图表类型
科普平台最容易犯的毛病是"一图到底",从头到尾全是折线图。实际展示中不同类型的图表效果差异很大,我整理了常用的几类。
时间趋势类的数据用折线图或面积图。比如"AI 论文数量年度变化""深度学习算力增长趋势",这类数据的关键是展现变化过程,折线图配合 tooltip 能清楚地看到每个年份的数值。
分布占比类的数据用饼图或环形图。比如"AI 应用行业分布""算法类型占比",饼图直观但要注意只适合展示不超过 6 个类别的数据,类别太多时应该用柱状图横向排列。
多维度对比用雷达图。科普平台里我做过一个"AI 子领域成熟度评估",从算法、数据、算力、应用、人才五个维度打分,雷达图的封闭面积能让人一眼看出哪个维度是短板。
概念关联用关系图。AI 领域的概念之间不是孤立的,"神经网络"下面有"CNN""RNN""Transformer","机器学习"涵盖"监督学习""无监督学习""强化学习"。ECharts 的 graph 类型配合 force 布局可以做出可拖拽的知识图谱,交互体验比静态图强太多。
var graphOption = { tooltip: {}, series: [{ type: 'graph', layout: 'force', roam: true, label: { show: true, position: 'right' }, data: [ { name: '人工智能', symbolSize: 30 }, { name: '机器学习', symbolSize: 22 }, { name: '深度学习', symbolSize: 22 } ], links: [ { source: '人工智能', target: '机器学习' }, { source: '人工智能', target: '深度学习' } ] }] };这种知识图谱特别适合科普场景,因为 AI 最大的门槛就是概念又多又乱关系复杂,可视化之后学习曲线陡降。模型上我用一张 ConceptRelation 表维护概念节点和关系边,前端拉到数据后直接填充给 graph 系列。
3.4 大屏适配与性能控制的实操笔记
科普平台如果要做展示大屏,最先遇到的一定是分辨率适配问题。大屏尺寸可能是 1920,也可能更大,图表不能按固定的像素宽度设计。
我采用的方案是 rem + vw 组合。根字体大小用 vw 单位设置,图表容器宽高用百分比或者 rem。ECharts 实例本身是响应式组件,给父容器一个百分比宽度,它就会跟着缩放。
还必须要处理的是 resize 事件。大屏上窗口大小变化时,图表不会被自动重绘,需要手动调用 chart.resize()。我一般配合 debounce 使用,避免短时间连续触发多次 resize 导致性能问题。
var timer = null; window.addEventListener('resize', function () { if (timer) clearTimeout(timer); timer = setTimeout(function () { chart.resize(); }, 100); });性能方面的教训是:大屏页面不要堆太多图表。设计稿上放了十个图表看起来很酷,实际首屏加载时后端接口请求十个数据接口,前端同时渲染十个 ECharts 实例,低配展示机器直接卡死。我最后只保留了 6 个核心图表,其余内容用 Tab 切换按需加载,体验立刻好了很多。
另一个优化点是公共配置抽取。ECharts 的每个图表的 tooltip、grid、颜色主题其实差不多,把它们抽成一个基础对象,再用 Object.assign 跟各图表独有配置合并,代码能少写三分之一。颜色主题我也统一维护,科普平台整体视觉风格保持一致。
4. 从零搭建一套完整平台:实操全过程
4.1 环境准备与项目初始化
新手照着做的话,先把 Python 虚拟环境和依赖装好。
python -m venv .venv source .venv/bin/activate pip install django djangorestframework新手第一次跑 Django 项目时,经常在创建项目和创建应用之间搞混。项目是全局的配置容器,应用是具体的功能模块。命令上差别不大,但语义要清楚。
# 创建项目 django-admin startproject ai_knowledge cd ai_knowledge # 创建三个核心应用 python manage.py startapp content python manage.py startapp charts python manage.py startapp api项目创建好之后,先别急着写代码,把 settings.py 里的 INSTALLED_APPS 配置好。这是很多新手容易漏掉的步骤,创建了应用却忘记注册,结果 migrate 和 runserver 报一堆错。
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'content', 'charts', 'api', ]数据库迁移在 model 写完之前可以不用急着执行,但项目创建之后先跑一次 migrate,把 Django 自带的后台相关表建好,方便后面立刻打开 admin 页面检查。
python manage.py migrate python manage.py createsuperuser python manage.py runserver如果能顺利看到 Django 的默认页面并且能登录 admin,环境就完全没有问题了。
4.2 文章、API、图表 App 的开发节奏
三个应用之间我是按依赖顺序开发的。先做 content 模型,因为文章和分类是平台的基础数据;再做 charts 模型,因为图表数据依赖内容分类;最后做 api,因为接口要查询前两者的数据。
content 应用里把第一节设计的 Category、Article、KnowledgeCard 模型写进去,注册好 admin 后,立刻后台录入几条测试数据。这步非常关键,不要等到代码写完了再填数据,开发过程中随时要有真实数据可以看效果。
api 应用的开发则围绕图表数据接口展开。在 content 和 charts 的模型确定之后,把趋势图接口、分类文章列表接口、知识图谱接口写出来,用浏览器或者 Postman 验证 JSON 返回结果。
views.py 里最常用到的接口大概是这几个。
from rest_framework.views import APIView from rest_framework.response import Response from content.models import Article, Category, KnowledgeCard from charts.models import TrendData, ConceptRelation class CategoryArticlesView(APIView): def get(self, request, slug): articles = Article.objects.filter( category__slug=slug ).select_related('category') data = [{ 'id': a.id, 'title': a.title, 'category': a.category.name, 'created_at': a.created_at.strftime('%Y-%m-%d'), 'views': a.views, } for a in articles] return Response(data) class ConceptGraphView(APIView): def get(self, request): relations = ConceptRelation.objects.all() nodes = [] links = [] seen = set() for r in relations: if r.source not in seen: nodes.append({'name': r.source}) seen.add(r.source) if r.target not in seen: nodes.append({'name': r.target}) seen.add(r.target) links.append({'source': r.source, 'target': r.target}) return Response({'nodes': nodes, 'links': links})这段代码里 select_related('category') 的作用是通过 JOIN 把 category 信息一次性查出,避免循环文章列表时逐条查询分类。这在文章列表页是必做的优化。
4.3 前端页面组织与模板继承
Django 模板系统虽然不如 SPA 框架灵活,但内容是知识的平台非常适合用它做首屏渲染。SEO 友好,加载也快。关键是模板组织的整洁。
我先做一个 base.html 充当全局布局,把导航栏、页脚、公共 CSS 都放进去,用 block 定义子页面的插入区。
<!-- templates/base.html --> <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}AI 科普平台{% endblock %}</title> <link rel="stylesheet" href="/static/css/style.css"> </head> <body> <header>{% block header %}{% endblock %}</header> <main>{% block content %}{% endblock %}</main> <footer>{% block footer %}{% endblock %}</footer> </body> </html>子模板通过 extends 继承基础布局,只需填充内容块。
<!-- templates/charts/trend.html --> {% extends 'base.html' %} {% block title %}AI 趋势 - AI 科普平台{% endblock %} {% block content %} <div class="page-wrapper"> <h1>AI 发展趋势</h1> <div id="trendChart" style="width: 100%; height: 420px;"></div> </div> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <script> // 3.2 节中的图表初始化代码 </script> {% endblock %}模板继承避免了很多重复的 HTML 代码。需要改导航栏时只动 base.html 一个文件,所有页面同步更新。
前端页面组织上还有一个经验:页面里的图表脚本不要内联太多复杂逻辑。ECharts 相关代码我统一放在静态 JS 文件里,比如 charts/trend.js、charts/graph.js,页面上只做接口调用和模块初始化。这样代码不臃肿,后续排查也方便。
4.4 部署上线的完整配置
开发完成只是一半,部署上线才是考手艺的地方。科普平台的访问量不会特别高,但稳定性不能差。我的部署方案是 Nginx + Gunicorn。
在服务器上先把项目代码拉下来,安装依赖,然后收集静态文件。
pip install gunicorn python manage.py collectstaticsettings.py 里必须把 DEBUG 关掉并配置 ALLOWED_HOSTS。
DEBUG = False ALLOWED_HOSTS = ['your-domain.com'] STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles'Gunicorn 启动项目,监听本机端口。
gunicorn ai_knowledge.wsgi:application --bind 127.0.0.1:8000Nginx 配置反向代理和静态文件服务。
server { listen 80; server_name your-domain.com; location /static/ { alias /path/to/ai_knowledge/staticfiles/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }HTTPS 是必须要补的一步。现在主流浏览器对非 HTTPS 的站点限制越来越多,公开展示的科普平台如果不加证书,会有各种功能受限,比如地理位置权限、部分 API 请求。用 Let's Encrypt 申请免费证书,配置 Certbot 自动续期,整个过程也很快。
部署完还有一件事别忘:关掉 DEBUG 之后,如果代码中出现异常,页面会直接显示 500。为了快速定位问题,我在服务器上配置了日志输出,Django 的错误日志写到文件里,出问题先看日志而不是瞎猜。
5. 开发过程中踩过的坑与排查思路
5.1 静态文件 404 的经典问题
我做这个项目时最典型的翻车现场就是:开发环境一切正常,一部署到服务器,CSS、JS 全部 404。排查了半天发现是 STATIC_ROOT 没有配置,也没有执行 collectstatic。
Django 的静态文件机制有点绕,开发环境和生产环境处理方式不同。开发时 Django 会从每个 app 的 static 目录自动查找,生产环境则必须把所有静态文件收集到一个目录,让 Nginx 直接服务。
解决方式很简单,三步走完。
# 1. settings.py 里配置 STATIC_ROOT # STATIC_ROOT = BASE_DIR / 'staticfiles' # 2. 运行 collectstatic 收集所有静态文件到 staticfiles python manage.py collectstatic # 3. Nginx 配置静态文件 alias 指向 staticfiles 目录还有另一个高频坑是 admin 后台样式丢失。很多人的 admin 能登录但页面是纯 HTML 样式全无,同样是 collectstatic 没执行,或者 Nginx 的 /static/ 路径配置错误。
5.2 跨域、CSRF 与接口安全
前后端分离部署时,跨域问题几乎是必然遇到。前端页面部署在 www.xxx.com,后端 API 部署在 api.xxx.com,浏览器的同源策略会拦截跨域请求。我遇到过 API 接口在 Postman 里测试完全正常,一从浏览器调用就报 CORS,就是这个原因。
解决跨域用 django-cors-headers 最方便。
pip install django-cors-headerssettings.py 配置要注意中间件的顺序。
INSTALLED_APPS = [ # ... 'corsheaders', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # 必须放在 CommonMiddleware 之前 # ... ]CORS 白名单按域名配置,不要图省事开 CORS_ALLOW_ALL_ORIGINS。开了全允许之后,任何网站都能跨域请求你的接口,科普平台虽然数据不是特别敏感,但这个习惯不好。
CSRF 方面,纯 API 接口配合 Token 认证比较省心。如果接口不方便引入外部认证系统,我自己写了一个简单的 Token 校验,前端请求时在 Header 带上 token,后端用一个装饰器拦截。
def token_required(view_func): def wrapper(request, *args, **kwargs): token = request.headers.get('X-Api-Token') if token != settings.API_TOKEN: return JsonResponse({'error': 'invalid token'}, status=401) return view_func(request, *args, **kwargs) return wrapper这个方案配置简单,能在很大程度上防止接口被恶意刷。
5.3 ORM 查询的性能隐患
科普平台数据量不大,但代码里如果出现 N+1 查询,再小的数据量也会卡顿。最常见的情形是在模板里循环取关联对象。
# 反面示例:每个分类都会额外查一次数据库 categories = Category.objects.all() for category in categories: articles = category.articles.all() # 循环中执行查询 print(articles.count())正确做法是用 select_related 或者 Prefetch 把关联数据一次性查出来。
# 反面示例:每个分类都会额外查一次数据库 categories = Category.objects.all() for category in categories: articles = category.articles.all() # 循环中执行查询 print(articles.count())select_related 用于 ForeignKey 和 OneToOneField,Prefetch 用于 ManyToManyField 和反向关系。文章列表页用 select_related('category') 就能把分类一次性查出,避免每篇文章查一次分类表。文章详情页如果展示相关文章,应该用 Prefetch 批量查。
# 用 Prefetch 批量查询每篇文章的关联卡片 from django.db.models import Prefetch articles = Article.objects.prefetch_related( Prefetch('category__cards', queryset=KnowledgeCard.objects.all()) )这个优化在数据量上来之后效果非常明显。科普平台前期数据少时无感,但内容运营半年后,数据库里几百篇文章、上千张卡片,这时的查询效率直接决定页面响应速度。
5.4 ECharts 渲染异常的排查清单
我整理了一个排查顺序,遇到图表不显示的问题,按这个顺序检查基本能解决。
第一看 Network 面板。接口返回的 JSON 是不是符合预期格式,是不是 500 报错,字段名是否匹配。大部分问题在这一步就能暴露。
第二看数据内容。返回的数组是否为空,年份是不是连续的,数值是否异常大或异常小。图表显示了一片空白,经常是因为后端返回的数组是空的。
第三看图表配置。series.type 是否与数据结构匹配。type: 'line' 却传了一个对象数组,ECharts 自然不知道如何解析。
第四看容器尺寸。检查 div 是否有宽度和高度。曾经调试一个小时的问题,结果只是容器 height 没设置。
这个清单基本覆盖了常见的渲染问题。实际调试时配合浏览器 DevTools 的 Console 输出,看到 ECharts 的报错信息,再反向定位问题源,效率最高。
5.5 常见问题速查表
把开发过程中遇到的高频问题统一整理成表格,查起来一目了然。
| 现象 | 可能原因 | 解决方式 |
|---|---|---|
| 后台无法上传图片 | 未安装 Pillow | pip install pillow |
| 图表显示为空白 | 容器高度为 0 | 给 div 设置明确 height |
| 部署后 admin 样式丢失 | 未执行 collectstatic | python manage.py collectstatic |
| API 请求返回 403 | CSRF 未处理 | 改用 Token 认证或配置 csrf_exempt |
| 跨域请求被拦截 | 缺少 CORS 配置 | 安装 django-cors-headers 并配置白名单 |
| 页面加载很慢 | 图表一次性渲染太多 | 改为 Tab 按需加载 |
| 模板循环取分类卡顿 | N+1 查询 | 使用 select_related / Prefetch |
| 中文乱码 | 编码不一致 | 数据库连接配置 utf-8 |
| 修改模型后没有反应 | 未执行 makemigrations | python manage.py makemigrations && migrate |
另外一个非常容易踩的坑是 Django 版本和 Python 版本的兼容问题。Django 5.x 要求 Python 3.10 以上,如果服务器上装的是系统自带的 Python 3.8,会直接安装失败。建议在项目目录写好 requirements.txt 和 python 版本说明,换环境时少踩很多坑。
关于安全,还有一个细节值得提一下。科普平台的留言或者反馈表单,一定记得加 CSRF 防护。Django 的模板表单默认带了 {% csrf_token %},但如果用纯 AJAX 提交表单就容易被忽略,导致 403。用 fetch 提交时把 CSRF token 从 cookie 里取出来放请求头,这是个容易被新手忽略但很关键的细节。
function getCookie(name) { let cookieValue = null; if (document.cookie && document.cookie !== '') { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim(); if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; }这块代码在 Django 官方关于 CSRF 的文档里也有,直接复制即可,但很多人不知道要在 AJAX 请求里带上。
最后再分享几个经验
当我做完这个项目复盘时,最大的感触是:科普平台的技术难度其实谈不上高,真正的挑战在于如何让内容、数据和视觉表达三者协调。Django 的优势在于它是一个成熟的内容型框架,自带后台,ORM 简洁,模板系统实用。ECharts 的优势在于图表类型丰富、上手快,能让数据可视化快速落地。这两个工具组合起来,对于"内容 + 图表"型项目来说确实是又稳又快的选择。
如果你想在这个项目上继续扩展,我建议优先考虑两个方向。一是 AI 问答互动模块,把科普从单向输出变成双向交流,用户输入一个概念,系统返回相关介绍和可视化解释,互动性立刻提升。二是接入真实的 AI 模型体验,比如在页面上部署一个简单的图像分类演示,让访客拖一张图进去,前端调用模型返回预测标签。这种"亲手体验"式的科普远比文字更吸引人。
做科普平台也是一次很好的 Django 项目实战机会。整个开发过程中,你会接触到模型设计、后台配置、接口开发、模板渲染、性能优化、部署上线,几乎覆盖了 Web 开发的完整链路。我踩过的这些坑,希望你能绕过去。