
简介这份代驾平台源码包面向微信小程序开发者与后端工程师提供代驾业务从用户下单、司机接单到订单结算的完整实现适合有一定小程序或Java基础、希望快速搭建代驾系统或进行二次开发的技术人员。压缩包共约2000个文件整体7.63MB以934个js、221个vue、158个ts等前端脚本为主配合110个java后端服务代码、118个json配置、53个wxss与51个wxml页面结构另有png图标、yaml部署配置、less与scss样式及少量mp3音频资源前后端结构清晰。目前已有972人学习下载。源码涵盖司机服务、地图定位、订单处理等核心模块可直接运行体验完整流程也便于在此基础上扩展支付、计费与调度逻辑是研究代驾类小程序全栈实现与二次开发的实用参考。1. 代驾平台源码拆开看小程序和后端到底怎么配合代驾这个场景用户下单、司机接单、行程计费、订单结算每一步都卡在实时性上。你拿到一份「代驾小程序和后端共同开发的代驾平台源码.zip」第一反应可能是先跑起来看看但真正决定这套源码能不能用的是前后端分离架构下小程序端和后端服务之间的接口契约、状态同步和计费逻辑。这份源码解决的核心问题是把乘客叫代驾、司机抢单、行程追踪、费用结算这条链路用一套可运行的代码串起来。适合谁适合想拿一套完整代驾业务练手全栈开发的人也适合需要快速搭一个代驾平台原型去验证商业模式的小团队。但要注意源码能跑通不等于能上线计费规则、司机调度、支付回调这些地方每一处都藏着需要你按自己业务改写的逻辑。下面我按实际拆解顺序把这份代驾平台源码从结构到落地讲清楚。2. 代驾平台源码的模块拆解与运行环境搭建拿到一个代驾平台源码压缩包别急着双击运行。先搞清楚它由哪几块组成每块用什么技术栈依赖什么外部服务。代驾平台典型的前后端分离项目实战结构一般分三部分微信小程序端乘客端司机端、后端 API 服务、管理后台。有些源码还会带一个 WebSocket 服务专门推行程位置。2.1 先看目录结构判断技术栈和依赖解压之后根目录通常长这样daijia-platform/ ├── miniprogram/ # 微信小程序端乘客司机 ├── server/ # 后端服务 │ ├── src/ │ │ ├── controller/ # 接口层 │ │ ├── service/ # 业务逻辑 │ │ ├── mapper/ # 数据访问 │ │ └── entity/ # 数据库实体 │ ├── pom.xml # Maven 依赖Java 后端常见 │ └── application.yml # 配置文件 ├── admin/ # 管理后台前端 └── sql/ # 数据库初始化脚本看到pom.xml基本可以确定后端是 Java 技术栈常见组合是 Spring Boot MyBatis MySQL Redis。如果根目录是package.json加app.js那后端可能是 Node.js。代驾平台源码里 Java 后端占比很高因为订单状态流转、计费规则这类逻辑用 Java 写起来结构清晰后期也好维护。先确认三件事后端语言和框架版本、数据库类型和版本、小程序基础库版本要求。这三个对不上后面全是坑。2.2 数据库初始化别直接导入先改字符集和引擎sql/目录下一般有一个init.sql或者按模块拆分的多个 sql 文件。导入之前先做两件事# 1. 创建数据库指定字符集 mysql -u root -p -e CREATE DATABASE daijia DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 2. 导入前检查 sql 文件里有没有硬编码的数据库名 grep -i CREATE DATABASE\|USE sql/init.sql如果 sql 文件里写了CREATE DATABASE daijia而你本地数据库名不一样导入会报错。更稳妥的做法是手动建库然后只导入表结构和数据mysql -u root -p daijia sql/init.sql导入完成后检查关键表是否存在-- 代驾平台核心表一般包括这些 SHOW TABLES; -- 预期看到user用户、driver司机、order订单、 -- driver_location司机位置、price_rule计费规则、 -- coupon优惠券、payment_record支付记录注意如果导入时报Specified key was too long说明 sql 文件里用了 utf8 而不是 utf8mb4或者索引字段长度超了。改字符集或者缩短索引字段。2.3 后端配置数据库连接、Redis、微信参数一个不能少打开application.yml需要改的配置项集中在这几块spring: datasource: url: jdbc:mysql://127.0.0.1:3306/daijia?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 你的密码 redis: host: 127.0.0.1 port: 6379 password: # 本地没设密码就留空 database: 0 wechat: appid: wx你的小程序appid secret: 你的小程序secret mch-id: 微信支付商户号 mch-key: 微信支付密钥serverTimezoneAsia/Shanghai这个参数必须加否则订单创建时间会差 8 小时计费时长直接算错。Redis 用来存司机位置和订单锁代驾场景下司机位置更新频率高用 Redis 的 GEO 结构比 MySQL 查得快。微信相关的appid和secret去微信公众平台拿mch-id和mch-key需要开通微信支付才有。本地开发阶段如果还没申请先把支付相关的开关关掉不然启动就报错。2.4 启动后端先看日志里有没有 Bean 加载失败cd server # Maven 项目 mvn clean package -DskipTests java -jar target/daijia-server-1.0.0.jar # 或者直接跑 mvn spring-boot:run启动过程中重点看三行日志Tomcat 端口是否被占用、数据源是否连接成功、MyBatis 的 mapper 是否加载。如果报Invalid bound statement说明 mapper xml 文件没被扫描到检查application.yml里mybatis.mapper-locations的路径。后端起来之后用 curl 测一个不需要登录的接口curl http://localhost:8080/api/common/price-rule返回计费规则 JSON 就说明后端基本通了。如果返回 404检查 controller 的RequestMapping路径和 context-path 配置。2.5 小程序端改请求地址关掉域名校验用微信开发者工具打开miniprogram/目录。第一件事是改app.js或者config.js里的后端地址// config.js const config { baseUrl: http://localhost:8080/api, // 本地开发用这个 // baseUrl: https://你的域名/api, // 上线时换成这个 wsUrl: ws://localhost:8080/ws // WebSocket 地址 }本地开发时localhost在微信开发者工具里能通但真机调试不行。真机调试需要把后端部署到一台手机能访问的服务器上或者用内网穿透工具把本地端口暴露出去。开发者工具里勾上「不校验合法域名」否则请求全被拦截。这个选项在「详情」→「本地设置」里。小程序端有两个角色入口乘客端和司机端。有些源码是两套小程序有些是一套小程序里根据登录角色切换界面。打开app.json看 pages 列表如果看到pages/passenger/和pages/driver/两个目录就是一套代码两个角色。3. 订单状态流转与司机调度的核心逻辑代驾平台最核心的业务逻辑就两件事订单状态怎么流转、司机怎么被调度。这两块写不好平台要么订单卡死要么司机接不到单。3.1 订单状态机从下单到完成的 7 个状态代驾订单典型的状态流转是这样的状态值状态名触发动作谁触发0待接单乘客下单成功乘客1已接单司机抢单司机2司机已到达司机到达起点司机3行程中乘客上车开始计费司机4待支付行程结束生成账单司机5已完成支付成功系统6已取消乘客或司机取消双方后端代码里一般用一个枚举类管理这些状态public enum OrderStatus { WAITING(0, 待接单), ACCEPTED(1, 已接单), ARRIVED(2, 司机已到达), IN_TRIP(3, 行程中), UNPAID(4, 待支付), COMPLETED(5, 已完成), CANCELLED(6, 已取消); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } }状态流转必须加校验不能从「待接单」直接跳到「行程中」。常见做法是在 service 层写一个canTransfer(from, to)方法每次更新状态前先判断。public boolean canTransfer(OrderStatus from, OrderStatus to) { switch (from) { case WAITING: return to ACCEPTED || to CANCELLED; case ACCEPTED: return to ARRIVED || to CANCELLED; case ARRIVED: return to IN_TRIP || to CANCELLED; case IN_TRIP: return to UNPAID; case UNPAID: return to COMPLETED; default: return false; } }这个校验逻辑看着简单但少了它后面订单状态乱了根本查不出问题出在哪。3.2 司机调度Redis GEO 加抢单锁司机调度有两种模式派单和抢单。代驾平台源码里抢单模式更常见实现也简单。核心逻辑是乘客下单后把订单推给附近一定范围内的司机司机抢单。附近司机怎么查用 Redis 的 GEO 结构// 司机上报位置时写入 Redis GEO public void updateDriverLocation(Long driverId, double lng, double lat) { String key driver:location; redisTemplate.opsForGeo().add(key, new Point(lng, lat), driverId.toString()); // 同时设置过期时间避免离线司机一直留在集合里 redisTemplate.expire(key, 5, TimeUnit.MINUTES); } // 查询附近 3 公里的司机 public ListLong findNearbyDrivers(double lng, double lat, double radiusKm) { String key driver:location; Circle circle new Circle(new Point(lng, lat), new Distance(radiusKm, Metrics.KILOMETERS)); GeoResultsRedisGeoCommands.GeoLocationString results redisTemplate.opsForGeo().radius(key, circle); return results.getContent().stream() .map(r - Long.parseLong(r.getContent().getName())) .collect(Collectors.toList()); }司机位置上报频率建议 5 到 10 秒一次太频繁 Redis 写入压力大太慢附近司机查不准。抢单环节必须加锁否则两个司机同时抢同一单会出问题public boolean grabOrder(Long orderId, Long driverId) { String lockKey order:lock: orderId; // SETNX 加过期时间保证原子性 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, driverId.toString(), 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 再次检查订单状态防止已被抢 Order order orderMapper.selectById(orderId); if (order.getStatus() ! OrderStatus.WAITING.getCode()) { return false; } order.setDriverId(driverId); order.setStatus(OrderStatus.ACCEPTED.getCode()); orderMapper.updateById(order); return true; } finally { redisTemplate.delete(lockKey); } } return false; }注意锁的过期时间设 10 秒是因为抢单操作本身很快10 秒足够。但如果业务逻辑里有远程调用要评估调用超时时间锁过期时间必须大于业务执行时间否则锁提前释放并发问题又回来了。3.3 计费规则起步价、里程费、时长费怎么算代驾计费一般由三部分组成起步价含一定公里数、超出里程费、等待时长费。计费规则存在price_rule表里按城市或时间段区分。public BigDecimal calculateFee(Order order, PriceRule rule) { // 起步价 BigDecimal fee rule.getStartPrice(); // 超出起步里程的部分 if (order.getDistance() rule.getStartDistance()) { BigDecimal extraDistance BigDecimal.valueOf(order.getDistance()) .subtract(BigDecimal.valueOf(rule.getStartDistance())); fee fee.add(extraDistance.multiply(rule.getPerKmPrice())); } // 等待时长费按分钟算 if (order.getWaitMinutes() rule.getFreeWaitMinutes()) { int chargeMinutes order.getWaitMinutes() - rule.getFreeWaitMinutes(); fee fee.add(BigDecimal.valueOf(chargeMinutes) .multiply(rule.getPerMinutePrice())); } // 夜间加价 if (isNightTime(order.getCreateTime())) { fee fee.multiply(rule.getNightMultiplier()); } return fee.setScale(2, RoundingMode.HALF_UP); }setScale(2, RoundingMode.HALF_UP)这行别省金额保留两位小数四舍五入。不处理的话前端展示会出现23.999999这种数字。计费规则表的关键字段字段名类型说明start_pricedecimal(10,2)起步价start_distancedecimal(10,2)起步包含公里数per_km_pricedecimal(10,2)超出每公里单价free_wait_minutesint免费等待分钟数per_minute_pricedecimal(10,2)超出后每分钟等待费night_multiplierdecimal(3,2)夜间加价系数如 1.20这些参数改一个最终费用差很多。上线前一定要用真实场景跑一遍3 公里白天、10 公里夜间、等待 20 分钟分别算出来对不对。4. 前后端联调与接口对接的避坑清单前后端分离项目实战里联调阶段出的问题比写代码还多。代驾平台源码涉及小程序端、后端、管理后台三端接口对不上的情况太常见了。4.1 跨域问题后端加配置别在前端瞎折腾小程序端不存在浏览器跨域问题但管理后台是 Web 端调后端接口会跨域。后端加一个全局 CORS 配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }allowedOriginPatterns(*)和allowCredentials(true)同时用时Spring Boot 2.4 以上版本必须用allowedOriginPatterns而不是allowedOrigins否则启动报错。这个坑我踩过日志里报When allowCredentials is true, allowedOrigins cannot contain *换成allowedOriginPatterns就好了。4.2 按钮重复提交前端防抖加后端幂等乘客点「呼叫代驾」按钮网络卡的时候用户会连点结果生成两笔订单。前后端对于按钮重复提交校验方法常见做法是双管齐下前端加防抖// 小程序端按钮点击防抖 let submitting false; function callDriver() { if (submitting) return; submitting true; wx.showLoading({ title: 正在呼叫... }); wx.request({ url: config.baseUrl /order/create, method: POST, data: { ... }, complete: () { submitting false; wx.hideLoading(); } }); }后端加幂等// 用 Redis 做幂等同一个用户 5 秒内只能下一单 public Result createOrder(Long userId, OrderDTO dto) { String idempotentKey order:create: userId; Boolean first redisTemplate.opsForValue() .setIfAbsent(idempotentKey, 1, 5, TimeUnit.SECONDS); if (Boolean.FALSE.equals(first)) { return Result.error(操作太频繁请稍后再试); } // ... 创建订单逻辑 }前端防抖是体验优化后端幂等才是兜底。只做前端不做后端用户抓包重放照样能刷单。4.3 时间格式前后端统一用时间戳小程序端new Date()和后端LocalDateTime直接传时区一乱就出问题。统一做法是接口传输用时间戳毫秒前端展示时再格式化。// 小程序端格式化时间戳 function formatTime(timestamp) { const date new Date(timestamp); const y date.getFullYear(); const m String(date.getMonth() 1).padStart(2, 0); const d String(date.getDate()).padStart(2, 0); const h String(date.getHours()).padStart(2, 0); const min String(date.getMinutes()).padStart(2, 0); return ${y}-${m}-${d} ${h}:${min}; }后端返回给前端的字段统一用Long类型的时间戳不要返回格式化好的字符串。字符串格式一变前端就得跟着改。4.4 司机位置推送WebSocket 断线重连司机位置实时推送给乘客用 WebSocket 比轮询省资源。但 WebSocket 会断必须加重连机制。// 小程序端 WebSocket 重连 let socketTask null; let reconnectTimer null; function connectWs() { socketTask wx.connectSocket({ url: config.wsUrl ?token getToken() }); socketTask.onClose(() { // 断线后 3 秒重连 reconnectTimer setTimeout(connectWs, 3000); }); socketTask.onError(() { socketTask.close(); }); } function closeWs() { clearTimeout(reconnectTimer); if (socketTask) socketTask.close(); }重连间隔别设太短3 到 5 秒比较合适。太短了服务端压力大太长了乘客看到司机位置半天不动。5. 代驾平台源码上线前必须排查的 5 个坑源码本地跑通只是第一步上线前这几个坑不排掉生产环境必翻车。5.1 支付回调没验签订单被伪造现象订单支付状态被随意改成「已支付」但实际没收到钱。原因支付回调接口没有验证微信支付的签名任何人构造一个 POST 请求就能把订单改成已支付。解决回调接口必须验签用微信支付 SDK 提供的WXPayUtil.isSignatureValid()方法校验。验签通过后再更新订单状态并且要校验回调里的订单金额和本地订单金额是否一致。PostMapping(/pay/callback) public String payCallback(RequestBody String xmlData) { MapString, String result WXPayUtil.xmlToMap(xmlData); // 验签 if (!WXPayUtil.isSignatureValid(result, mchKey)) { return xmlreturn_code![CDATA[FAIL]]/return_code/xml; } // 校验金额 String orderId result.get(out_trade_no); BigDecimal callbackAmount new BigDecimal(result.get(total_fee)) .divide(BigDecimal.valueOf(100)); // 微信金额单位是分 Order order orderMapper.selectById(orderId); if (order.getAmount().compareTo(callbackAmount) ! 0) { return xmlreturn_code![CDATA[FAIL]]/return_code/xml; } // 更新订单状态 order.setStatus(OrderStatus.COMPLETED.getCode()); orderMapper.updateById(order); return xmlreturn_code![CDATA[SUCCESS]]/return_code/xml; }5.2 司机位置数据没清理Redis 内存暴涨现象Redis 内存持续增长几天后 OOM。原因司机每次上报位置都往 GEO 集合里写但司机下线后没有清理集合越来越大。解决两个措施。一是每次写入位置时给整个 GEO key 设过期时间前面代码里已经加了expire。二是司机主动下线时调用redisTemplate.opsForGeo().remove()删掉该司机的位置记录。另外定时任务每天凌晨清理一次超过 24 小时没更新的司机位置。5.3 订单超时未支付库存没释放现象乘客下单后不支付订单一直挂在「待支付」状态司机被占用无法接新单。原因没有超时取消机制。解决用 Redis 的过期键或者延迟队列实现。简单做法是下单时往 Redis 写一个 key设置 15 分钟过期同时起一个定时任务扫描过期 key把对应订单取消。// 下单时写入 redisTemplate.opsForValue().set(order:timeout: orderId, 1, 15, TimeUnit.MINUTES); // 定时任务每分钟扫描 Scheduled(cron 0 * * * * ?) public void cancelTimeoutOrders() { SetString keys redisTemplate.keys(order:timeout:*); for (String key : keys) { String orderId key.replace(order:timeout:, ); Order order orderMapper.selectById(orderId); if (order ! null order.getStatus() OrderStatus.UNPAID.getCode()) { order.setStatus(OrderStatus.CANCELLED.getCode()); orderMapper.updateById(order); } } }注意redisTemplate.keys()在生产环境慎用数据量大时会阻塞 Redis。更好的做法是用 Redis 的过期事件通知或者用专门的延迟队列。5.4 小程序端请求没带 token接口全返回 401现象登录后调其他接口全部返回 401。原因登录成功后 token 存了但请求拦截器没把 token 加到 header 里。解决在小程序端封装统一的请求方法自动带上 token。function request(options) { const token wx.getStorageSync(token); return new Promise((resolve, reject) { wx.request({ url: config.baseUrl options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: token ? Bearer token : }, success: (res) { if (res.statusCode 401) { // token 过期跳转登录 wx.redirectTo({ url: /pages/login/login }); reject(res); } else { resolve(res.data); } }, fail: reject }); }); }5.5 数据库连接池太小高峰期接口超时现象白天低峰期正常晚上代驾高峰期接口大量超时。原因默认连接池HikariCP最大连接数只有 10高峰期并发上来后连接不够用。解决根据实际并发调整连接池参数。spring: datasource: hikari: maximum-pool-size: 50 # 最大连接数 minimum-idle: 10 # 最小空闲连接 connection-timeout: 3000 # 获取连接超时时间毫秒 idle-timeout: 600000 # 空闲连接超时时间 max-lifetime: 1800000 # 连接最大存活时间maximum-pool-size不是越大越好一般设为 CPU 核数乘以 2 再加磁盘数。50 对于中小型代驾平台够用了。改完用SHOW PROCESSLIST看 MySQL 实际连接数别超过数据库的max_connections。6. 从源码到可运营平台二次开发的关键改动点源码能跑通、坑也排完了但直接拿开源代驾平台源码上线运营还差几步关键改动。这部分我按实际改造经验说几个必须动的地方。6.1 计费规则要支持按城市配置源码里计费规则通常只有一套全局配置。实际运营中不同城市起步价、里程费都不一样。改造方式是在price_rule表加一个city_code字段查询时根据订单所在城市匹配规则。ALTER TABLE price_rule ADD COLUMN city_code VARCHAR(10) DEFAULT GLOBAL COMMENT 城市编码; CREATE INDEX idx_city_code ON price_rule(city_code);对应的查询逻辑改成public PriceRule getPriceRule(String cityCode) { // 先查城市专属规则没有则用全局规则 PriceRule rule priceRuleMapper.selectByCityCode(cityCode); if (rule null) { rule priceRuleMapper.selectByCityCode(GLOBAL); } return rule; }6.2 司机端要加接单范围限制源码里司机能看到所有待接单订单实际运营中司机只能接自己服务范围内的单。改造方式是在司机表加service_radius字段查询待接单列表时用 Redis GEO 过滤。public ListOrder getAvailableOrders(Long driverId) { Driver driver driverMapper.selectById(driverId); // 查司机当前位置 Point driverPoint getDriverPoint(driverId); // 查司机服务半径内的待接单订单 return orderMapper.selectNearbyWaitingOrders( driverPoint.getX(), driverPoint.getY(), driver.getServiceRadius()); }对应的 SQL 用 MySQL 的空间函数或者手动算经纬度距离。数据量不大时手动算就行SELECT *, (6371 * ACOS( COS(RADIANS(#{lat})) * COS(RADIANS(start_lat)) * COS(RADIANS(start_lng) - RADIANS(#{lng})) SIN(RADIANS(#{lat})) * SIN(RADIANS(start_lat)) )) AS distance FROM order WHERE status 0 HAVING distance #{radius} ORDER BY distance ASC;6.3 加一层风控防止司机刷单代驾平台最常见的作弊行为是司机自己下单自己接刷平台补贴。风控逻辑不复杂但必须有public boolean checkRisk(Order order) { // 1. 同一司机同一乘客短时间内多次订单 int recentCount orderMapper.countRecentOrders( order.getDriverId(), order.getUserId(), 24); if (recentCount 3) { return false; } // 2. 订单距离异常短但费用异常高 if (order.getDistance() 0.5 order.getAmount().compareTo( BigDecimal.valueOf(50)) 0) { return false; } // 3. 司机和乘客设备指纹相同需要前端上报设备信息 if (order.getDriverDeviceId() ! null order.getDriverDeviceId().equals(order.getUserDeviceId())) { return false; } return true; }风控规则不用一开始就写全先上两三条最明显的后面根据实际数据慢慢加。6.4 日志和监控出问题能查到原因上线前把日志配好不然出了问题只能靠猜。关键接口的入参、出参、耗时都打出来Around(execution(* com.daijia.controller..*(..))) public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { long start System.currentTimeMillis(); String method joinPoint.getSignature().toShortString(); Object[] args joinPoint.getArgs(); log.info(请求开始: {} 参数: {}, method, JSON.toJSONString(args)); try { Object result joinPoint.proceed(); long cost System.currentTimeMillis() - start; log.info(请求结束: {} 耗时: {}ms 返回: {}, method, cost, JSON.toJSONString(result)); return result; } catch (Exception e) { log.error(请求异常: {} 参数: {}, method, JSON.toJSONString(args), e); throw e; } }日志里别打密码和 token用JsonIgnore或者手动过滤。日志文件按天切割保留 30 天就够了。6.5 我踩过的一个坑订单金额用 double 算差了一分钱最后说一个我自己的血泪教训。早期版本里订单金额用double类型计算测试环境没问题上线后财务对账发现每天差几毛钱。原因是double浮点运算有精度丢失0.1 0.2不等于0.3。后来全部改成BigDecimal并且所有金额字段在数据库里用decimal(10,2)问题才解决。如果你拿到的源码里金额字段是double或者float别犹豫全部改成BigDecimal。这个改动越早做越好等订单数据多了再改迁移成本翻倍。代驾平台源码从跑通到上线中间隔着的不是代码量是对业务细节的理解。计费规则、状态流转、并发控制、风控每一块都得按自己的运营场景调。我一般拿到一套新源码先跑通主流程然后花两天时间专门读订单和计费相关的代码把每个参数的含义搞清楚再动手改。希望帮到你。本文还有配套的精品资源点击获取