简介滴滴顺风车小程序V1.1.9完整源码包基于出行场景开发面向需要快速搭建或二次开发顺风车平台的开发者、个人站长及中小团队。资源整合微信小程序前端与PHP后端覆盖乘客与车主匹配、实时竞价、一键叫车、行程发布接单等核心环节适用于城市级共享交通运营管理。包体共2000个文件包含474个JS前端逻辑、433个PHP后端脚本、293个HTML页面及CSS、图片等配套资源压缩后约81.84MB结构清晰便于定位。目前已有48人学习。源码内含admin.php后台管理入口、module.php模块调度、m_upgrade.php升级脚本等核心文件支持小程序名称自定义、手机号显示开关、前端图标优化等配置1.1.9版本重点修复多开小程序时的数据隔离与页面显示异常需同步更新前端。另附多个readme说明及“点击查最新更新”批处理脚本便于部署前核对文件完整性适合本地测试、功能扩展或区域化顺风车平台搭建。1. 顺风车小程序源码包V1.1.9里最值得吃透的是竞价撮合和城市调度顺风车小程序源码包在技术社区里流转的版本不少V1.1.9是其中结构相对完整的一套前端小程序负责乘客下单、司机接单后端把订单、竞价和城市配置做成独立模块。我见过不少第一次拿到这套源码的人最先翻车的不是项目跑不起来而是竞价撮合在本地根本验证不出效果城市共享出行模块又被外部接口卡住。这篇文章就围绕这两个核心点展开讲清楚包内模块怎么拆、本地怎么跑通、竞价逻辑怎么改、城市管理参数调哪里。适合准备拿它做毕设、做共享出行产品Demo以及想从网约车切到顺风车场景的开发者参考。2. 源码包结构拆解:前后端边界、业务域与核心数据模型一套完整的源码包到手后先别急着点运行把目录结构和数据模型理清楚后面排错会省一半时间。V1.1.9这类顺风车包的常见组织方式是后端按业务域拆模块前端拆成乘客端和司机端两套小程序公共部分抽到 common 或 utils。下面按我处理这类包的惯例列一个对照用的目录组织可以拿你手上的包比对着看不必强求文件名完全一致。backend/ gateway/ # 统一入口处理登录态和路由转发 order-center/ # 订单主流程发布、状态流转、取消、支付回调 bid-center/ # 竞价模块出价、撮合、超时回落 city-service/ # 城市配置、运力池、计价倍率、共享出行管理 common/ # 公共组件分布式锁、序列化、统一异常处理 frontend/ passenger-app/ # 乘客端小程序发单、竞价、支付、行程 driver-app/ # 司机端小程序抢单、报价、接单、导航这套拆分逻辑里的关键点在于bid-center单独成一个模块而不是塞进order-center。这样做的直接好处是竞价策略调整时不需要动订单表结构两个模块通过订单号和状态字段协作互不侵入。很多简化版源码包会把竞价记录直接挂在订单表上短期看省事一旦要加“多轮竞价”“竞价超时自动回落”这类功能表结构就要推倒重来这也是我判断一套源码包是否“完整”的一个简单标准。2.1 从服务划分看设计意图为什么小程序要拆两端公共代码放哪里乘客端和司机端拆成两个独立小程序工程看起来是重复劳动实际上是为了避开微信小程序里“一个账号同时登录两个角色”的各种历史包袱。乘客端关心发布行程、查看报价、支付司机端关心附近订单、出价、确认接单。两者界面差异很大拆开后权限模型也清楚了乘客端调不到司机端的报价接口司机端也看不到乘客端支付记录。两个前端工程会共享一套请求封装和工具函数常见做法是在frontend/下放一个common/目录两个小程序通过 npm 包或直接复制的方式引用。这里不建议做第三方组件库的强依赖源码包大概率跑在一个相对独立的内网环境里能少一个安装步骤就少一个后面部署时踩坑也少。角色权限控制放在后端的gateway里处理前端只负责把 token 带给后端具体能调哪些接口由后端接口签名决定。2.2 订单、竞价、城市配置三张核心表字段设计思路与为什么要这样建源码包里能撑起“城市共享出行管理”这个概念的核心是t_city_config这张表它承载了城市维度的计价倍率、竞价上下限、调度半径等参数。下面是一份我在类似系统里惯用的建表结构和 V1.1.9 这类源码里常见的字段设计基本能对应上。CREATE TABLE t_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号业务维度唯一, passenger_id bigint(20) NOT NULL, driver_id bigint(20) DEFAULT NULL COMMENT 确认后写入司机ID, status tinyint(4) NOT NULL COMMENT 状态机见第6章, bid_type tinyint(4) NOT NULL COMMENT 1一口价 2竞价, base_amount decimal(10,2) NOT NULL COMMENT 推荐价/一口价, final_amount decimal(10,2) DEFAULT NULL COMMENT 最终成交价, city_code varchar(10) NOT NULL COMMENT 城市编码贯穿所有业务, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_order_no (order_no), KEY idx_city_status (city_code,status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_bid_record ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, driver_id bigint(20) NOT NULL, bid_amount decimal(10,2) NOT NULL, is_selected tinyint(1) NOT NULL DEFAULT 0, status tinyint(4) NOT NULL COMMENT 1报价中 2已过期 3已选中, valid_until datetime NOT NULL COMMENT 报价有效期超时自动置为过期, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_order_driver (order_no,driver_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_city_config ( id bigint(20) NOT NULL AUTO_INCREMENT, city_code varchar(10) NOT NULL COMMENT 城市行政编码, city_name varchar(20) NOT NULL, base_price decimal(10,2) NOT NULL COMMENT 起步价, per_km_price decimal(10,2) NOT NULL COMMENT 每公里单价, peak_factor decimal(4,2) NOT NULL DEFAULT 1.00 COMMENT 高峰期倍率, weather_factor decimal(4,2) NOT NULL DEFAULT 1.00 COMMENT 恶劣天气倍率, max_dispatch_radius int(11) NOT NULL DEFAULT 5 COMMENT 调度半径单位公里, min_bid_amount decimal(10,2) NOT NULL COMMENT 竞价最低出价, max_bid_amount decimal(10,2) NOT NULL COMMENT 竞价最高出价, PRIMARY KEY (id), UNIQUE KEY uk_city_code (city_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;三个表放在一起能看出几个关键设计决定。第一所有金额都用decimal(10,2)不用 float/double这是计费系统的底线浮点误差在单价计算上会被放大成资损问题。第二t_order里的city_code不是冗余城市配置的计价、竞价上下限、天气倍率都要靠它关联订单表不存城市编码后面所有按城市维度的统计和调度计算都要多一次 join。第三t_bid_record单独维护valid_until字段比在代码里判断“订单创建时间固定超时时长”更灵活因为不同类型订单的竞价时长可能不同把超时时间落库定时任务直接按这个字段扫过期记录就行。peak_factor和weather_factor是城市共享出行调度里最常调的两个参数。比如晚高峰和暴雨叠加时系统可以临时把城市倍率调高吸引司机出车竞价上限同步抬高让乘客的出价空间更接近真实市场水平。这些参数不用改代码DBA或运营在后台改一行配置立即生效这正是配置独立成表的价值。3. 本地跑通这套源码包:环境版本、启动顺序与联调验证源码包能跑起来是底线但很多本地环境问题不是代码 bug而是版本错位。V1.1.9按技术栈推断后端面向 JDK 1.8 Spring Boot 2.x数据库 MySQL 5.7 或 8.0缓存 Redis前端是 uni-app 或原生小程序工程。先按下面这套版本组合装环境能省掉大部分装完跑不起来的尴尬。3.1 环境版本清单与几个容易忽略的配置点组件建议版本补充说明JDK1.8不要先用 JDK 17 试源码包的依赖多数没适配高版本MySQL5.78.0 也可用但驱动和时区配置有差异Redis5.x 及以上不需要开集群单机即可Maven3.6.x3.8 对镜像源有默认拦截容易下载依赖失败Node.js14/16仅前端 uni-app 需要原生小程序用不到微信开发者工具稳定版即可建议打开“不校验合法域名”选项MySQL 建库时务必指定utf8mb4这在第 5 章会展开讲。Redis 不需要密码就保持空密码源码包里的application.yml默认配置大多是空密码强行设密码反而会导致连接失败。微信开发者工具里如果导入的是 uni-app 编译后的 dist 目录记得在“详情-本地设置”里勾选“不校验合法域名”否则所有请求都会被拦截。3.2 后端启动三步走改配置、建库、跑模块拿到源码包第一步打开backend下各模块的application.yml把数据库连接、Redis 地址改成你自己的环境。下面这段配置是典型的最小改动server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/carpool?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 # 密码留空则删掉 password 这一行 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8characterEncodingutf8mb4是给数据库连接层用的它和建表语句里的DEFAULT CHARSETutf8mb4必须同时存在否则即使表建对了写入时还是会因为连接层字符集不对而报错。serverTimezoneAsia/Shanghai解决的是 MySQL 8.x 默认时区问题不写这一行时间字段经常比实际时间早 8 小时竞价超时判断会全面错乱。确认配置无误后执行启动命令mvn clean package -DskipTests java -jar backend/gateway/target/gateway-1.1.9.jar如果只想单独启动订单中心做调试不必起全链路服务可以指定 profile 并调整端口避免冲突java -jar backend/order-center/target/order-center-1.1.9.jar \ --spring.profiles.activedev \ --server.port8081--server.port覆盖配置这个习惯建议保留。本地调试竞价撮合时我通常会同时起order-center和bid-center两个模块各占一个端口用 postman 模拟乘客和司机两端请求这样比启动完整网关链路更快暴露问题。启动过程中盯着日志里是否有Started xxxApplication没有出现这行说明某个模块的依赖没有加载完按异常堆栈去找缺什么即可。3.3 前端小程序跑通首页从登录态到行程列表的一次完整请求链后端起来了接着验证小程序到后端的链路。V1.1.9 这类源码包里小程序端基本都封装了一个统一的请求对象核心逻辑类似下面这段换汤不换药// common/request.js const baseUrl http://localhost:8080; const request (url, data {}, method GET) { return new Promise((resolve, reject) { wx.request({ url: baseUrl url, method, data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || }, success: res { if (res.statusCode 401) { wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/index }); reject(res); return; } if (res.data.code ! 0) { wx.showToast({ title: res.data.message, icon: none }); reject(res.data); return; } resolve(res.data.data); }, fail: err { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); }; module.exports request;这里的baseUrl是本地联调的关键。真机预览时localhost指向的是手机自己而不是电脑所以想用手机测要把baseUrl改成电脑的局域网 IP例如http://192.168.31.100:8080。开发者工具里用localhost没问题但注意模拟器的网络环境和真实手机不同部分接口在模拟器正常、真机失败排查时先确认 baseUrl。请求链路跑通的标志是开发者工具 Network 面板看到请求返回业务数据控制台没有报错。如果啥都返回不了先看后端日志里有没有请求进来再往前端看请求是否发出去按这个方向排查比盲目改代码有效得多。登录流程里 token 由后端登录接口签发前端存到wx.setStorageSync竞价接口的登录态校验也走同一个 token这条链路通了后面调竞价接口才不会被拦在门外。4. 竞价叫车功能拆解:从乘客出价到司机接单的完整代码路径竞价是这套源码包最有辨识度的功能。传统顺风车是一口价乘客接受就下单竞价模式把价格决定权交给市场乘客发布行程时选择竞价系统按城市配置生成建议价格区间乘客设定自己的心理价位司机端看到符合条件的行程后可报价乘客再确认其中一位司机订单进入锁定状态。整个过程比一口价多出两个状态节点也是最容易改出 bug 的地方。4.1 竞价工作流程与订单状态多出来的两个节点竞价模式下订单状态流转会经历初始创建 → 竞价中 → 待乘客确认 → 已确认 → 行程中 → 已完成。和一口价订单相比核心差异在“竞价中”和“待乘客确认”这两个节点。状态含义谁触发下一步竞价中司机可出价乘客不可取消司机出价/系统超时待乘客确认至少一个司机出价等乘客选择乘客确认/系统超时已确认乘客选定司机订单锁定双方进入履约已取消超时无人出价/乘客主动取消系统/乘客这里要注意一个边界乘客在“竞价中”不建议支持直接取消否则司机出了价、乘客直接关单走人会严重影响司机端体验。常见的做法是把取消权限放到“待乘客确认”之后扣取一定违约费或者完全不允许取消只允许“乘客放弃选择并等待超时”。V1.1.9 这种偏完整的源码包里默认策略通常是允许乘客取消但限制为每天有限次数具体看业务常量定义。4.2 出价校验与超时过期两个绕不开的边界处理司机端提交报价时后端必须先做两道校验订单是否还在竞价状态、出价是否在城市配置的区间内。下面是一段典型的处理逻辑public BidResult submitBid(BidRequest request) { // 1. 校验订单不存在或已离开竞价状态 Order order orderService.getByOrderNo(request.getOrderNo()); if (order null || order.getStatus() ! OrderStatus.BIDDING.getCode()) { return BidResult.fail(订单不存在或已结束竞价); } // 2. 按城市配置校验出价上下限 CityConfig config cityConfigService.getByCityCode(order.getCityCode()); if (request.getBidAmount().compareTo(config.getMinBidAmount()) 0 || request.getBidAmount().compareTo(config.getMaxBidAmount()) 0) { return BidResult.fail(出价超出该城市允许的竞价区间); } // 3. 同一司机重复出价覆盖上一轮而不是插入新记录 BidRecord record bidRecordMapper.getByOrderAndDriver( request.getOrderNo(), request.getDriverId()); if (record ! null) { record.setBidAmount(request.getBidAmount()); record.setValidUntil(DateUtil.addMinutes(new Date(), config.getBidTimeoutMinutes())); bidRecordMapper.updateById(record); return BidResult.success(报价已更新); } // 4. 首次出价插入新记录 BidRecord newRecord new BidRecord(); newRecord.setOrderNo(request.getOrderNo()); newRecord.setDriverId(request.getDriverId()); newRecord.setBidAmount(request.getBidAmount()); newRecord.setValidUntil(DateUtil.addMinutes(new Date(), config.getBidTimeoutMinutes())); newRecord.setStatus(BidStatus.BIDDING.getCode()); bidRecordMapper.insert(newRecord); return BidResult.success(报价成功); }先说校验顺序的意义。先查订单状态再查金额是因为如果连订单都不是竞价中就没有必要继续读城市配置减少一次缓存或数据库查询。compareTo替代使用 和 是因为BigDecimal的 equals 方法会同时比较精度用 compareTo 才是数值比较。bidTimeoutMinutes这个参数写在t_city_config里而不是写死在代码中因为不同城市的竞价活跃度不一样一线城市司机多竞价窗口可以设短一点比如 5 分钟二三线城市司机密度低窗口延长到 15 分钟让更多司机看到订单。城市共享出行管理模块的核心价值就在这里一个城市一套参数而不是全平台一套逻辑打天下。4.3 撮合与防超卖乘客确认时用乐观锁守住唯一性乘客从报价列表里选中一位司机这个操作必须防并发。想象一下同一个订单有 5 位司机报价乘客在手机屏幕上同时点了两位司机或者网络重试导致重复提交如果后端不做控制订单可能同时确认给两个人这在顺风车场景里就是重大事故。常见的可靠做法是用数据库条件更新来实现乐观锁// 利用 status 原本状态 作为乐观锁条件 int rows orderMapper.updateStatusWithCondition( orderNo, OrderStatus.CONFIRMED.getCode(), // 目标状态 OrderStatus.BIDDING.getCode(), // 期望当前状态 driverId, confirmAmount, new Date()); if (rows ! 1) { throw new BizException(订单已被处理请刷新后重试); } // 同时把这条司机报价置为选中其他报价置为过期 bidRecordMapper.updateSelected(orderNo, driverId); bidRecordMapper.updateExpiredExcept(orderNo, driverId);核心逻辑只有一行update ... where order_no? and status竞价中数据库自带行锁两个并发请求同时执行时只有一个能更新成功影响行数为 0 的那个直接返回失败。这比先查后改安全得多先查后改在“查”和“改”之间有空隙并发请求会同时读到“竞价中”状态然后都去执行更新造成超卖。confirmAmount是乘客确认时最终接受的价格它可能和司机的报价不一致——乘客可以对司机报价进行小幅还价这属于产品策略的延伸。源码包里如果没做还价这个参数直接用司机的出价即可。确认成功后t_bid_record里同一个订单的其他报价全部置为过期司机端列表里这些订单会被刷掉避免司机继续报价被拒。5. 避坑指南:这套源码包最常见的五个翻车现场源码包部署翻车十次有八次不是代码逻辑问题而是环境配置和边界条件。下面这五条是从这类项目中沉淀下来的高频问题按“现象-原因-解决”拆开讲能帮你省下不少排查时间。5.1 Redis 里的城市配置读出来变成 LinkedHashMap强转报错现象服务启动正常但调用城市配置接口时报ClassCastException: java.util.LinkedHashMap cannot be cast to CityConfig竞价接口跟着全部报错。原因代码里把CityConfig对象直接存进 Redis取的时候用Object接收再强转成CityConfig。Spring 默认用 JDK 序列化一旦配置改成 Jackson 或 Fastjson 序列化反序列化结果就是 LinkedHashMap强转必炸。解决改为显式存取 JSON 字符串取的时候用JSON.parseObject(json, CityConfig.class)指定目标类型。同时顺手确认t_city_config表里的peak_factor这类字段在 JSON 串里是数字而不是字符串否则又会踩精度坑。5.2 小程序请求全部失败调试器显示 statusCode 为 0现象后端接口用 postman 测都正常小程序端请求全部失败控制台报request:fail。原因开发工具默认校验合法域名本地调试时接口地址是http://localhost:8080既不是 HTTPS 也不在域名白名单里请求直接被拦截。解决微信开发者工具右上角“详情-本地设置”里勾选“不校验合法域名”。注意这个选项只对开发者工具有效真机预览还需要在手机上开启调试模式。真机连不上时优先检查baseUrl是不是被写成了localhost真机上要换成电脑的局域网 IP。5.3 同一竞价订单被两个司机同时接走重复确认现象乘客端只确认了一次但订单表和竞价记录表里出现两个司机同时处于is_selected1线上反馈“这个单子怎么显示被两个人接了”。原因确认逻辑是“先查订单状态再执行更新”两个请求同时读到了“竞价中”先后执行更新都认为自己是合法的。解决用第 4 章的条件更新方案更新 SQL 里带上where status 竞价中数据库行锁会保证只有一个请求更新成功影响行数为 0 的直接抛异常。这是防超卖最可靠的做法不依赖任何中间件。代码上线前用两个线程同时调确认接口压一次能看到预期效果。5.4 用户微信昵称写入数据库报错Incorrect string value现象新用户登录写入昵称时MySQL 抛出Incorrect string value: \xF0\x9F\x98\x81 for column nickname订单模块也跟着回滚。原因数据库或连接串字符集是utf8而微信昵称里包含 emoji 表情emoji 在 Unicode 里属于补充平面必须用utf8mb4存储。建表语句写utf8mb4但连接串漏了等于没改。解决建库语句用CREATE DATABASE carpool DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci连接串里加characterEncodingutf8mb4。如果线上库已经建了用ALTER TABLE t_user CONVERT TO CHARACTER SET utf8mb4批量转换注意转换期间会锁表要在低峰期操作。5.5 城市共享出行模块估价为空日志提示地图服务 key 未配置现象乘客端输入起点终点后车型列表能出来但预估价一直是 0后台日志报地图服务鉴权失败或key不存在。原因V1.1.9 的计价模块依赖外部地图接口做距离计算源码包里一般留了一个MapService接口默认实现是调外部服务商key 没填就返回空。解决要么申请一个地图服务商 key 填到配置里要么把MapService的实现换成本地 mock用两个经纬度坐标走 Haversine 公式算直线距离再乘以 1.3 的绕路系数本地演示完全够用。我在本地调竞价逻辑时就是这么处理的不依赖外网接口随时返回结果开发效率高很多。真正上生产再换回正式的路径规划服务接口签名不变对上层业务代码零侵入。6. 二次开发建议:先把订单流转改成显式状态机再动业务代码绝大多数源码包在订单状态这一块用的是“魔法数字 if 判断”的散装写法V1.1.9 这类包也不例外。比如if (status 2)表示竞价中if (status 3)表示确认中代码里到处散落着含义不明的数字改一个状态后面漏一个分支是这类项目二次开发最大的隐性成本。我拿到任何源码包第一件事不是加功能而是把订单状态重构成显式枚举状态机。public enum OrderStatus { INIT(0, 初始创建), BIDDING(1, 竞价中), CONFIRMING(2, 待乘客确认), CONFIRMED(3, 已确认), IN_SERVICE(4, 行程中), FINISHED(5, 已完成), CANCELLED(6, 已取消); private final int code; private final String desc; private static final MapString, ListOrderStatus TRANSITION_MAP new HashMap(); static { TRANSITION_MAP.put(INIT, Arrays.asList(BIDDING, CANCELLED)); TRANSITION_MAP.put(BIDDING, Arrays.asList(CONFIRMING, CANCELLED)); TRANSITION_MAP.put(CONFIRMING, Arrays.asList(CONFIRMED, BIDDING, CANCELLED)); TRANSITION_MAP.put(CONFIRMED, Arrays.asList(IN_SERVICE, CANCELLED)); TRANSITION_MAP.put(IN_SERVICE, Arrays.asList(FINISHED, CANCELLED)); TRANSITION_MAP.put(FINISHED, Collections.emptyList()); TRANSITION_MAP.put(CANCELLED, Collections.emptyList()); } public boolean canTransferTo(OrderStatus target) { return TRANSITION_MAP.get(this.name()).contains(target); } }状态流转条件明确写在一个枚举里任何代码要改状态都必须经过canTransferTo校验非法跳转直接抛异常。比如“初始创建”不能直接跳到“已确认”必须走完竞价流程“已取消”是终态不允许再改到其他任何状态。这样一来不用全局搜数字判断逻辑收敛到一处二次开发时新加一个“乘客还价中”状态只需要在枚举里加值、在状态机里补流转关系改动的类和文件数量极少。当前状态允许流转到说明INITBIDDING, CANCELLED发布成功后进入竞价BIDDINGCONFIRMING, CANCELLED有司机出价后等确认CONFIRMINGCONFIRMED, BIDDING, CANCELLED可回落重新竞价CONFIRMEDIN_SERVICE, CANCELLED双方开始履约IN_SERVICEFINISHED, CANCELLED行程结束或异常取消这套重构做完原来订单模块里一半以上的 if/else 都能删掉剩下的业务代码只关心“动作”不再关心“合法性”。我早期偷懒直接用数字 status 到处赋值结果加一个行车状态时漏改了订单完成逻辑线上出现已完成的订单还能被取消追查花了两天。后来所有接手的项目都先做状态机重构这个习惯帮我避开了大量低级故障。你拿 V1.1.9 做二次开发时不妨先从这一处入手用最少的代码量把最核心的订单边界守住再放开手脚去改竞价策略和城市调度逻辑。希望帮到你。本文还有配套的精品资源点击获取