Django毕业生去向反馈平台:从模型设计到部署上线全攻略 毕业生去向反馈调查平台这是我在实际带毕业设计过程中被问到最多的一类题目。原因很简单它是典型的管理信息系统覆盖用户登录、问卷设计、数据填报、统计导出全流程技术栈成熟、业务边界清晰非常适合作为Django方向的毕设选题。但我在评审和调试过程中发现很多同学做出来的东西只是“能跑”距离“能交付”差得很远——要么Excel导出是乱码要么填报表单刷新一下就能重复提交要么统计图表是写死的假数据。这篇文章就把我实际带队开发这个项目时沉淀下来的完整设计思路、核心代码实现和一批实战中踩过的坑整理出来给正在做或打算做这类系统的同学一个可以直接参考的路径。文章会覆盖从需求分析到模型设计、从管理端功能到填报端防重复逻辑、从统计可视化到远程调试部署的全过程所有代码都基于Django 4.xPython 3.10以上环境验证过。1. 选题背景与需求梳理为什么毕业生去向反馈需要一套独立系统很多同学看到“毕业生去向反馈调查平台”这个题目时第一反应是“这不就是一个表单页面吗”。如果真这么想做出来的东西大概率撑不起一篇合格毕设的体量更经不住答辩时老师追问。实际调研之后你会发现这个业务场景的数据结构和权限关系比看上去要复杂得多。1.1 高校就业管理工作的真实痛点高校每年要对毕业生做去向统计过去常用的方式是用问卷星或腾讯文档发一个链接让学生填。这种方式在数据量少的时候没问题一旦涉及几千人问题就暴露了第一问卷平台里的数据导出格式混乱学号、专业、单位名称这些字段经常被填得五花八门后期清洗非常痛苦第二管理员根本没有办法对每个学生的填报状态做精细跟踪谁填了、谁没填、谁的就业单位存疑全靠手工查Excel第三问卷平台是通用产品不能做身份校验同一个学生可以反复提交数据可信度大打折扣。所以要设计一个独立平台的第一个理由不是“为了毕设有东西写”而是这个业务场景确实需要一套带“身份识别—问卷管理—填报校验—数据审核—统计分析”闭环的系统。毕业生用学号和姓名登录后系统自动识别身份锁定该学生所属的院系和专业管理员在后台配置调查问卷发布后学生才能看到已提交的学生被标记为“已完成”重复访问时直接提示管理员可以按院系查看填报进度对异常数据进行审核最后导出标准格式的Excel给就业办。这套逻辑用通用问卷工具是实现不了的而用Django做刚好每一个环节都有对应的成熟组件。1.2 技术选型为什么是Django技术选型是这个项目里我最想多说两句的地方。毕业生去向反馈调查平台的业务特征就是“重数据、中权限、轻交互”——核心是大量结构化数据的录入、校验、检索、聚合页面本身不需要像电商前端那样玩花活。Django在这类场景里有几个天然优势ORM对象关系映射做得非常完整模型定义好后迁移、查询、联表、聚合、分页都有现成API毕业生信息和反馈记录这种一对多的关系用外键处理得干净利落。Django Admin后台开箱即用管理员的问卷管理和数据审核可以直接基于Admin二次开发比自己从零写一套后台管理界面省掉大量工作答辩时也可以说“基于Admin进行了深度定制”。内置表单校验机制Form或ModelForm自带的验证逻辑能覆盖填报端大部分需求——必填校验、字段长度、正则匹配邮箱、自定义验证器检查重复提交这些都不需要额外装包。模板引擎 简洁的URL路由前端页面用Bootstrap控制样式即可不需要引入前后端分离的复杂度整套系统一个人完全hold住。当然如果只选Python后端Flask也能做但Flask需要自己集成Django里已经内置的Admin后台、表单校验、CSRF防护、迁移工具等一整套东西。作为毕设项目工期有限我不建议把时间浪费在重复造轮子上。Django“全家桶”的特性在这个场景里就是实打实的生产效率。2. 数据模型设计从业务字段到Django ORM建模这是整个项目的地基。我见过太多同学一上来就写views.py模型类随便建两个“意思一下”结果做到统计功能时发现字段不够、关联关系不对回头改模型又要重建数据库耗时耗力。所以我把模型设计放在最前面也建议你真正动手写代码前先花半天时间把模型表结构定死。2.1 核心模型类毕业生信息、调查问卷、反馈记录围绕这个业务最核心的实体有三个毕业生Student、问卷Survey、反馈记录Feedback。先看毕业生模型。这里有一个细节要注意学生所属的院系和专业都会变但毕业时的院系专业是固定的所以不能用简单的字符串字段最好用ForeignKey关联到一张“专业班级表”同时在Student模型上冗余保存毕业年份。这样以后做“按届别统计”时只需要过滤一个字段。from django.db import models class College(models.Model): name models.CharField(max_length100, verbose_name学院名称) class Meta: verbose_name 学院 verbose_name_plural verbose_name def __str__(self): return self.name class Major(models.Model): college models.ForeignKey(College, on_deletemodels.CASCADE, verbose_name所属学院) name models.CharField(max_length100, verbose_name专业名称) class Meta: verbose_name 专业 verbose_name_plural verbose_name def __str__(self): return self.name class Student(models.Model): student_no models.CharField(max_length20, uniqueTrue, verbose_name学号) name models.CharField(max_length50, verbose_name姓名) major models.ForeignKey(Major, on_deletemodels.PROTECT, verbose_name专业) graduation_year models.CharField(max_length4, verbose_name毕业年份) phone models.CharField(max_length11, blankTrue, verbose_name联系电话) email models.EmailField(blankTrue, verbose_name邮箱) is_filled models.BooleanField(defaultFalse, verbose_name是否已完成填报) create_time models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: verbose_name 毕业生信息 verbose_name_plural verbose_name ordering [-graduation_year, student_no] def __str__(self): return f{self.student_no}-{self.name}这里有几个关键取舍。on_deletemodels.PROTECT是刻意的学院或专业被删掉时如果还有毕业生关联数据库会阻止删除避免统计数据悬空。如果你用CASCADE一个误操作删掉专业关联的一批毕业生记录全没了回头排查非常麻烦。phone和email允许blankTrue是因为部分毕业生确实不愿意留联系方式后端绝不能因为这两个字段强制校验导致学生无法提交问卷。再看问卷和反馈记录。问卷本身要支持两种题型单选和主观填空。单选题目需要关联到一个选项表因为一道题可能有“国内就业”“自主创业”“升学深造”“待就业”等若干选项选项数量不是固定的。反馈记录则是对应“某个学生提交了对某份问卷的作答”从数据角度看是一对多关系——一份问卷对应多条反馈一条反馈里包含多个题目的答案。class Survey(models.Model): title models.CharField(max_length200, verbose_name问卷标题) description models.TextField(blankTrue, verbose_name问卷说明) start_time models.DateTimeField(verbose_name开始时间) end_time models.DateTimeField(verbose_name结束时间) is_published models.BooleanField(defaultFalse, verbose_name是否发布) create_time models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: verbose_name 调查问卷 verbose_name_plural verbose_name def __str__(self): return self.title class Question(models.Model): QUESTION_TYPES [ (single, 单选题), (text, 主观题), ] survey models.ForeignKey(Survey, on_deletemodels.CASCADE, related_namequestions, verbose_name所属问卷) content models.CharField(max_length500, verbose_name题目内容) question_type models.CharField(max_length10, choicesQUESTION_TYPES, verbose_name题型) is_required models.BooleanField(defaultTrue, verbose_name是否必填) sort_order models.IntegerField(default0, verbose_name排序权重) class Meta: ordering [sort_order] def __str__(self): return self.content[:50] class QuestionOption(models.Model): question models.ForeignKey(Question, on_deletemodels.CASCADE, related_nameoptions, verbose_name所属题目) content models.CharField(max_length200, verbose_name选项内容) sort_order models.IntegerField(default0, verbose_name排序权重) class Meta: ordering [sort_order] def __str__(self): return self.content class Feedback(models.Model): student models.ForeignKey(Student, on_deletemodels.CASCADE, verbose_name填报学生) survey models.ForeignKey(Survey, on_deletemodels.CASCADE, verbose_name所属问卷) submit_time models.DateTimeField(auto_now_addTrue, verbose_name提交时间) ip_address models.GenericIPAddressField(nullTrue, blankTrue, verbose_name提交IP) remark models.TextField(blankTrue, verbose_name备注) class Meta: unique_together (student, survey) verbose_name 反馈记录 verbose_name_plural verbose_name class FeedbackAnswer(models.Model): feedback models.ForeignKey(Feedback, on_deletemodels.CASCADE, related_nameanswers, verbose_name所属反馈) question models.ForeignKey(Question, on_deletemodels.CASCADE, verbose_name所属题目) answer_text models.TextField(blankTrue, verbose_name文本答案) selected_option models.ForeignKey(QuestionOption, nullTrue, blankTrue, on_deletemodels.SET_NULL, verbose_name单选选项) class Meta: verbose_name 反馈答案 verbose_name_plural verbose_name你可能会问为什么不直接在Student表里加一个employment_status字段非得拆出Survey、Feedback、FeedbackAnswer三张表。原因在于学校每年可能发多轮问卷毕业时一次、毕业半年后一次、毕业一年后一次如果直接把字段写在学生表上第二轮问卷的答案就没地方存。拆成独立反馈表后每次调查就是一份新的Survey记录学生提交的记录通过student survey唯一约束天然隔离后续做“两次调查数据对比”也只需要按survey分组查询。2.2 ORM查询与删除对象的实战细节热搜词里有“django执行查询-删除对象”这个知识点在毕业生管理场景里非常实用。Django的删除逻辑和你想的可能不太一样QuerySet.delete()返回的不只是一个数字而是一个包含总删除行数和每个模型删除行数的字典。 Student.objects.filter(graduation_year2020).delete() (12, {myapp.Feedback: 4, myapp.Student: 8, myapp.FeedbackAnswer: 6})这个返回值的含义值得展开8个学生里有4个学生有反馈记录按照on_deleteCASCADE的设置Django会自动把关联的Feedback和FeedbackAnswer也删除。很多同学会在这里踩坑——只想删一条测试数据结果因为级联关系把关联的回答记录也全删了。所以在删除涉及外键关联的对象时我建议先做一次“试删除”确认影响范围或者在模型层面把关键关联改成PROTECT或SET_NULL。另外delete()不会触发模型的save()方法覆盖也就不会走你自定义的信号量逻辑这点在数据审计时容易造成遗漏。查询方面这个项目最频繁的操作是“按学院统计各去向我的人数”。用Django的聚合查询可以一条语句搞定不需要在Python里循环数数from django.db.models import Count from django.db.models.functions import Coalesce stats ( FeedbackAnswer.objects .filter(question__content__contains当前去向, survey__is_publishedTrue) .values(selected_option__content) .annotate(totalCount(id)) .order_by(-total) )annotate配合Count是Django最常用的分组统计手段把values()放在annotate()前面就是按字段分组后再计数。如果选项没有答案selected_option__content会是NULL这时想保留空分组可以使用Coalesce把NULL替换成“未填写”。这套写法是后期统计模块的核心我把热词里的“django执行查询”具体展开成了这条链路的完整解释。3. 管理员端核心功能问卷配置、数据审核与Excel导出管理员端是这个项目工作量最大的部分。学生填写问卷可能只需要30秒但管理员配置问卷、查看进度、审核数据、导出报表这些操作每天都可能会做。Django Admin固然能省掉最基础的增删改查但真正的业务逻辑需要自己写进视图和模板里。3.1 基于Django Admin的深度定制问卷发布的权限控制直接暴露原始Admin给管理员使用体验并不好。我的建议是单独建一个admin.py里的自定义管理类把问卷发布做成一个“两步动作”第一步管理员在后台创建问卷和题目第二步问卷配置完毕后再将is_published字段置为True学生端才能看到。实现上最简单可靠的方式是给Survey模型注册一个actionfrom django.contrib import admin from .models import Survey, Question, QuestionOption, Feedback, FeedbackAnswer admin.register(Survey) class SurveyAdmin(admin.ModelAdmin): list_display (title, is_published, start_time, end_time, create_time) list_filter (is_published, graduation_year) actions [publish_survey, close_survey] admin.action(description发布选中的问卷) def publish_survey(self, request, queryset): updated queryset.update(is_publishedTrue) self.message_user(request, f已发布 {updated} 个问卷) admin.action(description关闭选中的问卷) def close_survey(self, request, queryset): updated queryset.update(is_publishedFalse) self.message_user(request, f已关闭 {updated} 个问卷)这里有个细节queryset.update()是直接生成UPDATE语句不经过模型实例的save()所以执行效率高但也意味着不会触发信号。如果你在Signal里写了日志或缓存清理逻辑记得用queryset.update()或instance.save()二者选其一不要混用。对于毕业生信息后台的默认列表页也要做定制按学院筛选、按是否填报筛选、支持学号搜索。这些在ModelAdmin里配置一行代码的事但对管理者来说是刚需。3.2 数据审核与Excel导出的完整实现Admin负责日常操作真正的“数据审核”和“导出报表”我会单独写两个视图做成管理后台侧边栏的页面。数据审核的核心是标记异常记录。比如学生填的“单位名称”和“单位所在地”明显不匹配、或者升学学校名称格式错误这些需要管理员手动审核打标。我给Feedback模型增加一个status字段用“待审核/正常/异常”三个状态流转即可。管理员在审核页面勾选记录批量更新状态。导出Excel我强烈建议用pandas配合openpyxl引擎而不是网上很多旧教程用的xlwt——xlwt对.xlsx格式支持不好导出数据量大时还容易崩溃。核心代码如下import pandas as pd from django.http import HttpResponse def export_feedback_excel(request, survey_id): survey get_object_or_404(Survey, idsurvey_id) answers ( FeedbackAnswer.objects .filter(feedback__surveysurvey) .select_related(feedback__student, question, selected_option) .order_by(feedback__student__student_no) ) rows [] for answer in answers: rows.append({ 学号: answer.feedback.student.student_no, 姓名: answer.feedback.student.name, 专业: answer.feedback.student.major.name, 题目: answer.question.content, 答案: answer.selected_option.content if answer.selected_option else answer.answer_text, 提交时间: answer.feedback.submit_time.strftime(%Y-%m-%d %H:%M:%S), }) df pd.DataFrame(rows) response HttpResponse( content_typeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet ) response[Content-Disposition] fattachment; filenamesurvey_{survey.id}.xlsx with pd.ExcelWriter(response, engineopenpyxl) as writer: df.to_excel(writer, indexFalse, sheet_name毕业生去向数据) return response这个实现有三个关键点。第一select_related()是必加的否则循环里每次访问answer.feedback.student都会触发一次数据库查询几百条数据就能把页面卡成蜗牛这是Django性能优化里最常见也最有效的一招。第二直接用pd.DataFrame装数据再导出比逐行写Excel的代码量少了一半且pandas自带数据清洗能力像空值填充、类型转换都可以顺手做掉。第三文件名用Content-Disposition设置中文文件名一定要用filename*UTF-8格式否则浏览器会乱码。4. 毕业生填报端身份验证、防重复提交与草稿续填填报端是毕业生直接接触的部分核心要求就三个字稳、快、不重复。很多毕设在这个环节只做了“显示表单保存数据”我建议把另外两个重要逻辑也加进去——身份验证和防重复提交。4.1 基于学号姓名验证码的三重身份校验毕业生登录不能用注册功能因为用户列表是学校导入的。系统需要提供一个“毕业生入口”学生输入学号、姓名和验证码后系统比对数据库中的Student记录匹配通过就允许进入填报页面。校验逻辑我用一个独立的表单类处理这样可以在Django自带的表单验证体系上叠加业务校验from django import forms from .models import Student class StudentLoginForm(forms.Form): student_no forms.CharField(label学号, max_length20, widgetforms.TextInput(attrs{class: form-control})) name forms.CharField(label姓名, max_length50, widgetforms.TextInput(attrs{class: form-control})) captcha forms.CharField(label验证码, max_length6) def clean(self): cleaned_data super().clean() student_no cleaned_data.get(student_no) name cleaned_data.get(name) if student_no and name: student Student.objects.filter(student_nostudent_no, namename).first() if not student: raise forms.ValidationError(学号或姓名不正确请核对后重新输入) return cleaned_data验证码部分我建议直接用django-simple-captcha这个第三方库它在Django表单集成上做得很完善不用自己折腾图形验证码。安装后只需两步INSTALLED_APPS里加captcha表单字段里加一个CaptchaField()。提示校验通过后不要只把student_no放进session就了事。Session里建议保存完整的student_id后续查询、提交、判断重复时都以主键为准避免以后改动学号规则导致session失效。4.2 自定义模板标签驱动的动态问卷渲染问卷是动态的——管理员发布了几道题、每道题是单选还是主观题学生端页面要根据这些数据实时渲染。传统的做法是视图里组织好questions上下文变量模板里用{% for %}遍历。但单选选项需要区分name属性否则浏览器分不清它们属于哪道题动态题目数量不固定还要保证提交后后台能分清答题归属。我的做法是给每道题目在模板里生成一个唯一的name在视图里按题号动态接收数据。def fill_survey(request, survey_id): survey get_object_or_404(Survey, idsurvey_id, is_publishedTrue) student request.session.get(student_id) if request.method POST: answers {} for question in survey.questions.all(): if question.question_type single: selected request.POST.get(fquestion_{question.id}) if question.is_required and not selected: messages.error(request, 请完成所有必填题) return render(request, survey/fill.html, {survey: survey}) answers[question.id] selected else: text_value request.POST.get(fquestion_{question.id}, ).strip() if question.is_required and not text_value: messages.error(request, 请完成所有必填题) return render(request, survey/fill.html, {survey: survey}) answers[question.id] text_value # 保存并防重复逻辑 feedback, created Feedback.objects.get_or_create( student_idstudent, surveysurvey, defaults{ip_address: get_client_ip(request)} ) for question_id, answer in answers.items(): question Question.objects.get(idquestion_id) FeedbackAnswer.objects.update_or_create( feedbackfeedback, questionquestion, defaults{ answer_text: answer if question.question_type text else , selected_option_id: answer if question.question_type single else None, } ) Student.objects.filter(idstudent).update(is_filledTrue) messages.success(request, 提交成功感谢您的参与) return redirect(survey:success) return render(request, survey/fill.html, {survey: survey})这里最关键的是get_or_create和update_or_create。get_or_create用student survey唯一约束在数据库层面防止重复提交就算学生连开两个浏览器同时提交数据库唯一性约束也会兜底报错而不是产生两条脏数据。update_or_create保证“多次编辑覆盖”而不是“每次提交都新增答案”这符合毕业生可能修改去向信息的真实场景。4.3 草稿续填被很多毕设忽略但很加分的功能如果一份问卷有十几道题学生一次没填完关掉页面下次重新填一遍会很崩溃。实名登录后我们可以做“草稿续填”每次保存回答都写库但Session记录“提交中”状态只有学生点了“正式提交”才把状态置为“已完成”。这样学生中途退出下次进来时把已填内容回显到表单。实现回显的要点是视图里构建一个initial_data字典传给表单或模板模板里做选中状态判断。我在Project实战里还会把“填写进度”展示出来——已答题目数量除以总题目数量用一个进度条显示在页面顶部。这个功能答辩时演示效果很好因为评审老师一看就知道系统考虑了真实使用场景。5. 数据可视化与统计报表让去向数据真正“能说话”统计报表是毕业生去向反馈平台的“价值高地”。系统如果只是把数据存起来再导出Excel那跟问卷星没有区别。真正的加分项是能够按维度做交叉统计把结果以图表方式直观呈现。5.1 按学院、专业、单位类型做多维统计统计模块的核心思路是“先用ORM聚合再喂给前端图表库”。比如按学院统计“就业/升学/创业/待就业”分布可以一次查出全部问题选项的计数然后在Python里构建二维表。def statistics_view(request, survey_id): survey get_object_or_404(Survey, idsurvey_id) # 假设第1题是“当前去向”单选题 destination_question survey.questions.filter(content__contains去向).first() if not destination_question: return render(request, survey/statistics.html, {error: 问卷中未找到当前去向题目}) raw_data ( FeedbackAnswer.objects .filter(questiondestination_question) .values(selected_option__content) .annotate(countCount(id)) ) total_count sum(item[count] for item in raw_data) chart_data { labels: [item[selected_option__content] for item in raw_data], values: [item[count] for item in raw_data], total: total_count, } return JsonResponse(chart_data)这里要特别强调的是filter(content__contains去向)这种做法虽然方便但不稳一旦管理员的题目措辞变了统计就跑不出来。更严谨的做法是把“统计主题目”的定位做得更通用——比如在Survey模型上增加一个is_destination_question布尔字段配置问卷时把这个题标记为“去向主问题”统计直接按这个字段过滤。5.2 Chart.js与后端JSON交互的轻量实现绘制图表的方案我选的是Chart.js离线版本不引入ECharts那种重型库。Chart.js的优点是体积小、API简洁、对Bootstrap页面友好而且直接依赖Canvas渲染不需要额外插件。前端页面里用Fetch获取后端JSON接口再初始化图表。canvas iddestinationChart height100/canvas script fetch(/api/statistics/destination/1/) .then(response response.json()) .then(data { const ctx document.getElementById(destinationChart).getContext(2d); new Chart(ctx, { type: doughnut, data: { labels: data.labels, datasets: [{ data: data.values, backgroundColor: [#4e73df, #1cc88a, #36b9cc, #f6c23e, #e74a3b] }] }, options: { responsive: true, maintainAspectRatio: false } }); }); /script饼图之外还有一个“填报进度”指标建议用柱状图展示——按学院列出“应填人数、已填人数、完成率”这个数据对就业办老师来说比饼图更实用。完成率计算注意要把“应填人数”定义为该学院在Student表里的总人数而不是反馈表里有记录的人数不然分母永远是100%。统计页面做出来之后记得测试边界情况没有任何数据时图表是否还能正常渲染只有一个学院数据时布局是否变形某学院完成率为0%时柱状图能不能正确显示。这些边界情况是答辩演示时最容易翻车的地方。6. 远程调试与项目讲解毕设交付中容易被低估的两个环节标题里写了“远程调试讲解定制”这三个词在毕设交易和辅导场景里都是高频诉求。作为一个实际带过项目的开发者我想说这些技能不只是“商业交付”需要更是你自己开发大型项目时必须要有的工程能力。6.1 远程调试的环境准备与常见手法远程调试最常见的技术方案是Visual Studio Code的Remote-SSH插件。流程是这样的先在服务器上配置好SSH登录本地VSCode安装Remote-SSH扩展通过配置文件连接远程主机然后在远程环境里打开项目代码。这样可以实现本地编辑、远程运行Django开发服务器可以直接跑在远程一边改动一边调试。但有几个细节需要注意代码同步如果用Remote-SSH直接编辑远程文件记得在VSCode设置里开启files.autoSave否则代码改动后要手动保存才能生效。端口转发远程Django跑在8000端口本地浏览器要预览页面需要在VSCode的“端口”面板里把8000端口转发到本地。这个操作Remote-SSH是自动完成的但有时会冲突遇到白屏先检查端口转发状态。静态文件调试Django开发服务器不会自动处理静态文件缓存浏览器经常出现改了CSS不生效的情况。刷新时用CtrlF5强制刷新或者直接在Django配置里加一个runserver --insecure参数。真正遇到棘手的Bug光靠打印日志效率太低。我建议在代码里采用Django的日志模块加一个简单的debug视图把关键变量序列化成JSON输出到页面比一层层print省事得多。远程调试最大的坑往往是“本地环境正常、远程环境出错”这种问题八成出在依赖版本不一致上pip freeze requirements.txt生成锁定版本在远程用pip install -r requirements.txt安装能消掉一批莫名其妙的兼容性问题。6.2 讲解项目时怎么讲才不冷场项目做完之后向别人讲解是有方法论的。我的经验是准备一条“从数据流出发”的主线告诉对方这个系统里有哪几种角色、每种角色进入系统后会看到什么、每做一个操作背后数据是怎么流转的。不要一上来就贴代码讲类而是先让听者建立整体画面再进入细节。比如我当时讲这个项目会这样说 “这个系统有两个入口。管理员从后台进入先配置问卷、发布问卷然后每天打开统计页面看填报进度毕业生从首页登录通过学号姓名验证身份后进入问卷提交后系统把答案写入反馈表。最后管理员审核数据并导出Excel。整个系统的核心是Feedback这张表它把学生和问卷关联起来同时又通过FeedbackAnswer把每道题的答案挂到反馈上。”接下来再挑两三个核心代码点深入ORM的select_related优化、get_or_create防重复提交、coalesce处理空值统计。这三个点足以撑起一场15分钟的技术演示而且都是可以即时讲清楚逻辑的模块。7. 部署上线与常见坑从开发机到服务器的完整路径毕设交付时老师大概率会要求“在服务器上跑起来”而不仅仅是本地运行。很多同学在本地一切正常一到Linux服务器上就各种报错。这里分享一条我从开发机到服务器最稳妥的部署路径以及踩过的最深的几个坑。7.1 uWSGI Nginx Django的部署组合部署架构我推荐Nginx uWSGI Django这套经典组合不推荐用runserver直接跑——那是开发服务器并发能力弱而且会被操作系统杀掉。教程里的完整流程大概是第一步在服务器上安装Python虚拟环境并安装依赖第二步安装并配置uWSGI第三步安装Nginx并反代到uWSGI端口第四步设置静态文件目录并配置STATIC_ROOT第五步用collectstatic收集静态文件。uWSGI配置文件我通常写成独立文件方便复用[uwsgi] chdir /home/deploy/graduation_feedback module config.wsgi:application master true processes 4 threads 2 http 0.0.0.0:8000 vacuum true daemonize /home/deploy/graduation_feedback/logs/uwsgi.log pidfile /tmp/graduation_feedback.pidprocesses 4和threads 2的组合对应机器2核4G的配置是比较合理的。如果机器内存只有1G建议把processes降到2。uWSGI这里需要注意的是module config.wsgi:application这里的config是Django项目里存放wsgi.py的那个目录名千万别照抄教程里的project_name不改成自己的实际目录名这是新手犯的最常见的错。Nginx配置反代时最核心的一段server { listen 80; server_name your_domain_or_ip; location /static/ { alias /home/deploy/graduation_feedback/staticfiles/; } location / { include uwsgi_params; uwsgi_pass 127.0.0.1:8000; } }注意location /static/的alias路径末尾一定要带斜杠否则静态文件会404。另外uwsgi_pass如果写的端口和应用监听端口不一致会出现502错误这类问题排查起来往往费时间。7.2 部署踩坑实录时区、CSRF、静态文件与进程管理我在实际部署中踩过的坑里最有价值的是下面几个CSRF验证失败问题。服务器换了域名后如果Settings里的CSRF_TRUSTED_ORIGINS没有配置任何POST请求都会报403。这个配置在Django 4.x里是必填项格式是CSRF_TRUSTED_ORIGINS [http://your_server_ip]注意要带上协议和端口。很多教程只讲了模板里的{% csrf_token %}完全没提这个全局配置导致项目一部署就翻车。时区问题。Django默认TIME_ZONE UTC如果服务器在境内统计“提交时间”时会比北京时间慢8小时。毕业后统计报表要按小时分布的话数据就会错位。部署时务必把TIME_ZONE Asia/Shanghai并USE_TZ True数据库连接里的时区参数也要同步。这块我要专门提醒即使前端显示是对的后台查询时间做__date过滤时也完全可能错一天。静态文件404。DEBUG False之后Django不再托管静态文件必须靠Nginx或collectstatic。如果你用白名单方式配置ALLOWED_HOSTS记得把服务器的公网IP或域名加进去否则Django会给你返回“Invalid HTTP_HOST”错误。进程守护。uWSGI用daemonize方式后台运行看起来好好的但服务器重启后进程就消失了。我建议用systemd写一个service文件来管理uWSGI进程设置好Restartalways这样进程崩了或开机后都会自动恢复。这个细节很多同学觉得麻烦但它恰恰是“像工程而非玩具”的分水岭。部署完成后还有一件容易被忽略的事用python manage.py check --deploy跑一次生产环境安全检查Django会自动列出当前配置在生产模式下可能存在的安全隐患比如SECRET_KEY明文暴露、DEBUG开启等。在答辩时主动提到做过这步安全自检印象分会明显不一样。写在最后的经验总结这个项目从需求分析到最终部署上线如果每天投入三四个小时两周能做完核心功能再加一周用来打磨统计报表和异常边界。我的最大体会是毕业生去向反馈调查平台这种管理信息系统技术上没有特别炫酷的难点难的是把“业务规则”正确地翻译成“数据模型和逻辑判断”。几个让我最后再强调一遍的经验第一模型设计永远值得多花一天时间。宁可前期慢不要后期推倒重来。第二凡是涉及用户提交数据的流程务必考虑防重复和状态回显这是评审老师最喜欢追问的点。第三ORM的select_related和prefetch_related要形成条件反射只要是列表页先想想能不能少查几次库。第四部署上的坑基本都集中在时区、CSRF、静态文件、进程守护这四个问题上提前配好能省掉大半调试时间。我在实际带项目的过程中发现很多同学不是不会写代码而是缺乏“把问题翻译成代码”的分解能力。如果这篇文章能让你在动手之前先想清楚数据怎么流转、权限怎么划分、异常怎么兜底那比直接复制任何一段代码都有价值。