☰
家政项目源码实战:订单流转、状态机与派单分配的核心设计
2026/10/13 4:14:46 网站建设 项目流程

简介:面向家政服务行业信息化开发与学习者的完整Web项目源码,整合前台用户交互与后台业务管理,适合具备Java Web基础、希望系统理解SSH框架整合流程和MVC分层结构的开发人员。资源共571个文件,约28.74MB,包含Java源码、class编译文件、JSP页面、XML/Spring配置文件、数据库相关脚本及CSS/JS/图片等静态资源,覆盖前后端主要开发环节。已有2174人学习下载。项目基于Struts、Spring、Hibernate三大框架构建,从前台页面交互、Ajax局部刷新到后台Action控制器、Service业务层与Dao数据访问层均有完整实现;同时提供用户注册、服务预约、订单跟踪、家政人员管理等典型模块,并配套jiazhengdb数据库设计,可支撑二次开发、毕业设计参考或SSH框架实战练习。

1. 家政项目的源码:先别急着写代码,把订单这张流转表想清楚

“家政项目的源码”这个搜索词背后,绝大多数人不是来读源码赏析的,而是手里有个真实的业务要落地——可能是想自建一个本地家政平台,可能是接了一个家政公司的数字化订单,也可能是想评估一套现成系统能不能二次开发。家政项目的源码,核心不是那些花哨的派单算法,而是一张订单从用户手机端发起到服务人员手机端接单、上门、结算、评价的完整流转链路。我见过不少团队在这个项目上翻车,翻车点几乎都集中在数据结构设计和对业务状态的表达上,而不是某个功能写不出来。这篇内容就把家政项目源码里最该重视的模块拆开讲,从选型到表结构、从接口实现到部署排查,给你一条能照着落地的路径。

2. 家政系统源码的技术选型:先定数据模型,再谈功能实现

2.1 技术栈选型的三个硬约束:成本、运维、二次开发

家政项目在大多数情况下不是一个高并发的互联网应用,而是一个业务逻辑复杂的行业管理系统。选技术栈时我建议先看三个硬约束:部署成本、维护门槛、二次开发的难易度。

常见的做法有几种:使用 Java Spring Boot 构建后端,适合有专职 Java 工程师的团队,生态成熟,但一台 2C4G 的云服务器跑起来会比较吃力,而且开发节奏偏重;使用 PHP 系的 ThinkPHP 或 Laravel,部署成本最低,虚拟主机都能跑,但后续扩展成多端或者接入复杂推送时,代码会越来越难维护;如果团队以前端为主,用 Node.js + Express 或 NestJS 写接口会非常顺畅。我一般会建议以 Node.js + MySQL 起步,理由是家政系统的接口逻辑大多是 CRUD 加状态流转,Node 的事件模型处理这种 IO 密集场景足够,而且前后端同构能降低团队沟通成本。

选择 MySQL 而不是 NoSQL 的原因更直接:订单、服务人员、用户、结算这些数据之间有强关联关系,事务是刚需。别被“性能不够用缓存顶”的说法带偏,家政项目的性能瓶颈从来不在数据库,而在业务流程的混乱。

2.2 订单、服务人员、商品价目表:三张核心表的结构设计

家政系统最核心的三张数据表是服务人员表、价目表、订单表。服务人员表不能只存姓名和手机号,还必须有服务区域、服务项目、接单状态、评分、定位信息,因为派单逻辑全部依赖这些字段。

