简介:这是一套面向Python初学者与Web开发入门者的Django实战项目源码,完整实现了一个功能完备的投票系统,涵盖模型设计、视图逻辑、模板渲染及后台管理等核心模块。资源基于Django 2.1.2与Python 3.7.0构建,适配PyCharm开发环境,可直接运行学习MVC架构实践与典型Web应用开发流程。压缩包共36个文件,包含14个Python源码(如models.py、views.py、urls.py等关键模块)、7个PNG界面截图(含admin-login.png、index.png等真实页面效果)、4个HTML模板文件、1个README.md项目说明文档,以及XML配置、CSS样式与GIF动图等辅助资源,整体大小5.67MB,结构清晰、开箱即用。已有120人学习下载,读者可获得从项目初始化(django-admin startproject)到功能迭代的完整目录组织范式、可运行的前后端代码、配套界面截图及本地部署指引,是理解Django项目标准结构与快速上手Web开发的优质教学参考。
1. 用 Django 快速搭一个可运行、可扩展、带管理后台的投票系统,不是 demo,是能上线的最小生产级骨架
你下载了一个叫python开发基于Django的投票系统源码+项目说明.zip的压缩包,解压后看到manage.py、settings.py、polls/和requirements.txt——但直接python manage.py runserver却报错ModuleNotFoundError: No module named 'django',或者页面打开后“Question does not exist”,又或者管理员登录进不去后台。这不是代码写错了,而是Django 投票系统天然依赖明确的环境隔离、应用注册顺序、数据库迁移时机和 URL 路由层级。它不像 Flask 那样靠一个文件就能跑通,也不像静态页面那样丢到 Nginx 就能访问。这个项目本质是 Django 官方 Tutorial 的工程化延伸:它把“创建问题→添加选项→记录投票→统计结果→后台管理”这一闭环,封装成可独立部署、支持多用户、带权限控制、留有 API 扩展点的结构。适合刚学完 Django MTV 模式、想拿真实项目练手的开发者;也适合需要快速交付内部调研/问卷/评选类轻量 Web 工具的运维、产品或测试人员——你不需要重写模型,但必须理解models.py中Question与Choice的外键约束如何影响 admin 注册方式,以及views.py里get_object_or_404和redirect的组合为什么比裸HttpResponse更安全。
2. 从零复现:用标准 Django 流程还原投票系统核心结构
2.1 环境初始化与依赖安装:避开 Python 版本和 pip 源导致的常见阻塞
Django 对 Python 版本有明确要求。当前主流稳定版本 Django 4.2+ 要求 Python ≥ 3.8;若你本地是 Python 3.7 或更低,pip install django会默认装旧版(如 3.2),而项目requirements.txt中若指定Django>=4.2.0,就会失败。先确认 Python 版本:
python --version # 输出应为 Python 3.8.10 或更高若版本不符,推荐使用pyenv管理多版本(Linux/macOS)或直接下载 python.org 官方安装包(Windows)。接着创建虚拟环境并激活:
python -m venv venv_polls source venv_polls/bin/activate # Linux/macOS # venv_polls\Scripts\activate.bat # Windows提示:绝对不要用
sudo pip install django或全局安装。Django 项目强依赖环境隔离,混装会导致ImportError: cannot import name 'patterns'等静默崩溃。
安装依赖时,优先使用国内镜像源加速:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple/ -r requirements.txt典型requirements.txt内容应包含:
Django==4.2.15 django-compressor==4.4 Pillow==10.2.0注意Django==4.2.15是精确锁定版本,避免因Django>=4.2导致升级到 5.x 后django.conf.urls.patterns等已移除 API 报错。若pip install过程中提示Failed building wheel for Pillow,Linux 用户需先安装系统依赖:sudo apt-get install libjpeg-dev zlib1g-dev(Ubuntu/Debian)或brew install jpeg zlib(macOS)。
2.2 数据库迁移与初始数据加载:让Question表真正落地
Django 投票系统依赖两个核心模型:Question(问题)和Choice(选项),定义在polls/models.py中。但模型只是 Python 类,不执行迁移就不会生成数据库表。执行前先检查迁移状态:
python manage.py showmigrations # 输出类似: # polls # [ ] 0001_initial # [ ] 0002_auto_20240315_1020若全是[ ],说明尚未迁移。执行:
python manage.py makemigrations polls python manage.py migratemakemigrations会扫描polls/models.py变更,生成polls/migrations/0001_initial.py;migrate则将该文件转换为 SQL 并执行。成功后验证表是否存在:
python manage.py dbshell # 进入 SQLite 命令行(默认数据库),输入: .tables # 应看到 polls_question、polls_choice 等表名注意:若
settings.py中DATABASES配置为 MySQL/PostgreSQL,需确保服务已启动且账号密码正确。常见错误django.db.utils.OperationalError: (1045, "Access denied for user...")表明settings.py中USER/PASSWORD未更新为实际值。
为快速验证功能,可加载初始测试数据。项目通常附带polls/fixtures/initial_data.json。加载命令:
python manage.py loaddata polls/fixtures/initial_data.json该 JSON 文件应包含Question实例(含pub_date字段),格式严格遵循 Django fixture 规范。若报错No fixture named 'initial_data' found,说明路径错误——确认文件位于polls/fixtures/initial_data.json,且polls是已注册的 app(检查settings.py中INSTALLED_APPS是否含'polls')。
2.3 URL 路由与视图绑定:解决 “The current path, ‘/’, didn’t match any of these.” 错误
Django 投票系统的 URL 结构是分层的:根路由urls.py分发到polls/urls.py,后者再映射/questions/1/→views.detail。若直接访问http://127.0.0.1:8000/报 404,大概率是根路由未包含polls.urls。
检查mysite/urls.py(项目主路由文件)是否包含:
from django.contrib import admin from django.urls import path, include # ← 必须导入 include urlpatterns = [ path('admin/', admin.site.urls), path('polls/', include('polls.urls')), # ← 关键:将 /polls/ 下所有请求交给 polls 应用处理 path('', include('polls.urls')), # ← 可选:若希望根路径即投票页,启用此行并注释上一行 ]再检查polls/urls.py是否正确定义了具体路径:
from django.urls import path from . import views app_name = 'polls' # ← 必须设置,否则 {% url 'polls:detail' %} 模板标签失效 urlpatterns = [ path('', views.index, name='index'), path('<int:question_id>/', views.detail, name='detail'), path('<int:question_id>/results/', views.results, name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ]<int:question_id>是路径参数捕获器,Django 会自动将其转为整数传给views.detail(request, question_id)。若浏览器访问http://127.0.0.1:8000/polls/1/仍 404,请用python manage.py show_urls(需先pip install django-extensions)查看实际注册的 URL 列表,确认polls:index是否存在且路径为^polls/$。
3. 核心功能实现:模型设计、视图逻辑与模板渲染的协同闭环
3.1Question与Choice模型的关键字段设计及其业务约束
polls/models.py中的模型不是随意定义的,每个字段都承载明确业务语义和数据库约束:
from django.db import models from django.utils import timezone class Question(models.Model): question_text = models.CharField(max_length=200) # 问题标题,最长200字符 pub_date = models.DateTimeField('date published') # 发布时间,用于排序和时效判断 # ↓ 新增字段:控制是否允许投票(避免过期问题) is_active = models.BooleanField(default=True) def __str__(self): return self.question_text # ↓ 自定义方法:判断问题是否“最近发布”(过去一天内) def was_published_recently(self): now = timezone.now() return now - timezone.timedelta(days=1) <= self.pub_date <= now class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) # 外键关联,on_delete=CASCADE 表示删除问题时自动删所有选项 choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) # 投票计数,非实时聚合,提升写性能 def __str__(self): return self.choice_text关键点解析:
pub_date使用DateTimeField而非DateField,因为投票可能按小时粒度截止;was_published_recently()方法返回布尔值,不能在数据库层面过滤(它是 Python 方法),但可在模板中用{% if question.was_published_recently %}显示“NEW”标签;is_active字段是常见扩展点:管理员可在后台勾选/取消,控制问题是否接受新投票,避免硬编码逻辑。
注意:修改模型后必须重新
makemigrations+migrate。若仅新增字段(如is_active),Django 会提示You are trying to add a non-nullable field...,此时需输入1并提供默认值(如True),或选择2允许为空(null=True, blank=True)。
3.2 视图函数中的事务控制与防重复提交机制
polls/views.py中的vote视图是核心业务逻辑所在,必须处理并发写入和用户误操作:
from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.db import transaction from .models import Question, Choice def vote(request, question_id): question = get_object_or_404(Question, pk=question_id) try: selected_choice = question.choice_set.get(pk=request.POST['choice']) except (KeyError, Choice.DoesNotExist): return render(request, 'polls/detail.html', { 'question': question, 'error_message': "You didn't select a choice.", }) else: # ↓ 关键:使用 select_for_update() 加行锁,防止并发投票导致计数错误 with transaction.atomic(): selected_choice = Choice.objects.select_for_update().get(pk=selected_choice.pk) selected_choice.votes += 1 selected_choice.save() return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))逻辑拆解:
get_object_or_404替代Question.objects.get(),避免DoesNotExist异常需手动捕获;request.POST['choice']直接取表单name="choice"的值,对应<input type="radio" name="choice" value="{{ choice.id }}">;select_for_update()在数据库层面锁定该Choice记录,确保同一选项被多人同时点击时,votes += 1不会因读-改-写竞争而丢失计数;reverse('polls:results', args=(question.id,))生成/polls/1/results/URL,比硬编码字符串更安全(URL 改变时无需改视图)。
若未加transaction.atomic()和select_for_update(),高并发下votes可能少于实际投票数——这是 Django 投票系统最典型的“数据一致性”坑。
3.3 模板继承与上下文传递:让index.html动态显示最新问题列表
Django 模板系统通过{% extends %}和{% block %}实现复用。polls/templates/polls/index.html通常继承自base.html:
<!-- polls/templates/polls/base.html --> <!DOCTYPE html> <html> <head><title>{% block title %}Polls{% endblock %}</title></head> <body> <div class="content"> {% block content %}{% endblock %} </div> </body> </html><!-- polls/templates/polls/index.html --> {% extends 'polls/base.html' %} {% block title %}Latest Polls{% endblock %} {% block content %} <h1>Latest questions:</h1> {% if latest_question_list %} <ul> {% for question in latest_question_list %} <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li> {% endfor %} </ul> {% else %} <p>No polls are available.</p> {% endif %} {% endblock %}views.index必须将latest_question_list传入模板:
def index(request): latest_question_list = Question.objects.filter( is_active=True # ← 过滤掉已关闭的问题 ).order_by('-pub_date')[:5] # ← 取最近5条 context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context)提示:
order_by('-pub_date')中的-表示降序;若漏写,问题会按发布时间从旧到新排列,不符合“最新”语义。[:5]是切片,Django 会将其转为LIMIT 5SQL,而非 Python 层面截取,高效。
4. 后台管理与部署准备:让非技术人员也能维护问题与选项
4.1admin.py中的定制化注册:暴露is_active字段并优化列表展示
Django Admin 是内置的强力管理后台,但默认只显示模型基础字段。要让运营人员能开关问题、查看投票总数,需定制polls/admin.py:
from django.contrib import admin from .models import Question, Choice @admin.register(Question) class QuestionAdmin(admin.ModelAdmin): list_display = ('question_text', 'pub_date', 'was_published_recently', 'is_active') list_filter = ('pub_date', 'is_active') # 右侧边栏筛选器 search_fields = ('question_text',) # 顶部搜索框 date_hierarchy = 'pub_date' # 按日期分层导航 # ↓ 允许在列表页直接编辑 is_active 字段 list_editable = ('is_active',) @admin.register(Choice) class ChoiceAdmin(admin.ModelAdmin): list_display = ('question', 'choice_text', 'votes') list_filter = ('question',) # 按所属问题筛选list_display定义列表页显示的列;was_published_recently是模型方法,Django 会自动调用并显示 True/False 图标;list_editable让is_active可直接勾选修改,无需点进详情页。部署后访问http://127.0.0.1:8000/admin/,用python manage.py createsuperuser创建的账号登录,即可图形化管理所有问题。
4.2 静态文件收集与生产环境配置:绕过DEBUG=True的安全陷阱
开发时DEBUG=True允许 Django 自动提供 CSS/JS,但上线必须关闭,否则会暴露敏感路径和 SQL 错误详情。修改settings.py:
DEBUG = False # ← 必须设为 False ALLOWED_HOSTS = ['your-domain.com', '192.168.1.100'] # ← 替换为实际域名或 IP,禁止 ['*']静态文件(CSS、图片)需集中收集到STATIC_ROOT目录供 Nginx/Apache 服务:
python manage.py collectstatic --noinput该命令将polls/static/和django.contrib.admin.static/等所有静态资源复制到STATIC_ROOT(如./staticfiles/)。Nginx 配置示例:
location /static/ { alias /path/to/your/project/staticfiles/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; }注意:
collectstatic前确保STATICFILES_DIRS正确指向各 app 的static/目录,且STATIC_ROOT是空目录(Django 不会自动创建)。
4.3 使用 Gunicorn 启动服务:替代runserver的生产级方案
python manage.py runserver仅用于开发,绝不能用于生产环境(单线程、无进程管理、无 HTTPS 支持)。推荐 Gunicorn:
pip install gunicorn gunicorn mysite.wsgi:application --bind 0.0.0.0:8000 --workers 3mysite.wsgi:application指向 WSGI 入口模块;--workers 3启动 3 个 worker 进程处理并发请求。配合 systemd 或 supervisor 管理进程生命周期。例如 systemd service 文件/etc/systemd/system/gunicorn-polls.service:
[Unit] Description=Gunicorn for Django Polls After=network.target [Service] Type=notify User=www-data Group=www-data WorkingDirectory=/path/to/your/project ExecStart=/path/to/venv/bin/gunicorn mysite.wsgi:application --bind 0.0.0.0:8000 --workers 3 [Install] WantedBy=multi-user.target启用服务:
sudo systemctl daemon-reload sudo systemctl enable gunicorn-polls sudo systemctl start gunicorn-polls5. 进阶技巧:用 Django Shell 快速验证模型关系与数据状态
5.1 在 Django Shell 中交互式调试外键关联与查询性能
当Question.objects.get(id=1).choice_set.all()返回空列表,但数据库明明有数据时,Shell 是最快定位问题的工具。启动:
python manage.py shell然后逐行执行:
>>> from polls.models import Question, Choice >>> q = Question.objects.get(id=1) >>> q.choice_set.all() # 查看关联选项 <QuerySet [<Choice: Option A>, <Choice: Option B>]> >>> q.choice_set.count() # 快速计数,比 len(q.choice_set.all()) 更优(不加载全部对象) 2 >>> Choice.objects.filter(question__pub_date__year=2024).count() # 跨表查询:查2024年发布的问题的所有选项数 15question__pub_date__year是 Django 的双下划线链式查询语法,等价于 SQL 的JOIN。若choice_set为空,检查Choice表中question_id是否真为1(用dbshell查),或确认Question实例q的id是否正确(q.id)。
5.2 用django-debug-toolbar定位 N+1 查询问题
投票首页若加载缓慢,可能是 N+1 查询:模板中循环{{ question.choice_set.all }}导致每条问题都触发一次SELECT * FROM polls_choice WHERE question_id = ?。安装调试工具:
pip install django-debug-toolbar在settings.py中添加:
INSTALLED_APPS += ['debug_toolbar'] MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware'] INTERNAL_IPS = ['127.0.0.1']在urls.py中加入:
if settings.DEBUG: import debug_toolbar urlpatterns = [ path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns访问http://127.0.0.1:8000/polls/,右上角出现调试面板。点击 “SQL” 标签,若看到 10 条SELECT ... FROM polls_choice语句,说明存在 N+1。修复方法是在views.index中使用select_related或prefetch_related:
def index(request): latest_question_list = Question.objects.prefetch_related('choice_set').filter( is_active=True ).order_by('-pub_date')[:5] # ...prefetch_related会预先执行一次SELECT ... FROM polls_choice并缓存结果,后续question.choice_set.all()直接从内存取,将 10 次查询降至 2 次。
5.3 为投票结果页添加图表:用 Chart.js 渲染votes数据
polls/templates/polls/results.html可嵌入前端图表。在模板中引入 Chart.js CDN:
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <canvas id="resultsChart" width="400" height="200"></canvas> <script> const ctx = document.getElementById('resultsChart').getContext('2d'); new Chart(ctx, { type: 'bar', data: { labels: [{% for choice in question.choice_set.all %}"{{ choice.choice_text }}"{% if not forloop.last %},{% endif %}{% endfor %}], datasets: [{ label: 'Votes', data: [{% for choice in question.choice_set.all %}{{ choice.votes }}{% if not forloop.last %},{% endif %}{% endfor %}], backgroundColor: 'rgba(54, 162, 235, 0.2)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: false } }); </script>此代码将question.choice_set.all的选项文本和票数动态注入 Chart.js 配置,生成柱状图。无需后端 API,纯前端渲染,适合轻量需求。若需服务端渲染(如 PDF 导出),则需用matplotlib或plotly生成图像二进制流。
提示:
{% for %}中的{% if not forloop.last %},{% endif %}确保 JSON 数组末尾无逗号,避免 JS 解析错误。这是 Django 模板处理数组的常用模式。
本文还有配套的精品资源,点击获取