
前言
游戏陪玩搭子服务随着电竞产业的爆发,已成为一个不容忽视的细分赛道。作为技术人,从架构设计到落地实战,踩了不少坑,也积累了一些经验。
今天从技术架构的角度,拆解一套陪玩系统。这套方案基于PHP(ThinkPHP6)+ UniApp实现,涵盖了老板、店员、客服三个核心角色的全流程管理。我会重点分享数据库设计、高并发抢单的锁机制、订单状态机等核心模块的实战经验,以及如何与阿里云产品深度结合。
注:本文为技术经验分享,代码为伪代码/逻辑示意,实际项目需根据业务调整。
一、业务模型与云原生架构设计
1.1 核心业务角色
陪玩平台的业务逻辑围绕订单展开,涉及三个核心角色:
角色 核心诉求 关键操作
老板(用户端) 快速下单、精准匹配 筛选游戏、选择服务模式(指定/抢单/派单)、支付、查看进度
店员(接单端) 便捷接单、打造个人IP 抢单/接单、上传截图、申请结算、发布动态、管理个人战绩卡
客服(管理端) 全程管控、保障履约 派单分配、审核截图、线下报单录入、实时沟通
1.2 架构设计
基于阿里云产品的整体架构设计:
│ 客户端层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │微信小程序│ │H5/公众号 │ │ 安卓APP │ │ iOS APP │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ↑ UniApp 多端编译 │
├─────────────────────────────────────────────────────────────┤
│ 负载均衡层 │
│ ┌─────────────────────────┐ │
│ │ SLB (负载均衡) │ │
│ └─────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 应用层 │
│ ┌─────────────────────────────────────────────┐ │
│ │ ECS 集群 (PHP-FPM) │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │业务服务│ │订单服务│ │支付服务│ │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ └─────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 中间件层 │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Redis (缓存/锁) │ │ RocketMQ (队列) │ │
│ └─────────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 数据层 │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ RDS MySQL (主库) │ │ OSS (图片/文件) │ │
│ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
1.3 阿里云产品选型
服务类型 产品选型 规格建议 核心用途
计算 ECS 4核8G * 2台(起步) 部署PHP应用、Nginx
数据库 RDS MySQL 4核8G 存储用户、订单等核心数据
缓存 Redis 2G主从版 会话管理、抢单锁、热点缓存
对象存储 OSS 按量付费 存储用户头像、游戏截图
负载均衡 SLB 按量付费 流量分发
CDN CDN 按量付费 加速静态资源
二、核心数据库设计(RDS MySQL实践)
2.1 用户与角色解耦
采用主表+扩展表模式,避免单表字段爆炸:
不便展示
2.2 订单表设计与索引优化
订单表是系统的核心,必须精心设计索引:
不便展示
索引设计思路:
order_no设置唯一索引,保证业务幂等
boss_id和player_id高频查询,必须建索引
status用于后台批量查询待处理订单
create_time用于分页排序和时间范围查询
2.3 订单流水表(用于对账)
CREATE TABLE `order_logs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL,
`action` varchar(50) NOT NULL COMMENT '操作类型:CREATE/ASSIGN/START/UPLOAD/VERIFY/SETTLE/CANCEL',
`operator_type` tinyint(4) DEFAULT NULL COMMENT '操作人类型:1老板 2打手 3客服 4系统',
`operator_id` int(11) DEFAULT NULL,
`content` varchar(500) DEFAULT '' COMMENT '操作内容',
`create_time` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_order_no` (`order_no`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单操作日志';
三、高并发场景实战:抢单系统的分布式锁
3.1 业务场景
当老板发布一个订单(如"王者荣耀星耀段位代练,价格100元"),所有符合条件的打手都可以抢单。必须保证:
不能超抢:一个订单只能被一个打手抢到
不能漏抢:抢单成功的打手必须锁定订单
高并发:热门订单可能同时有几十个打手点击抢单
3.2 基于Redis的分布式锁实现
使用阿里云Redis,采用SET NX PX原子指令实现分布式锁:
/**
* 抢单处理
* @param int $orderId 订单ID
* @param int $playerId 打手ID
* @return array
*/
public function grabOrder($orderId, $playerId)
{
// 1. 参数校验
$order = OrderModel::find($orderId);
if (!$order || $order->status != 1) {
return ['code' => 0, 'msg' => '订单不存在或已被抢'];
}
// 2. Redis分布式锁 - 原子操作防止并发超抢
$lockKey = "order:grab:{$orderId}";
$lockValue = uniqid($playerId . '_', true); // 唯一值,用于释放锁时验证
// 使用SET NX PX原子指令
$result = Redis::set($lockKey, $lockValue, 'NX', 'PX', 5000); // 5秒超时
if (!$result) {
return ['code' => 0, 'msg' => '手速慢了点,订单已被其他打手抢到'];
}
try {
// 3. 再次检查订单状态(防止在加锁过程中被修改)
$order = OrderModel::find($orderId);
if ($order->status != 1) {
Redis::del($lockKey);
return ['code' => 0, 'msg' => '订单状态已变化'];
}
// 4. 更新订单 - 使用事务保证数据一致性
Db::beginTransaction();
try {
$order->player_id = $playerId;
$order->status = 2; // 已接单
$order->save();
// 记录操作日志
OrderLogModel::create([
'order_no' => $order->order_no,
'action' => 'GRAB',
'operator_type' => 2,
'operator_id' => $playerId,
'content' => '打手抢单成功'
]);
Db::commit();
// 5. 释放锁
$this->releaseLock($lockKey, $lockValue);
return ['code' => 1, 'msg' => '抢单成功'];
} catch (\Exception $e) {
Db::rollback();
$this->releaseLock($lockKey, $lockValue);
throw $e;
}
} catch (\Exception $e) {
// 异常时释放锁
$this->releaseLock($lockKey, $lockValue);
throw $e;
}
}
/**
* 安全释放锁(使用Lua脚本保证原子性)
*/
private function releaseLock($key, $value)
{
$lua = <<<LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
Redis::eval($lua, [$key, $value], 1);
}
3.3 为什么不用数据库行锁?
MySQL行锁 实现简单,事务一致 性能差,连接占用高 低并发场景
Redis分布式锁 性能高,支持高并发 需处理锁超时/释放 高并发抢单
消息队列 削峰填谷,异步处理 实时性略差 可接受排队场景
陪玩平台的抢单场景属于高并发写操作,Redis分布式锁是最优选择。
四、订单状态机设计(告别if-else)
4.1 状态定义
1 PENDING 待接单 客服/打手 派单/抢单 → 2
2 ASSIGNED 已接单 打手 开始执行 → 3
3 EXECUTING 执行中 打手 上传截图 → 4
4 REVIEWING 待验收 客服 审核通过 → 5 / 驳回 → 3
5 SETTLED 已结算 系统 自动分账 → 完成
6 CANCELLED 已取消 系统 -
7 DISPUTE 申诉中 客服 人工处理
4.2 状态机实现(状态模式)
/**
* 订单状态机
*/
class OrderStateMachine
{
private $stateMap = [
1 => PendingState::class, // 待接单
2 => AssignedState::class, // 已接单
3 => ExecutingState::class, // 执行中
4 => ReviewingState::class, // 待验收
5 => SettledState::class, // 已结算
6 => CancelledState::class, // 已取消
7 => DisputeState::class, // 申诉中
];
/**
* 执行状态流转
*/
public function transition(Order $order, $action, $operator)
{
$currentState = $this->getState($order->status);
// 检查当前状态是否允许该操作
if (!$currentState->can($action)) {
throw new \Exception("当前状态不允许执行{$action}操作");
}
// 执行操作前的验证
$currentState->validate($order, $action, $operator);
// 执行状态流转
$nextStatus = $currentState->next($action);
// 更新订单状态
$order->status = $nextStatus;
$order->save();
// 记录日志
$this->logTransition($order, $action, $operator);
return $order;
}
private function getState($status)
{
$class = $this->stateMap[$status];
return new $class();
}
}
/**
* 待接单状态
*/
class PendingState
{
public function can($action)
{
return in_array($action, ['ASSIGN', 'GRAB', 'CANCEL']);
}
public function validate($order, $action, $operator)
{
if ($action == 'GRAB' && $operator['role'] != 2) {
throw new \Exception("只有打手可以抢单");
}
if ($action == 'ASSIGN' && $operator['role'] != 3) {
throw new \Exception("只有客服可以派单");
}
}
public function next($action)
{
switch ($action) {
case 'ASSIGN':
case 'GRAB':
return 2; // 已接单
case 'CANCEL':
return 6; // 已取消
default:
throw new \Exception("未知操作");
}
}
}
状态机的优势:
代码清晰:每个状态的行为封装在自己的类中
扩展性强:新增状态只需添加新的状态类
避免if-else:消除面条式代码
五、UniApp多端适配实战技巧
5.1 条件编译处理各端差异
不同端(小程序/H5/APP)在交互细节上总有差异,用条件编译优雅处理:
// 登录模块适配不同端
methods: {
handleLogin() {
// #ifdef MP-WEIXIN
// 微信小程序:使用wx.login获取code
wx.login({
success: (res) => {
this.code = res.code
this.wxMpLogin()
}
})
// #endif
// #ifdef H5
// H5/公众号:跳转OAuth授权页
window.location.href = `${oauthUrl}?redirect=${encodeURIComponent(location.href)}`
// #endif
// #ifdef APP-PLUS
// A+ plus.oauth登录
plus.oauth.getServices((services) => {
const wx = services.find(s => s.id === 'weixin')
wx.authorize((res) => {
this.wxAppLogin(res.code)
})
})
// #endif
}
}
5.2 图片上传组件封装
打手需要上传游戏截图作为完成凭证,封装统一的图片上传组件:
// utils/upload.js
export const uploadImage = (filePath, options = {}) => {
return new Promise((resolve, reject) => {
const uploadUrl = options.uploadUrl || `${baseURL}/api/upload`
// #ifdef MP-WEIXIN || H5
uni.uploadFile({
url: uploadUrl,
filePath: filePath,
name: 'file',
formData: options.data || {},
success: (res) => {
try {
const data = JSON.parse(res.data)
resolve(data)
} catch (e) {
reject(e)
}
},
fail: reject
})
// #endif
// #ifdef APP-PLUS
// APP端支持压缩和预览
plus.compressImage({
src: filePath,
quality: options.quality || 80,
width: options.width || '1280px',
success: (compressRes) => {
uni.uploadFile({
url: uploadUrl,
filePath: compressRes.target,
name: 'file',
success: (res) => {
try {
const data = JSON.parse(res.data)
resolve(data)
} catch (e) {
reject(e)
}
},
fail: reject
})
},
fail: reject
})
// #endif
})
}
六、总结与思考
6.1 技术要点回顾
架构设计:采用ECS+RDS+Redis的经典云原生架构,弹性伸缩应对业务波动
高并发处理:用Redis分布式锁解决抢单并发,Lua脚本保证原子性
状态管理:用状态机替代if-else,代码清晰易维护
多端适配:UniApp条件编译优雅处理各端差异
性能优化:索引设计、字段裁剪、批量查询、缓存策略
6.2 对陪玩赛道的思考
从技术角度看,陪玩系统的核心难点不在技术栈本身,而在于把三个角色的业务逻辑理顺,保证订单流转不出错,分账逻辑清晰。
PHP负责稳,UniApp负责快,结合阿里云的云原生产品,确实能帮助创业团队快速上线、抢占市场窗口期。
6.3 一点建议
对于想入局游戏陪玩赛道的技术创业者,我的建议是:
初期:用成熟的垂直行业系统(如多客代练系统)快速验证商业模式
中期:根据业务增长逐步引入云原生架构优化
后期:精细化运营,用数据驱动业务决策
技术是为业务服务的,先跑通业务,再优化技术,这才是创业的正确姿势。