CREATE TABLE `service_provider` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `name` varchar(50) NOT NULL COMMENT '服务人员姓名', `phone` varchar(20) NOT NULL COMMENT '联系电话', `service_area` varchar(255) NOT NULL COMMENT '服务区域编码,多个用逗号分隔', `service_project` varchar(255) NOT NULL COMMENT '服务项目,多个用逗号分隔', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '接单状态:0休息中 1空闲 2服务中', `lat` decimal(10,7) DEFAULT NULL COMMENT '当前纬度', `lng` decimal(10,7) DEFAULT NULL COMMENT '当前经度', `rating` decimal(2,1) NOT NULL DEFAULT '5.0' COMMENT '综合评分', `order_count` int(11) NOT NULL DEFAULT '0' COMMENT '累计接单数', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_area_status` (`service_area`, `status`), KEY `idx_latlng` (`lat`, `lng`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务人员表';

这张表里最关键的细节是service_area和service_project用了逗号分隔的字符串。这是妥协方案,不是最优方案——如果是新项目,我更推荐单独建关联表。

CREATE TABLE `provider_project` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `provider_id` bigint(20) NOT NULL COMMENT '服务人员ID', `project_id` bigint(20) NOT NULL COMMENT '服务项目ID', PRIMARY KEY (`id`), UNIQUE KEY `uk_provider_project` (`provider_id`, `project_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务人员-服务项目关联表';

这样做的好处是,后续要统计“哪个服务项目的人员缺口最大”时,一条GROUP BY就能出结果,不用在字符串里做FIND_IN_SET的操作。关联表的额外成本是一条 JOIN,但对于家政系统这种量级来说完全可以忽略。

订单表的设计要点是状态字段。家政订单的状态比一般电商订单更复杂,至少要覆盖:待支付、已支付待分配、已派单待接单、服务中、待验收、已完成、已取消、售后中。这些状态建议用tinyint整数存储而不是字符串,因为代码里要做状态机校验,整数比较比字符串比较误判概率低。

2.3 用 SQL 把“一键派单”写出来:调度分配的数据基础

很多人以为派单是个算法问题,要有智能匹配、要有路径规划。现实是,绝大多数家政公司的派单规则就三条:服务项目匹配、服务区域匹配、当前空闲。把这三条翻译成 SQL,就是派单的核心逻辑。

SELECT p.id, p.name, p.phone, p.rating FROM service_provider p INNER JOIN provider_project pp ON pp.provider_id = p.id WHERE pp.project_id = 8 AND p.`status` = 1 AND FIND_IN_SET('310115', p.service_area) ORDER BY p.rating DESC, p.order_count ASC LIMIT 10;

这条 SQL 先通过关联表筛选能做这个服务项目的人,再限制当前空闲,然后用区域字段匹配。排序用rating DESC加order_count ASC是一种朴素的“优质但接单少优先”策略。实际业务里,派单距离是一个重要参考,可以加一个ST_Distance函数计算当前位置到订单地址的距离,但只有在用户 App 能拿到经纬度时才有意义。如果用户端没有采集定位,放弃距离排序是明智的,不要为了显得专业去写一个永远取不到值的 SQL。

注意:FIND_IN_SET在数据量过万时会明显变慢。如果服务人员表超过一万行,建议把区域拆成子表或者用 JSON 字段配合虚拟列索引,否则每次派单全表扫描得不偿失。

3. 从零搭建家政项目源码:最小可运行版本这样落地

3.1 项目目录结构:哪些代码文件是刚需

按我习惯的工程实践,家政项目前后端可以分离,也可以不分离。追求最快跑通的话,一个 Node.js 后端加一个 Vue 后台管理端最省事。目录结构上,我一般会按业务模块分目录,而不是按技术类型分。

home-service/ ├── app.js # 应用入口,初始化 Express ├── config/ │ └── db.js # 数据库连接配置 ├── routes/ │ ├── order.js # 订单相关:下单、取消、查询 │ ├── provider.js # 服务人员相关:注册、状态切换 │ ├── user.js # 用户相关:微信登录、地址管理 │ └── admin.js # 后台管理:派单、结算 ├── controllers/ │ ├── orderController.js │ ├── providerController.js │ └── adminController.js ├── services/ │ ├── dispatchService.js # 派单服务:候选查询、分配、通知 │ └── settleService.js # 结算服务:订单完成后自动结算 ├── models/ │ ├── order.js │ ├── provider.js │ └── user.js └── utils/ ├── response.js # 统一响应格式 └── auth.js # 登录态校验中间件

