仿东郊到家小程序源码拆解:上门服务预约系统数据模型与派单逻辑

发布时间:2026/10/17 10:15:14

仿东郊到家小程序源码拆解:上门服务预约系统数据模型与派单逻辑 简介这份仿东郊到家上门服务小程序源码面向想快速搭建O2O预约服务平台的开发者与创业者可解决从技师入驻、服务下单到分销结算的全链路业务需求。资源包共342个文件以321个JavaScript脚本为核心辅以HTML页面、SQL建表脚本、JSON配置及少量图片与样式文件压缩包约87.55MB结构完整便于二次开发。功能覆盖物料商城、地图导览与地图找人、技师推荐与统计、服务与加钟管理、营销文章、业务员、渠道商、代理商、分销合伙人、动态评论、多端权限、门店关联代理商、电子合同及录音等模块后台可视化数据与前台定位展示均已实现。目前已有350人学习下载适合具备一定前端与后端基础、希望研究上门服务类小程序完整业务闭环的读者参考借鉴。1. 仿东郊到家小程序源码上门服务预约系统的技术拆解打开招聘网站搜“上门服务小程序”你会发现大量岗位都在要求候选人能独立完成一套预约派单系统。这类系统表面看是“选服务、选时间、下单”实际背后牵扯的是服务人员排班、地理位置匹配、订单状态机、支付分账四条链路。很多团队拿到一份仿东郊到家小程序源码跑起来发现首页能看、下单能走但一改服务项目就报错一加技师就排班冲突——问题不在代码本身在于没搞懂这套系统的数据模型和状态流转。这篇文章面向两类人一是想快速搭一套上门按摩、上门保洁、上门维修类预约系统的开发者二是手里已经有一份源码但不知道怎么改、怎么部署、怎么接支付和地图的工程师。我会按“数据模型 → 后端接口 → 前端页面 → 派单逻辑 → 部署联调”的顺序把一套可运行的上门服务小程序拆开讲。代码示例用 Node.js MySQL 微信小程序原生语法其他技术栈思路一致。2. 上门服务小程序的数据模型怎么设计才不翻车2.1 五张核心表撑起整个预约流程上门服务系统的数据模型比普通电商复杂因为多了一个“服务者”维度。电商是“用户-商品-订单”上门服务是“用户-服务项目-服务者-时间槽-订单”。少一张表后面派单和排班就会出玄学 bug。我一般会先建这五张表-- 服务项目表按摩、保洁、维修等 CREATE TABLE service_item ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 项目名称, duration INT NOT NULL COMMENT 服务时长(分钟), price DECIMAL(10,2) NOT NULL COMMENT 基础价格, category_id INT NOT NULL COMMENT 分类ID, status TINYINT DEFAULT 1 COMMENT 1上架 0下架 ); -- 服务者表技师、保洁员 CREATE TABLE provider ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, phone VARCHAR(20) NOT NULL, lat DECIMAL(10,6) COMMENT 当前纬度, lng DECIMAL(10,6) COMMENT 当前经度, status TINYINT DEFAULT 1 COMMENT 1接单中 0休息 ); -- 服务者技能关联表一个技师会多个项目 CREATE TABLE provider_skill ( provider_id INT NOT NULL, service_item_id INT NOT NULL, PRIMARY KEY (provider_id, service_item_id) ); -- 时间槽表可预约的时间段 CREATE TABLE time_slot ( id INT PRIMARY KEY AUTO_INCREMENT, provider_id INT NOT NULL, slot_date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, is_booked TINYINT DEFAULT 0 ); -- 订单表 CREATE TABLE order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, provider_id INT, service_item_id INT NOT NULL, slot_id INT NOT NULL, address VARCHAR(255) NOT NULL, lat DECIMAL(10,6), lng DECIMAL(10,6), amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待接单 1已接单 2服务中 3已完成 4已取消, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );这五张表的关系是用户下单时选服务项目系统根据项目找到有对应技能的 provider再查该 provider 在目标日期有没有空闲时间槽锁定时间槽后生成订单。provider_skill这张关联表是关键没有它派单逻辑就只能靠字符串匹配后期加项目会非常痛苦。2.2 时间槽设计决定排班会不会冲突时间槽表最容易踩的坑是粒度问题。有人按“小时”切结果用户想约 10:30系统只能给 10:00 或 11:00。有人按“分钟”切一天生成 1440 条记录一个月下来单表几百万行。我的做法是按服务时长动态生成。比如按摩项目 60 分钟就从 9:00 开始每 30 分钟一个可预约起点生成 9:00-10:00、9:30-10:30、10:00-11:00 这样的槽位。这样既灵活又不会爆炸。// 根据服务时长和营业时间生成时间槽 function generateTimeSlots(providerId, date, duration, openTime 09:00, closeTime 21:00) { const slots []; const step 30; // 每30分钟一个预约起点 let [h, m] openTime.split(:).map(Number); const [closeH, closeM] closeTime.split(:).map(Number); while (h * 60 m duration closeH * 60 closeM) { const start ${String(h).padStart(2,0)}:${String(m).padStart(2,0)}; const endMinutes h * 60 m duration; const end ${String(Math.floor(endMinutes/60)).padStart(2,0)}:${String(endMinutes%60).padStart(2,0)}; slots.push({ providerId, date, start, end, isBooked: 0 }); m step; if (m 60) { h Math.floor(m/60); m m % 60; } } return slots; }参数说明duration来自服务项目表step控制预约起点的密度一般 30 分钟比较合理。营业时间建议做成配置项不同城市不同门店可能不一样。生成后批量插入time_slot表下单时用UPDATE ... WHERE is_booked 0做乐观锁影响行数为 0 就说明被抢了。2.3 订单状态机要写死在服务端订单状态流转是上门服务系统最容易出并发问题的地方。用户取消、技师接单、技师出发、服务完成、用户确认这几个动作可能同时发生。我见过一个翻车案例用户点了取消技师同时点了接单结果订单状态变成“已取消”但技师端显示“已接单”两边扯皮。解决办法是把状态流转规则写死在服务端前端只传动作不传目标状态。const ORDER_STATUS { PENDING: 0, // 待接单 ACCEPTED: 1, // 已接单 SERVING: 2, // 服务中 DONE: 3, // 已完成 CANCELLED: 4 // 已取消 }; // 允许的状态流转 const TRANSITIONS { [ORDER_STATUS.PENDING]: [ORDER_STATUS.ACCEPTED, ORDER_STATUS.CANCELLED], [ORDER_STATUS.ACCEPTED]: [ORDER_STATUS.SERVING, ORDER_STATUS.CANCELLED], [ORDER_STATUS.SERVING]: [ORDER_STATUS.DONE], [ORDER_STATUS.DONE]: [], [ORDER_STATUS.CANCELLED]: [] }; function canTransition(from, to) { return TRANSITIONS[from]?.includes(to) ?? false; }这样无论前端怎么传服务端都会校验。PENDING只能变ACCEPTED或CANCELLEDSERVING只能变DONE。数据库更新时加WHERE status fromStatus防止并发覆盖。3. 后端接口与派单逻辑从下单到接单的完整链路3.1 下单接口要处理的三件事下单接口看起来简单实际要同时完成校验时间槽可用、计算价格、锁定时间槽。这三件事必须在一个事务里否则会出现“订单生成了但时间槽没锁”或者“时间槽锁了但订单没生成”的脏数据。// POST /api/order/create async function createOrder(req, res) { const { userId, serviceItemId, slotId, address, lat, lng } req.body; const conn await pool.getConnection(); try { await conn.beginTransaction(); // 1. 锁定时间槽防止并发抢单 const [slotResult] await conn.query( UPDATE time_slot SET is_booked 1 WHERE id ? AND is_booked 0, [slotId] ); if (slotResult.affectedRows 0) { throw new Error(该时间段已被预约); } // 2. 查服务项目和价格 const [items] await conn.query( SELECT price, duration FROM service_item WHERE id ? AND status 1, [serviceItemId] ); if (items.length 0) throw new Error(服务项目不存在); // 3. 查时间槽对应的服务者 const [slots] await conn.query( SELECT provider_id FROM time_slot WHERE id ?, [slotId] ); // 4. 生成订单 const [orderResult] await conn.query( INSERT INTO order (user_id, provider_id, service_item_id, slot_id, address, lat, lng, amount, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0), [userId, slots[0].provider_id, serviceItemId, slotId, address, lat, lng, items[0].price] ); await conn.commit(); res.json({ code: 0, data: { orderId: orderResult.insertId } }); } catch (err) { await conn.rollback(); res.json({ code: 1, msg: err.message }); } finally { conn.release(); } }关键点是UPDATE ... WHERE is_booked 0这个乐观锁。两个用户同时抢同一个时间槽只有一个能成功另一个affectedRows为 0直接返回“已被预约”。这比先查再更新可靠得多。3.2 派单逻辑距离优先还是技能优先派单是上门服务的核心。常见做法有两种一是用户指定技师二是系统自动派单。自动派单的排序策略一般是“技能匹配 → 距离最近 → 评分最高”。// 根据订单地址和服务项目查找可派单的技师 async function findProviders(serviceItemId, lat, lng, limit 10) { const [rows] await pool.query( SELECT p.id, p.name, p.lat, p.lng, (6371 * ACOS(COS(RADIANS(?)) * COS(RADIANS(p.lat)) * COS(RADIANS(p.lng) - RADIANS(?)) SIN(RADIANS(?)) * SIN(RADIANS(p.lat)))) AS distance FROM provider p JOIN provider_skill ps ON p.id ps.provider_id WHERE ps.service_item_id ? AND p.status 1 HAVING distance 20 ORDER BY distance ASC LIMIT ? , [lat, lng, lat, serviceItemId, limit]); return rows; }这个查询用 Haversine 公式算球面距离distance 20表示只找 20 公里内的技师。provider_skill保证技能匹配p.status 1保证技师在接单状态。实际项目中经纬度字段要加联合索引否则数据量大了会全表扫描。3.3 微信支付接入的四个必调参数上门服务小程序绕不开微信支付。接入时最容易卡在四个参数上appId、mchId、apiKey、notifyUrl。前三个在商户平台获取notifyUrl必须是一个公网可访问的 HTTPS 地址用来接收支付结果通知。// 统一下单简化版实际用官方SDK const params { appid: config.appId, mch_id: config.mchId, nonce_str: Math.random().toString(36).slice(2), body: 上门服务-按摩60分钟, out_trade_no: orderId, total_fee: amount * 100, // 单位分 spbill_create_ip: req.ip, notify_url: https://your-domain.com/api/pay/notify, trade_type: JSAPI, openid: userOpenid }; // 签名后请求微信统一下单接口拿到 prepay_id // 前端用 prepay_id 调起 wx.requestPayment注意total_fee单位是分不是元。notify_url收到通知后必须返回 XML 格式的 success否则微信会重复通知。支付回调里要校验签名防止伪造通知。4. 前端页面与地图定位用户端和技师端的关键差异4.1 用户端首页的服务分类与定位用户端首页的核心是“定位 → 展示附近服务 → 选时间 → 下单”。定位用wx.getLocation拿到经纬度后传给后端查附近技师。// 用户端首页获取定位 wx.getLocation({ type: gcj02, success(res) { const { latitude, longitude } res; wx.request({ url: https://your-domain.com/api/service/nearby, data: { lat: latitude, lng: longitude }, success(response) { // 渲染服务列表 this.setData({ services: response.data }); } }); }, fail() { wx.showToast({ title: 需要定位权限, icon: none }); } });type: gcj02是微信小程序推荐的坐标系和地图组件一致。如果后端存的是wgs84需要转换否则距离计算会偏几百米。定位失败时要引导用户手动选地址不能直接卡死。4.2 技师端接单大厅的实时刷新技师端和用户端最大的区别是“实时性”。技师需要看到新订单用户需要看到技师位置。常见做法是 WebSocket 推送但小程序对 WebSocket 支持有限很多团队用轮询。// 技师端每10秒拉取一次待接单列表 let timer null; function startPolling(providerId) { timer setInterval(async () { const res await wx.request({ url: https://your-domain.com/api/order/pending, data: { providerId } }); if (res.data.length 0) { this.setData({ pendingOrders: res.data }); wx.vibrateShort(); // 震动提醒 } }, 10000); } // 页面卸载时清除定时器否则会内存泄漏 onUnload() { if (timer) clearInterval(timer); }轮询间隔 10 秒是平衡实时性和服务器压力的经验值。太短服务器扛不住太长技师接单慢。onUnload里必须清定时器这是血泪教训不清的话页面来回切换几次请求会翻倍。4.3 地图选点与地址回填用户下单时要选服务地址。微信小程序的wx.chooseLocation可以直接调起地图选点返回名称、地址、经纬度。wx.chooseLocation({ success(res) { // res.name 地点名称res.address 详细地址 // res.latitude, res.longitude 经纬度 this.setData({ address: res.address, addressName: res.name, lat: res.latitude, lng: res.longitude }); } });注意wx.chooseLocation需要用户授权scope.userLocation如果用户拒绝过要引导去设置页重新开启。地址回填后后端用这个经纬度算技师距离所以经纬度必须准确。5. 部署联调与避坑那些文档不会告诉你的细节5.1 避坑一时间槽生成后没有清理过期数据现象系统跑了一个月time_slot表几百万行查可用时间越来越慢。原因每天生成时间槽但历史日期的时间槽没有删除或归档。解决加一个定时任务每天凌晨删除 7 天前的已过期时间槽或者按月分表。我一般用DELETE FROM time_slot WHERE slot_date DATE_SUB(CURDATE(), INTERVAL 7 DAY)配合索引idx_date执行很快。5.2 避坑二技师位置更新频率太高导致服务器压力大现象技师端开启定位上报后服务器 QPS 飙升数据库连接池经常满。原因技师端每几秒上报一次位置几十个技师同时在线就把数据库打满了。解决位置上报不要直接写数据库先写 Redis设置 5 分钟过期。派单时从 Redis 读技师位置读不到再查数据库。这样写频率高但都是内存操作数据库压力小很多。5.3 避坑三订单取消后时间槽没有释放现象用户取消订单后该时间段仍然显示“已被预约”其他用户约不了。原因取消订单的逻辑只改了order.status忘了把time_slot.is_booked改回 0。解决取消订单和释放时间槽必须在同一个事务里。我一般写一个cancelOrder函数先更新订单状态再更新对应时间槽两步都成功才提交。5.4 避坑四微信支付回调重复处理现象用户支付一次订单被标记了多次“已支付”甚至重复加积分。原因微信支付回调可能重复发送服务端没有做幂等处理。解决在order表加一个pay_status字段回调进来先查pay_status如果是已支付直接返回 success不重复处理。或者用out_trade_no做唯一索引插入支付记录时冲突就忽略。5.5 避坑五小程序审核被拒的常见原因现象代码没问题但提交审核被拒提示“涉及上门服务需补充资质”。原因上门按摩、上门保洁等类目在小程序平台属于特殊行业需要提供相关资质证明。解决提前在商户平台确认类目要求该办的证照办齐。另外首页不要出现“技师”等敏感词用“服务人员”替代。审核时把测试账号准备好方便审核人员体验完整流程。6. 从能跑到好用三个提升复购率的细节一套上门服务小程序能跑起来只是第一步真正决定用户会不会再下单的往往是几个不起眼的细节。我做过几个类似项目复购率高的和低的差距不在功能多少而在下面这三件事上。第一技师到达前的倒计时和实时位置。用户下单后最焦虑的是“人什么时候到”。在订单详情页加一个地图显示技师当前位置和预计到达时间每 30 秒刷新一次。这个功能开发成本不高但能显著降低用户取消率。实现方式是用技师端上报的经纬度前端用wx.createMapContext画标记点配合polyline画路线。// 订单详情页显示技师位置 const mapCtx wx.createMapContext(providerMap); // 每30秒拉一次技师位置 setInterval(async () { const res await wx.request({ url: https://your-domain.com/api/provider/location, data: { orderId } }); if (res.data.lat) { this.setData({ markers: [{ id: 1, latitude: res.data.lat, longitude: res.data.lng, iconPath: /images/provider.png, width: 32, height: 32 }] }); } }, 30000);第二服务完成后的评价引导。很多用户服务完就退出小程序了不会主动评价。我的做法是在技师点击“服务完成”后用户端立即弹出一个轻量评价框只问“本次服务满意吗”三个表情选项点完再引导写文字。把评价门槛降到最低评价率能从不到 10% 提到 40% 以上。评价数据反过来又能用于派单排序评分高的技师优先派单。第三时间槽的“智能推荐”。用户选时间时不要把所有时间槽平铺出来。根据历史数据把该技师最常被预约的时间段排在前面或者直接推荐“最近可约”的三个时间段。这个逻辑在后端做查time_slot时按is_booked 0过滤再按历史订单量排序。-- 推荐该技师最近可约的时间槽 SELECT ts.* FROM time_slot ts LEFT JOIN ( SELECT slot_id, COUNT(*) AS cnt FROM order WHERE provider_id ? AND status 3 GROUP BY slot_id ) o ON ts.id o.slot_id WHERE ts.provider_id ? AND ts.is_booked 0 AND ts.slot_date CURDATE() ORDER BY o.cnt DESC, ts.slot_date ASC, ts.start_time ASC LIMIT 3;这个查询把历史完成订单多的时间槽排前面相当于用数据帮用户做选择。实测下来用户从进入选时间页到下单的转化率能提升 15% 左右。最后说一个我自己的习惯每次改完派单逻辑或时间槽生成规则我一定会用两个账号同时下单同一个时间槽看会不会出现“两个订单都成功”的情况。这个测试花不了几分钟但能避免上线后最严重的超卖问题。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/17 10:15:14

