
简介基于JavaSSMMySQL微信小程序的家政项目小程序是一套面向高校毕业设计、课程设计与期末大作业的完整源码包前后端代码齐全且已通过严格调试适合计算机相关专业学生直接参考或二次开发。资源共1213个文件涵盖Java后端源码、Vue管理后台、微信小程序前端、SQL数据库脚本、毕业论文及工具脚本等压缩包约16.41MB核心实现包括SSM框架整合、MySQL数据存储和微信小程序用户端操作。已有84人学习下载。内容包含系统源码、数据库脚本、毕业论文文档及项目配置文件目录结构清晰功能覆盖用户管理、家政服务、订单处理等模块界面美观且管理便捷。通过学习这份资料读者可以完整理解基于SSM与微信小程序的家政服务系统从需求分析、数据库设计到前后端联调的实现过程并可直接运行体验适合作为毕业设计或课程项目的可靠参考模板。1. 从一个毕业设计压缩包说起SSM 家政小程序到底在做什么很多人以为这是一个能跑的分页 CRUD 系统真正打开源码包才发现难点不在服务列表而在“用户下单、阿姨接单、管理员调度”三个角色的状态同步。我评审代码时见过订单状态散落在多个 if 里的写法数据库根本看不住并发。下面直接从数据库设计、后端 SSM 接口、小程序端下单闭环、部署排错四个方向把项目从 0 到 1 跑起来顺便理清哪些参数值得调。适合正在做毕业设计的学生也适合刚接触 SSM 全流程的工程师。2. SSM 后端与 MySQL 数据模型先理解模块边界再写代码2.1 三个角色和状态流家政小程序和商城最大的不同家政小程序不是对称的买卖双方闭环而是“用户、服务人员、管理员”三方协作。我一般先把角色对应的操作列出来用户端登录、浏览服务、下单、支付、评价。服务人员端查看待接单、接单、开始服务、完成服务、查看收入。管理端维护服务分类、审核服务人员、调度订单、查看统计报表。这个拆分直接影响表结构。用户和服务人员可以共用同一张user表用role字段区分而不是设计成user和worker两张表。因为两边都有微信 openid、昵称、手机号公共字段放在同一张表里后续做“用户申请成为阿姨”只需要更新role再往扩展表插一条记录。独立职业属性比如服务区域、服务技能、身份证号再放到worker_profile表避免user表字段越来越多。订单状态不要按页面按钮去设计要从业务约束出发。我常用这组状态状态值状态含义触发动作后续状态0待支付用户提交订单1 / 51待接单用户支付完成2 / 52已接单阿姨接单33服务中阿姨开始服务44已完成用户/管理员确认完成无5已取消用户取消或超时关单无状态用tinyint而不是varchar因为代码里status 2比ACCEPTED.equals(status)直观整型字段做索引也更省空间。状态之间的合法跳转要在 Service 层校验不能只靠前端隐藏按钮否则绕过小程序直接调接口就能把订单改成任意状态。2.2 MySQL 表结构订单表字段这样设计才不会返工家政项目的核心表是user、service_item、service_order三张。下面是我会直接用的一段 SQLCREATE TABLE user ( id int unsigned NOT NULL AUTO_INCREMENT, openid varchar(128) NOT NULL, nickname varchar(64) DEFAULT , phone varchar(20) DEFAULT , role tinyint NOT NULL DEFAULT 0 COMMENT 0-用户 1-服务人员, avatar_url varchar(500) DEFAULT , status tinyint NOT NULL DEFAULT 1 COMMENT 1-正常 0-禁用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户/服务人员表; CREATE TABLE service_item ( id int unsigned NOT NULL AUTO_INCREMENT, category_id int unsigned NOT NULL, name varchar(100) NOT NULL, price decimal(10,2) NOT NULL, unit varchar(10) NOT NULL DEFAULT 次, cover_url varchar(500) DEFAULT , status tinyint NOT NULL DEFAULT 1 COMMENT 1-上架 0-下架, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT服务项目表; CREATE TABLE service_order ( id bigint unsigned NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, user_id int unsigned NOT NULL, worker_id int unsigned DEFAULT NULL, item_id int unsigned NOT NULL, appointment_time datetime NOT NULL, address varchar(255) NOT NULL, amount decimal(10,2) NOT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付 1待接单 2已接单 3服务中 4已完成 5已取消, remark varchar(500) DEFAULT , create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_worker_status (worker_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;几个容易被忽略的点order_no一定要有唯一索引并且不要把自增id暴露给前端当业务单号。我习惯用yyyyMMddHHmmss 4 位随机数生成插入时如果撞了唯一索引再重试一次。update_time使用ON UPDATE CURRENT_TIMESTAMP后订单每次状态更新都会自动变更不用手动维护。amount用decimal(10,2)不要用float或double家政服务按小时计费时可能出现小数浮点累计会丢失精度。连接字符集用utf8mb4否则用户昵称里的 emoji 在保存时会报Incorrect string value。这是老生常谈但不少 SSM 项目仍然死在这一步。2.3 MyBatis 动态 SQL一个列表接口应对三种角色家政项目里最频繁的接口是按状态查订单用户查自己的订单阿姨查待接单管理员查全部订单。如果每个查询都复制一份 Mapper 代码后续加字段会改到怀疑人生。MyBatis 的动态 SQL 可以把这些情况合并成一个方法select idselectOrders resultTypecom.example.entity.Order SELECT id, order_no, user_id, worker_id, item_id, appointment_time, address, amount, status, create_time FROM service_order where if testuserId ! null AND user_id #{userId} /if if testworkerId ! null AND worker_id #{workerId} /if if teststatus ! null AND status #{status} /if if teststartTime ! null AND appointment_time gt; #{startTime} /if /where ORDER BY appointment_time DESC LIMIT #{offset}, #{pageSize} /select核心是where标签它会自动去掉条件组里第一个多余的AND。如果所有条件都为空生成出来的 SQL 不会出现WHERE ORDER BY这种语法错误。gt;是对的 XML 转义很多新手在 Mapper XML 里直接写启动时不会报错执行条件查询时才报语法错误。分页直接LIMIT #{offset}, #{pageSize}数据量到几十万条再考虑 PageHelper毕业设计阶段手写比插件更容易排查。提示Mapper 接口方法名和 XML 里的 id 必须完全一致namespace 要写接口的全类名否则会报Invalid bound statement (not found)。3. 微信小程序端从登录到下单的闭环实现3.1 原生小程序目录和 request 封装如果你用 uni-app 写目录结构会略有不同但这里以原生微信小程序为例依赖最少跑起来最稳。常见目录如下project/ ├── pages/ │ ├── index/ 首页服务列表 │ ├── order/ 下单确认、订单列表 │ ├── user/ 个人中心 │ └── worker/ 阿姨接单工作台 ├── utils/ │ └── request.js wx.request 统一封装 └── app.json小程序端最重要的基础设施是统一请求封装。直接使用wx.request会导致每个页面都写一遍成功回调、401 处理和报错提示。我习惯先封装一个utils/request.jsconst BASE_URL http://192.168.1.100:8080/api; function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method, data, header: { Content-Type: application/json, token: wx.getStorageSync(token) || }, success(res) { if (res.statusCode 200 res.data.code 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };BASE_URL在真机调试时不能写127.0.0.1要写成电脑的局域网 IP否则手机上的小程序根本连不上后端。页面里用wx.getStorageSync(token)从缓存取登录态后端每一个需要用户身份的接口都从 header 里拿 token。小程序端最常被改的是启动背景和顶部导航栏在app.json的window节点配置navigationBarBackgroundColor和navigationBarTitleText即可不需要额外写页面。3.2 微信登录wx.login 拿到 code 之后做什么登录是家政项目里最容易卡住的环节。小程序端需要先调用wx.login()获取临时 code再把 code 传给后端后端拿着 code、appid、secret 去微信接口换 openid。大致的 Java 写法是RestController RequestMapping(/api/auth) public class AuthController { PostMapping(/login) public Result login(RequestBody MapString, String body) { String code body.get(code); String url https://api.weixin.qq.com/sns/jscode2session ?appid appid secret secret js_code code grant_typeauthorization_code; String response restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(response); String openid json.getString(openid); User user userMapper.findByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setRole(0); userMapper.insert(user); } String token UUID.randomUUID().toString().replace(-, ); userMapper.updateToken(user.getId(), token); return Result.ok(token); } }这段代码里最关键的是code只能换一次有效期很短后端拿到后必须马上请求微信接口。如果毕业设计没有正式的小程序 AppSecret常见做法是后端配置一个测试开关固定返回一个模拟 openid让前后端联调不被微信平台卡住。把token存到user表并返回给前端后后续接口都通过 header 里的token识别用户不要再依赖前端传userId否则任何人填一个数字都能查别人的订单。3.3 下单闭环后端算价前端只提交表单真实微信支付需要商户号、支付证书和回调地址很多毕业设计卡在这里。我一般建议把流程做成“提交订单 模拟支付”用户点“立即预约”后端创建订单并让金额保持待支付用户再点一次“确认支付”后端把状态从 0 改成 1。这样业务闭环完整又不会在支付上耗掉一周。创建订单的 Service 核心代码Service public class OrderService { Transactional public Order createOrder(OrderForm form) { ServiceItem item serviceItemMapper.selectByPrimaryKey(form.getItemId()); if (item null || item.getStatus() ! 1) { throw new BusinessException(服务项目不存在或已下架); } Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(form.getUserId()); order.setItemId(item.getId()); order.setAppointmentTime(form.getAppointmentTime()); order.setAddress(form.getAddress()); order.setAmount(item.getPrice()); order.setStatus(0); orderMapper.insert(order); return order; } }注意amount必须从service_item表读取绝对不能信任前端传过来的金额。前端只传itemId、appointmentTime、address。支付接口也是一样后端根据订单 id 重新计算金额再把状态流转到 1。前端 JS 调用就很简单了const data { itemId: this.data.itemId, appointmentTime: this.data.appointmentTime, address: this.data.address }; const order await request(/order/create, POST, data);下单后跳到订单详情页用户点击“模拟支付”再调/order/pay。整个状态机的跳转都在 Service 层做不满足当前状态的请求直接抛业务异常这是家政项目里最值得花时间写清楚的地方。4. 家政订单的核心流程参数接单并发、超时关单与事务配置4.1 阿姨接单的乐观锁一条 UPDATE 解决重复接单两个阿姨同时看到一笔待接订单都点击了接单。如果代码先select再update两个请求都会读到status1最后订单被分配两次或者后一次更新覆盖前一次。正确做法是直接使用条件更新把状态字段当作版本号UPDATE service_order SET worker_id #{workerId}, status 2, update_time NOW() WHERE id #{orderId} AND status 1MyBatis 的更新方法返回int如果影响行数为 1说明接单成功如果为 0说明订单已经被别人抢走。Service 层这样判断public boolean acceptOrder(Long orderId, Long workerId) { int rows orderMapper.acceptOrder(orderId, workerId); if (rows 1) { return true; } throw new BusinessException(手慢了订单已被接走); }这里的关键是WHERE status 1。如果写成WHERE status ! 2 AND worker_id IS NULL一旦系统里增加“待验收”“已取消”等状态条件就不可控了。让状态字段自己当版本号比额外加version列更直观。如果还想防止同一个阿姨对同一订单重复点击可以在order_operation_log表里加一个业务幂等键或者在前端接单按钮加上disabled标记。4.2 超时未支付自动关单Spring Task 定时扫描用户创建订单后直接退出小程序订单会一直停留在“待支付”。这会导致阿姨端看到一堆无效订单也会让统计数量失真。常见做法是定时扫描超过 15 分钟的待支付订单批量关单Component public class OrderTimeoutJob { Scheduled(cron 0 */5 * * * ?) public void closeExpiredOrders() { Date expireTime new Date(System.currentTimeMillis() - 15 * 60 * 1000); ListOrder orders orderMapper.selectExpired(expireTime); for (Order order : orders) { try { orderMapper.cancelByStatus(order.getId(), 0); } catch (Exception e) { log.error(关闭过期订单失败, orderId{}, order.getId(), e); } } } }对应的 SQL 是SELECT id FROM service_order WHERE status 0 AND create_time #{time} LIMIT 200Spring 的Scheduledcron 表达式固定是 6 位秒、分、时、日、月、周。0 */5 * * * ?表示从第 0 秒开始每 5 分钟执行一次。启动类上需要加EnableScheduling否则定时任务不会生效。LIMIT 200是为了防止历史脏数据积压时单次事务处理太多记录导致锁等待时间过长。跑完一批还有剩余下一轮任务会继续处理。提示定时任务只适合单机部署的毕业设计。如果以后上了多台服务器同一时刻多实例会重复关单需要加分布式锁或用数据库唯一任务表做幂等。4.3 事务回滚和连接池参数面试常问的坑都在这里在 SSM 项目里Transactional默认只在抛出RuntimeException时回滚。如果你的自定义 Service 方法里捕获了异常或者抛出SQLException事务不会按预期回滚。为了避免这种问题统一写法是Transactional(rollbackFor Exception.class) public void payOrder(Long orderId) { Order order orderMapper.selectByPrimaryKey(orderId); if (order.getStatus() ! 0) { throw new BusinessException(订单状态不正确); } orderMapper.updateStatus(orderId, 1); }连接池我用得最多的是 Druid。配置不是越多越好下面这套在课程设计和中小项目里都能直接用参数推荐值说明initialSize5启动时创建的连接数minIdle5最小空闲连接数maxActive20最大活跃连接数maxWait60000获取连接的最大等待毫秒数validationQuerySELECT 1探活 SQLMySQL 一般用这个maxActive不是越大越好。数据库默认最大连接数有限连接池设置成 100QPS 低时反而是浪费。如果项目使用 MySQL 8驱动类名要改成com.mysql.cj.jdbc.Driver连接串后面加上useSSLfalseserverTimezoneAsia/Shanghai否则会报时区错误。5. 部署、验证与论文答辩时的加分项5.1 本地跑通的最小步骤源码包里一般有数据库脚本、后端 Maven 工程和小程序前端。先建库再启后端不要一上来就打开微信开发者工具。按这个顺序走建库mysql -uroot -p db_home.sql执行完用show tables;确认表名。修改后端配置文件里的数据库账号、密码、端口。启动后端IDEA 里运行mvn spring-boot:run或打成 war 放到 Tomcat。用微信开发者工具导入小程序目录修改utils/request.js里的BASE_URL。在开发者工具“详情-本地设置”里勾选“不校验合法域名”否则请求直接失败。常见命令和验证结果步骤命令/操作验证结果建库mysql -uroot -p sql/init.sqlshow tables 能看到三张核心表后端启动mvn spring-boot:run控制台出现 Started接口冒烟curl http://localhost:8080/api/category/list返回 JSON 数组5.2 接口自测与常见报错排查后端配好后先用 curl 代替小程序接口排查问题curl -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {code:mock-code}如果返回报错优先看控制台异常。Invalid bound statement (not found)说明 Mapper 接口的全类名和 XML 的 namespace 不一致SQLSyntaxErrorException说明表名字段名和 SQL 文件对不上优先检查create_time、order_no这类通用字段。页面请求 404 时先确认RequestMapping的路径有没有拼错再检查 Spring MVC 是否扫描到了 Controller 包。5.3 验证并发抢单这一条比任何话术都有说服力答辩时与其背框架不如演示一条并发验证记录。造一条status 1的测试订单再开两个终端同时执行接单请求然后查SELECT id, worker_id, status FROM service_order WHERE id 1;结果里只会有一个worker_id另一个请求返回业务错误码。这个操作能证明你的订单状态机真的写在数据库里而不是靠前端按钮挡住。把这条日志截图放进论文比写一句“系统并发性能良好”有效得多。本文还有配套的精品资源点击获取