作为一个经历过多个票务系统开发的老手,我深知动物园这类场景的特殊性。与普通电商不同,它需要应对节假日瞬时客流高峰、多种票型组合、现场快速验票等独特需求。这次基于ThinkPHP/Laravel框架实现的系统,核心解决三个痛点:
选择PHP生态不是偶然。相比Java系的笨重和Node.js的运维成本,PHP在中小型票务系统中展现出独特优势:
框架层面采用Laravel为主、ThinkPHP为备选的策略。Laravel的队列系统(Queue)对处理峰值时段的支付回调特别有用,而ThinkPHP的DB类在简单查询场景下性能更优。实际部署时可以根据服务器配置灵活选择。
关键决策点:当预计日均票量超过1万张时,建议使用Laravel + Horizon监控队列;小型动物园(日票量<3000)用ThinkPHP更轻量
门票系统的生死线在于秒杀场景下的稳定性。我们通过三级缓存化解压力:
php复制// Laravel中防止超卖的典型代码
DB::transaction(function() use ($ticket_id) {
$ticket = Ticket::where('id', $ticket_id)
->where('stock', '>', 0)
->lockForUpdate()
->first();
if ($ticket) {
$ticket->decrement('stock');
// 生成订单逻辑...
}
});
动物园的票价不是一成不变的,我们的系统实现了多维度的定价规则:
这在数据库设计中体现为price_rules表:
| 字段名 | 类型 | 说明 |
|---|---|---|
| rule_type | enum('date','group','early') | 规则类型 |
| condition | json | 适用条件(如节假日列表) |
| discount | decimal | 折扣系数 |
| start_date | date | 规则生效时间 |
| end_date | date | 规则失效时间 |
php复制// 票价计算服务类片段
public function calculateFinalPrice($basePrice, $userType, $visitDate, $purchaseDate)
{
$rules = PriceRule::active()->get();
$finalPrice = $basePrice;
foreach ($rules as $rule) {
if ($rule->matches($userType, $visitDate, $purchaseDate)) {
$finalPrice *= $rule->discount;
}
}
return round($finalPrice, 2);
}
我们遇到过太多因为网络问题导致验票失败的投诉,最终设计出这套混合验证方案:
在线验证(首选):
离线备份:
php复制// 生成带时效性的二维码内容
public function generateQrCodeContent($orderId)
{
$payload = [
'order_id' => $orderId,
'exp' => Carbon::now()->addDays(1)->timestamp
];
return JWT::encode($payload, config('app.qr_secret'));
}
在压力测试中,我们发现90%的慢请求都源于N+1查询问题。例如获取订单列表时:
php复制// 错误示范 - 会产生N+1查询
$orders = Order::where('date', $today)->get();
foreach ($orders as $order) {
echo $order->user->name; // 每次循环都查询用户表
}
// 正确做法 - 预加载关联
$orders = Order::with('user')->where('date', $today)->get();
优化手段包括:
with()预加载关联select()指定字段而非SELECT *根据数据变化频率设计多级缓存:
| 缓存级别 | 存储内容 | 过期时间 | 更新策略 |
|---|---|---|---|
| L1: Redis | 实时库存 | 不过期 | 事务同步更新 |
| L2: File | 票价规则 | 1小时 | 定时任务刷新 |
| L3: CDN | 静态资源 | 1年 | 版本号控制 |
特别提醒:门票库存必须用Redis的DECR原子操作,绝对不能用先读后写的模式!
php复制// 危险的库存扣减方式(高并发会超卖)
$stock = Redis::get('ticket_stock');
if ($stock > 0) {
Redis::set('ticket_stock', $stock - 1); // 多个请求可能同时执行到此
}
// 正确的原子操作
Redis::decr('ticket_stock');
初期我们没做支付去重,导致:
解决方案:
php复制// 支付回调处理关键代码
DB::transaction(function () use ($paymentNo) {
$lock = Cache::lock("payment_{$paymentNo}", 10);
if ($lock->get()) {
if (Order::where('payment_no', $paymentNo)->exists()) {
return 'already_processed';
}
// 正常处理逻辑...
}
});
曾发生过一起投诉:用户购买次日票,但在23:50生成二维码,系统默认设置了24小时有效期,导致次日入园时二维码已过期。现在我们采用动态有效期策略:
针对票务系统的特殊风险点:
SQL注入:
php复制// 危险做法
DB::select("SELECT * FROM users WHERE id = $_GET[id]");
// 安全做法
User::find($request->input('id'));
CSRF防护:
XSS防御:
php复制// 身份证加密示例
public function setIdCardAttribute($value)
{
$this->attributes['id_card'] = encrypt($value);
}
public function getIdCardAttribute($value)
{
return decrypt($value);
}
根据预估客流量选择配置:
| 日均票量 | CPU | 内存 | 推荐环境 |
|---|---|---|---|
| <3000 | 2核 | 4GB | 虚拟主机 |
| 3000-1万 | 4核 | 8GB | 云服务器 |
| >1万 | 8核 | 16GB | 集群部署 |
必备组件:
三个必须监控的指标:
推荐使用Prometheus+Grafana搭建监控看板,关键指标包括:
系统初期设计时就预留了多园区扩展能力:
park_id字段php复制Schema::create('tickets', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('park_id'); // 多园区关键字段
$table->string('type');
$table->decimal('price');
// ...
});
为第三方合作预留接口:
采用OAuth2.0认证,接口限流设计:
php复制// API限流中间件
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(100)->by($request->user()?->id ?: $request->ip());
});
这套系统在多个中型动物园落地后,售票效率提升40%以上,人力成本降低60%。最大的收获是:票务系统的稳定性不是测试出来的,而是设计出来的。每个异常流程都要当作必然发生的情况来处理,这才是高可用系统的精髓。