城市电竞陪玩调度系统实战:从派单算法到多端协同的架构拆解
城市电竞陪玩调度系统,本质是一套「带实时位置与技能属性的双边撮合平台」:一端是发布陪玩需求的用户,另一端是在线可接单的陪玩师。它和同城跑腿、代驾在调度层面同源,但匹配维度更多------不止看距离,还要看游戏品类、段位区间、语音/线下场景、时段偏好与服务评价。本文结合 Spring Boot + MyBatis Plus + MySQL + UniApp + Vue/ElementUI 这套常见技术组合,拆解一套可落地的调度系统该怎么设计与实现。
一、先厘清业务边界:城市电竞陪玩调度系统要解决什么
把需求拆成四层,后面的技术选型才有依据:
- 发现层:用户按城市、游戏(如 MOBA、FPS、棋牌)、服务形式(线上语音/同城线下)筛选陪玩师。
- 调度层:订单创建后,系统在候选池中筛选并派单,支持抢单、派单、竞价三种模式。
- 履约层:接单后进入会话(IM/语音房间),同步开始时间、时长、战绩截图等凭证。
- 治理层:评价、申诉、风控(虚拟号码、异常行为识别、安全提醒)。
其中调度层是系统的心脏,也是新手容易写崩的地方------常见问题是把「查询附近的人」直接写成全表扫描,或者用数据库行锁硬扛并发派单。
二、技术选型与整体架构
沿用知识库中同类同城系统的成熟组合,分层如下:
| 层次 | 技术 | 说明 |
|---|---|---|
| 后台服务 | Spring Boot + MyBatis Plus | 单体起步,按调度、订单、IM、结算拆分模块 |
| 数据库 | MySQL | 订单与陪玩师主数据 |
| 缓存/位置 | Redis(GEO + 分布式锁) | 在线状态、地理位置、幂等控制 |
| 实时通信 | WebSocket / Netty | 派单推送、IM 消息 |
| 用户端 | UniApp(Vue 语法) | 一套代码发布小程序 / H5 / 公众号 / App |
| 管理后台 | Vue + ElementUI | 调度监控、账号审核、订单干预 |
多端复用的关键是把「端能力差异」收敛到一层适配:小程序拿不到后台定位时降级为手动选点,App 端则启用持续定位上报。业务代码只依赖统一的 LocationProvider 接口。
三、调度引擎:从「附近的人」到「合适的人」
1. 位置索引用 Redis GEO,不用 MySQL 的经纬度计算
MySQL 里 WHERE 里写三角函数算距离,数据量上万后基本不可用。正确做法是陪玩师每次心跳上报位置时写入 Redis GEO:
java
// 位置上报:陪玩师心跳
public void reportLocation(Long providerId, double lng, double lat) {
String key = "geo:provider:" + cityCode;
redisTemplate.opsForGeo().add(key, new Point(lng, lat), String.valueOf(providerId));
// 在线状态单独用 String + TTL,避免僵尸账号
redisTemplate.opsForValue().set("online:provider:" + providerId, "1", 90, TimeUnit.SECONDS);
}
// 候选集查询:3 公里内、多 50 人
public List<Long> findCandidates(String cityCode, double lng, double lat) {
Circle circle = new Circle(new Point(lng, lat),
new Distance(3, Metrics.KILOMETERS));
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
redisTemplate.opsForGeo().radius("geo:provider:" + cityCode,
circle, RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeDistance().sortAscending().limit(50));
return results.getContent().stream()
.map(r -> Long.valueOf(r.getContent().getName()))
.collect(Collectors.toList());
}
2. 多因子打分排序
拿到候选集后,再做一轮加权打分。权重建议做成配置项,便于运营侧调整:
java
public double score(Provider p, Order o, double distanceKm) {
double distanceScore = 1.0 / (1 + distanceKm); // 距离越近越高
double skillScore = p.getGames().contains(o.getGameId()) ? 1.0 : 0.0;
double rankScore = matchRank(p.getRankLevel(), o.getRankRange());
double loadScore = 1.0 / (1 + p.getTodayOrderCount()); // 均衡接单量
double creditScore = normalize(p.getCreditScore()); // 评价与完单率
return 0.35 * distanceScore + 0.25 * skillScore
+ 0.15 * rankScore + 0.15 * loadScore + 0.10 * creditScore;
}
排序后取 Top N 进入派单队列,而不是直接「谁先抢到算谁」------后者在高峰期会造成严重的接单倾斜。
3. 派单的幂等与锁
一个订单同一时刻只能有一个有效接单方,这是典型的并发控制问题。推荐三段式:
- Redis 分布式锁锁订单号,
SET order:lock:{id} value NX EX 10; - 加锁成功后校验订单状态机(
待接单 → 已接单),非法流转直接拒绝; - 用 Lua 脚本保证「校验 + 写入」原子性,避免锁过期后的临界区穿透。
订单状态机建议显式定义,禁止业务代码随意改 status 字段:
CREATED → DISPATCHING → ACCEPTED → SERVING → FINISHED
↘ CANCELED / TIMEOUT
四、实时通信与消息可靠性
派单的体验取决于推送是否及时。WebSocket 长连接是标配,但要处理三个工程问题:
- 连接与用户映射 :网关层维护
userId → channel的本地表,配合 Redis Pub/Sub 做多实例广播,避免单机内存表在扩容后失效。 - 消息不丢:推送只作为「提醒」,客户端收到后必须回源查询订单详情;同时服务端记录消息表,支持离线补偿拉取。
- 超时重派:订单 15 秒内无人接单,进入下一轮候选,同时给用户一个「正在扩大范围」的反馈,而不是静默等待。
IM 部分注意内容审核前置:文本走敏感词过滤,图片走内容安全接口,未过审的消息不落库直接拦截。
五、安全与合规:同城线下场景的必备设计
线上陪玩风险较低,一旦涉及同城线下,安全模块必须做进架构里,而不是事后补丁:
- 虚拟号码:双方在前 30 分钟通过中间号联系,订单结束后自动解绑,避免真实号码泄露。
- 一键报警与行程共享:服务开始后向紧急联系人推送服务时段与位置轨迹。
- 行为风控:同一设备多账号、短时间高频取消、异常接单集中在同一地点等,打标签进入人工复核队列。
- 账号审核:陪玩师入驻需提交游戏战绩截图与实名信息,审核状态与接单权限强绑定。
这些能力在知识库里同类同城系统中通常以「安全中心」的形式统一承载,技术上就是一套规则引擎 + 事件表,不必过度设计。
六、数据库与性能的几个落地建议
- 订单表按
city_code分片或按时间归档,历史订单冷热分离; - 陪玩师列表页的筛选条件组合多,热点查询走 Redis 缓存,写操作后主动失效;
- 战绩、聊天记录等大字段单独建表,主表保持窄行,利于索引效率;
- 所有对外接口做分页上限,避免
limit被无限放大拖垮数据库。
常见问题(FAQ)
Q1:城市电竞陪玩调度系统和普通外卖派单系统在技术上差别大吗?
骨架一致,都是「位置检索 + 候选排序 + 并发接单控制」。差异在匹配因子更多(技能、段位、服务形式),以及需要 IM 与语音房间等履约能力,调度层本身可以复用同一套抽象。
Q2:小团队初期一定要用 Redis GEO 吗?
如果陪玩师在线规模在数百以内,MySQL 加空间索引也能跑。但只要涉及跨城市、频繁位置更新,建议直接上 Redis GEO,迁移成本远低于后期重构。
Q3:如何避免同一订单被多人同时接单?
核心是「单键锁 + 状态机校验 + 原子写入」三件套,并保证锁的过期时间大于业务处理时间,业务侧再做一次乐观锁版本号兜底。
Q4:多端(小程序 / H5 / App)如何减少重复开发?
用 UniApp 统一业务层与请求层,把定位、支付、推送、分享等平台差异封装成条件编译的适配模块,页面与状态管理保持单套实现。