
简介基于微信小程序的智慧家政系统毕业设计项目采用 Vue、SpringBoot 与 MySQL 构建面向需要完成 Java 毕业设计的学生以及希望快速上手家政服务类小程序开发的工程师。项目围绕家政服务线上管理场景覆盖地址管理、订单管理、家政分类、家政服务、用户反馈等核心模块并集成用户管理、部门管理、角色管理、菜单管理、日志管理、数据字典等基础功能支持基于角色的访问控制权限可精确到按钮级别适合需要自定义角色并严格分配权限的毕业设计题目。资源为 zip 压缩包共 409 个文件大小约 1.9MB以 161 个 Java 源码文件、72 个 Vue 组件、72 个 JS 脚本为主体另有 SQL 数据库脚本、YML 环境配置、小程序页面结构与样式文件、PNG 图片素材等后端、前端、数据库与静态资源覆盖完整目录结构清晰便于按需检索和二次开发。已有 543 人学习下载可直接作为选题参考和功能拆解对象也可通过部署运行观察订单流转、服务分类与权限控制的实际效果串联起小程序端、接口层和数据表设计之间的协作关系。1. 基于微信小程序的智慧家政让排单、派单、催进度从打电话变成看手机排单靠微信群里吼一嗓子阿姨上工全靠电话确认用户催进度只能一遍遍打电话——这是我接到「基于微信小程序的智慧家政系统」这个入口时最先想解决的三个问题。这个项目用 Vue 写小程序前端后端用 JAVA 配 SpringBoot数据落在 MySQL 里核心就是把「找服务 → 下单 → 派单 → 上门 → 完结」这条链路从线下搬进手机。它适合做毕业设计时想练全栈的学生也适合家政公司老板想低成本做信息化。下面我按自己做这类系统时的顺序先讲设计边界再给数据库和两端代码最后把最容易翻车的地方挨个点一遍。2. 技术选型与数据库设计为什么是 VueSpringBootMySQL三张核心表怎么定2.1 选型理由这套组合在毕业设计里到底图什么微信小程序是前端容器用户不用下载 App扫码或搜索就能进对家政这种低频但刚需的场景来说获客成本低。Vue 用来组织小程序页面列表、表单、状态切换都是典型的前端交互用 Vue 的数据绑定比原生小程序写起来顺手很多。后端选 SpringBoot是因为它内置了 Web 服务器和自动配置一个 Main 方法就能起服务不需要像传统 SSH 那样堆一堆 XML 配置。MySQL 则是数据落地的稳定选择订单、服务项目、阿姨信息这些结构化数据用它管理最方便而且招聘市场上熟悉 MySQL 的人多后续维护不愁没人接。如果你只是应付毕业设计这套组合还有个隐性好处前端、后端、数据库三层拆分得很清楚答辩时每一层都能单独讲出设计思路。常见做法是用一个支持 Vue 语法的小程序框架先把页面跑通后端用 SpringBoot 暴露 JSON 接口小程序端通过 wx.request 去调。把每一层的边界划清楚后面写代码和写论文都省力。2.2 MVP 功能边界先砍掉「抢单大厅」这类伪需求我第一次做这个项目时列了一堆功能用户评论、阿姨抢单、自动派单、优惠券、积分商城结果数据库建了十几个表最后只完成了下单和列表。后来我把功能砍到最小可用版本反而一周就打通了全流程。如果你也在拿这个题目做毕业设计建议先锁住三个角色用户端能浏览服务项目、提交订单、查看订单状态管理端能维护服务项目和阿姨信息、手动派单、更新订单状态系统层面必须有明确的订单状态流转这是整个项目的骨架。砍功能的标准很简单没有它订单无法从下单走到完结就不算核心功能。优惠券、评分、积分全部往后放。等到核心链路跑通了再加这些作为亮点功能比一开始就铺开要稳妥得多。2.3 三张核心表把业务钉死在数据结构上数据库里最能看出设计功力的不是表多而是订单表的状态设计。这里给出最小可用的三张表服务项目表 service_item、订单表 orders、服务人员表 employee。状态字段我单独抽出来设计不搞成「已接单所以就不保存取消时间」这种隐含逻辑。CREATE TABLE service_item ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 服务项目ID, name VARCHAR(50) NOT NULL COMMENT 服务名称如日常保洁, price DECIMAL(10,2) NOT NULL COMMENT 单价单位元, unit VARCHAR(10) DEFAULT 次 COMMENT 计费单位次/小时, cover_url VARCHAR(255) COMMENT 封面图URL, description VARCHAR(500) COMMENT 服务描述, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT服务项目表; CREATE TABLE employee ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 服务人员ID, name VARCHAR(30) NOT NULL COMMENT 姓名, phone VARCHAR(20) NOT NULL COMMENT 联系方式, service_type BIGINT COMMENT 关联service_item.id擅长项目, order_count INT DEFAULT 0 COMMENT 累计接单数, status TINYINT DEFAULT 1 COMMENT 1可接单 0休息, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT服务人员表; CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 订单ID, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务订单号全局唯一, user_id BIGINT NOT NULL COMMENT 下单用户ID, service_item_id BIGINT NOT NULL COMMENT 服务项目ID, employee_id BIGINT COMMENT 服务人员ID派单后写入, service_date DATE NOT NULL COMMENT 预约上门日期, service_time_slot VARCHAR(20) COMMENT 时间段如09:00-11:00, address VARCHAR(255) NOT NULL COMMENT 服务地址, amount DECIMAL(10,2) NOT NULL COMMENT 订单金额, order_status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1待派单 2待服务 3服务中 4已完成 5已取消, pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未支付 1已支付 2已退款, cancel_reason VARCHAR(255) COMMENT 取消原因, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;逻辑说明orders 表把 order_status 和 pay_status 分开是因为「支付了但还没派单」和「派单了但还没支付」是完全不同的业务状态如果只用一个状态字段查询时要把两个维度揉在一起写 SQL 容易出错。employee 表里的 service_type 是逻辑外键没有创建物理外键约束因为实际项目里外键约束会影响插入性能和后续分表业务层面用应用代码保证一致性更灵活。参数说明order_no 设置为 UNIQUE这是防止重复下单的第一道闸门生成策略在后面接口里讲。service_date 用 DATE 类型而非 DATETIME因为预约只精确到天时间段用单独的字符串字段存避免把日期和时间混在一个字段里导致索引失效。amount 用 DECIMAL(10,2)不用 FLOAT家政项目金额虽然小但涉及结算时浮点数精度问题属于自作孽。3. SpringBoot 后端接口下单流程、状态机与支付回调的最小实现3.1 项目骨架分层建好后面写接口不迷路我用 SpringBoot MyBatis-Plus 搭后端结构按 controller → service → mapper 三层拆。MyBatis-Plus 能少写很多单表 CRUD这对毕业设计来说节省的时间非常可观。目录结构大致是这样src/main/java/com/example/housekeeping ├── controller │ ├── OrderController.java │ └── ServiceItemController.java ├── service │ ├── OrderService.java │ └── impl/OrderServiceImpl.java ├── mapper │ ├── OrderMapper.java │ └── ServiceItemMapper.java ├── entity │ ├── Orders.java │ └── ServiceItem.java └── common ├── Result.java └── GlobalExceptionHandler.java启动类不用额外配置SpringBoot 的自动装配会把内嵌 Tomcat 启起来你只需要在 application.yml 里配好端口和数据源。常见的做法是生成一个全局返回体 Result让所有接口返回 {code, message, data} 的统一格式前端小程序拦截起来才方便。3.2 下单接口生成订单号、校验服务项、写库一步到位下单是整个系统最关键的一个接口用户从小程序点「立即预约」后前端会把服务项目 ID、预约时间、地址传给后端。后端要做三件事校验服务项目是否上架、生成唯一订单号、把订单记录插入数据库。RestController RequestMapping(/api/order) public class OrderController { Autowired private OrderService orderService; PostMapping(/create) public ResultString create(RequestBody Valid CreateOrderDTO dto) { // dto: serviceItemId, serviceDate, serviceTimeSlot, address String orderNo orderService.createOrder(dto); return Result.success(orderNo); } }Service public class OrderServiceImpl implements OrderService { Autowired private ServiceItemMapper serviceItemMapper; Autowired private OrderMapper orderMapper; Override public String createOrder(CreateOrderDTO dto) { ServiceItem item serviceItemMapper.selectById(dto.getServiceItemId()); if (item null || item.getStatus() ! 1) { throw new BizException(服务项目不存在或已下架); } String orderNo generateOrderNo(); Orders order new Orders(); order.setOrderNo(orderNo); order.setServiceItemId(dto.getServiceItemId()); order.setServiceDate(dto.getServiceDate()); order.setServiceTimeSlot(dto.getServiceTimeSlot()); order.setAddress(dto.getAddress()); order.setAmount(item.getPrice()); order.setOrderStatus(0); // 初始状态待支付 order.setPayStatus(0); orderMapper.insert(order); return orderNo; } private String generateOrderNo() { return HK System.currentTimeMillis() RandomUtil.randomNumbers(4); } }逻辑说明generateOrderNo 用时间戳加 4 位随机数配合 orders 表的 UNIQUE 约束来保证不重复。如果极端情况下随机数撞了插入时会抛 DuplicateKeyException全局异常处理器捕获后返回「请重试」即可不用在业务代码里加分布式锁毕业设计的数据量完全不需要引入 Redis 那套复杂度。参数说明CreateOrderDTO 用 Valid 注解做参数校验serviceDate 要求格式 yyyy-MM-ddaddress 限制长度不超过 255。前端传过来的服务时间段我只做存储不做合法性校验——因为时间的合法性应该由你在管理后台维护服务时间模板时控制而不是在下单接口里硬编码。3.3 支付回调验签、幂等、更新状态少一个都是隐患用户在小程序里支付后微信支付会往你配置的回调地址发 POST 请求。很多新手在这里犯的错是回调里直接改订单状态为「已支付」然后返回 success。这看似简单但漏了两个关键点——验签和重复通知处理。PostMapping(/api/pay/callback) public String payCallback(RequestBody String notifyData) { // 1. 验签用微信支付平台证书验证通知签名 boolean valid WxPayUtil.verifySignature(notifyData); if (!valid) { return failure; } // 2. 解析通知拿到订单号和支付结果 String orderNo WxPayUtil.parseOrderNo(notifyData); String tradeStatus WxPayUtil.parseTradeStatus(notifyData); if (SUCCESS.equals(tradeStatus)) { // 3. 幂等更新只有待支付状态才更新为已支付 LambdaUpdateWrapperOrders wrapper new LambdaUpdateWrapper(); wrapper.eq(Orders::getOrderNo, orderNo) .eq(Orders::getPayStatus, 0) .set(Orders::getPayStatus, 1); orderMapper.update(null, wrapper); } return success; }逻辑说明第 3 步是整段代码的命门。回调可能因为网络抖动被微信重复发送多次如果不用 pay_status0 作为条件第二次回调会重复执行「待支付转已支付」的更新。加了条件后第二次回调影响行数为 0不会产生副作用。验签必须放在最前面否则任何人都可以伪造回调把你订单改成已支付这是资金安全红线。注意个人开发者的微信小程序无法直接开通微信支付商户号。我在做模拟项目X时本地和演示阶段先写了一个假的支付接口前端点击「立即支付」后直接跳转到模拟支付页后端收到一个测试回调把订单置为已支付。到答辩演示时把回调地址切到真实环境即可整套流程不用大改。这样既跑通了业务链路又不会被商户号审批卡住进度。3.4 订单状态机用一张字典把状态流转写清楚订单状态我定义成 5 个数字0 待支付、1 待派单、2 待服务、3 服务中、4 已完成、5 已取消。合法流转是0 → 1 → 2 → 3 → 4其中 0 可以在用户主动取消时跳到 53 之后管理员可以标记完成。任何非法的跳跃比如从 1 直接跳到 4都应该被写死在代码的校验逻辑里。我一般在管理端的接单接口里加一个前置校验只有当前状态是 1待派单才允许指派阿姨并把状态改成 2待服务。这样做的好处是无论前端怎么调接口数据状态都不会被破坏哪怕有人绕过前端直接抓包改数据也无济于事。把这个状态机画成表格放进论文里答辩老师一眼就能看出你考虑了业务边界。4. 小程序端 Vue 页面请求封装、服务列表与一键预约下单4.1 小程序框架与页面结构用 Vue 的思维写小程序小程序端最常见的做法是采用支持 Vue 语法的小程序框架页面写法与 Vue SFC 基本一致上手成本低。如果你只想练 Java 后端用原生小程序写页面也能实现同等功能只是 Vue 风格的组件化写法在维护体验上会好很多。我在页面结构上按 TabBar 分成首页、订单、我的三块核心页面只有五个首页服务列表、服务详情、预约下单、订单列表、订单详情。src/pages ├── index/index.vue ├── service-detail/detail.vue ├── order-create/create.vue ├── order-list/list.vue └── order-detail/detail.vue目录结构里的每个页面只做一件事index 负责展示服务项目order-create 负责收集预约信息并提交。别把页面拆得太细但也不能把列表、详情、下单全塞进一个文件里后面改需求时你会想抽自己。4.2 请求封装把 wx.request 变成可复用的 Promise小程序原生的 wx.request 写起来啰嗦而且不支持 Promise每个页面都要处理 success / fail 两个回调代码重复度很高。我的习惯是封装一个 request.js统一管理接口地址和 token顺便把登录态失效跳转也处理了。const BASE_URL https://your-domain.com/api function request({ url, method GET, data {} }) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method, data, header: { Content-Type: application/json, // 如果有登录态自动带上 token Authorization: wx.getStorageSync(token) || }, success: (res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data) } else if (res.statusCode 401) { wx.navigateTo({ url: /pages/login/login }) } else { wx.showToast({ title: res.data.message || 请求失败, icon: none }) reject(res.data) } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) } export default request逻辑说明统一在 Promise 的 resolve 里只返回业务数据 data页面里拿到的直接就是用了不用反复解包 result 里的 data。code 0 是我在 Result 里定义的业务成功码接口返回非 0 时统一弹 toast 提示。401 的处理很关键——用户登录态过期后任何接口都会返回 401统一跳登录页而不是在十几个页面里各写一套。参数说明BASE_URL 在开发阶段可以指向本地电脑的局域网 IP比如 http://192.168.1.100:8080/api但真机预览时小程序必须用「合法域名」。你可以在开发者工具里勾选「不校验合法域名」来本地调试这个坑后面专门说。4.3 首页服务列表一页看懂前后端链路首页从后端拉服务项目列表并渲染成卡片按类别展示。这一页麻雀虽小五脏俱全覆盖了「页面加载 → 请求后端 → 渲染数据」的完整链路。template view classservice-list view v-foritem in serviceList :keyitem.id classcard clickgoDetail(item.id) image :srcitem.coverUrl classcover modeaspectFill / view classinfo text classname{{ item.name }}/text text classprice¥ {{ item.price }}/{{ item.unit }}/text /view /view /view /template script import request from ../../utils/request export default { data() { return { serviceList: [] } }, onLoad() { this.fetchServiceList() }, methods: { async fetchServiceList() { this.serviceList await request({ url: /service/item/list, method: GET }) }, goDetail(id) { uni.navigateTo({ url: /pages/service-detail/detail?id${id} }) } } } /script逻辑说明onLoad 在页面初始化时只调用一次适合做首屏数据拉取。模板里 v-for 渲染列表点击卡片带 id 跳详情页下一页通过 onLoad 的 query 参数拿到 id 再请求详情接口。注意这里的/service/item/list后端接口返回的字段是驼峰格式 coverUrl、unit因为我在后端做了全局 Jackson 配置把下划线转驼峰前端代码不用写一堆字段映射。参数说明modeaspectFill 让图片按比例裁剪填充避免家政服务封面图被拉伸变形。price 保留两位小数是后端格式化的前端不要用 toFixed 再去截断容易在不同小程序基础库上显示不一致。4.4 预约下单页把用户选择组装成下单参数下单页是参数密集的页面服务项目、预约日期、时间段、地址每一项都要做校验提交时一键下单。template view classcreate-order picker modedate changeonDateChange view classfield预约日期{{ serviceDate || 请选择 }}/view /picker picker :rangetimeSlots changeonTimeChange view classfield服务时间{{ serviceTimeSlot || 请选择 }}/view /picker input v-modeladdress placeholder请输入服务地址 / button clicksubmitOrder立即支付/button /view /template script export default { data() { return { serviceItemId: , serviceDate: , serviceTimeSlot: , address: , timeSlots: [09:00-11:00, 14:00-16:00, 19:00-21:00] } }, onLoad(query) { this.serviceItemId query.id }, methods: { async submitOrder() { if (!this.serviceDate || !this.serviceTimeSlot || !this.address.trim()) { wx.showToast({ title: 请完善预约信息, icon: none }) return } const orderNo await request({ url: /order/create, method: POST, data: { serviceItemId: this.serviceItemId, serviceDate: this.serviceDate, serviceTimeSlot: this.serviceTimeSlot, address: this.address.trim() } }) wx.navigateTo({ url: /pages/pay/pay?orderNo${orderNo} }) } } } /script逻辑说明submitOrder 先做前端基础校验再调后端下单接口成功后带着 orderNo 跳转到支付页。支付页拿到 orderNo 后发起微信支付在模拟支付阶段支付页可以直接放一个「模拟支付成功」按钮调用后端测试接口触发回调把订单置为已支付。这套设计的好处是支付环节和后端回调解耦不会因为商户号没申请下来就阻塞整体开发。参数说明timeSlots 目前是写死在前端的。如果以后要做后台可配置的服务时间这个数组应该从后端拉取并且下单接口需要校验传入的时间段在允许的模板内。毕业设计阶段写死没问题答辩时你说得出「这里预留了动态配置扩展点」就够了。5. 避坑与排查5 个最常见的翻车点与修复路径5.1 本地一切正常真机白屏request 合法域名是个黑匣子现象开发者工具里预览服务列表、下单都很流畅一用手机真机预览页面转圈后直接空白。控制台报的就是 request 的 url 不在合法域名列表中。原因小程序真机环境强制校验 request 域名必须是 HTTPS且要在小程序管理后台配置。开发者工具默认勾选了「不校验合法域名」所以本地没问题真机上直接拦截。解决开发调试阶段不要慌在开发者工具「详情 → 本地设置 → 勾选不校验合法域名」先用局域网 IP 调通功能和 UI。要上真机完整演示就得准备一个已备案的 HTTPS 域名后端用 Nginx 反向代理到 SpringBoot 端口然后在微信公众平台「开发管理 → 开发设置 → 服务器域名」里加 request 合法域名。这是微信的规则不是你的代码问题别在代码里找原因。5.2 小程序包体积超限主包超过 2M构建直接失败现象前端框架构建产物超过 2M上传代码时提示主包大小超出限制真机加载异常缓慢。原因我把服务列表的封面图全部放进了小程序静态资源目录又引入了整个 UI 组件库构建产物直接膨胀。图片资源才是元凶代码本身通常没多少。解决第一步将项目按 TabBar 页面和分包页面拆成 subpackage下单页、订单详情这些非首屏页面放到分包里主包只留首页和公共组件。第二步封面图全部改成网络图片 URL由后端接口返回小程序端只在云存储或服务器上保存压缩后的 80KB 以内缩略图。第三步UI 组件库按需引入别一键引入全部组件。5.3 支付回调重复通知导致状态错乱现象用户支付成功后服务端收到两条回调通知订单最终状态停留在「待派单」而不是「已支付」后台查流水才发现是回调处理里覆盖了状态。原因微信支付回调在网络抖动时会重发通知。我在回调处理里直接 update 订单状态没有校验「当前状态是否为待支付」导致第二次回调把已经派单的订单状态又覆盖了。解决回调更新永远带上状态前置条件用乐观锁思路。比如wrapper.eq(Orders::getOrderNo, orderNo) .eq(Orders::getPayStatus, 0) .in(Orders::getOrderStatus, 0, 1)同时处理完回调后立即返回 success微信收到 success 后不会再重发。如果业务处理失败返回 failure微信会按策略重试你的接口必须保证幂等这个逻辑怎么强调都不过分。5.4 模拟器显示正常真机时间差 8 小时现象订单列表页显示的下单时间比实际时间晚了 8 个小时但开发者工具里完全正常。原因后端从 MySQL 查出 DATETIME 后Jackson 序列化默认转成 UTC 时间字符串小程序端 new Date() 解析时按本地时区转换真机时区设置和模拟器不一致就出现了偏差。解决前后端统一用时间戳传输。后端在 Jackson 配置里把 LocalDateTime 序列化成 epoch 毫秒值前端直接 new Date(timestamp) 渲染完全绕开时区问题。数据库里存什么格式不重要传输层格式统一才是关键。5.5 线上接口突然全部超时数据库连接池被慢查询拖垮现象家政公司实际使用一周后某个下午所有接口集体卡死重启 SpringBoot 后恢复但没过多久又超时。原因订单列表页做了一个多表 join 查询用户反复刷新时每次请求都要扫描全表MySQL 连接数被慢查询占满后续请求全部排队。解决第一SQL 里去掉多余的 join改为两次简单查询并在内存中组装数据第二在常用查询字段上建索引比如 orders 表的 user_id 和 order_status 联合索引、service_item 表的 status 索引第三给 HikariCP 连接池设置最大连接数和连接超时时间防止极端情况下连接被耗尽。排查时先在 MySQL 执行 SHOW PROCESSLIST看到大量长时间未完成的 select 基本就能锁定是慢查询问题。6. 进阶让订单从「查得到」变成「推得动」到这里核心链路已经通了但用户还是会问「阿姨什么时候到」「订单做完没有」你再做个页面让他来查是一种方案另一种方案是让系统把状态主动推给他。小程序有个能力叫订阅消息用户授权后后端可以在订单状态变化时通过微信服务器给用户发一条服务通知。订阅消息有个前提用户必须主动点击授权并且是一次性的——每发一条消息就要授权一次。最简单的做法是在用户下单成功后立刻弹出一个授权框让他勾选「同意接收服务进度通知」把订阅消息的调用放在 wx.requestSubscribeMessage 里// 下单成功后触发 wx.requestSubscribeMessage({ tmplIds: [TEMPLATE_ID], success(res) { if (res[TEMPLATE_ID] accept) { wx.showToast({ title: 已开启通知, icon: none }) } } })后端收到这个授权结果后不需要同消息内容存库。等阿姨确认接单、开始服务、完成服务时调用微信的统一消息发送接口把状态变化推给用户。模板内容建议配置成「订单号 当前状态 温馨提示」三段比如「您预约的日常保洁服务已完成感谢使用」——用户看一条消息就懂进度不用再进小程序翻订单详情。我在做模拟项目X时把订阅消息只接在「服务完成」这一个节点上没有做全部状态推送。原因很简单每个用户一个订单只有一次授权如果中间状态推太多用户会关掉通知最后一条关键完成通知反而推不出去。留最关键的一条转化效果最好。再往后可做的扩展就多了订单完成后让用户对阿姨做评价评价数据回流到 employee 表作为派单参考给管理端做一个按日维度的营收统计接口前端用图表展示阿姨端小程序单独拆出来做接单和开工打卡。这些方向都建立在核心链路稳定之上。这个项目做完后我最大的教训是毕业设计或小公司信息化功能堆得越多死得越快。用心把下单、派单、完成这条主链路打磨顺把状态设计得经得起推敲比做十个半成品功能有用得多。希望帮到你。本文还有配套的精品资源点击获取