家政系统的本质不是"电商 + 预约",而是服务商品化 + 人员调度 + 履约凭证 + 资金托管的四层叠加。
本文基于 ThinkPHP 8 技术栈,完整拆解一套可开源、可二开、可私有化部署的家政系统该如何设计与落地。
源码及演示:j.yunzes.top/er
一、先厘清业务边界:家政系统到底在解决什么
很多团队一开始把家政系统当成"卖保洁小时包的小商城",做到派单环节就崩了。真实业务复杂度在三个地方:
- 服务非标:保洁 2 小时、开荒按平米、月嫂按天、家电清洗按件——定价维度完全不同
- 人员即资源:阿姨不是库存商品,有排班、有技能、有服务半径、有档期冲突
- 履约不可逆:上门服务一旦发生,照片、签到、验收记录就是法律凭证,不能随便改
所以一套成熟的家政源码,至少要覆盖四条主线:
C端用户:选服务 → 选时段 → 填地址 → 支付 → 看进度 → 验收评价
B端阿姨:接单 → 导航 → 签到打卡 → 上传凭证 → 完工 → 提现
平台后台:服务配置 → 智能/手动派单 → 订单监控 → 分账结算 → 客诉仲裁
门店/城市:多门店库存、城市开关、区域定价、员工权限
ThinkPHP 在这套业务里的定位是稳定的业务中枢——不追求极致并发,追求开发效率、二开门槛低、私有化交付简单。
二、技术栈选型与分层架构
2.1 推荐生产级组合
| 层 | 选型 | 理由 |
|---|---|---|
| 后端 | ThinkPHP 8 + PHP 8.1+ | 多应用、RBAC、JWT、API 生成开箱即用 |
| 数据库 | MySQL 8.0 + Redis 6+ | 结构化数据 + 排班锁/缓存/地理索引 |
| 管理后台 | Vue3 + Vite + Element Plus | 服务、订单、财务、人员一体化 |
| 用户/阿姨端 | uni-app(微信小程序 + H5 + App) | 一套代码多端编译 |
| 文件存储 | 阿里云 OSS / 腾讯云 COS | 开工/完工照片、资质证书 |
| 地图服务 | 腾讯地图 / 高德 WebService | 距离计算、路线规划、服务半径 |
| 消息推送 | WebSocket + 短信 + 企业微信 | 派单实时触达 |
| 部署 | Docker + Nginx + 宝塔 | 私有化交付友好 |
2.2 多应用目录结构
app/
├── common/ # 公共模型、工具、枚举
├── api/ # C端用户接口(小程序/H5)
├── staff/ # 阿姨端接口
├── admin/ # 平台/门店管理后台
└── install/ # 安装向导(首次部署自动建表)
ThinkPHP 多应用的优势是权限域天然隔离:admin.order.list 和 api.order.create 不会串。
三、核心数据建模:家政系统的命门
3.1 服务商品化建模
家政系统通常把"服务"当商品管理,但要比普通电商多一层规格 + 时长 + 人员要求。
-- 服务分类
CREATE TABLE jz_service_category (
id INT PRIMARY KEY AUTO_INCREMENT,
pid INT DEFAULT 0,
name VARCHAR(50) NOT NULL,
sort INT DEFAULT 0
);
-- 服务主表(非标服务商品化)
CREATE TABLE jz_service (
id INT PRIMARY KEY AUTO_INCREMENT,
category_id INT NOT NULL,
title VARCHAR(120) NOT NULL,
cover VARCHAR(255),
duration INT NOT NULL COMMENT '基础服务时长(分钟)',
base_price DECIMAL(10,2) NOT NULL,
unit_type TINYINT COMMENT '1按时 2按次 3按平米 4按件',
attr JSON COMMENT '扩展属性:是否带工具/人数/面积阶梯',
city_ids JSON COMMENT '开放城市',
status TINYINT DEFAULT 1
);
💡 定价模型别写死。把"按时长阶梯计价""按面积计价"做成
attrJSON 配置,后面加新服务类型不用改表结构。
3.2 阿姨(服务人员)与排班解耦

