简介:这是一份基于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_length=50) sort = models.IntegerField('排序', default=0) 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_length=100) description = models.TextField('问题描述') category = models.ForeignKey( RepairCategory, verbose_name='报修分类', on_delete=models.SET_NULL, null=True, related_name='orders' ) location = models.CharField('报修地点', max_length=200) reporter = models.ForeignKey( User, verbose_name='报修人', on_delete=models.CASCADE, related_name='reported_orders' ) assignee = models.ForeignKey( User, verbose_name='维修工', on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_orders' ) status = models.CharField( '状态', max_length=20, choices=Status.choices, default=Status.PENDING ) priority = models.CharField( '优先级', max_length=10, choices=[('low', '低'), ('medium', '中'), ('high', '高')], default='medium' ) images = models.JSONField('报修图片', default=list, blank=True) created_at = models.DateTimeField('提交时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) finished_at = models.DateTimeField('完成时间', null=True, blank=True) class Meta: verbose_name = '报修单' verbose_name_plural = verbose_name ordering = ['-created_at']Status用TextChoices枚举出来后,整个系统里所有判断逻辑都以RepairOrder.Status.PENDING这种形式引用,不会出现字符串散落各处的魔法值问题。images用JSONField存图片路径列表,而不是单独建一张报修图片表,对毕设体量来说够用且省事。assignee设置null=True,是因为待受理状态下没有维修工,强制ForeignKey会在创建报修单时就没法落库。
字段设计上常见的一个错误是直接用ForeignKey指向一个新的Repairman表来记录维修工姓名和电话,其实维修工本身也是系统用户。正确做法是像上面一样指向auth.User,维修工姓名和电话直接用User.first_name、User.phone之类的字段存。这样既能复用 django 自带登录,后面做权限控制也顺理成章。
2.3 角色划分:用 Django 自带权限还是外键标记
报修系统里至少有三类角色:学生、维修工、管理员。最简单直接的做法是给User模型加一个role字段。但在 django 自带的 admin 体系里,维护起来反而更别扭,因为很多后台功能依赖is_staff和is_superuser。我一般会这么做:
| 角色 | 权限方案 | 后台登录 |
|---|---|---|
| 学生 | 普通登录用户,is_active=True | 不开放 admin |
| 维修工 | is_staff=True,加入repair_staff分组 | 开放 admin,只看到已分配的单 |
| 管理员 | is_superuser=True | 全部权限 |
这里选Group而不是自定义role字段,是为了配合user.has_perm()和后面admin页面的权限过滤逻辑。is_staff=True的维修工能进后台,但只给他报修单的查看和修改权限,不给用户管理权限,这样能用最少代码做出一个可用的维修工端。
from django.contrib.auth.models import Group repair_group, created = Group.objects.get_or_create(name='repair_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(commit=False) 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', pk=order.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(commit=False)拿到的 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(name='repair_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(), pk=pk, status=RepairOrder.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', pk=order.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, user=None): 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(status=RepairOrder.Status.PROCESSING) count = valid_orders.update(status=RepairOrder.Status.RESOLVED) self.message_user(request, f'已将 {count} 张报修单置为待验收') @admin.action(description='驳回选中的待受理报修单') def mark_as_rejected(self, request, queryset): valid_orders = queryset.filter(status=RepairOrder.Status.PENDING) count = valid_orders.update(status=RepairOrder.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(name='repair_staff').exists(): return qs.filter(assignee=request.user) return qs.filter(reporter=request.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 class="button" 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/media'STATIC_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,不开放公网端口 | 必填 |
OPTIONS | utf8mb4支持表情符号和中文 | 推荐 |
数据库先把字符集和排序规则建好,避免后面出现中文乱码:
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,因为前面还挂着 nginx,uwsgi 只跟 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_superuser=True).first() categories = list(RepairCategory.objects.all()) students = list(User.objects.filter(is_staff=False)) staffs = list(User.objects.filter(is_staff=True)) for i in range(200): reporter = random.choice(students) category = random.choice(categories) order = RepairOrder.objects.create( title=f'报修示例-{i}', description='演示数据:设备无法正常使用,需要维修人员现场检查处理。', category=category, location=f'{random.randint(1, 6)}号楼{random.randint(101, 612)}室', reporter=reporter, status=random.choice(list(RepairOrder.Status)), priority=random.choice(['low', 'medium', 'high']), created_at=timezone.now() - timedelta(days=random.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(days=random.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,然后从学生端新建一张报修单开始讲,让老师看到一条数据从「待受理」到「已接单」,再到「维修中」「待验收」「已完成」的完整路径。要展示后台时,切到维修工账号演示接单和状态流转,再切回管理员账号演示搜索、筛选和驳回操作。最后打开首页的月度统计图表,说明这份演示数据如何支撑起可视化模块。整场演示用同一条数据线索串下来,比零散展示十几个页面更有说服力。
本文还有配套的精品资源,点击获取