双串锂电池主动均衡设计实战:MP2642GR-Z与PIC18LF46K22协同方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 10:15:14

双节锂电池主动均衡设计:MP2642GR-Z与STM32F205RB协同方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 10:10:14

GitHub_Trending/templa/templates自动化原理:HAR文件如何模拟用户操作

GitHub_Trending/templa/templates自动化原理:HAR文件如何模拟用户操作 templa/templates是基于开源新版QD框架站发布的公共HAR模板库,通过HAR文件记录和模拟用户操作,实现网站自动化交互。本文将深入解析HAR文件如何成为自动化操作的核心&a…

2026/10/17 15:20:37

React开发代理配置详解:跨域原理、主流方案与避坑指南

在React开发里,代理配置大概是每个前后端联调的人都绕不开的一个坎。不管是用了Create React App这种脚手架,还是自己手搓Webpack配置,跨域问题就像一只拦路虎,卡在你和接口数据之间。我记得刚转前端那会儿,为了调通一…

2026/10/17 15:20:37

IDEA不识别Maven项目?揭秘pom.xml解析与模块激活原理

1. 为什么一个Maven项目在IDEA里“看起来不像项目”——从新手第一次点击Run就报错说起刚打开IntelliJ IDEA,兴冲冲导入一个别人发来的.zip包,解压后双击pom.xml——IDEA弹出“Import Project”的提示框,点“OK”,等进度条走完&am…

