当前位置: 首页 > news >正文

高校专用Django投票系统:学号实名注册、Excel批量导入学生、投票结果一键导出

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

简介:这个Django搭建的校园投票平台专为高校使用场景优化,学生注册时必须输入符合本校规则的学号,系统自动校验格式并拦截无效输入,确保只有在校生能参与投票;登录后可进行单选投票,界面干净无干扰,操作一步到位。管理员拥有独立后台,支持上传Excel或纯文本文件批量添加学生信息(含学号、姓名),省去逐条录入的麻烦;创建投票活动时可自定义标题和候选选项,实时查看各选项得票数变化,投票结束后直接下载完整结果文件,包含每位投票人的学号、所选选项及提交时间戳。项目结构规范,包含完整templates模板、static静态资源、poll核心应用、mysite2主配置目录及venv虚拟环境,附带清晰的README说明和requirements依赖列表,无需额外配置即可本地运行或部署到轻量服务器,适用于班级选举、社团投票、课程互动等小型校园投票需求。

1. 项目概述:为什么高校投票不能用现成的问卷工具?

我带过三届本科生的《Web开发实践》课,每年期末都要组织一次“课程最佳实践案例”匿名投票。最开始用过某知名在线问卷平台——结果第一年就翻车了:有校外培训机构的人混进来投了27票,还有学生用小号重复刷票,最后统计时发现同一学号出现了4次提交记录。第二年改用Excel收集再人工核对,光是整理300人的投票数据就花了我整整一个下午,更别说导出结果还要手动加时间戳、去重、排序。直到第三年,我决定自己搭一个真正贴合高校场景的投票系统,核心诉求就三条:学号必须真实可验、学生不能随便注册、结果必须能一键归档

市面上的通用投票工具,比如那些带“匿名投票”“实时统计”噱头的产品,在高校落地时几乎全军覆没。它们默认把用户当“访客”,而高校需要的是“身份可信的在校生”。学号不是普通用户名,它是教务系统的唯一凭证,格式固定(比如我校是8位纯数字,前两位代表入学年份),且必须与真实姓名绑定。所以这个Django投票系统从设计第一天起,就放弃了“邮箱注册”“手机号验证”这类通用方案,直接锚定“学号即身份”的底层逻辑。学生注册页不叫“Sign Up”,而叫“学号认证注册”;管理员后台不叫“User Management”,而叫“学生名录管理”。这不是UI文案的差别,而是整个权限模型和数据流向的根本重构。

它也不是为千万级并发设计的“高可用系统”,而是为50人到500人规模的班级、社团、教研室量身定制的轻量工具。没有微服务、不接消息队列、不搞分布式缓存——所有逻辑都压在Django单体应用里,数据库用SQLite起步(生产环境换PostgreSQL也只需改两行配置),静态文件直接由Django serve(上线后自然切到Nginx)。这意味着什么?意味着你下载代码包,cd mysite2 && python manage.py runserver,5分钟内就能在本地浏览器看到一个可运行的投票页面。不需要Docker、不需要K8s、不需要运维同事帮你配反向代理。它解决的不是技术难题,而是高校老师、班干部、社团负责人每天都在面对的“行政效率黑洞”。

关键词里的“Django投票系统”是技术栈,“学号注册验证”是安全边界,“Excel批量导入”是管理效率,“投票结果导出”是闭环交付,“校园投票平台”是场景定位——这五个词串起来,就是一套完整的工作流:学生凭学号进门 → 管理员用Excel铺路 → 投票过程干净利落 → 结果文件可审计可归档。下面我会带你一层层拆开这个系统,不是讲Django语法,而是告诉你每一行关键代码背后,我们到底在防什么、省什么、保什么。

2. 核心设计思路:用Django的“约定优于配置”守住高校场景的三条红线

很多初学者一上来就想给Django加JWT、上OAuth2、搞前后端分离,结果花两周搭完架子,连一个学号校验都没跑通。这个系统之所以能“开箱即用”,核心在于它严格遵循Django原生能力的边界,并把高校场景的约束条件,全部翻译成Django最擅长处理的“模型约束+表单验证+权限控制”三层防线。我们不造轮子,只把Django的轮子拧紧到刚好卡住学号、姓名、投票行为这三个关键点。

2.1 第一道红线:学号不是字符串,是结构化身份凭证

poll/models.py里,学生用户模型不是继承Django默认的AbstractUser,而是自定义了一个StudentUser

class StudentUser(AbstractBaseUser, PermissionsMixin): student_id = models.CharField( max_length=12, unique=True, validators=[MinLengthValidator(6), MaxLengthValidator(12)], help_text="请输入8位或10位本校学号,如:20211234" ) name = models.CharField(max_length=32, verbose_name="姓名") is_active = models.BooleanField(default=True) date_joined = models.DateTimeField(default=timezone.now) USERNAME_FIELD = 'student_id' REQUIRED_FIELDS = ['name'] objects = StudentUserManager()

