
简介一套面向计算机相关专业毕业设计的洗衣店订单管理系统项目基于PythonDjangoVueMySql开发覆盖管理员、店家、顾客三个角色包含店铺信息、衣服类型、洗衣信息、订单信息、订单进度管理以及交流区等功能模块从系统分析到数据库设计均有完整支撑适合用于毕业设计选题、课程设计或项目实战参考。资源压缩包共785个文件约67.86MB核心内容包括57个py后端源码、48个vue前端组件、155个js脚本、46个css样式、40个html页面以及2个sql数据库脚本另附svg/png/jpg/gif等静态资源和可执行的安装/启动bat脚本结构清晰易于定位。目前已吸引323人学习下载。此外还包含开题报告、毕业论文和演示视频能帮助读者快速理解前后端分离架构、角色权限控制、订单状态流转等关键实现下载后可按脚本直接部署运行适合在此基础二次改造或直接作为毕设交付材料。1. 洗衣店订单管理系统从毕业设计选题到 DjangoVueMySQL 落地洗衣店订单管理系统这类选题在毕业设计里出镜率极高——场景具体、业务边界清楚恰好能把 Python、Django、Vue、MySQL 串成一条前后端分离链路。真正的难点不在 CRUD 能不能跑通而在订单状态流转怎么设计、账单与衣物明细怎么关联、多人同时改单时如何保证数据一致这些才是答辩时能讲出东西的地方。这篇按我实际做这套系统的顺序展开先定 Django 侧的订单模型与状态机再写 Vue 侧的订单面板最后收口 MySQL 的索引、统计与并发控制末尾给出一份照着就能录的演示与论文素材清单。适合正在选型或搭了一半想补技术细节的同学也适合帮学生盯进度的工程师快速理解这套技术栈的常见落法。2. Django 后端订单模型与状态机设计先建项目与环境。常见操作序列是python -m venv venv source venv/bin/activate pip install django djangorestframework django-admin startproject laundry_backend cd laundry_backend python manage.py startapp orders注意把rest_framework和orders注册进 settings.py 的INSTALLED_APPS然后执行python manage.py makemigrations orders和python manage.py migrate生成数据表。卡在这一步的人多半是 MySQL 连接问题DATABASES 配置里 HOST 别写localhost写127.0.0.1更稳妥有些环境 localhost 会被解析到 IPv6 的::1MySQL 没监听这个地址就连不上。数据库连接配置里有一个必调的字符集参数# settings.py DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: laundry_db, USER: root, PASSWORD: 你的密码, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }字符集必须显式指定utf8mb4否则顾客备注里出现 emoji 或者其他四字节字符时MySQL 会直接报Incorrect string value。这是洗衣店订单系统里最常踩的坑。2.1 订单核心表结构主单与衣物明细分开建模真实洗衣店的订单和外卖订单很像主单存顾客信息、总价、状态、时间明细存每件衣物的品类、单价、数量和特殊要求。如果图省事把所有衣物塞进一个 JSONField代码写起来快但论文里少了一个「一对多建模」的论述点后期做按品类统计营收的报表时也只能在 Python 里拆 JSON 处理。常见做法是拆成Order和OrderItem两张表# orders/models.py from django.db import models class Order(models.Model): PENDING pending WASHING washing IRONING ironing READY ready FINISHED finished CANCELED canceled STATUS_CHOICES [ (PENDING, 已接单), (WASHING, 洗涤中), (IRONING, 整烫中), (READY, 待取件), (FINISHED, 已取件), (CANCELED, 已取消), ] order_no models.CharField(订单号, max_length32, uniqueTrue) customer_name models.CharField(顾客姓名, max_length32) customer_phone models.CharField(联系电话, max_length20, db_indexTrue) status models.CharField(订单状态, max_length16, choicesSTATUS_CHOICES, defaultPENDING) total_amount models.DecimalField(订单金额, max_digits8, decimal_places2) remark models.TextField(备注, blankTrue) created_at models.DateTimeField(下单时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: db_table t_order ordering [-created_at] class OrderItem(models.Model): order models.ForeignKey(Order, verbose_name所属订单, related_nameitems, on_deletemodels.CASCADE) clothes_type models.CharField(衣物类型, max_length16) wash_type models.CharField(洗涤方式, max_length16) quantity models.PositiveIntegerField(件数, default1) unit_price models.DecimalField(单价, max_digits6, decimal_places2) class Meta: db_table t_order_item主单把status设计成 CharField 而不是 BooleanField是为了后面能自然扩展「取消」「返洗」这类分支状态。total_amount在主单里冗余一份明细增删后同步刷新总价这是订单系统的常见做法——列表页和报表经常只查主单没必要每次都做聚合求和。db_table显式写成t_order而不是 Django 默认的orders_order原因很实际课程设计验收时老师会直接查看数据表论文里写t_order更直观。删除顺序也要讲清。OrderItem的外键设了on_deletemodels.CASCADE所以删Order时明细会被 MySQL 自动带掉不需要手动清理。反过来如果有一天要保留审计记录就得把外键改成SET_NULL并允许为空这是业务变化导致建模变化的典型例子值得写进论文的「不足与改进」。状态合法性用一张表控制最直观答辩时贴这张表能直接说清业务约束当前状态可推进到说明pending 已接单washing 洗涤中前台录单后进入洗涤washing 洗涤中ironing 整烫中洗涤完成进入整烫ironing 整烫中ready 待取件整烫完挂起等待取件ready 待取件finished 已取件顾客取件订单终结以上任一非完结态canceled 已取消顾客取消或门店取消2.2 用 DRF ViewSet 把订单 API 收敛到一个路由下前后端分离后路由要稳定。我一般把所有订单接口挂到一个 ViewSet 下列表、详情、创建、状态推进、删除五个动作由一个类承担DRF 的 router 注册让 URL 规则非常整齐。# orders/views.py from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from .models import Order from .serializers import OrderSerializer class OrderViewSet(viewsets.ModelViewSet): queryset Order.objects.prefetch_related(items) serializer_class OrderSerializer action(detailTrue, methods[post]) def advance(self, request, pkNone): 把订单推进到下一个合法状态返回新的状态和描述。 order self.get_object() allowed { Order.PENDING: Order.WASHING, Order.WASHING: Order.IRONING, Order.IRONING: Order.READY, Order.READY: Order.FINISHED, } next_status allowed.get(order.status) if next_status is None: return Response({detail: 当前状态不能继续推进}, statusstatus.HTTP_400_BAD_REQUEST) updated Order.objects.filter(pkorder.pk, statusorder.status) \ .update(statusnext_status) if updated 0: return Response({detail: 订单状态已被其他操作修改请刷新后重试}, statusstatus.HTTP_409_CONFLICT) order.refresh_from_db() return Response({ order_no: order.order_no, status: order.get_status_display(), })serializers.py 里用嵌套序列化器把明细一起输出前端一次请求拿到完整订单详情不用再单独请求明细接口# orders/serializers.py from rest_framework import serializers from .models import Order, OrderItem class OrderItemSerializer(serializers.ModelSerializer): class Meta: model OrderItem fields [id, clothes_type, wash_type, quantity, unit_price] class OrderSerializer(serializers.ModelSerializer): items OrderItemSerializer(manyTrue) class Meta: model Order fields [id, order_no, customer_name, customer_phone, status, total_amount, remark, created_at, items] def create(self, validated_data): items_data validated_data.pop(items) order Order.objects.create(**validated_data) total 0 for item in items_data: total item[quantity] * item[unit_price] OrderItem.objects.create(orderorder, **item) order.total_amount total order.save(update_fields[total_amount]) return order逻辑说明ViewSet 把list、create、retrieve、update、destroy五个动作映射到标准 HTTP 方法advance通过action挂成POST /orders/{id}/advance/。状态推进先查合法映射表再做条件更新updated 0说明目标行的status已经被别的请求改过直接回 409 让前端刷新。参数说明action(detailTrue)表示动作作用在单条订单上URL 带主键url_path不写时默认用方法名所以最终地址就是/orders/{id}/advance/。这个接口只做「下一步」推进不接受跳步参数因为洗衣流程里跳步是业务漏洞——还没洗涤就变成待取件演示时容易被老师追问。删除接口保留 ModelViewSet 自带的destroy毕业设计用物理删除可以接受但论文里要写清为什么没做软删除原型系统追求简洁且删除前有确认弹窗。2.3 权限、分页与 Django admin 的取舍完整版订单系统应该有店员端和管理员端。毕业设计建议做两层基础权限Django 自带 User 表配登录接口前端控制按钮显隐API 层至少加 DRF 的IsAuthenticated否则同一局域网下任何人调删单接口都能清空数据演示时很容易翻车。权限粒度做到店员可以 advance只有is_staff用户才能 destroy在 ViewSet 里重写get_permissions或直接判断request.user.is_staff都可以。分页也要给上不然订单数据过百后列表接口会把整张表一次性返回。settings.py 里配置REST_FRAMEWORK { DEFAULT_PAGINATION_CLASS: rest_framework.pagination.PageNumberPagination, PAGE_SIZE: 20, }admin 后台要做但别花太多时间。注册 Order 模型后python manage.py createsuperuser建管理员admin 里按状态筛选、按手机号搜索都是现成的录测试数据和演示「后台管理」节点时非常好用。用list_display、list_filter、search_fields三个属性就能把后台整理得像模像样这是性价比最高的一段配置代码。后端到这里已经具备订单增删改查和状态流转能力。下一步 Vue 面板要处理的核心交互是「状态跟着按钮走」和「双重防连点」。3. Vue 前端订单面板与 API 联调3.1 环境初始化与跨域代理配置前端部分独立成一个 Vue 项目和 Django 完全分离。前端项目本身不依赖 Python 环境但联调时必须保证 Django 的python manage.py runserver在 8000 端口运行代理才能转发成功。新手最容易踩的第一个坑是没配代理axios 请求 URL 写死成http://127.0.0.1:8000浏览器直接报 CORS。开发环境最省事的方案是在 Vite 配置里加 proxy让前端请求统一走/api前缀// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })这段配置只影响开发环境。Vue 开发服务器在 5173Django 在 8000代理把以/api开头的请求转发给 Django 并剥掉前缀。生产环境把 Vue build 出来的 dist 目录交给 Django 托管后根本不存在跨域所以后端不需要额外装 django-cors-headers。装了反而容易让学生把「前端必须跨域才能访问后端」的错误认知写进答辩稿。依赖安装方面用npm create vitelatest选 Vue 模板创建项目组件库用 Element Plus路由用 vue-router请求用 axios。如果你更熟 Ant Design Vue组件替换不影响后文逻辑。安装完跑一遍npm run dev能看到默认页面再继续往下写。3.2 路由设计与订单列表渲染路由拆三页就够列表页、创建页、详情页详情页里放状态推进按钮和完整明细。三页与后端 API 的对应关系如下前端路由视图组件对应后端接口/ordersOrderList.vueGET /orders//orders/createOrderCreate.vuePOST /orders//orders/:idOrderDetail.vueGET /orders/{id}/、POST /orders/{id}/advance/// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, redirect: /orders }, { path: /orders, component: () import(../views/OrderList.vue) }, { path: /orders/create, component: () import(../views/OrderCreate.vue) }, { path: /orders/:id, component: () import(../views/OrderDetail.vue) } ] export default createRouter({ history: createWebHistory(), routes })createWebHistory是 HTML5 模式URL 干净但生产部署时 Django 要把所有非/api请求回退到 index.html否则刷新/orders/5会 404。想省事可直接用createWebHashRouter地址里多个#任何静态服务器都能开代价是 URL 不太好看。毕业设计演示环境里两种都行但论文里要写清选了哪种以及为什么。axios 封装统一处理 BaseURL 和业务错误。我把 baseURL 固定成/api用响应拦截器剥一层 data组件里就不用每个请求都写res.data.data// src/utils/request.js import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.response.use( response response.data, error { const detail error.response?.data?.detail || 请求失败请检查网络 alert(detail) return Promise.reject(error) } ) export default request列表页的渲染逻辑是「拿列表、绑定表格、按状态上色」三步。状态用statusMap把英文状态码映射成中文el-tag 的颜色再按状态切换!-- src/views/OrderList.vue核心部分 -- script setup import { ref, onMounted } from vue import request from ../utils/request const orders ref([]) const loading ref(false) const statusMap { pending: 已接单, washing: 洗涤中, ironing: 整烫中, ready: 待取件, finished: 已取件, canceled: 已取消 } async function fetchOrders() { loading.value true try { const data await request.get(/orders/) orders.value data.results || data } finally { loading.value false } } onMounted(fetchOrders) /script template el-table :dataorders v-loadingloading el-table-column proporder_no label订单号 width180 / el-table-column propcustomer_name label顾客 width120 / el-table-column propcustomer_phone label电话 width150 / el-table-column label状态 width120 template #default{ row } el-tag :typerow.status finished ? success : primary {{ statusMap[row.status] }} /el-tag /template /el-table-column el-table-column proptotal_amount label金额 width120 / el-table-column label操作 template #default{ row } el-button link click$router.push(/orders/${row.id})详情/el-button /template /el-table-column /el-table /template逻辑说明request.get(/orders/)经过代理后实际请求 Django 的订单列表。后端开了分页时响应是{ count, results, next, previous }所以data.results || data同时兼容分页和未分页两种返回。el-table-column的prop对应 OrderSerializer 里的字段嵌套的items明细这页不渲染。3.3 创建订单的动态明细表单与状态推进按钮创建页是「主单 明细数组」的表单。衣物明细用动态列表最少一行点「添加衣物」往数组里 push 一行空数据!-- src/views/OrderCreate.vue动态明细表单部分 -- script setup import { reactive } from vue import { ElMessage } from element-plus import request from ../utils/request import { useRouter } from vue-router const router useRouter() const form reactive({ customer_name: , customer_phone: , remark: , items: [{ clothes_type: , wash_type: 水洗, quantity: 1, unit_price: 20 }] }) async function submit() { if (!form.customer_name || !form.customer_phone) { ElMessage.warning(请填写顾客姓名和电话) return } if (form.items.length 0) { ElMessage.warning(至少添加一件衣物) return } const saved await request.post(/orders/, form) router.push(/orders/${saved.id}) } /script提交后拿返回值直接跳详情页演示录屏就能连续看到「下单即见详情」的完整链路。表单校验不用上重型库Element Plus 自带 rules 加上人工判断已经足够——把篇幅留给状态机部分比把表单校验写出花更有答辩价值。详情页的状态推进按钮是这套系统的核心交互。按钮的禁用条件不能只看当前状态还要把请求 pending 状态算进去const advancing ref(false) async function handleAdvance() { if (advancing.value) return advancing.value true try { await request.post(/orders/${order.value.id}/advance/) await fetchOrder() } finally { advancing.value false } }advancing是前端轻量锁作用是在请求进行期间禁用按钮防止双击产生两个 advance 请求。但真正的兜底在 Django 的条件更新上——即使用户多开页面、两台电脑同时操作同一订单后到的那次更新也会因为状态不匹配而失败。双端防护才是这套系统值得写进论文的设计点。Vue 侧到这里已能完成「列表-创建-详情-状态推进」主闭环。界面代码量不大但状态流转的交互细节按钮禁用、409 提示、刷新是演示时的加分项。下一章把视角切到 MySQL订单号、索引、统计报表这些数据层问题是答辩提问的重灾区。4. MySQL 表设计与订单统计的坑4.1 建库、字符集与订单号唯一索引动手写代码前先把数据库建好CREATE DATABASE laundry_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;用 MySQL Workbench 建 Schema 也一样但要把 Default collation 显式选成utf8mb4_general_ciWorkbench 默认选的可能是 latin1。Django 侧OPTIONS里的 charset 要与此一致。排序规则不一致的两张表做 JOIN 时MySQL 会报Illegal mix of collations这个报错直译就是「字符集混了」网上搜到的答案多半让你直接改表ALTER TABLE t_order CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;订单号不要用自增主键直接展示给顾客否则同行看订单号连续就能估出门店单量。常见做法是日期加随机数Django 侧创建订单时自动生成import random from django.utils import timezone def generate_order_no(): now timezone.localtime() return now.strftime(%Y%m%d%H%M%S) f{random.randint(1000, 9999)}这种方案数据库层不保证唯一所以模型里order_no必须带uniqueTrue并发撞号时抛IntegrityError捕获后重新生成即可。除了唯一索引高频查询路径要单独加索引customer_phone、status、created_at。最常用的是「按状态筛列表 按时间倒序」MySQL 里直接用联合索引ALTER TABLE t_order ADD INDEX idx_status_created (status, created_at);加了联合索引后WHERE statuspending ORDER BY created_at DESC直接走索引避免 filesort。单列索引 status 也能用但会多一步排序数据量上万后用EXPLAIN能看到Extra: Using filesort那就是该上联合索引的信号。4.2 并发推进用乐观锁条件更新比 FOR UPDATE 更合适洗衣店订单系统是典型的低写并发场景但对「同一订单被两个店员同时操作」要有防护。2.2 节已经用了条件更新的乐观锁思路这一节讲清楚为什么不用悲观锁。悲观锁写法是SELECT ... FOR UPDATE事务持锁期间其他会话只能等。问题在于 FOR UPDATE 是在事务里如果事务后面还跟着外部接口调用或耗时的 Python 逻辑锁的持有时间会变长数据库连接占用也变久。订单状态推进是毫秒级操作用悲观锁属于杀鸡用牛刀。乐观锁不锁行而是用 WHERE 条件保证更新不覆盖别人改过的数据UPDATE t_order SET status washing WHERE id 1 AND status pending;这条语句同一时刻只能有一个会话成功。MySQL 行锁让两个 UPDATE 串行执行后执行那个 WHERE 匹配不到行affected rows 返回 0程序里据此回 409。注意 Django 里不能用save()做这件事save()会无条件整行写回GET 出来再 save 会把别人刚改过的status覆盖回旧值正确姿势是 2.2 节的filter(...).update(...)条件更新。事务隔离级别顺带说一句。MySQL InnoDB 默认是 REPEATABLE READ订单读写模式用默认值即可。但论文里如果写了「隔离级别如何保证一致性」至少要能说清 READ COMMITTED 和 REPEATABLE READ 在快照读上的区别——前者每次读都拿最新快照后者一个事务内读到的是事务开始时的快照。这个点不难却是区分「会调代码」和「懂数据库」的经典题。4.3 统计报表日营收视图与衣物分类汇总报表是毕业设计的必答题。日营收用「按天分组」就能做SQL 要能讲清 GROUP BY 和聚合函数CREATE VIEW v_order_daily AS SELECT DATE(created_at) AS stat_date, COUNT(*) AS total_orders, SUM(total_amount) AS total_amount, SUM(CASE WHEN status finished THEN 1 ELSE 0 END) AS finished_orders FROM t_order WHERE status ! canceled GROUP BY DATE(created_at) ORDER BY stat_date DESC;Django 侧查视图有两种方式connection.cursor()跑原生 SQL或者 ORM 的annotate。视图已建好时原生 SQL 更顺查询退化成一行SELECT * FROM v_order_daily WHERE stat_date BETWEEN 2024-06-01 AND 2024-06-30。论文里若要求写「存储过程」把 CREATE VIEW 换成 CREATE PROCEDURE 并接收日期参数即可多一层 BEGIN...END 包裹。按品类汇总的维度是衣物类型和洗涤方式SELECT oi.clothes_type, oi.wash_type, SUM(oi.quantity) AS qty, SUM(oi.quantity * oi.unit_price) AS amount FROM t_order_item oi JOIN t_order o ON o.id oi.order_id WHERE o.status finished AND o.created_at 2024-06-01 AND o.created_at 2024-07-01 GROUP BY oi.clothes_type, oi.wash_type ORDER BY amount DESC;两个细节写进论文是加分项。第一日期条件不要写DATE(created_at) BETWEEN对 created_at 做函数包裹后索引失效改成和的半开区间范围查询能走索引。第二o.status finished过滤掉未取件订单因为只有已取件才计入实际营收这和门店对账口径一致——洗衣店不是下单就收钱取件才算完成。报表页前端简单做一个表格展示查询结果即可。数据量小时不需要图表库el-table 足够答辩时打开 MySQL Workbench 展示同一条 SQL 的结果比页面截图更有说服力。5. 交付物整合演示脚本、环境检查与论文技术点5.1 可按顺序录制的演示用例视频演示建议按「登录-下单-状态推进-报表-权限拦截」五步录每步控制在 15 秒内。状态推进步骤务必演示一次终止态保护把订单推进到「已取件」后再点一次「下一步」页面返回 400「当前状态不能继续推进」证明状态机有边界。另一个高价值镜头是双开页面并发演示——两个标签页打开同一订单先在一个标签页推进再到另一个标签页推进触发 409 提示并自动刷新数据。开发联调时打开 Vue Devtools 的 Network 面板观察状态码能直观看到 400 和 409 的响应结构。报表步骤把 MySQL Workbench 切出来执行 4.3 节的视图查询再切回 Vue 页面展示一致的日报数字这个镜头比任何话术都有说服力。5.2 换机运行的环境一致性检查换一台机器最容易挂三处MySQL 版本与字符集、Python 依赖版本、前端是否已构建。标准做法是把命令固化到 READMEpython -m venv venv source venv/bin/activate pip install -r requirements.txt python manage.py makemigrations orders python manage.py migrate python manage.py runserver 0.0.0.0:8000前端构建后用 Django 托管是演示最稳的形态老师不用装 Node。把 Vue 的 dist 静态文件放进 Django 静态目录urls.py 末尾加一个 catch-all 路由指向 index.html。静态文件服务别用 Django 自带的 static view 硬扛加个 WhiteNoise 中间件就能避免一堆 404。如果老师要求公网访问常见做法是宝塔面板加 Nginx 反代 Django前端构建产物交给 Nginx 托管数据库单独开库。5.3 论文与开题报告值得展开的三个技术点论文别写成「基于某某框架的系统设计与实现」流水账。三个点值得展开订单状态机及流转约束的建模对应 2.2 节的条件更新乐观锁与事务隔离在订单并发下的取舍对应 4.2 节前后端分离下跨域策略与统一异常处理对应 3.2 节。每一点配一张图——状态图、时序图、架构图开题报告里的技术路线就按这三张图的脉络写。开题报告的目标定成「一套可用于小规模洗衣门店的订单管理原型系统支持订单全流程跟踪与经营日报」即可验收标准写清楚「多用户并发操作不产生脏数据」「状态流转不允许跳步」。最后再对着 README 用两台不同配置的电脑各跑一遍 5.2 的命令序列第一台能通、第二台也能通这套交付物才算真正闭环。本文还有配套的精品资源点击获取