
简介一套面向毕业设计场景的洗衣店订单管理系统基于Python、Django、Vue与MySQL技术栈开发分管理员、店家、顾客三种角色覆盖店铺信息维护、衣服类型管理、洗衣信息登记、订单进度追踪、交流区等功能模块能帮助读者快速理解业务闭环与前后端分离实现思路。资源压缩包共785个文件以Python后端源码、Vue前端组件、JS/CSS/HTML静态资源、SQL数据库脚本为主另含开题报告、毕业论文、演示视频等多种材料整体大小67.86MB。目前已有323人学习下载。除可直接运行的完整项目源码和数据库外这套资源还附带安装、运行、构建批处理脚本以及操作录屏目录结构清晰便于按模块研究权限设计、订单流转流程和接口调用方式。适合正在准备毕设、需要参考完整项目方案或希望快速复现同类型管理系统的学习者。1. 洗衣店订单管理系统用DjangoVue拆开订单状态流转洗衣店订单管理系统看起来就是一套CRUD真正动手时会卡在三个地方订单状态怎么流转、店家与顾客的角色权限怎么隔离、Vue前端如何把进度更新实时刷新。这套基于Python Django Vue MySQL的毕业设计恰好把这三件事都落地了压缩包里不只是源码还有数据库脚本、开题报告、毕业论文和视频演示。系统是B/S模式有管理员、店家、顾客三个角色顾客在首页看店铺信息、下洗衣订单、在交流区发消息店家维护店铺和洗衣信息、确认订单、更新进度管理员做全局数据管理。对于要完成毕业设计或者想把Django前后端分离流程跑通的人这是一份可以直接对照复现的样板工程。2. 表结构设计与订单状态流转Django models如何映射MySQL脚本2.1 从三个角色抽取数据实体洗衣店订单管理的核心不是“订单”这一张表而是把顾客、店家、管理员三类账号的关系先理清。常见做法是直接用Django内置的auth_user存登录账号再通过role字段区分角色顾客和店家各自由外键指向自己的业务表。这样登录、Session、权限框架都不用重造Django 的User模型本身也带is_staff管理员可以用这个标准字段标识不必额外扩展太多。我习惯把表拆成店铺信息、衣服类型、洗衣信息、订单信息、订单进度、交流区六大块。店铺信息关联店家账号洗衣信息挂在店铺下面订单主表把顾客、店铺、洗衣信息串起来订单进度单独做一张流水表方便顾客查看每一次状态变化。下面是一个典型的数据表结构数据表作用关键字段关联关系auth_user登录账号username, password, role顾客、店家、管理员都在这张表shop_info店铺信息name, address, owner_id, phoneowner_id 关联 auth_userclothes_type衣服类型type_name, price供订单选择laundry_info洗衣服务项name, price, shop_id挂在店铺下order_info订单主表order_no, customer_id, shop_id, status关联顾客和店铺order_progress订单进度流order_id, progress_status, operator_id, create_time关联订单forum_post交流区帖子title, content, user_id关联 auth_user对应的 Django 模型可以这样写# laundry/models.py from django.db import models from django.contrib.auth.models import User class Shop(models.Model): name models.CharField(店铺名, max_length128) address models.CharField(地址, max_length255) phone models.CharField(联系电话, max_length20) owner models.OneToOneField(User, on_deletemodels.CASCADE, related_nameshop) def __str__(self): return self.name class Order(models.Model): STATUS_PENDING pending STATUS_WASHING washing STATUS_FINISHED finished STATUS_CANCELLED cancelled STATUS_CHOICES [ (STATUS_PENDING, 待确认), (STATUS_WASHING, 洗涤中), (STATUS_FINISHED, 已完成), (STATUS_CANCELLED, 已取消), ] order_no models.CharField(订单号, max_length32, uniqueTrue) customer models.ForeignKey(User, on_deletemodels.CASCADE, related_nameorders) shop models.ForeignKey(Shop, on_deletemodels.CASCADE, related_nameorders) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultSTATUS_PENDING) total_amount models.DecimalField(总金额, max_digits8, decimal_places2) remark models.TextField(备注, blankTrue) created_at models.DateTimeField(下单时间, auto_now_addTrue) class OrderProgress(models.Model): order models.ForeignKey(Order, on_deletemodels.CASCADE, related_nameprogresses) progress_status models.CharField(进度说明, max_length128) operator models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue) create_time models.DateTimeField(更新时间, auto_now_addTrue)OrderProgress单独做表而不是在订单上只改一个状态字段是为了保留历史记录。顾客在前台看到的“洗衣进度”实际是这张表里最近几条记录不是只有一个状态字符。operator用SET_NULL店家账号被删后进度记录不会跟着消失这在答辩时也更容易说清楚。2.2 MySQL脚本和Django迁移如何对齐项目自带数据库脚本通常是一个.sql文件。导入前先建库否则会报Unknown databasemysql -uroot -p -e CREATE DATABASE laundry DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; mysql -uroot -p laundry laundry.sqlutf8mb4必须加洗衣订单的备注、交流区帖子很容易出现中文标点和emoji如果用老式utf8部分字符会变成问号。导入完成后在 Django 的settings.py里把 MySQL 连接指向这个库DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: laundry, USER: root, PASSWORD: 123456, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }如果不想用现成SQL而是从模型反向建表执行python manage.py makemigrations laundry python manage.py migrate但已经有laundry.sql的情况下直接跑migrate可能因为表已存在而报错。常见处理是先导入脚本再执行python manage.py migrate --fake-initial--fake-initial会跳过那些已经被数据库脚本建好的表只对后续新增字段做迁移既不会破坏已有数据也不会让迁移记录和实际表结构对不上。注意只有确认SQL脚本里的表结构和当前models代码完全一致时才适合用--fake-initial。如果后来改过模型字段直接执行可能造成迁移记录错乱。2.3 订单状态流转不要让前端随意改status订单状态是系统的业务核心。最简单的实现是把status做成文本框但那样顾客可以直接把订单改成“已完成”店家也无法看到完整进度。常见做法是只允许几个固定动作并且把状态变更和进度插入放在一个事务里from django.db import transaction def update_order_progress(order, status, operator, remark): with transaction.atomic(): order.status status order.save(update_fields[status]) OrderProgress.objects.create( orderorder, progress_statusremark or status, operatoroperator, )transaction.atomic()保证状态修改和进度记录要么都成功要么都回滚不会出现订单状态已经到“已完成”但进度表里没有记录的情况。update_fields[status]只写这一个字段避免把同时被其他地方修改的total_amount等字段覆盖掉。状态流转可以限定为下面这张表当前状态操作目标状态允许角色待确认店家确认接单洗涤中店家/管理员洗涤中店家写入进度洗涤中店家/管理员洗涤中店家完成订单已完成店家/管理员待确认顾客取消订单已取消顾客这里没有出现直接从“待确认”跳到“已完成”因为洗衣业务中必须先接单再进入洗涤跳状态会丢掉进度记录。真正的校验逻辑不写在save里而是放在后续的 ViewSet 或 Service 层让接口只暴露合法的动作。3. Django后端接口实现DRF ViewSet、JWT认证和三种角色权限3.1 为什么选择DRF而不是手写JSONResponse后端的价值不止是给前端返回JSON还要把角色权限、字段校验和业务动作收口。如果每个视图都手写JsonResponse订单列表要自己判断请求人创建订单要自己拼错误信息改路径时还要同步改前端。使用 Django REST Framework 的ModelViewSet能把序列化、分页、过滤和路由生成一次搞定这也是Django前后端分离项目里最常见的选型。安装后需要把rest_framework加入INSTALLED_APPS同时配置JWT认证pip install djangorestframework# settings.py INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, laundry, ] REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], }使用JWT后Vue端只需要在请求头带Authorization: Bearer token后端不需要维护Session适合前后端分离部署。3.2 序列化器把嵌套进度一并返回订单列表页面需要同时显示顾客名、店铺名和进度记录所以序列化器不能只映射订单字段。嵌套序列化器可以省去前端一个个id再查表的操作# laundry/serializers.py from rest_framework import serializers from .models import Order, OrderProgress class OrderProgressSerializer(serializers.ModelSerializer): operator_name serializers.CharField(sourceoperator.username, read_onlyTrue) class Meta: model OrderProgress fields [id, progress_status, operator_name, create_time] class OrderSerializer(serializers.ModelSerializer): customer_name serializers.CharField(sourcecustomer.username, read_onlyTrue) shop_name serializers.CharField(sourceshop.name, read_onlyTrue) progresses OrderProgressSerializer(manyTrue, read_onlyTrue) class Meta: model Order fields [id, order_no, customer_name, shop_name, status, total_amount, remark, created_at, progresses]sourcecustomer.username会把关联对象的username直接作为customer_name输出前端不用再处理嵌套对象。progresses使用manyTrue对应订单模型里的related_nameprogresses默认按正向查询返回所有进度记录。如果希望最新进度排前面可以在模型Meta里加ordering [-create_time]。3.3 权限控制三种角色看三种订单Django内置的IsAuthenticated只能确认登录分不清角色。自定义BasePermission放在每个视图上逻辑最清晰# laundry/permissions.py from rest_framework import permissions class IsOwnerOrShopOrAdmin(permissions.BasePermission): def has_object_permission(self, request, view, obj): user request.user if user.is_staff: return True if getattr(user, role, ) shop: return obj.shop.owner user if getattr(user, role, ) customer: return obj.customer user return False这里假设auth_user上扩展了role字段。has_object_permission只对单个对象生效列表页的过滤还是要在get_queryset里做否则一个店家直接访问/api/orders/?page2会把别人的订单也遍历到内存里# laundry/views.py from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated from .models import Order from .serializers import OrderSerializer from .permissions import IsOwnerOrShopOrAdmin class OrderViewSet(viewsets.ModelViewSet): serializer_class OrderSerializer permission_classes [IsAuthenticated, IsOwnerOrShopOrAdmin] def get_queryset(self): user self.request.user if user.is_staff: return Order.objects.select_related(customer, shop).all() if getattr(user, role, ) shop: return Order.objects.select_related(customer, shop).filter(shop__owneruser) return Order.objects.select_related(customer, shop).filter(customeruser)select_related会把customer和shop用SQL JOIN 一次性取出避免每行订单都额外发一次查询。顾客访问时filter(customeruser)直接限定到自己店家访问时通过shop__owneruser找到自己店铺下的所有订单。管理员不走前面两个分支返回全表。路由注册使用DefaultRouter这样订单的 CRUD 接口自动生成from rest_framework.routers import DefaultRouter from laundry.views import OrderViewSet router DefaultRouter() router.register(rorders, OrderViewSet) urlpatterns router.urls对应的接口路径如下方法路径权限用途GET/api/orders/登录用户按角色返回订单列表POST/api/orders/顾客创建洗衣订单GET/api/orders/{id}/订单关联者查看订单详情PATCH/api/orders/{id}/订单关联者修改备注或金额DELETE/api/orders/{id}/管理员删除异常订单3.4 用action接口处理进度更新订单创建和删除可以走通用CRUD但“更新进度”不是简单地PATCHstatus还需要校验状态合法并追加一条进度记录。把业务动作定义为视图上的方法比在前端拼两个请求更可靠from rest_framework.decorators import action from rest_framework.response import Response class OrderViewSet(viewsets.ModelViewSet): action(detailTrue, methods[post], permission_classes[IsAuthenticated]) def update_progress(self, request, pkNone): order self.get_object() user request.user if order.customer user: if request.data.get(status) ! Order.STATUS_CANCELLED: return Response({detail: 顾客只能取消订单}, status403) elif user.is_staff or order.shop.owner user: pass else: return Response({detail: 没有权限}, status403) status request.data.get(status, order.status) remark request.data.get(remark, ) update_order_progress(order, status, user, remark) return Response(self.get_serializer(order).data)action(detailTrue, methods[post])会生成POST /api/orders/{id}/update_progress/这样的自定义路径。self.get_object()会先经过get_queryset()过滤也就是说一个顾客就算知道要调用接口也拿不到别人的order。这里顾客分支只允许传cancelled其他状态必须由店家或管理员操作。测试这个接口可以用curlcurl -X POST http://127.0.0.1:8000/api/orders/1/update_progress/ \ -H Authorization: Bearer token \ -H Content-Type: application/json \ -d {status: washing, remark: 已经开始水洗}返回的 JSON 里会带着完整的progresses列表前端拿到后直接替换当前订单数据即可不需要再重新拉整个列表。4. Vue前端联调axios拦截器、订单列表和进度按钮刷新4.1 .bak备份文件与前端项目结构源码里能看到IndexAsideStatic.vue.bak、IndexHeader.vue.bak、update-password.vue.bak这类文件这是典型的改前端前的备份习惯。.bak后缀不会被Vue CLI当成组件编译所以可以放心留在目录里。遇到功能改崩时把.vue.bak复制回去就能快速回滚。正常可运行的文件是.vue比如侧边栏组件IndexAsideStatic.vue、顶部栏IndexHeader.vue、面包屑BreadCrumbs.vue组成了整个后台布局。前后端分离项目里Vue开发服务器默认跑在8080Django跑在8000。为了避免每个接口都写完整域名常见做法是在vue.config.js里配置 devServer 转发让/api开头的请求直接打到 Django// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } }这样前端请求/api/orders/时开发服务器会把请求转发给 Django不会产生跨域问题。生产环境则由 Nginx 统一处理/api和静态文件。4.2 axios拦截器把token和错误处理集中起来每个接口都手动拼Authorization头很啰嗦也容易漏。拦截器是标准做法// src/utils/request.js import axios from axios import { Message } from element-ui import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } Message.error((error.response error.response.data error.response.data.detail) || 请求失败) return Promise.reject(error) } ) export default requestresponse response.data让调用方直接拿到后端 JSON 数据不用再写response.data.data。401 时统一踢回登录页这个逻辑必须是集中处理否则每个页面对axios的错误处理不一致用户会看到空白页。4.3 订单列表页面按角色显示按钮订单列表是三个角色最常用的页面。侧边栏根据本地存储的role动态渲染订单页根据角色控制“更新进度”按钮是否可见。示例组件如下template div classorder-list el-table :dataorders v-loadingloading border el-table-column proporder_no label订单号 min-width160 / el-table-column propcustomer_name label顾客 min-width100 / el-table-column propshop_name label店铺 min-width120 / el-table-column propstatus label状态 width100 template slot-scope{ row } el-tag :typestatusType(row.status){{ statusText(row.status) }}/el-tag /template /el-table-column el-table-column proptotal_amount label金额 width100 / el-table-column label操作 width180 template slot-scope{ row } el-button v-ifcanUpdate(row) typeprimary sizemini clickopenDialog(row)更新进度/el-button /template /el-table-column /el-table /div /template script import { fetchOrders, updateOrderProgress } from /api/order export default { data() { return { orders: [], loading: false, currentOrder: null } }, created() { this.loadOrders() }, methods: { async loadOrders() { this.loading true try { this.orders await fetchOrders() } finally { this.loading false } }, canUpdate(row) { const role localStorage.getItem(role) return role shop || role admin }, async handleProgressSubmit(status, remark) { await updateOrderProgress(this.currentOrder.id, { status, remark }) this.$message.success(进度已更新) this.currentOrder null this.loadOrders() } } } /script对应的接口封装// src/api/order.js import request from /utils/request export function fetchOrders(params) { return request({ url: /orders/, method: get, params }) } export function updateOrderProgress(orderId, data) { return request({ url: /orders/${orderId}/update_progress/, method: post, data }) }接口路径不带/api前缀因为request.js里的baseURL已经加了/api。注意fetchOrders的params会把筛选条件转成查询字符串比如?statuswashingDjango的django-filter或手动get_queryset里可以根据request.query_params过滤。loadOrders()在进度更新成功后重新拉取列表而不是手动去改orders里某个对象的status。原因是updateOrderProgress返回的最新订单数据虽然也有progresses但列表页可能还有分页、排序和权限过滤直接替换一个对象容易和前后端约定不一致。重新拉列表多一次请求但能避免“明明改了状态列表不刷新”的经典bug。4.4 交流区与个人后台的联调要点交流区比较简单本质是一个帖子列表加一个表单。Django端用PostSerializer嵌套user_name字段Vue端在首页拉/posts/展示发帖时POST /posts/。需要注意的点是交流区接口也要走拦截器未登录用户只能看不能发这个可以在前端通过v-if!token禁用按钮但真正的校验必须由后端permission_classes完成。个人后台里的“店铺信息管理”在顾客和店家眼里内容不同。店家登录后打下了/api/shops/my/拿自己的店铺资料顾客只能查看公开店铺列表不能进入PATCH /api/shops/{id}/修改数据。这还是靠后端get_queryset控制而不是前端隐藏入口。接口对应关系如下前端组件后端接口说明Login.vuePOST /api/token/获取JWTOrderList.vueGET /api/orders/订单列表ProgressDialog.vuePOST /api/orders/{id}/update_progress/更新进度ShopInfo.vueGET /api/shops/{id}/店铺信息Forum.vueGET/POST /api/posts/交流区如果前端修改密码功能对应的是update-password.vue.bak需要的接口是POST /api/user/password/后端用 Django 内置的set_password加密后再save不要直接存明文。换密码后让用户重新登录清掉本地token和role重新走登录流程。5. 启动脚本与部署排错install.bat、run.bat和MySQL8高频坑5.1 一键脚本在做什么压缩包里的1-install.bat、2-run.bat、3-build.bat、安装.bat、运行.bat是Windows下常用的启动链。第一个脚本最核心内容一般是echo off cd /d %~dp0 pip install -r requirements.txt python manage.py migrate先装依赖再做迁移确保数据库表结构与代码版本一致。2-run.bat通常是echo off cd /d %~dp0 python manage.py runserver 0.0.0.0:80000.0.0.0允许局域网访问方便用手机测试订单页面。如果MySQL没有启动Django会在启动时报2003, Cant connect to MySQL server先检查服务再检查settings.py里的密码。5.2 高频排错MySQL 8 最典型的坑是默认认证插件caching_sha2_password导致mysqlclient连不上。改为mysql_native_password即可ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456; FLUSH PRIVILEGES;如果中文字符乱码确认建表语句里DEFAULT CHARSETutf8mb4并且在Django的OPTIONS里也加上charsetutf8mb4。前端构建后3-build.bat会生成dist目录如果用Django托管前端静态文件需要在settings.py配置模板指向dist/index.html同时把dist/static加到STATICFILES_DIRS。检查部署时执行python manage.py check再访问http://127.0.0.1:8000/admin看到一个能正常显示的Django后台说明后端服务与数据库连接正常然后打开Vue开发服务器完成登录、下单、更新进度、交流区发帖这一串动作整个系统的三角色闭环就验证通过了。本文还有配套的精品资源点击获取