注意三个细节:
第一,student_id字段加了unique=True,这是数据库层面的硬约束,杜绝重复学号注册;
第二,validators里没写正则,而是用MinLengthValidatorMaxLengthValidator——因为不同高校学号长度不同(有的8位,有的10位,有的带字母前缀),硬编码正则会锁死扩展性;
第三,USERNAME_FIELD = 'student_id',这意味着登录框输入的不再是“用户名”,而是学号,Django认证系统会自动用它查库,无需额外写查询逻辑。

真正的校验逻辑藏在poll/forms.py的注册表单里:

class StudentRegistrationForm(UserCreationForm): student_id = forms.CharField( max_length=12, widget=forms.TextInput(attrs={'placeholder': '请输入学号,例如:20211234'}), help_text="学号必须为本校有效格式,仅限在校生注册" ) name = forms.CharField( max_length=32, widget=forms.TextInput(attrs={'placeholder': '请输入真实姓名'}) ) class Meta: model = StudentUser fields = ("student_id", "name", "password1", "password2") def clean_student_id(self): sid = self.cleaned_data.get('student_id') # 规则1:必须纯数字(我校规则) if not sid.isdigit(): raise ValidationError("学号只能包含数字,请检查输入") # 规则2:长度必须为8位 if len(sid) != 8: raise ValidationError("学号长度应为8位,请确认是否输入完整") # 规则3:前两位必须是20或21或22(限定近3年入学) if not sid.startswith(('20', '21', '22')): raise ValidationError("学号年份段无效,请确认是否为本校在读学生") # 规则4:查库确认该学号未被注册 if StudentUser.objects.filter(student_id=sid).exists(): raise ValidationError("该学号已被注册,请勿重复提交") return sid

这里的关键不是“写了多少行代码”,而是把校验拆解成可维护、可替换的原子规则。如果下学期学校升级学号规则(比如2024级改成10位),你只需要改clean_student_id里的三行判断,不用动模型、不动路由、不碰模板。我试过把这段代码交给大三的学生助教,他花15分钟就理解了逻辑,并成功把规则适配到了他们学院的12位学号(带Y前缀)。

2.2 第二道红线:Excel导入不是“上传文件”,而是“名录同步”

管理员后台的“批量导入”功能,最容易被做成“上传CSV然后for循环insert”,但这样会埋下两个坑:一是Excel里可能有空行、合并单元格、乱码姓名;二是导入失败时,用户不知道哪一行出错了,只能重传重试。这个系统把导入流程拆成了三步:预检 → 映射 → 执行,全部在Django Admin界面完成,不写一行JavaScript。

核心在poll/admin.py里注册的StudentImportAdmin

@admin.register(StudentImport) class StudentImportAdmin(admin.ModelAdmin): list_display = ('file_name', 'imported_at', 'status', 'error_log') actions = ['process_import'] def process_import(self, request, queryset): for obj in queryset: if obj.status == 'pending': try: # 步骤1:用openpyxl读取Excel,跳过空行,校验列数 wb = load_workbook(obj.file.path) ws = wb.active rows = list(ws.iter_rows(min_row=2, values_only=True)) # 跳过标题行 valid_rows = [] errors = [] for i, row in enumerate(rows, start=2): # 行号从2开始计 if not any(row): # 全空行跳过 continue if len(row) < 2: errors.append(f"第{i}行:数据列数不足(需至少学号、姓名两列)") continue sid, name = str(row[0]).strip(), str(row[1]).strip() if not sid or not name: errors.append(f"第{i}行:学号或姓名为空") continue # 步骤2:复用注册表单的clean_student_id逻辑做预检 try: form = StudentRegistrationForm({'student_id': sid}) form.clean_student_id() # 复用已有校验规则 except ValidationError as e: errors.append(f"第{i}行:学号校验失败 - {e}") continue valid_rows.append((sid, name)) if errors: obj.status = 'failed' obj.error_log = "\n".join(errors) obj.save() self.message_user(request, f"导入失败,共{len(errors)}处错误:{errors[0][:50]}...", level='error') continue # 步骤3:批量创建,用bulk_create提升性能 students = [ StudentUser(student_id=sid, name=name, is_active=True) for sid, name in valid_rows ] StudentUser.objects.bulk_create(students, ignore_conflicts=True) obj.status = 'success' obj.error_log = f"成功导入{len(valid_rows)}名学生" obj.save() self.message_user(request, f"成功导入{len(valid_rows)}名学生", level='success') except Exception as e: obj.status = 'failed' obj.error_log = f"系统错误:{str(e)}" obj.save() self.message_user(request, f"导入过程发生异常:{e}", level='error')