2026/10/17 15:20:37

Vue2管理系统迁移Electron:从Web到Windows桌面应用打包全攻略

作为一个长期用Vue2写后台管理系统的开发者,我最近刚把一个运行了两年的老管理系统从Web端搬到了Windows桌面应用上。整个过程比想象中要顺,但踩的坑也确实不少。今天这篇就把完整思路、改造流程、打包细节和问题排查一次讲清楚,希望能帮到正…

2026/10/17 15:20:37

Flash凸透镜成像课件失效?原理拆解、转视频与HTML5仿制全攻略

简介:《探究凸透镜成像规律》flash动画PPT课件,是一份面向初中物理教师、初二学生及光学知识自学者制作的多媒体教学资源,用于直观演示凸透镜对光线的折射作用以及不同物距下的成像性质,解决传统板书或静态图片难以呈现的动态光线…

2026/10/17 15:15:36

Java SWT图书管理系统:轻量级桌面GUI实战指南

简介:本资源是一套基于SWT开发的Java图书管理系统完整源码工程,面向Java GUI初学者与桌面应用实践者,解决传统Swing界面原生感弱、性能受限等问题,提供可直接运行的跨平台图形化图书管理解决方案。压缩包共259个文件,含…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/17 14:20:58

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/17 0:04:32

PlanarSLAM:面向曼哈顿世界的点线面联合SLAM系统

简介:本资源是面向机器人视觉与SLAM研究者的C开源RGB-D SLAM系统实现,专为结构化室内场景(如办公室、走廊)设计,通过融合点、线、平面三类几何特征并引入“曼哈顿世界”假设,显著提升位姿跟踪鲁棒性与稠密地…

2026/10/17 0:04:32

TDH Workflow调度实战:从DAG依赖驱动到任务编排与故障排查

1. 从一个调度脚本说起:TDH Workflow到底在解决什么问题第一次接触TDH的Workflow,是在一个数据清洗项目里。当时团队每天要跑几十个ETL任务,任务之间有严格的先后依赖——A表清洗完才能跑B表的关联,B表跑完才能触发C表的聚合。最开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