排班表必须独立于订单表——这是预约系统和电商最大的区别。订单是"交易视角",排班是"资源视角",两者通过 staff_id + 时间段 关联。
-- 阿姨档案
CREATE TABLE jz_staff (
id INT PRIMARY KEY AUTO_INCREMENT,
tenant_id INT DEFAULT 0,
store_id INT DEFAULT 0,
name VARCHAR(30),
avatar VARCHAR(255),
phone VARCHAR(20),
skill_tags JSON COMMENT '保洁/月嫂/家电清洗',
geo_hash CHAR(8) COMMENT '常驻位置网格',
dynamic_score DECIMAL(3,2) DEFAULT 5.00,
status TINYINT DEFAULT 1
);
-- 排班表(资源占用视图)
CREATE TABLE jz_staff_schedule (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
staff_id INT NOT NULL,
work_date DATE NOT NULL,
slot_snap TINYINT COMMENT '时段编号 0-47(每半小时一格)',
status TINYINT DEFAULT 1 COMMENT '1空闲 2已锁 3休息',
order_id BIGINT DEFAULT 0,
UNIQUE KEY uk_staff_slot (staff_id, work_date, slot_snap)
);
uk_staff_slot 唯一索引是并发派单的底层防线——同一个阿姨同一时段不可能被派两单。
3.3 订单主表与状态机
CREATE TABLE jz_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id INT DEFAULT 0,
store_id INT DEFAULT 0,
user_id INT NOT NULL,
service_id INT NOT NULL,
staff_id INT DEFAULT 0,
address_id INT NOT NULL,
appoint_date DATE,
time_slot VARCHAR(20),
duration INT,
amount DECIMAL(10,2),
pay_status TINYINT DEFAULT 0 COMMENT '0未付 1已付 2退款',
order_status TINYINT DEFAULT 0
COMMENT '0待支付 1待派单 2已派单 3上门中 4服务中 5待验收 6完成 7取消',
dispatch_type TINYINT DEFAULT 0 COMMENT '0系统 1手动',
create_time DATETIME
);
-- 状态变更日志(仲裁凭证)
CREATE TABLE jz_order_status_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
from_status TINYINT,
to_status TINYINT,
operator VARCHAR(40) COMMENT '用户/阿姨/系统/管理员',
remark VARCHAR(255),
create_time DATETIME
);
3.4 履约凭证表
CREATE TABLE jz_order_evidence (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
type TINYINT COMMENT '1签到 2完工 3验收',
images JSON,
geo_point POINT,
create_time DATETIME
);
开工/完工照片和工单 ID 强绑定,售后纠纷时和状态日志一起作为仲裁依据。
四、订单状态机:家政系统最容易被写烂的地方
4.1 合法状态流转图
待支付 ──支付成功──▶ 待派单
待派单 ──派单──────▶ 已派单
已派单 ──阿姨出发──▶ 上门中
上门中 ──签到开工──▶ 服务中
服务中 ──完工──────▶ 待验收
待验收 ──用户确认──▶ 完成
任意态 ──取消/退款─▶ 已取消(资金回滚)
非法跳转直接拦截。比如"待支付"不能直接跳"服务中","完成"不能再改回"派单中"。
4.2 ThinkPHP 状态机实现
class OrderStateMachine
{
const TRANSITIONS = [
0 => [1], // 待支付 -> 待派单
1 => [2, 7], // 待派单 -> 已派单/取消
2 => [3, 7], // 已派单 -> 上门中/取消
3 => [4], // 上门中 -> 服务中
4 => [5, 6, 7], // 服务中 -> 待验收/完成/取消
5 => [6, 7], // 待验收 -> 完成/取消
];
public static function canTransfer(int $from, int $to): bool
{
return in_array($to, self::TRANSITIONS[$from] ?? [], true);
}
public function change(Order $order, int $to, string $operator, string $remark = '')
{
if (!self::canTransfer($order->order_status, $to)) {
throw new BusinessException("非法状态流转:{$order->order_status} -> {$to}");
}
$from = $order->order_status;
$order->order_status = $to;
$order->save();
OrderStatusLog::create([
'order_id' => $order->id,
'from_status'=> $from,
'to_status' => $to,
'operator' => $operator,
'remark' => $remark,
'create_time'=> date('Y-m-d H:i:s'),
]);
}
}
每一次状态变更都必须走这个方法,禁止业务代码直接 $order->save() 改状态。
五、派单引擎:手动 + 智能两条路