这个设计的精妙之处在于:错误日志直接显示到Admin界面的error_log字段里,管理员点开就能看到“第15行:学号校验失败 - 学号只能包含数字”。不需要翻日志文件、不需要开终端、不依赖任何外部工具。我让班长试过,她导入时手误把“20211234”打成“2021123a”,系统立刻标红报错,她删掉a重传,30秒搞定。这才是高校场景要的“零学习成本”。

2.3 第三道红线:投票结果不是“数据表”,是可审计的归档文件

很多投票系统导出的是CSV,但高校行政要求的是“可追溯、不可篡改”的记录。这个系统导出的是.xlsx文件,且每张工作表都带时间戳水印和结构化字段。关键不在格式,而在导出动作本身是原子性的、带权限隔离的、且结果文件存储在独立路径

导出逻辑封装在poll/views.pyexport_vote_results视图里:

@login_required @user_passes_test(lambda u: u.is_staff) # 仅管理员可访问 def export_vote_results(request, poll_id): poll = get_object_or_404(Poll, id=poll_id) votes = Vote.objects.filter(poll=poll).select_related('student').order_by('-created_at') # 创建Excel工作簿 wb = Workbook() ws = wb.active ws.title = "投票结果" # 写入表头(带中文说明) headers = [ ("学号", "学生在校唯一标识"), ("姓名", "学生真实姓名"), ("所选选项", "投票时选择的候选项目"), ("提交时间", "精确到秒的时间戳"), ("IP地址", "提交时客户端IP(用于事后核查)"), ("User-Agent", "浏览器及设备信息") ] for col, (header, desc) in enumerate(headers, 1): cell = ws.cell(row=1, column=col, value=header) cell.font = Font(bold=True) # 在第二行写入字段说明(隐藏行,但保留可读性) ws.cell(row=2, column=col, value=desc).font = Font(size=9, italic=True) # 写入数据行 for row_idx, vote in enumerate(votes, start=3): ws.cell(row=row_idx, column=1, value=vote.student.student_id) ws.cell(row=row_idx, column=2, value=vote.student.name) ws.cell(row=row_idx, column=3, value=vote.choice.text) ws.cell(row=row_idx, column=4, value=vote.created_at.strftime("%Y-%m-%d %H:%M:%S")) ws.cell(row=row_idx, column=5, value=vote.ip_address or "未知") ws.cell(row=row_idx, column=6, value=vote.user_agent or "未知") # 自动调整列宽 for column in ws.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = min(max_length + 2, 50) ws.column_dimensions[column_letter].width = adjusted_width # 添加页脚水印 ws.oddFooter.left.text = f"导出时间:{timezone.now().strftime('%Y-%m-%d %H:%M:%S')} | 投票活动:{poll.title}" ws.oddFooter.center.text = "本文件由高校投票系统自动生成,具备审计效力" # 构建响应 response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') filename = f"{poll.title}_投票结果_{timezone.now().strftime('%Y%m%d_%H%M%S')}.xlsx" response['Content-Disposition'] = f'attachment; filename="{filename}"' wb.save(response) return response

这里有两个关键设计:
第一,@user_passes_test(lambda u: u.is_staff)确保只有Django Admin用户(即管理员)能触发导出,普通学生登录后根本看不到这个URL;
第二,导出文件名自带时间戳%Y%m%d_%H%M%S,避免覆盖,且文件内容里嵌入了导出时间、投票标题、页脚水印——这不是为了好看,而是为了满足高校档案管理中“原始记录可追溯”的硬性要求。去年我们系里用这个系统做助教评选,教务处来抽查时,直接打开导出的Excel,看到页脚水印和精确到秒的时间戳,当场就签字通过了。

3. 实操部署指南:从解压到上线,全程无命令行恐惧症

我见过太多老师,看到pip install -r requirements.txt就皱眉。这个系统的部署哲学是:“让第一步比安装微信还简单”。资源包里已经为你准备好了一键启动脚本、预置的SQLite数据库、甚至包含了测试用的Excel样例文件。下面我带你走一遍真实场景:假设你是计算机系的王老师,今天下午要给2023级软件工程班(48人)组织一次“最受欢迎课程设计选题”投票。

3.1 本地快速启动(Windows/macOS/Linux通用)

第一步:解压你下载的wCA8NThQ01VhwHpyr53g-master-43c24f2bcb4c224a641efea62d2f486baba2cdc7.zip,进入投票网站文件夹。你会看到这样的目录结构:

投票网站/ ├── mysite2/ ← Django项目根目录 ├── venv/ ← 已预装好依赖的虚拟环境(Windows下是venv/Scripts/activate.bat) ├── sample_data/ ← 包含test_students.xlsx(48条模拟数据)、test_poll.xlsx(投票题目模板) ├── README.md ← 中文使用说明(重点看“三分钟启动指南”章节) └── start_server.bat ← Windows双击即可运行(macOS/Linux对应start_server.sh)

提示:venv文件夹在压缩包里已经存在,里面预装了Django 4.2、openpyxl、XlsxWriter等全部依赖。你不需要pip install,不需要python -m venv,直接激活就行。

Windows用户操作
双击start_server.bat,弹出的黑窗口会自动执行三步:
1.cd mysite2切换到项目目录
2.venv\Scripts\activate.bat激活虚拟环境(你会看到命令行前缀变成(venv)
3.python manage.py runserver 8000启动服务器

几秒钟后,窗口显示Starting development server at http://127.0.0.1:8000/,此时打开浏览器访问http://127.0.0.1:8000,就能看到首页。整个过程不需要敲任何命令,就像打开一个桌面程序。

macOS/Linux用户操作
右键点击start_server.sh→ “显示简介” → 勾选“允许执行” → 双击运行。原理完全一样,只是脚本调用方式不同。

注意:首次运行时,Django会自动创建db.sqlite3数据库文件,并执行迁移(python manage.py migrate已内置在启动脚本中)。如果你看到Operations to perform:开头的日志,说明数据库初始化成功,这是正常现象。

3.2 管理员后台初始化:三分钟完成48人名录导入

打开浏览器,访问http://127.0.0.1:8000/admin/,用默认账号登录:
- 用户名:admin
- 密码:admin123(首次登录后请立即在Admin界面修改)

登录后,左侧菜单找到PollStudent imports,点击右上角“ADD STUDENT IMPORT”。这时你会看到一个上传表单:

  • File:点击“选择文件”,找到sample_data/test_students.xlsx(这就是为你准备好的48条模拟学生数据,学号从20230001到20230048,姓名随机生成)
  • Description:可以填“2023级软工班学生名录”

点击“SAVE”,页面跳转回列表页,你会看到新添加的记录状态是pending。此时点击右侧的PROCESS操作按钮(不是“Save”),系统开始执行前面讲过的三步预检流程。10秒后刷新页面,状态变成successError log显示成功导入48名学生

实操心得:我让班长试过,她第一次上传时把Excel另存为了.xls格式(老版本),系统报错Unsupported format, or corrupt file。解决方案很简单:用WPS或Excel重新打开,另存为.xlsx即可。这个错误提示直接写在Admin界面,她没问我就自己解决了。

3.3 创建投票活动:像发微信消息一样简单

回到Admin首页,点击PollPolls→ “ADD POLL”。填写以下内容:

  • Title:填“2023级软件工程班课程设计选题投票”
  • Description:填“请选择你最想做的课程设计题目,每人限投1票”
  • Start date:选今天日期
  • End date:选3天后(比如今天是5月20日,就选5月23日)
  • Status:保持Active

然后滚动到下方Choices区域,点击“Add another Choice”三次,分别填入:
- Choice 1:基于Spring Boot的校园二手交易平台
- Choice 2:基于Vue3的在线考试系统前端
- Choice 3:基于Python的校园舆情分析爬虫

填完点击“SAVE”,投票活动就创建好了。此时学生访问http://127.0.0.1:8000/poll/,就能看到这个投票页面,学号注册后即可投票。

注意事项:投票页面的“提交”按钮是灰色的,直到学生输入符合规则的学号并点击“验证”才会变亮。这是前端加的一层防呆设计,避免学生输错学号后直接提交空白票。

3.4 投票结果导出:一秒钟生成带水印的归档文件

投票截止后(或者你想提前看实时数据),回到Admin界面,点击PollPolls,找到你创建的投票,点击右侧的EXPORT RESULTS链接(这是一个自定义Action按钮,代码在poll/admin.py里注册)。浏览器会自动下载一个文件,名字类似2023级软件工程班课程设计选题投票_投票结果_20240520_153022.xlsx

用Excel打开它,你会看到:
- A列是48个学号(20230001~20230048)
- B列是对应姓名
- C列是每人选择的题目(比如20230001选了“基于Spring Boot的校园二手交易平台”)
- D列是精确到秒的提交时间(如2024-05-20 14:22:35
- E列是IP地址(本地测试显示为127.0.0.1,部署到服务器后会显示真实IP)
- F列是浏览器信息(如Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...

页脚有清晰水印:“导出时间:2024-05-20 15:30:22 | 投票活动:2023级软件工程班课程设计选题投票”。这份文件可以直接打印签字,作为教学档案提交。

4. 关键模块深度解析:学号校验、Excel解析、结果导出的底层实现

上面讲了怎么用,现在说清楚“为什么这么写”。很多教程只告诉你pip install openpyxl,却不解释为什么不用pandas;只教你HttpResponse,却不说明为什么导出用.xlsx而不是.csv。这些选择背后,全是高校场景的真实约束。

4.1 学号校验:为什么不用正则,而用分步逻辑校验?

初版我确实写过正则:r'^20\d{6}$',匹配20开头的8位数字。但很快遇到三个问题:
1.法学院同学抗议:他们学号是F2021001(F代表法学),正则得改成r'^[A-Z]\d{7}$',但这样又和计算机系冲突;
2.继续教育学院反馈:他们的学号带年份和月份,如202309001(2023年9月入学),长度变成9位;
3.教务系统升级:某天学校通知,2024级起学号统一为10位,前四位年份+后六位序号。

如果校验逻辑散落在models.pyforms.pyviews.py多处,每次改规则都要全局搜索替换,极易遗漏。所以最终方案是:把校验逻辑收口到clean_student_id一个方法里,并设计成可插拔的规则链

现在的clean_student_id实际是这样组织的(简化版):

def clean_student_id(self): sid = self.cleaned_data.get('student_id') # 规则链:每个规则是一个独立函数,返回True/False和错误信息 rules = [ self._rule_digit_only, self._rule_length_check, self._rule_year_prefix, self._rule_duplicate_check, ] for rule in rules: ok, msg = rule(sid) if not ok: raise ValidationError(msg) return sid def _rule_digit_only(self, sid): return sid.isdigit(), "学号只能包含数字" def _rule_length_check(self, sid): # 这里可以动态读取配置,比如从settings.STUDENT_ID_LENGTH expected = getattr(settings, 'STUDENT_ID_LENGTH', 8) return len(sid) == expected, f"学号长度应为{expected}位" def _rule_year_prefix(self, sid): # 支持多个年份前缀,配置在settings里 valid_prefixes = getattr(settings, 'VALID_YEAR_PREFIXES', ['20', '21', '22']) return any(sid.startswith(p) for p in valid_prefixes), "学号年份段无效"

这样,当学校规则变更时,你只需要改settings.py里的两个变量:

# settings.py STUDENT_ID_LENGTH = 10 VALID_YEAR_PREFIXES = ['20', '21', '22', '23', '24']

所有校验逻辑自动生效,模型、表单、API接口全部无需改动。这是我带学生做毕设时总结出的经验:把业务规则和代码逻辑解耦,是应对高校行政多变性的唯一可靠方案

4.2 Excel解析:为什么用openpyxl,而不是pandas或csv?

很多人第一反应是pandas.read_excel(),但它有三个致命短板:
-内存爆炸:pandas会把整个Excel加载进内存,一个10MB的Excel(约5万行)可能吃掉2GB内存,而高校服务器大多是2核4G的轻量机型;
-类型混乱:pandas会自动把“20230001”识别为数字20230001.0,再导出时变成20230001.0,学号末尾的.0是行政绝对不能接受的;
-无空行处理:pandas的skiprows参数无法智能跳过Excel里的合并单元格、空行、注释行。

openpyxl的优势恰恰补上了这些缺口:
-流式读取ws.iter_rows(min_row=2, values_only=True)只按需读取指定行,内存占用恒定在几MB;
-保留原始格式row[0]拿到的就是字符串"20230001",不会被转成浮点数;
-精准控制:可以ws.max_row获取总行数,ws['A1'].value精确读取任意单元格,完美处理表头合并、跨列数据等高校Excel常见乱象。

我在poll/utils.py里封装了一个ExcelParser类,专门处理高校Excel的“脏数据”:

class ExcelParser: def __init__(self, file_path): self.wb = load_workbook(file_path) self.ws = self.wb.active def parse_students(self): """解析学生名录,自动跳过空行、合并单元格、标题行""" students = [] for row in self.ws.iter_rows(min_row=2, values_only=True): # 从第2行开始 # 跳过全空行 if not any(cell for cell in row): continue # 处理合并单元格:如果A列为空,但B列有值,尝试向上找A列值(常见于分组标题) sid, name = self._extract_sid_name(row) if sid and name: students.append((sid.strip(), name.strip())) return students def _extract_sid_name(self, row): """智能提取学号和姓名,兼容合并单元格""" sid, name = None, None # 优先取前两列 if len(row) >= 2: sid, name = row[0], row[1] # 如果第一列为空,尝试用上一行的学号(处理“张三、李四”在同一学号下的情况) elif len(row) >= 1 and row[1] is None: # 这里可以扩展逻辑,比如查上一行 pass return str(sid) if sid else None, str(name) if name else None

这个类在导入时被调用,它不追求“全自动”,而是提供parse_students这个稳定接口,把脏数据清洗的逻辑全部封装在里面。管理员上传的Excel哪怕有10种奇葩格式,只要parse_students能返回(sid, name)元组,后续流程就完全不受影响。

4.3 结果导出:为什么用XlsxWriter,而不是openpyxl?

导出环节用了另一个库XlsxWriter,而不是前面解析用的openpyxl,原因很实在:
-openpyxl适合读+改:它能打开现有Excel,修改单元格、增删工作表,但生成新文件时性能一般;
-XlsxWriter专为写而生:它不加载Excel到内存,而是直接写二进制流,生成10万行数据只要2秒,且内存占用极低;
-水印和样式支持更好XlsxWriterset_header()set_footer()方法能直接写入页眉页脚,add_table()可以创建带筛选器的表格,这些都是高校归档文件的刚需。

导出视图的核心代码(poll/views.py):

import xlsxwriter from io import BytesIO def export_vote_results(request, poll_id): # ... 获取votes数据 ... # 创建内存中的Excel文件(不写磁盘) output = BytesIO() workbook = xlsxwriter.Workbook(output, {'in_memory': True}) worksheet = workbook.add_worksheet("投票结果") # 设置表头格式 header_format = workbook.add_format({ 'bold': True, 'bg_color': '#DCE6F1', 'border': 1, 'align': 'center', 'valign': 'vcenter' }) # 写入表头 headers = ["学号", "姓名", "所选选项", "提交时间", "IP地址", "User-Agent"] for col, header in enumerate(headers): worksheet.write(0, col, header, header_format) # 写入数据行(逐行写,内存友好) for row_idx, vote in enumerate(votes, start=1): worksheet.write(row_idx, 0, vote.student.student_id) worksheet.write(row_idx, 1, vote.student.name) worksheet.write(row_idx, 2, vote.choice.text) worksheet.write(row_idx, 3, vote.created_at.strftime("%Y-%m-%d %H:%M:%S")) worksheet.write(row_idx, 4, vote.ip_address or "未知") worksheet.write(row_idx, 5, vote.user_agent or "未知") # 添加页脚水印 worksheet.set_footer(f"导出时间:{timezone.now().strftime('%Y-%m-%d %H:%M:%S')} | 投票活动:{poll.title}") # 关闭workbook,写入BytesIO workbook.close() # 构建HTTP响应 output.seek(0) response = HttpResponse(output.read(), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') filename = f"{poll.title}_投票结果_{timezone.now().strftime('%Y%m%d_%H%M%S')}.xlsx" response['Content-Disposition'] = f'attachment; filename="{filename}"' return response

这里的关键是{'in_memory': True}参数,它告诉XlsxWriter不要创建临时文件,所有数据都在内存中组装,最后output.seek(0)定位到开头,output.read()直接吐出字节流。整个过程不产生任何临时文件,不占用磁盘IO,非常适合部署在共享主机或轻量云服务器上。

5. 常见问题与实战排障:从“打不开网页”到“导出文件打不开”的全场景应对

再完美的系统,上线第一天也会遇到各种意料之外的问题。我把过去三年在教学、社团、院系部署中踩过的所有坑,整理成这张速查表。这些问题90%都源于高校环境的特殊性:老旧电脑、公共机房网络策略、学生用手机访问、管理员不熟悉技术术语。

问题现象可能原因快速排查步骤解决方案
访问http://127.0.0.1:8000显示“拒绝连接”1. 启动脚本没运行成功
2. 防火墙拦截了8000端口
3. 浏览器用了代理
1. 查看黑窗口是否有Starting development server...字样
2. 在命令行执行netstat -ano \| findstr :8000(Windows)或lsof -i :8000(macOS)确认端口监听
3. 换Chrome无痕模式访问
重启启动脚本;关闭防火墙临时测试;禁用浏览器代理
管理员登录后看不到Student imports菜单Django Admin未正确注册模型检查poll/admin.py是否包含admin.site.register(StudentImport, StudentImportAdmin)打开poll/admin.py,确认注册语句存在且未被注释
上传Excel后状态一直是pending,点PROCESS没反应1. Excel格式错误(如.xls旧格式)
2. 文件路径含中文或空格
3.openpyxl版本冲突
1. 用Excel另存为.xlsx
2. 把文件移到纯英文路径(如C:\data\
3. 运行venv\Scripts\python.exe -c "import openpyxl; print(openpyxl.__version__)"
确保openpyxl>=3.1;文件路径避免中文;用最新版Office保存
学生注册时提示“学号已被注册”,但查Admin发现没有这条记录SQLite数据库被其他进程锁定(常见于强制关机后)删除mysite2/db.sqlite3文件(会清空所有数据,仅限测试环境)重启电脑;或运行python manage.py dbshell后执行.exit释放锁
导出的Excel文件用WPS打不开,提示“文件损坏”WPS对XlsxWriter生成的文件兼容性差用Microsoft Excel或LibreOffice打开验证settings.py中添加XLSXWRITER_OPTIONS = {'default_date_format': 'yyyy-mm-dd hh:mm:ss'}优化兼容性
投票页面显示“服务器内部错误”,但Admin后台正常学生访问时触发了未捕获异常(如投票选项为空)查看黑窗口最后一行红色错误信息(如Choice.DoesNotExist进入Admin,确认该投票的Choices至少有一条记录

实操心得:最常被忽略的问题是“学生用手机访问”。高校学生习惯用手机浏览器,而我们的投票页面是PC优先设计。解决方案是在poll/templates/poll/vote.html里加了一行viewport:
html <meta name="viewport" content="width=device-width, initial-scale=1.0">
并给所有按钮加了min-width: 120px的CSS,确保手指能准确点击。这个改动让手机投票成功率从62%提升到98%,是我带学生做用户测试时发现的。

另一个血泪教训:永远不要相信Excel的“自动保存”。有次班长导入前没手动保存,上传的是Excel的临时缓存文件,里面全是乱码。后来我们在sample_data/里放了一个README_for_admin.txt,第一行就写:“请务必在上传前,点击Excel的‘文件’→‘另存为’→选择‘.xlsx’格式→点击‘保存’”。

最后分享一个偷懒技巧:如果只是临时用,不想部署服务器,可以把整个mysite2文件夹打包,发给同事,他双击start_server.bat就能用。我们系里几位老师就是这么传的,U盘拷贝、微信发送、钉钉传输,没有任何技术门槛。真正的技术,是让使用者感觉不到技术的存在。

6. 安全与合规实践:如何在不引入复杂方案的前提下守住底线

高校系统最怕的不是高并发,而是“合规风险”。去年有兄弟院校的投票系统被审计,问题出在两点:一是学生IP地址明文存储在数据库里,违反《个人信息保护法》;二是导出文件没做脱敏,学号和姓名直接暴露。这个系统从设计之初就把合规当作功能需求,而不是事后补救。

6.1 学号与姓名的存储策略:最小必要原则

StudentUser模型里,student_idname字段都是必需的,但它们的用途被严格限定:
-student_id:仅用于登录认证和投票关联,不参与任何计算、不对外展示、不进日志
-name:仅在管理员后台的“学生名录”和“投票结果导出”中显示,学生前台页面永远只显示“已投票”或“未投票”,绝不出现任何同学姓名

关键在poll/views.py的投票视图:

@login_required def vote_view(request, poll_id): poll = get_object_or_404(Poll, id=poll_id) # 学生只能看到自己的投票状态,看不到别人投了谁 if hasattr(request.user, 'vote_set'): user_vote = request.user.vote_set.filter(poll=poll).first() if user_vote: return render(request, 'poll/voted.html', {'poll': poll, 'choice': user_vote.choice}) # 未投票时显示选项,但选项描述里不包含人名(如不说“张三的提案”,而说“提案A”) return render(request, 'poll/vote.html', {'poll': poll})

这意味着,即使数据库被拖库,攻击者也只能拿到学号和姓名的映射关系,但无法知道“20230001投了哪个选项”,因为Vote表里存的是choice_id(外键),不是选项文本。选项文本只在Choice模型里,而Choice表不和学号关联。

6.2 IP地址与User-Agent的处理:存储即脱敏

Vote模型里确实有ip_addressuser_agent字段,但这不是为了监控学生,而是为了事后核查异常行为(比如同一IP投了50票)。存储时做了两层脱敏:

class Vote(models.Model): # ... 其他字段 ... ip_address = models.GenericIPAddressField(null=True, blank=True) user_agent = models.TextField(null=True, blank=True) def save(self, *args, **kwargs): # 对IPv4做脱敏:192.168.1.100 → 192.168.1.xxx if self.ip_address and '.' in self.ip_address: parts = self.ip_address.split('.') if len(parts) == 4: parts[-1] = 'xxx' self.ip_address = '.'.join(parts) # 对User-Agent做截断:只保留前100字符,去掉敏感token if self.user_agent: self.user_agent = self.user_agent[:100] super().save(*args, **kwargs)

导出时,ip_address字段显示的是192.168.1.xxxuser_agent是截断后的字符串。这既满足了“可追溯”的审计要求,又履行了“最小必要”的合规义务。教务处来检查时,看到这个设计,直接给了“合规”盖章。

6.3 数据库备份与恢复:用Django原生命令搞定

高校老师不需要懂SQL dump,但需要“一键备份”。我们在mysite2/manage.py同级目录下,放了一个backup_db.bat(Windows)和backup_db.sh(macOS/Linux):

# backup_db.sh #!/bin/bash TIMESTAMP=$(date +"%Y%m%d_%H%M%S") cp db.sqlite3 "backup_db_$TIMESTAMP.sqlite3" echo "数据库备份完成:backup_db_$TIMESTAMP.sqlite3"

双击运行,就在当前目录生成一个带时间戳的备份文件。恢复时,把备份文件重命名为db.sqlite3,覆盖原文件即可。整个过程不需要sqlite3命令行工具,不依赖任何外部软件。

最后一点个人体会:技术的价值不在于多炫酷,而在于多“隐形”。这个投票系统没有用Redis缓存、没上Celery异步、没搞Docker容器化,但它让王老师少花了3小时整理数据,让班长少跑了5趟办公室,让教务处审核时一次通过。当你写的代码,能让非技术人员说“这东西真省事”,你就做到了技术的最高境界——润物细无声。

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

简介:这个Django搭建的校园投票平台专为高校使用场景优化,学生注册时必须输入符合本校规则的学号,系统自动校验格式并拦截无效输入,确保只有在校生能参与投票;登录后可进行单选投票,界面干净无干扰,操作一步到位。管理员拥有独立后台,支持上传Excel或纯文本文件批量添加学生信息(含学号、姓名),省去逐条录入的麻烦;创建投票活动时可自定义标题和候选选项,实时查看各选项得票数变化,投票结束后直接下载完整结果文件,包含每位投票人的学号、所选选项及提交时间戳。项目结构规范,包含完整templates模板、static静态资源、poll核心应用、mysite2主配置目录及venv虚拟环境,附带清晰的README说明和requirements依赖列表,无需额外配置即可本地运行或部署到轻量服务器,适用于班级选举、社团投票、课程互动等小型校园投票需求。


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

http://www.cnnetsun.cn/news/2840786.html

相关文章:

  • 3个技术突破:ChanlunX如何将缠论理论转化为可执行算法
  • 别再死记硬背了!用TensorFlow 2.x手把手复现Google的WideDeep推荐模型
  • 立创EDA手动拼板实战:什么时候必须自己画?复制粘贴整板的关键步骤与重建铺铜
  • 毕业紧急救稿!热门 AI 降重合集,从飘红到合格,在职 / 自考论文都能用
  • PyTorch LSTM权重对数量化实战包:含9种实现、门控参数分离与一键运行脚本
  • XUnity Auto Translator:高效配置智能翻译插件的深度解析与实战指南
  • Day8|杂乱拖延人群专属:AI智能收纳规划,如何治好生活里的习惯性乱糟糟?
  • 孩子学书法,合肥这家少儿书法社体验如何?
  • UiPath自动化包:WI5工作项客户信息哈希值本地计算与ACME系统集成
  • 从直播卡顿到秒开流畅:一次搞定FFmpeg播放器参数调优全流程
  • 3分钟搞定MusicBee网易云歌词插件:告别无歌词的音乐播放体验
  • Hindsight 内存爆炸 4 个词排查清单:9,284 条 6 成是 SSH 调试日志——Agent 标签系统的实战复盘
  • GD32F303项目实战:用片内FLASH存储用户配置,告别外部EEPROM
  • Web应用项目开发学习心得|从零基础到实战开发的成长总结
  • Numba @jit 加速实战:从“能用”到“飞快”,我踩过的那些坑和最佳实践
  • LibSVM 3.23多平台源码包:含C核心、Python/Java/Matlab绑定、Windows/Linux编译脚本与实用工具集
  • 从‘能跑就行’到‘赏心悦目’:用openpyxl给你的Python数据导出Excel加点设计感
  • 别再纠结选CNN还是Transformer了!手把手带你用PyTorch复现CoAtNet核心模块
  • 告别应用商店限制!手动部署Win11安卓子系统(WSA)最新版,附APK安装器推荐
  • 傅里叶单像素成像(FSI) vs. 传统单像素成像:在低光、非可见光场景下谁更胜一筹?
  • Cesium画点总被‘吃掉’一半?别急着关深度检测,试试这3个更优雅的解法
  • 钢结构工程施工常见缺陷分析及防治
  • 工控机二次开发必看:用 AI「重构」开源软件,能绕过开源协议吗?
  • 【LeetCode刷题日记】78.子集
  • 3分钟生成专业短视频:Pixelle-Video AI全自动视频创作工具完全指南
  • 多维聚合数据操作:预计算、实时补丁与语义层三层架构
  • OneNet MQTT接入避坑指南:手把手解决Python连接、数据上报和Topic订阅的常见问题
  • Mythos安全大模型:自动漏洞利用与开发者原生安全实践
  • 从发送报文到过滤接收:用USB-CAN TOOL软件做车载ECU通信调试的实战笔记
  • 云存储与数据库的本质区别:从分层契约看数据服务选型