这个结构里最容易出问题的是 services 层。很多新手把逻辑全写在 controllers 里,最终结果是每个接口三五百行,代码没法维护。派单、结算这类多步骤业务逻辑必须抽到 services 层,这是家政项目源码里最值得坚持的代码规范。

3.2 核心接口实现:下单、派单、接单的代码逻辑

下单接口的代码本身不复杂,复杂的是事务边界。一次下单涉及:生成订单记录、锁定服务项目、初始化状态。这三个操作必须在一个事务里,否则会出现订单表里多了一条状态异常记录的情况。

// routes/order.js 中的下单接口 router.post('/create', authMiddleware, async (req, res) => { const { project_id, service_time, address, remark } = req.body; const user_id = req.userId; // 事务开始:用 connection 而不是 pool 直接操作 const conn = await db.getConnection(); try { await conn.beginTransaction(); const [projectRows] = await conn.query( 'SELECT id, price_base FROM service_project WHERE id = ? AND status = 1', [project_id] ); if (projectRows.length === 0) { throw new Error('PROJECT_NOT_AVAILABLE'); } const orderNo = 'HG' + Date.now() + Math.random().toString(10).slice(2, 8); const [orderResult] = await conn.query( `INSERT INTO orders (order_no, user_id, project_id, service_time, address, status, price_base, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 0, ?, NOW(), NOW())`, [orderNo, user_id, project_id, service_time, address, projectRows[0].price_base] ); await conn.commit(); // 事务提交后再返回成功,避免客户端拿到订单号但事务未落库 res.json({ code: 0, data: { order_id: orderResult.insertId, order_no: orderNo } }); } catch (err) { await conn.rollback(); res.json({ code: 1, msg: '下单失败,请稍后重试' }); } finally { conn.release(); } });

这段代码的关键点有三个:下单前先校验服务项目是上架状态,防止关停下单;订单号用时间戳加随机数生成,不用自增主键直接暴露给用户,避免被遍历;事务结束后才响应,同时捕获错误回滚并给用户一个通用提示。

接单接口的逻辑是状态机校验:只有状态为“已派单待接单”的订单才能接单,且接单人必须是当前指派的服务人员。这个校验必须放在 UPDATE 语句里,同时用受影响行数判断是否成功。

// 派单后服务人员接单 router.post('/accept', authMiddleware, async (req, res) => { const { order_id } = req.body; const providerId = req.providerId; const [result] = await db.query( `UPDATE orders SET status = 2, accept_provider_id = ?, accept_time = NOW(), updated_at = NOW() WHERE id = ? AND status = 1 AND accept_provider_id = ?`, [providerId, order_id, providerId] ); if (result.affectedRows === 0) { // 这里要区分:订单已被别人接走?还是根本没派给这个人? return res.json({ code: 1, msg: '接单失败,请刷新订单列表确认状态' }); } res.json({ code: 0, msg: '接单成功' }); });

用 UPDATE 的条件判断代替先 SELECT 再 UPDATE 是很关键的做法。因为先查询再更新会有时间窗口,容易被并发请求穿透,老手有时候赶时间也会写这种有隐患的代码,务必留意。这里用affectedRows === 0感知失败,但失败原因需要业务层面排查——这是后话,在排查章节会展开。

3.3 后台管理端:服务人员与订单列表的联动实现

后台管理端常见的诉求是:订单列表、派单操作、服务人员管理。派单操作在后台需要展示候选服务人员,这一步建议前端只负责展示,候选列表的生成逻辑全部放后端接口。

// adminController.js 中获取候选派单人员 async function getDispatchCandidates(orderId) { const [orderRows] = await db.query( 'SELECT project_id, area_code FROM orders WHERE id = ? AND status = 1', [orderId] ); if (orderRows.length === 0) return []; const candidates = await dispatchService.findCandidates( orderRows[0].project_id, orderRows[0].area_code ); // 可以在这里附加一个字段:预计距离和预计上门时间(如果有定位数据) return candidates.map(c => ({ provider_id: c.id, name: c.name, phone: c.phone, rating: c.rating, order_count: c.order_count })); }

