
简介这是一份基于Django框架构建的校园报修系统毕业设计项目面向计算机专业学生及开发者适用于毕业设计、课程设计或全栈项目参考。系统覆盖用户注册登录、报修工单提交、状态跟踪与管理员后台处理等完整流程并融合Python、Java与Vue技术栈可帮助理解前后端分离与多语言协作开发模式。压缩包共2000个文件大小30.86MB以1590个Python源文件为主搭配192个JavaScript脚本、131个HTML模板、46个CSS样式以及少量配置与文档结构完整。主目录django-web-main中可看到settings、urls、views、models等典型Django模块便于对照学习项目分层与业务逻辑。目前已有87人学习参考适合希望通过完整项目源码提升Django后端开发、Vue前端交互及Java服务端设计能力的学习者。1. 解开这个 zip 容易解开毕设里的一层层需求不容易校园报修系统在毕业设计里出现频率不低题目看起来不复杂学生提交报修、维修工接单处理、管理员管理全流程似乎三张表就能讲完。但真动手做会发现报修单的状态怎么流转、不同角色能看到哪些数据、后台怎么改才好用、换 MySQL 部署到服务器上要踩多少坑每一项都比预想中花时间。这套基于 django 的校园报修系统要做的就是把「报修」这个看似简单的业务做成一个角色清晰、状态可控、能拿去答辩也能部署上线的完整 web 项目。下面按从模型设计到工单流转再到 admin 定制、部署上线和演示准备的顺序讲完整套落地路径。2. django 校园报修系统的核心模型设计与字段规划2.1 项目骨架怎么搭最省返工刚拿到这个题目时先做django-admin startproject还是先做startapp顺序其实没那么重要但 app 拆分值得想清楚。报修这个业务放在一个repairapp 里就够了用户体系直接用 django 自带的auth.User不需要另建用户表后面再通过Group或is_staff区分角色。django-admin startproject campus_repair . python manage.py startapp repair python manage.py startapp system_config项目根目录下的campus_repair是配置包repair放报修业务system_config用来放校区、楼栋、报修分类这类基础数据。startapp后面那个点号表示在当前目录创建项目文件实际部署到服务器时路径会干净一些。很多毕设把基础数据表直接塞进主业务 app短时间看没问题答辩时老师问起模块划分还得现场找补。2.2 报修单模型状态机比字段多少更重要写RepairOrder模型时状态字段是整个系统的灵魂。不要用布尔值表达状态比如is_finished报修单后面会多出「待受理、已接单、维修中、待验收、已完成」这些中间态布尔字段一次只能表达两条路径后期改造成本不小。from django.db import models from django.contrib.auth import get_user_model User get_user_model() class RepairCategory(models.Model): name models.CharField(分类名称, max_length50) sort models.IntegerField(排序, default0) class Meta: verbose_name 报修分类 verbose_name_plural verbose_name ordering [sort] def __str__(self): return self.name class RepairOrder(models.Model): class Status(models.TextChoices): PENDING pending, 待受理 ASSIGNED assigned, 已接单 PROCESSING processing, 维修中 RESOLVED resolved, 待验收 CLOSED closed, 已完成 REJECTED rejected, 已驳回 title models.CharField(报修标题, max_length100) description models.TextField(问题描述) category models.ForeignKey( RepairCategory, verbose_name报修分类, on_deletemodels.SET_NULL, nullTrue, related_nameorders ) location models.CharField(报修地点, max_length200) reporter models.ForeignKey( User, verbose_name报修人, on_deletemodels.CASCADE, related_namereported_orders ) assignee models.ForeignKey( User, verbose_name维修工, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_nameassigned_orders ) status models.CharField( 状态, max_length20, choicesStatus.choices, defaultStatus.PENDING ) priority models.CharField( 优先级, max_length10, choices[(low, 低), (medium, 中), (high, 高)], defaultmedium ) images models.JSONField(报修图片, defaultlist, blankTrue) created_at models.DateTimeField(提交时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) finished_at models.DateTimeField(完成时间, nullTrue, blankTrue) class Meta: verbose_name 报修单 verbose_name_plural verbose_name ordering [-created_at]Status用TextChoices枚举出来后整个系统里所有判断逻辑都以RepairOrder.Status.PENDING这种形式引用不会出现字符串散落各处的魔法值问题。images用JSONField存图片路径列表而不是单独建一张报修图片表对毕设体量来说够用且省事。assignee设置nullTrue是因为待受理状态下没有维修工强制ForeignKey会在创建报修单时就没法落库。字段设计上常见的一个错误是直接用ForeignKey指向一个新的Repairman表来记录维修工姓名和电话其实维修工本身也是系统用户。正确做法是像上面一样指向auth.User维修工姓名和电话直接用User.first_name、User.phone之类的字段存。这样既能复用 django 自带登录后面做权限控制也顺理成章。2.3 角色划分用 Django 自带权限还是外键标记报修系统里至少有三类角色学生、维修工、管理员。最简单直接的做法是给User模型加一个role字段。但在 django 自带的 admin 体系里维护起来反而更别扭因为很多后台功能依赖is_staff和is_superuser。我一般会这么做角色权限方案后台登录学生普通登录用户is_activeTrue不开放 admin维修工is_staffTrue加入repair_staff分组开放 admin只看到已分配的单管理员is_superuserTrue全部权限这里选Group而不是自定义role字段是为了配合user.has_perm()和后面admin页面的权限过滤逻辑。is_staffTrue的维修工能进后台但只给他报修单的查看和修改权限不给用户管理权限这样能用最少代码做出一个可用的维修工端。from django.contrib.auth.models import Group repair_group, created Group.objects.get_or_create(namerepair_staff)这段代码通常在data migration或初始化脚本里执行保证在迁移数据库后能自动把维修工分组建出来在 admin 后台手动创建维修工账号时再勾选这个组。3. 工单流转从提交报修到完成的全链路实现3.1 学生提交报修表单校验与视图处理报修单创建视图可以走CreateView类视图也可以直接写函数视图。CreateView 写起来快但要在form_valid里塞入当前登录用户反而绕了一步。这里用函数视图逻辑更直白答辩时也能讲得更清楚。from django.shortcuts import render, redirect from django.contrib.auth.decorators import login_required from django.contrib import messages from .models import RepairOrder, RepairCategory from .forms import RepairOrderForm login_required def create_order(request): if request.method POST: form RepairOrderForm(request.POST, request.FILES) if form.is_valid(): order form.save(commitFalse) order.reporter request.user order.status RepairOrder.Status.PENDING order.save() images request.FILES.getlist(images) image_paths [] for image in images: image_paths.append(save_uploaded_image(image)) order.images image_paths order.save(update_fields[images]) messages.success(request, 报修单提交成功请等待维修工接单) return redirect(repair:order_detail, pkorder.pk) else: form RepairOrderForm() return render(request, repair/order_form.html, { form: form, categories: RepairCategory.objects.all() })forms.py里的RepairOrderForm通过fields [title, description, category, location, priority]指定表单字段images是文件上传不走 normal widget。form.save(commitFalse)拿到的 instance 不会落库先把reporter和status填上再save()这是 django 使用CreateView之外的常见流程写法。request.FILES.getlist(images)是因为多图上传时request.FILES[images]只能拿到最后一个文件。3.2 维修工接单与状态变更事务和锁怎么用报修单被两个维修工同时抢单后台状态会怎样如果代码只做if order.status pending: order.assignee request.user两个请求几乎同时读到 pending然后都写入了后写的覆盖先写的状态就乱套了。处理这个问题的标准做法是使用select_for_update()配合事务from django.db import transaction from django.contrib.auth.decorators import user_passes_test from django.shortcuts import get_object_or_404 def is_repair_staff(user): return user.is_staff and user.groups.filter(namerepair_staff).exists() user_passes_test(is_repair_staff) def accept_order(request, pk): with transaction.atomic(): order get_object_or_404( RepairOrder.objects.select_for_update(), pkpk, statusRepairOrder.Status.PENDING ) order.assignee request.user order.status RepairOrder.Status.ASSIGNED order.save(update_fields[assignee, status, updated_at]) messages.success(request, f已接单{order.title}) return redirect(repair:order_detail, pkorder.pk)select_for_update()会在数据库层面给这行记录加锁事务提交前其他事务的修改操作会被阻塞。这里先锁定记录再检查状态为PENDING保证了并发场景下不会出现一单一接的情况。user_passes_test装饰器可以拦截非维修工角色跳转默认的登录页。update_fields里只更新变更的字段可以减少数据库写入量。3.3 报修单状态机合法迁移路径提前定好状态字段不是随便改的每个角色能执行的动作是有限的。提前定好状态迁移矩阵比在后面写一堆if判断要有条理得多也少出 bug当前状态可执行操作目标状态执行角色待受理接单已接单维修工已接单开始维修维修中维修工维修中提交完成待验收维修工待验收确认完工已完成学生/管理员待受理驳回已驳回管理员任意状态转单待受理管理员在模型里写一个transition_status方法集中管理状态迁移逻辑业务调用时就不用各处重复校验class RepairOrder(models.Model): ALLOWED_TRANSITIONS { Status.PENDING: {Status.ASSIGNED, Status.REJECTED}, Status.ASSIGNED: {Status.PROCESSING}, Status.PROCESSING: {Status.RESOLVED}, Status.RESOLVED: {Status.CLOSED, Status.PROCESSING}, } def transition_status(self, new_status, userNone): if new_status not in self.ALLOWED_TRANSITIONS.get(self.status, set()): raise ValueError(f不允许从 {self.get_status_display()} 变更为 {new_status}) self.status new_status if new_status self.Status.CLOSED: self.finished_at timezone.now() self.save(update_fields[status, finished_at, updated_at])ALLOWED_TRANSITIONS里把每个状态能跳转的目标状态写进集合非法迁移直接抛异常。这样做的好处在 admin 后台批量操作时体现得最明显不用担心某个按钮在错误的状态下被调用。4. django admin 界面定制让后台好用又不失毕设水准4.1 admin 后台注册报修单并优化列表页答辩时老师一定会打开 admin 后台看几眼。默认的 djang admin 列表页字段少、筛选弱、搜索不可用进后台第一印象就差一截。优化 admin 是花费小、见效快的投入。from django.contrib import admin from .models import RepairOrder, RepairCategory admin.register(RepairOrder) class RepairOrderAdmin(admin.ModelAdmin): list_display [ id, title, category, location, reporter, assignee, status, priority, created_at ] list_filter [status, priority, category, created_at] search_fields [title, description, location, reporter__username] list_per_page 20 date_hierarchy created_at readonly_fields [created_at, updated_at]上面这段配置可以实际写进admin.py里。list_display是列表页展示哪些列category、assignee这种外键会自动显示关联对象的__str__返回值list_filter是右侧筛选项状态和优先级这两个字段的筛选在报修系统里使用频率最高search_fields可以跨外键搜索reporter__username搜索的就是报表人用户名的关键字。list_per_page控制每页显示条数默认 100改成 20 页面上看起来整齐得多。date_hierarchy会在列表顶部生成按时间穿梭的下钻筛选对查看某天集中报修的情况很有用。4.2 给 admin 加批量操作按钮接单、完成、驳回list_display优化的是查看体验批量操作优化的是处理效率。默认 admin 只有删除动作把「标记已完成」「批量驳回」做成actions后台使用者就能勾选多条记录一键操作。from django.contrib import messages from django.utils import timezone admin.register(RepairOrder) class RepairOrderAdmin(admin.ModelAdmin): actions [mark_as_resolved, mark_as_rejected] admin.action(description标记选中的报修单为待验收) def mark_as_resolved(self, request, queryset): valid_orders queryset.filter(statusRepairOrder.Status.PROCESSING) count valid_orders.update(statusRepairOrder.Status.RESOLVED) self.message_user(request, f已将 {count} 张报修单置为待验收) admin.action(description驳回选中的待受理报修单) def mark_as_rejected(self, request, queryset): valid_orders queryset.filter(statusRepairOrder.Status.PENDING) count valid_orders.update(statusRepairOrder.Status.REJECTED) self.message_user(request, f已驳回 {count} 张报修单)actions函数接收的三个参数分别是 request、queryset 和 admin 实例。queryset.filter先按状态过滤一遍避免把「已完成」的单重新置为「待验收」这一步不可省。update()是批量更新不会触发模型的save()方法所以finished_at这种需要在 save 里自动赋值的字段要在操作里单独处理。message_user是 admin 里最规范的给操作者反馈的方式。4.3 让维修工在后台只处理属于自己的单维修工登录 admin 后台后默认能看到所有报修单这显然不合适。在get_queryset方法中按登录用户过滤是 admin 定制里必写的一段逻辑。class RepairOrderAdmin(admin.ModelAdmin): def get_queryset(self, request): qs super().get_queryset(request) if request.user.is_superuser: return qs if request.user.groups.filter(namerepair_staff).exists(): return qs.filter(assigneerequest.user) return qs.filter(reporterrequest.user)get_queryset是 admin 列表页数据源的入口。超管直接看全部数据维修工只看到assignee是自己也就是接单的报修单普通学生则只能看到自己提交的报修单。这样不同角色登录同一个后台首页看到的数据天然隔离不用额外实现一套前端工作台。如果还觉得列表页操作需要点进编辑页太繁琐可以在 admin 列表页的每条记录后面加上自定义按钮。在list_display中添加一个方法字段from django.utils.html import format_html from django.urls import reverse class RepairOrderAdmin(admin.ModelAdmin): list_display [..., action_buttons] admin.display(description快捷操作) def action_buttons(self, obj): if obj.status RepairOrder.Status.PENDING: url reverse(admin:repair_repairorder_change, args[obj.pk]) return format_html(a classbutton href{}受理/a, url) return -这段代码生成一个「受理」链接点击后进入编辑页但省掉了在列表里找记录的步骤。reverse(admin:repair_repairorder_change, args[obj.pk])中的repair是 app 名repairorder是模型名的小写形式两个名称拼成 admin 里每条记录的编辑页路径。5. 宝塔部署 django 校园报修系统到服务器5.1 settings 里的部署前必改配置本地跑runserver和真正放到服务器上有几个明显的差别DEBUG要关掉ALLOWED_HOSTS要写服务器 IP 或域名静态文件要统一收集。改少了会出两类问题白屏和静态文件 404。DEBUG False ALLOWED_HOSTS [your_server_ip, www.example.com] STATIC_URL /static/ STATIC_ROOT /www/wwwroot/campus_repair/static MEDIA_URL /media/ MEDIA_ROOT /www/wwwroot/campus_repair/mediaSTATIC_ROOT是collectstatic收集静态文件的目标目录nginx 会从这个目录读文件。MEDIA_ROOT对应上传的报修图片nginx 也要单独配置。DEBUG False之后admin 后台的静态文件不会再由 django 直接吐出所以必须先执行python manage.py collectstatic否则打开后台会是一个没有样式的裸页面。5.2 换 MySQL 时 mysqlclient 安装与数据库配置本地开发一般用 SQLite部署到服务器很多人会换成 MySQL。安装mysqlclient是常见卡点因为它依赖系统编译环境。sudo apt-get install default-libmysqlclient-dev build-essential pkg-config pip install mysqlclientdefault-libmysqlclient-dev提供 MySQL 客户端库文件build-essential提供 gcc 编译工具链缺了这两个依赖pip install mysqlclient会在编译阶段报错。安装完成后在settings.py里改数据库配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: campus_repair, USER: repair_user, PASSWORD: YourStrongPass123, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }下面是部署时校验各配置项用到的表和说明配置项含义是否必填NAME数据库名需提前建库必填USER连接数据库的用户名必填PASSWORD密码避免用 root 弱密码必填HOST建议127.0.0.1不开放公网端口必填OPTIONSutf8mb4支持表情符号和中文推荐数据库先把字符集和排序规则建好避免后面出现中文乱码CREATE DATABASE campus_repair DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;utf8mb4_unicode_ci排序规则对中文和 emoji 都友好。建完库再执行python manage.py migrate同步数据表。5.3 uwsgi 与 nginx 站点配置宝塔面板里部署 django 项目常见做法是使用「Python 项目管理器」建一个项目指定项目路径、Python 版本和启动文件。如果你更习惯用命令行裸装下面这套配置也能照抄。uwsgi 的启动配置可以写在uwsgi.ini[uwsgi] chdir /www/wwwroot/campus_repair module campus_repair.wsgi:application master true processes 4 threads 2 socket 127.0.0.1:8001 vacuum true max-requests 5000 daemonize /www/wwwroot/campus_repair/uwsgi.logmodule指向campus_repair.wsgi里的application对象这是 django 项目自带的 wsgi 入口。processes和threads根据服务器核数调整一般学生服务器 2 核就设processes 2。socket用本地端口而不是 http因为前面还挂着 nginxuwsgi 只跟 nginx 通信。nginx 站点配置里加一个 server 块server { listen 80; server_name your_server_ip; location /static/ { alias /www/wwwroot/campus_repair/static/; } location /media/ { alias /www/wwwroot/campus_repair/media/; } location / { include uwsgi_params; uwsgi_pass 127.0.0.1:8001; uwsgi_read_timeout 120s; } }uwsgi_pass的地址要和uwsgi.ini里的socket完全一致。uwsgi_read_timeout 120s是给上传报修图片留的余量默认 60 秒对于大图瘦身不够。如果服务器上已经跑着宝塔自带的 nginx在站点设置里添加反向代理目标 URL 填127.0.0.1:8001效果一样。部署完成后访问首页若出现 502先确认 uwsgi 进程是否存活、日志里最后几行有没有 Traceback若出现样式全丢确认collectstatic有没有执行、nginx 的static路径写没写对。这两个排查步骤能覆盖大部分部署类报错。6. 答辩前造数据一条命令生成一年份的报修记录毕设答辩时空荡荡的数据库会让系统演示效果大打折扣。手动一条条在后台点着填数据既慢又假。django 的自定义 management command 是解决这个问题最合适的工具直接在项目里写一个命令一次执行就能生成大量符合业务逻辑的演示数据。# repair/management/commands/generate_demo.py import random from datetime import timedelta from django.core.management.base import BaseCommand from django.utils import timezone from django.contrib.auth import get_user_model from repair.models import RepairOrder, RepairCategory User get_user_model() class Command(BaseCommand): help 生成演示用报修数据 def handle(self, *args, **options): admin User.objects.filter(is_superuserTrue).first() categories list(RepairCategory.objects.all()) students list(User.objects.filter(is_staffFalse)) staffs list(User.objects.filter(is_staffTrue)) for i in range(200): reporter random.choice(students) category random.choice(categories) order RepairOrder.objects.create( titlef报修示例-{i}, description演示数据设备无法正常使用需要维修人员现场检查处理。, categorycategory, locationf{random.randint(1, 6)}号楼{random.randint(101, 612)}室, reporterreporter, statusrandom.choice(list(RepairOrder.Status)), priorityrandom.choice([low, medium, high]), created_attimezone.now() - timedelta(daysrandom.randint(0, 365)) ) if order.status in [RepairOrder.Status.RESOLVED, RepairOrder.Status.CLOSED]: order.assignee random.choice(staffs) order.finished_at order.created_at timedelta(daysrandom.randint(1, 5)) order.save(update_fields[assignee, finished_at])这个generate_demo.py放在repair/management/commands/目录下之后还需要在management和commands目录中各建一个空的__init__.py文件django 才能识别到命令。执行方式python manage.py generate_demo命令里固定了 200 条数据量created_at分布在过去一年内这样系统首页的趋势图表比如按月统计的报修量看起来会有真实起伏。同时不同status平均分布演示时不用担心某个状态下没有记录。演示时的完整流程我建议这样走清空数据库后重新执行generate_demo然后从学生端新建一张报修单开始讲让老师看到一条数据从「待受理」到「已接单」再到「维修中」「待验收」「已完成」的完整路径。要展示后台时切到维修工账号演示接单和状态流转再切回管理员账号演示搜索、筛选和驳回操作。最后打开首页的月度统计图表说明这份演示数据如何支撑起可视化模块。整场演示用同一条数据线索串下来比零散展示十几个页面更有说服力。本文还有配套的精品资源点击获取