Django投票系统实战:从数据模型到部署上线 📅 发布时间:2026/9/12 20:57:11 👁 浏览次数: 简介一份基于Django框架的投票系统完整源码面向Python后端初学者与Web开发入门者演示经典投票应用的创建、投票与结果展示流程涵盖Django MTV分层、ORM模型、模板渲染和Admin后台等核心知识点。压缩包共91个文件以Python源码py和编译缓存pyc为主搭配HTML/CSS/JS前端页面、XML配置项以及SQLite数据库包体仅1.14MB便于快速下载和本地运行。已有1685人学习适合通过实际项目理解Django项目结构、URL路由、视图函数、数据模型迁移及后台管理配置。源码目录清晰包含项目配置myDjango、应用模块myApp、migrations迁移文件夹与templates模板目录并附英文README和使用说明。从投票问题创建、选项设置到用户点选与统计结果形成完整后端开发闭环可在此基础上二次扩展或作为课程设计、毕业设计的参考实现。1. 拿到“Django 投票系统源代码.zip”后先别急着 runserver拿到一份《python开发基于Django的投票系统源代码.zip》最常见的动作是解压后立刻python manage.py runserver看到页面能打开就宣布“跑通了”。但这类项目真正的价值从来不在“能打开”而在背后那套结构Django 的 MVT 分层、Question 与 Choice 的外键关系、POST 表单处理、admin 后台配置以及投票场景里最容易被忽略的重复投票问题。我会按接手这类代码库的顺序来讲先搭数据模型再串 URL 与视图补上后台和模板最后落到部署和源码管理。新手能照着把项目从零跑起来写过几个 Django 项目的人也能在字段设计、删除行为和部署边界上找到能用的细节。2. 数据层先行投票系统的模型设计与迁移写 Django 项目我一般不从视图开始而是先把模型定下来。原因是 URL、视图、模板全都围绕模型展开模型字段定错后面查询、表单、模板渲染都要返工反过来模型稳定后视图层只是把数据搬进模板。投票系统的最小数据模型是三张表问题Question、选项Choice、投票记录Vote。问题与选项是一对多选项与投票记录也是一对多三张表就能覆盖列表、详情、投票、统计的完整业务。2.1 django-admin startproject 与 app 划分的常规做法创建项目时常见的做法是先建虚拟环境再安装 Django然后通过命令行工具生成项目和 app。python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install django django-admin startproject vote_project cd vote_project python manage.py startapp polls说明startproject生成的是项目级配置startapp生成的是业务模块。投票系统的业务逻辑放进polls这个 app不要直接写在项目同名目录下这样以后加用户模块、统计模块时不用动全局配置。执行完startapp后还要到settings.py的INSTALLED_APPS里补上polls这一步漏掉不会报错但之后的migrate不会为投票系统创建任何表新手排查半天往往就是这里丢了。2.2 模型三件套Question、Choice、Vote 的字段设计模型文件按 Django 约定放在polls/models.py一个常见实现如下# polls/models.py from django.db import models from django.utils import timezone class Question(models.Model): text models.CharField(投票问题, max_length200) pub_date models.DateTimeField(发布时间, defaulttimezone.now) def __str__(self): return self.text def choices_count(self): return self.choices.count() class Choice(models.Model): question models.ForeignKey(Question, on_deletemodels.CASCADE, related_namechoices, verbose_name所属问题) text models.CharField(选项内容, max_length200) votes models.IntegerField(得票数, default0) def __str__(self): return f{self.question.text} - {self.text} class Vote(models.Model): choice models.ForeignKey(Choice, on_deletemodels.CASCADE, related_namevote_records, verbose_name投票选项) session_key models.CharField(会话标识, max_length64, db_indexTrue) voted_at models.DateTimeField(投票时间, defaulttimezone.now)字段选用理由看这张表模型/字段类型说明Question.textCharField(max_length200)题目文案200 字符足够过长反而影响列表页Question.pub_dateDateTimeField(defaulttimezone.now)用 DateTimeField 才能按发布时间排序和按年/月筛选DateField 会丢掉时分秒Choice.votesIntegerField(default0)自增计数字段避免每次统计都 count 整个选项表Vote.session_keyCharField(max_length64, db_indexTrue)会话去重用的标识加索引后按人查询不会全表扫描两个设计点值得专门说。第一Choice.votes用数字字段做计数是这类小项目最常见也最直观的做法等并发量上来votes 1的锁竞争会成为瓶颈那时再换 Redis 计数或独立计数表模型改为记录流水即可。第二Django 外键必须写on_delete投票系统里Question被删时选项和投票记录一起删所以用CASCADE如果业务要求保留历史数据就需要改成PROTECT或SET_NULL这取决于产品对数据完整性的要求。2.3 makemigrations、migrate 与查询删除对象的边界模型写好后生成并执行迁移python manage.py makemigrations polls python manage.py migrate python manage.py shell -c from polls.models import Question; print(Question.objects.count())makemigrations只生成迁移文件migrate才真正写库。投票系统开发阶段推荐 SQLite部署时再切 MySQL如果服务器上要用 MySQL先确认mysqlclient装得上Debian/Ubuntu 下通常需要apt install default-libmysqlclient-dev否则pip install mysqlclient会在编译阶段报错。提示开发环境用 SQLite 时db.sqlite3文件记得加入.gitignore否则会把本地数据带到仓库里。Django 里查询和删除对象有几个容易踩的边界。get返回单个对象查不到抛DoesNotExistfilter返回 QuerySet是惰性求值的没被迭代前不真正执行 SQL。删除对象时question.choices.all().delete()会按外键级联把该问题下所有选项删掉连带Vote表里对应的投票记录一起消失因为前面两张外键都用了CASCADE。所以删除问题前要先想清楚是要物理删除还是软删除加is_active字段。投票系统的运营场景里下线一个旧问题通常比物理删除更安全。3. 从 URL 到视图投票业务逻辑的完整闭环模型就绪后业务逻辑集中在视图层。Django 的请求处理是典型的“路由匹配 URL → 视图函数处理 → 返回响应”投票系统的核心闭环是展示问题详情、接收 POST 提交、写入票数、重定向到结果页。下面按这个顺序拆开。3.1 URL 路由拆分与 path 参数项目级urls.py用include把/polls/前缀交给polls.urls业务路由单独维护# vote_project/urls.py from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(polls/, include(polls.urls)), ]# polls/urls.py from django.urls import path from . import views app_name polls urlpatterns [ path(, views.index, nameindex), path(int:question_id/, views.detail, namedetail), path(int:question_id/vote/, views.vote, namevote), path(int:question_id/results/, views.results, nameresults), ]app_name polls是命名空间模板里用{% url polls:detail question.id %}时不会和其他 app 的路由别名冲突。int:question_id是路径转换器它限定了 URL 段必须是整数非整数直接 404不用在视图里再写类型判断这是比早年正则路由更简洁的写法。路由与视图的对应关系如下URL 路径视图说明/polls/views.index问题列表分页后每页 10 条/polls/1/views.detail问题详情渲染单选表单/polls/1/vote/views.vote接收 POST写入票数后重定向/polls/1/results/views.results展示结果3.2 投票逻辑POST 表单、自增票数与重定向投票场景必须用 POST不能把选项参数挂在 GET 上否则会被浏览器历史、代理日志泄露。视图实现如下# polls/views.py from django.contrib import messages from django.shortcuts import render, get_object_or_404, redirect from django.urls import reverse from .models import Question, Choice, Vote def index(request): latest_question_list Question.objects.order_by(-pub_date)[:5] return render(request, polls/index.html, {latest_question_list: latest_question_list}) def detail(request, question_id): question get_object_or_404(Question, pkquestion_id) return render(request, polls/detail.html, {question: question}) def vote(request, question_id): question get_object_or_404(Question, pkquestion_id) try: choice question.choices.get(pkrequest.POST[choice]) except (KeyError, Choice.DoesNotExist): return render(request, polls/detail.html, { question: question, error_message: 请先选择一个选项, }) choice.votes 1 choice.save(update_fields[votes]) messages.success(request, 投票成功) return redirect(reverse(polls:results, args(question.id,))) def results(request, question_id): question get_object_or_404(Question, pkquestion_id) return render(request, polls/results.html, {question: question})逻辑说明request.POST[choice]直接取表单里的单选值取不到时抛KeyError和Choice.DoesNotExist一起被try/except接住重新渲染详情页并提示错误拿到选项后对votes做自增再用update_fields限定只更新这一列避免把整行都写一遍。redirect(reverse(...))是 POST 后的标准姿势用命名空间加参数解析出结果页地址能避免在视图里硬编码 URL。调试时如果想知道某个 URL 实际匹配到哪个视图在 shell 里调用django.urls.resolve(/polls/1/vote/)就能看到对应的func和kwargs。重定向后给用户提示用的messages.success属于 Django 的 messages 框架模板里渲染一下就能在结果页顶部显示“投票成功”这就是“重定向后传数据”这个问题的标准解法不要试图把消息塞进 URL 参数里。3.3 用 session 和 Vote 表防止同一个浏览器重复投业务上要防重复投票最简单的可落地方案是记录session_key# 放在 vote 视图函数中choice.votes 1 之前 session_key request.session.session_key if not session_key: request.session.create() session_key request.session.session_key already_voted Vote.objects.filter( choice__questionquestion, session_keysession_key, ).exists() if already_voted: messages.warning(request, 你已经投过票了) return redirect(reverse(polls:results, args(question.id,)))这段逻辑放在choice.votes 1之前。说明Django 的 session 落库后会给当前浏览器分配一个唯一session_key用choice__questionquestion这种跨外键的查询写法一次 SQL 就能判断该 session 是否对该问题投过票。注意session_key方案只能防同一个浏览器重复投换浏览器、清 Cookie 都能再投属于业务可接受的最小实现如果要求按用户维度去重再换成关联用户 ID并给Vote表加唯一约束。这个边界想清楚了面试里被问到“怎么防刷票”时也能讲出取舍。4. 管理后台与模板把投票系统改造成可运营的后台4.1 Django admin 界面美化从默认后台到可读的后台Django 自带 admin但默认列表页只有__str__运营人员很难用。常见做法是注册 ModelAdmin 并配置展示字段# polls/admin.py from django.contrib import admin from .models import Question, Choice, Vote admin.register(Question) class QuestionAdmin(admin.ModelAdmin): list_display (text, pub_date, id) list_filter (pub_date,) search_fields (text,) date_hierarchy pub_date ordering (-pub_date,) admin.register(Choice) class ChoiceAdmin(admin.ModelAdmin): list_display (question, text, votes) list_editable (text,) admin.register(Vote) class VoteAdmin(admin.ModelAdmin): list_display (choice, session_key, voted_at) search_fields (session_key,)参数作用list_display控制列表页显示的列list_filter在右侧生成筛选器search_fields启用顶部搜索框date_hierarchy让日期字段变成可逐级下钻的导航条。list_editable可以直接在列表页修改选项文案适合运营快速改错别字。想让后台整体更像产品在项目的urls.py或任意 app 的admin.py里配置站点属性即可# vote_project/urls.py from django.contrib import admin admin.site.site_header 投票系统管理后台 admin.site.site_title 投票系统 admin.site.index_title 内容管理这些都是 Django 自带能力不需要额外依赖如果追求更现代的交互再考虑 simpleui 或 django-jet但引入第三方 admin 组件要确认它与当前 Django 大版本兼容先用标准配置能少很多坑。4.2 模板渲染、CSRF 与静态文件组织模板放在polls/templates/polls/下一个带 Bootstrap 的详情页大致长这样!-- polls/templates/polls/detail.html -- {% load static %} !DOCTYPE html html langzh-cn head meta charsetutf-8 title投票详情/title link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.0/dist/css/bootstrap.min.css link relstylesheet href{% static polls/css/style.css %} /head body div classcontainer mt-4 h1{{ question.text }}/h1 form action{% url polls:vote question.id %} methodpost {% csrf_token %} {% for choice in question.choices.all %} div classform-check input classform-check-input typeradio namechoice idchoice{{ forloop.counter }} value{{ choice.id }} label classform-check-label forchoice{{ forloop.counter }} {{ choice.text }} /label /div {% endfor %} button typesubmit classbtn btn-primary mt-3投票/button /form {% if error_message %} div classalert alert-warning mt-3{{ error_message }}/div {% endif %} {% if messages %} {% for message in messages %} div classalert alert-info mt-3{{ message }}/div {% endfor %} {% endif %} /div /body /html{% csrf_token %}必须在 form 里Django 对 POST 请求做 CSRF 校验漏了这一行会在提交时 403。question.choices.all()依赖related_namechoices没有它就得写成choice_set.all()语义差不少。静态文件统一放各 app 的static/目录{% load static %}后再引用如果业务要做前后端分离那视图层就不再渲染模板改成 Django REST Framework 提供 JSON 接口模板主要负责运营后台页面。4.3 分页与列表页的常规优化首页只展示 5 条没有问题但问题数量多了以后列表页一定要分页。用 Django 内置的分页器# polls/views.py 中改造 index from django.core.paginator import Paginator question_list Question.objects.order_by(-pub_date) page_obj Paginator(question_list, 10).get_page(request.GET.get(page))Paginator(question_list, 10)每页 10 条get_page接收?page2这样的参数页码越界时返回最后一页而不是报 404。模板里加“上一页/下一页”链接时用page_obj.has_previous和page_obj.previous_page_number判断边界。列表页另外一个容易被忽略的优化是给排序字段加索引在Question的Meta里声明indexes [models.Index(fields[-pub_date])]数据量到十万级后排序代价肉眼可见。5. 部署上线与源码维护的三个硬技巧5.1 宝塔部署 Django 的最小流程服务器端按“装运行环境 → 传代码 → 配依赖 → 迁移与静态文件 → 重启”的顺序操作。宝塔面板中先安装 Python 项目管理器创建项目时选择源码目录、Python 版本和入口文件WSGI 入口通常指向vote_project/wsgi.py。命令行侧的最小流程是cd /www/wwwroot/vote_project python -m venv venv source venv/bin/activate pip install -r requirements.txt python manage.py migrate python manage.py collectstatic --noinputcollectstatic会把各 app 的静态文件复制到STATIC_ROOTnginx 直接指到该目录migrate在部署阶段执行一次避免把本地开发库直接传上去。上线前在settings.py里把DEBUG改成False并设置ALLOWED_HOSTS [你的域名或IP]。5.2 三个高频报错与排查路径报错现场根因处理方式ModuleNotFoundError: No module named MySQLdb环境里没有 mysqlclientDebian/Ubuntu 先apt install default-libmysqlclient-dev再pip install mysqlclient页面能访问但 CSS 全部 404DEBUGFalse后静态文件未收集或 nginx 没指向STATIC_ROOT执行collectstatic检查 nginx 的 location 是否映射到静态目录访问返回 500浏览器看不到任何信息线上DEBUGFalse屏蔽了错误页查看日志文件常见位置是项目目录下logs/*.log先看ALLOWED_HOSTS是否漏配这三个问题占了投票系统上线阶段的大部分排查时间。静态文件 404 和 mysqlclient 安装失败几乎每个项目都会遇到日志里没线索时优先怀疑ALLOWED_HOSTS。5.3 用 git 和 tag 管理投票系统源码最后一步是把“源代码.zip”变成可维护的代码库。初始化 git 并写入忽略规则git init cat .gitignore EOF __pycache__/ *.pyc db.sqlite3 venv/ .env staticfiles/ EOF git add . git commit -m init: django vote system git tag v1.0.gitignore里前两行排除 Python 缓存db.sqlite3避免把本地开发数据提交到仓库.env保护 SECRET_KEY 和数据库密码。之后每次改功能都按“提交信息写明改动点”的方式提交稳定版本打 tag比如v1.0、v1.1。这样即使团队里换人接手解压 zip 之外还有一份完整的变更历史可以追溯。本文还有配套的精品资源点击获取