后台派单提交时需要注意幂等性:同一个订单,两个管理员同时派单,必须只成功一次。做法是在派单接口的 UPDATE 语句里带上status = 1条件(此时订单是待分配状态),只有当订单还是待分配状态时才能更新为已派单。这一步和第二小节的接单更新逻辑同理,本质区别是这里更新的字段包括assign_provider_id和status。

4. 源码放进业务:从下单到结算的完整联调链路

4.1 用 Postman 串联核心接口:一条完整的请求链怎么设计

代码写完不等于项目跑通。家政项目的接口之间是强串联关系,建议拿到源码后第一件事不是看界面,而是把接口用 Postman 按业务顺序跑一遍,建立一条完整的请求链。

下单 -> 管理员派单 -> 服务人员接单 -> 开始服务 -> 完成服务 -> 用户确认 -> 结算

对应的接口顺序是:

POST /api/order/create # 用户下单 POST /api/admin/dispatch # 后台派单 POST /api/provider/accept # 服务人员接单 POST /api/provider/start # 上门打卡 POST /api/provider/complete # 完成服务 POST /api/user/confirm # 用户确认 GET /api/admin/settle # 管理员结算(或系统自动结算)

使用 Postman 时,建议把 token 从登录接口的响应里提取出来,配置成环境变量,这样后边的每个接口都能自动携带。实际操作中我见过不少人手动复制 token,一旦 token 过期就开始迷茫,排查半天发现是认证过期了。环境变量在 Postman 右上角设置,这属于基本功,但确实是新手最容易绊倒的地方。

4.2 订单状态机的实现:核心代码与状态流转约束

状态机是家政项目里最容易写乱的地方。最常见的错误是每个接口都随便 SET status,结果一个订单可以被用户从“服务中”直接改成“已完成”,跳过所有中间步骤。正确的做法是在代码里建立一个状态流转表,约束每一步的合法转换。

// services/orderStateService.js const ORDER_STATUS = { UNPAID: 0, // 待支付 PENDING_ASSIGN: 1, // 已支付待分配 ASSIGNED: 2, // 已派单待接单 IN_PROGRESS: 3, // 服务中 PENDING_CONFIRM: 4, // 待用户确认 COMPLETED: 5, // 已完成 CANCELED: 6, // 已取消 AFTER_SALE: 7 // 售后中 }; const STATUS_TRANSITIONS = { [ORDER_STATUS.UNPAID]: [ORDER_STATUS.PENDING_ASSIGN, ORDER_STATUS.CANCELED], [ORDER_STATUS.PENDING_ASSIGN]: [ORDER_STATUS.ASSIGNED, ORDER_STATUS.CANCELED], [ORDER_STATUS.ASSIGNED]: [ORDER_STATUS.IN_PROGRESS, ORDER_STATUS.CANCELED], [ORDER_STATUS.IN_PROGRESS]: [ORDER_STATUS.PENDING_CONFIRM], [ORDER_STATUS.PENDING_CONFIRM]: [ORDER_STATUS.COMPLETED, ORDER_STATUS.AFTER_SALE], [ORDER_STATUS.COMPLETED]: [ORDER_STATUS.AFTER_SALE], [ORDER_STATUS.CANCELED]: [], [ORDER_STATUS.AFTER_SALE]: [ORDER_STATUS.COMPLETED, ORDER_STATUS.CANCELED] }; function canTransit(fromStatus, toStatus) { return STATUS_TRANSITIONS[fromStatus]?.includes(toStatus) || false; }

每次更新订单状态前,先调用canTransit校验合法路径,不合法直接返回错误。这套状态机代码放在 services 层,业务侧接口只负责调用,不要自己再写一套判断逻辑。状态机是订单模块的地基,地基歪了后面全是坑。