5.1 派单锁机制(防并发重复派)
下单支付成功后,先对目标时段加锁,避免同一阿姨被重复派单;锁随支付超时自动释放。
public function lockSlot(int $staffId, string $date, int $slot): bool
{
return Db::execute(
"INSERT IGNORE INTO jz_staff_schedule
(staff_id, work_date, slot_snap, status)
VALUES (?, ?, ?, 2)",
[$staffId, $date, $slot]
) > 0;
}
INSERT IGNORE + 唯一索引,天然幂等。
5.2 智能派单评分模型
系统自动派单时,按权重打分:
score = w1 * 技能匹配度
+ w2 * 距离评分(geo_hash 前缀匹配)
+ w3 * 历史评分
+ w4 * 档期空闲度
- w5 * 当前待接单量
public function recommendStaff(Order $order): array
{
$candidates = Staff::whereJsonContains('skill_tags', $order->serviceType)
->where('status', 1)
->where('tenant_id', $order->tenant_id)
->select();
return $candidates->map(function ($s) use ($order) {
$s->score = $this->calcScore($s, $order);
return $s;
})->sortByDesc('score')->take(5)->values();
}
⚠️ 派单规则一定要留后台配置入口,别把"随机分配"写死在代码里。后期调权重会疯。
5.3 派单流水留存
每次自动派单都要落流水,记录候选列表、匹配分值、改派次数、异常原因——方便复盘派单质量。
CREATE TABLE jz_dispatch_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT,
candidates JSON,
matched_staff_id INT,
score DECIMAL(8,2),
dispatch_type TINYINT,
retry_count INT DEFAULT 0,
reason VARCHAR(255),
create_time DATETIME
);
六、支付与资金托管:重灾区中的重灾区
6.1 担保交易模型
家政业务不能"付完款立刻结算给阿姨",必须走担保交易:
用户支付 → 资金托管(平台待结算)
↓
阿姨完工 + 用户验收
↓
触发分账:平台抽成 + 阿姨收益
退款/取消时执行资金回滚。
6.2 支付回调幂等处理

这是最容易被攻破的点。老源码常用微信支付 V2,新版必须迁 V3,重点处理:
- 证书加载用平台证书管理器
- 回调验签必须走官方 SDK,别手写
- 订单状态用
lockForUpdate行锁 - 已处理订单直接返回
success,防重放
public function notify()
{
$data = $this->wxPay->verifyNotify();
$orderNo = $data['out_trade_no'];
return Db::transaction(function () use ($orderNo) {
$order = Order::where('order_no', $orderNo)
->lock(true)->find();
if (!$order || $order->pay_status === 1) {
return '<xml><return_code>SUCCESS</return_code></xml>';
}
$order->pay_status = 1;
$order->save();
// 支付成功 → 状态机流转 待支付→待派单
app(OrderStateMachine::class)->change($order, 1, 'system', '微信支付回调');
// 释放排班锁、触发派单队列
Queue::push(new DispatchJob($order->id));
return '<xml><return_code>SUCCESS</return_code></xml>';
});
}
6.3 分账逻辑
public function settle(Order $order)
{
$platformRate = 0.15; // 平台抽成可配置
$staffAmount = $order->amount * (1 - $platformRate);
Db::transaction(function () use ($order, $staffAmount) {
StaffWallet::where('staff_id', $order->staff_id)
->increment('balance', $staffAmount);
SettleLog::create([...]);
});
}
七、RBAC 权限:多门店多角色的硬约束
7.1 四张表模型(别用 JSON 存权限)
jz_admin_user 用户
jz_role 角色(总部/区域/店长/店员/财务)
jz_auth_rule 权限节点(name 必须是路由名)
jz_role_auth 角色-权限关联
auth_rule.name 必须写成 admin.order.dispatch,不要存 URL 路径,也不要把 HTTP 方法塞进去(单独用 method 字段)。
7.2 中间件鉴权
class AuthCheck
{
public function handle($request, \Closure $next)
{
$user = Jwt::parse($request->header('Authorization'));
$routeName = $request->rule()->getName();
if (!Auth::check($user->role_id, $routeName, $request->method())) {
return json(['code' => 403, 'msg' => '无权限']);
}
$request->user = $user;
return $next($request);
}
}
7.3 数据权限隔离
门店管理员只能看自己店的数据,区域经理看片区,总部看全部:
// BaseModel 里统一追加作用域
protected function baseQuery()
{
$user = request()->user;
if ($user->isSuperAdmin()) return;
if ($user->dataScope === 'store') {
$this->where('store_id', $user->store_id);
} elseif ($user->dataScope === 'region') {
$this->whereIn('store_id', $user->regionStoreIds());
}
}
⚠️ 列表接口漏掉归属过滤,是全库订单泄露的最高频漏洞。
八、uni-app 多端前端实践
8.1 端职责划分
| 端 | 核心页面 |
|---|---|
| 用户端 H5/小程序 | 首页分类、服务详情、预约下单、订单跟踪、评价 |
| 阿姨端 | 工单列表、导航、签到、凭证上传、收益提现 |
| 管理后台 | 服务/人员/订单/财务/数据看板 |
8.2 预约下单关键逻辑
// 选时段时实时校验阿姨档期
async function checkSlot(serviceId, date, slot) {
const res = await api.post('/api/order/check-slot', {
service_id: serviceId, date, slot
})
return res.data.available_staff // 返回可派阿姨数
}
下单前先锁档期,支付超时 15 分钟自动释放。
8.3 阿姨端签到打卡
async function signIn(orderId) {
const loc = await getLocation()
const photos = await chooseMedia()
await api.post('/staff/order/sign-in', {
order_id: orderId,
lat: loc.latitude,
lng: loc.longitude,
images: photos
})
}
签到坐标 + 照片落 jz_order_evidence,作为履约凭证。
九、开源系统设计:开源版 vs 商业版边界