4.3 订单结算逻辑:优惠券、平台抽成、服务人员结算一次算清

订单完成后的结算,是家政项目里最容易在财务层面出问题的模块。常见的结算需求是:用户实付金额 = 服务费 + 额外费用 - 优惠券抵扣;服务人员收入 = 服务费 - 平台抽成 - 额外分成。这些计算必须在服务端做,不能信任前端传值。

// services/settleService.js async function settleOrder(orderId) { const [orderRows] = await db.query( `SELECT o.id, o.price_base, o.extra_fee, o.coupon_amount, p.rate AS platform_commission_rate FROM orders o JOIN service_project p ON o.project_id = p.id WHERE o.id = ? AND o.status = 5`, [orderId] ); if (orderRows.length === 0) throw new Error('订单不存在或未完成'); const order = orderRows[0]; const userPay = order.price_base + order.extra_fee - order.coupon_amount; const commission = Math.round((order.price_base - order.coupon_amount) * order.platform_commission_rate * 100) / 100; const providerIncome = order.price_base - order.coupon_amount - commission; // 资金变动必须走事务 const conn = await db.getConnection(); try { await conn.beginTransaction(); await conn.query( `UPDATE orders SET user_pay = ?, provider_income = ?, platform_commission = ?, status = 5, settle_time = NOW() WHERE id = ? AND status = 5`, [userPay, providerIncome, commission, orderId] ); await conn.query( `UPDATE service_provider SET balance = balance + ? WHERE id = ?`, [providerIncome, order.accept_provider_id] ); await conn.commit(); } catch (err) { await conn.rollback(); throw err; } }

这段代码有几个容易忽略的点。Math.round的使用是必须的,否则浮点数计算会得出类似 0.30000000000000004 的价格。更新订单表时再次带上status = 5,是为了防止结算被重复触发,这是幂等性的关键。服务人员余额更新放在同一事务里,避免订单显示已结算但服务人员账户没到账。

5. 家政项目源码的避坑与常见问题排查:五个让我加班的真实坑

5.1 现象:凌晨下的订单,早上查询时“消失”了

项目上线后某天凌晨,有用户下单成功但早上在订单列表里看不到记录。排查时发现订单确实在数据库里,但列表查询查不出来,原因是列表接口的查询条件里写了service_time >= NOW(),认为没到服务时间的订单不展示。凌晨下的单服务时间约在了上午,用户早上查看时NOW()已经超过了服务时间,被过滤掉了。

原因是对“列表查询”和“进行中订单查询”两个语义混用了一组过滤条件。解决方式是把列表接口改成纯状态过滤,不带时间范围;把“待服务”单独做成一个筛选 Tab,用service_time > NOW()作为条件。建议所有涉及时间过滤的代码都确认:过滤条件代表“业务含义”还是“查询者主观意图”,两码事。

5.2 现象:高峰期重复派单,一个订单被两个服务人员接走

用户下单后,后台管理员几乎同时点了两次派单按钮,结果两个服务人员都收到了接单通知,其中一人的接单直接失败,但另一个成功了。这个问题的本质是并发更新缺乏状态约束。在派单接口的 UPDATE 语句中必须包含status = 1(待分配)条件,确保只有处于待分配状态的订单才能被派单。第一次更新成功后状态变为已派单,第二次更新带着旧状态条件必然失败。

解决方式:检查派单和接单两个接口的 UPDATE 语句,确认状态条件都写进去了。同时建议在订单表增加一个version字段,每次更新加 1,条件里带上version = ?,这是更稳妥的乐观锁方案。

5.3 现象:派单按距离排序,但服务地址和实际上门地址差了两公里

用户下单时选的家在 A 区,实际上门服务在 B 区。排查发现派单距离计算依赖的是用户填写的地址文字进行地理编码的结果,不是用户当前位置。像是用户帮父母下单、或者自己下错地址的增量场景,文字的经纬度解析天然不可靠。

解决方式:下单时同时采集用户当前定位,订单里新增user_lat和user_lng字段,派单优先按这两个字段计算距离。如果用户定位采集失败,再用地址解析结果兜底。这个优先级顺序一定要写清楚,否则后续接手的人容易改回“看起来更合理”的旧逻辑。

5.4 现象:微信支付回调成功,但订单状态还是待支付

支付回调接口报 200 但订单没更新,排查发现回调处理函数里先更新了订单表,再更新支付流水表,但流水表插入时遇到重复数据直接抛异常,导致整个事务回滚。微信支付对同一笔订单会多次发送回调通知,如果回调处理没有做幂等,第二次回调进来时流水表会因唯一键冲突触发异常,把之前订单状态的更新也一并回滚了。

解决方式:在回调处理的入口加一个幂等判断——先查支付流水表是否存在该订单的支付成功记录,存在则直接返回成功响应不再处理。顺序上先把最核心的订单状态更新完成,再补流水记录,并且把这两步放在事务里但要注意异常不会阻断订单更新。我的习惯是订单状态先更新,流水记录单独捕获异常不参与回滚。

5.5 现象:二次开发时新增的接口全部 404,但代码写得没问题

拿到了家政项目源码,在路由文件里加了新接口,本地测试却总是 404。排查后发现项目里同时存在两套路由加载方式:一套是手动app.use注册,另一套是自动扫描routes目录下的文件。新增的接口写进了新文件,但这个文件没有被自动扫描规则匹配到,也没有在入口文件里手动注册。源码里“部分自动部分手动”的混合路由加载模式,是二次开发最容易踩的坑。

解决方式:全局搜索app.use和路由注册代码,梳理出实际生效的路由加载逻辑。如果是自动扫描,需要确认文件名后缀或目录深度是否符合规则;如果是手动注册,新增文件后必须手动添加注册代码。建议给项目写一个接口清单文档,每次新增路由都在清单里标记,避免后续开发重复踩坑。

6. 进阶级做法:让用户在小程序里收到“服务人员已接单”的通知

家政项目源码跑通后,真正提升体验的是通知链路。用户下单后希望立刻知道服务人员有没有接单、什么时候上门。一个实用的实现是接入微信小程序订阅消息——注意,这里说的是业务合法的订阅消息,和任何网络通道无关。后端在下单时记录用户的小程序formId或者用一次性订阅消息模板,派单成功后推送一条模板消息通知用户。

推送的核心代码要注意异步和失败降级:

// services/notifyService.js async function sendSubscribeMessage(openid, templateId, data, page) { try { const accessToken = await getAccessToken(); // 从缓存获取,不要每次请求 const result = await axios.post( `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${accessToken}`, { touser: openid, template_id: templateId, page: page, data: data } ); // 订阅消息有频率限制,失败时静默降级,不能阻塞主流程 if (result.data.errcode !== 0) { console.error('subscribe message failed:', result.data.errcode); } } catch (err) { // 超时或网络异常不能影响订单流程,记录日志即可 console.error('sendSubscribeMessage error:', err.message); } }

这个做法的关键有几点:getAccessToken必须缓存,微信接口有每日配额限制,频繁获取会直接报错;消息推送要放到异步队列里执行,不能阻塞下单主流程;用户未授权订阅时,失败降级为不做通知,不影响业务主流程。这就是一个典型的“先让业务闭环走通,再逐步加体验优化”的思路。

家政项目的源码,说到底拼的不是某个黑科技,而是对订单生命周期每个环节的理解和代码上近似苛刻的约束。状态机、事务边界、幂等校验,这三个基本功做扎实了,剩下的功能都是时间问题。我接手过的家政项目源码里,凡是后来跑得稳的,都是把这三件事写进了代码规范里,不是靠某个大神在线救火。做这个方向,放平心态,一单一单把链路理清楚,比追任何花哨框架都实在。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询