9.1 模块切分建议
| 模块 | 开源版 | 商业版(插件激活) |
|---|---|---|
| 服务/订单/派单/支付 | ✅ | ✅ |
| 单店 + 基础 RBAC | ✅ | ✅ |
| 多门店/多城市 | 基础 | 完整 |
| 智能派单算法 | 简单规则 | 加权模型 + GIS |
| 分账/提现/税务 | 手动 | 自动 T+1 |
| 会员次卡/营销 | 基础 | 完整营销中心 |
| SaaS 多租户 | ❌ | ✅ |
采用 open-core 模式:开源版能跑真实单店业务,商业能力后台卡密激活,升级不丢数据。
9.2 项目结构(可直接抄)
homemaker/
├── app/ # ThinkPHP 多应用
│ ├── api/ # 用户端接口
│ ├── staff/ # 阿姨端接口
│ ├── admin/ # 后台接口
│ ├── common/ # 模型/服务/枚举
│ └── install/ # 安装向导
├── frontend/
│ ├── admin/ # Vue3 后台
│ ├── user/ # uni-app 用户端
│ └── staff/ # uni-app 阿姨端
├── data/sql/ # 建表脚本(按序导入)
├── docker/ # 部署编排
├── docs/ # 架构图 + API 文档
└── README.md
9.3 安装向导
首次访问自动检测环境 → 填数据库 → 按序导 SQL → 创建管理员。环境要求:PHP ≥ 8.0(pdo_mysql/fileinfo/mbstring/openssl)、MySQL ≥ 5.7、Node ≥ 16。
十、部署与性能清单
10.1 服务器基线
- 小规模:2 核 4G + 50G SSD,Redis 必开
- 日均 1000+ 单:4 核 8G + 读写分离 + 负载均衡
10.2 ThinkPHP 生产必做项
// .env
APP_DEBUG = false
APP_TRACE = false
// 开启
- OPcache
- 路由缓存:php think optimize:route
- 配置缓存:php think optimize:config
- 注解缓存
10.3 Nginx 伪静态
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
}
10.4 小程序上架避坑
- 配置 request 合法域名、downloadFile 域名
- 虚拟服务类目要过微信审核,隐私协议必填
- 地图 Key 用企业认证,别用个人 Key 限流
- 支付必须 HTTPS + 外网可达 + 回调幂等