You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
aquaculture/app/Http/Controllers/User/SeckillController.php

230 lines
8.5 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Http\Controllers\User;
use App\Enums\OrderTypeEnum; // 引入订单类型枚举
use App\Enums\SettingKeyEnum; // 引入设置键枚举
use App\Jobs\CancelUnPayOrder; // 引入取消未支付订单的任务
use App\Models\Address; // 引入地址模型
use App\Models\Order; // 引入订单模型
use App\Models\OrderDetail; // 引入订单详情模型
use App\Models\Product; // 引入产品模型
use App\Models\Seckill; // 引入秒杀模型
use App\Models\User; // 引入用户模型
use App\Utils\OrderUtil; // 引入订单工具类
use Carbon\Carbon; // 引入 Carbon 日期处理库
use Illuminate\Auth\SessionGuard; // 引入会话守卫
use Illuminate\Http\Request; // 引入请求类
use Illuminate\Support\Facades\DB; // 引入数据库门面
use Illuminate\Support\Facades\Redis; // 引入 Redis 门面
use Jenssegers\Agent\Agent; // 引入用户代理类
/**
* 秒杀控制器
*
* 该控制器处理与秒杀活动相关的操作,包括展示秒杀商品、参与秒杀、获取参与秒杀的用户等。
*/
class SeckillController extends PaymentController
{
protected $redisSeckill; // 存储秒杀活动的 Redis 数据
/**
* 显示秒杀活动的详细信息
*
* @param int $id 秒杀活动的 ID
* @return \Illuminate\View\View
*/
public function show($id)
{
$seckill = new Seckill(compact('id')); // 创建秒杀活动实例
$redisSeckill = $this->getSeckill($seckill); // 从 Redis 获取秒杀活动数据
$product = $redisSeckill->product; // 获取秒杀商品信息
/**
* @var $user User
* 如果用户已登录,则返回该用户的所有地址列表;否则返回一个空集合
*/
$addresses = collect(); // 初始化地址集合
if ($user = auth()->user()) {
$addresses = $user->addresses()->get(); // 获取用户地址
}
return view('seckills.show', compact('redisSeckill', 'product', 'addresses')); // 返回视图
}
/**
* 抢购秒杀商品
*
* @param Request $request 请求对象
* @param int $id 秒杀活动的 ID
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function storeSeckill(Request $request, $id)
{
/**
* 直接从 session 中读取用户 ID不经过数据库
*
* @var $user User
* @var $auth SessionGuard
*/
$seckill = new Seckill(compact('id')); // 创建秒杀活动实例
$auth = auth('web'); // 获取当前会话
$userId = session()->get($auth->getName()); // 获取用户 ID
try {
// 检查请求中是否包含地址 ID
if (!$request->has('address_id')) {
throw new \Exception('必须选择一个地址'); // 抛出异常
}
// 验证秒杀活动是否存在及是否已开始
$redisSeckill = $this->redisSeckill = $this->getSeckill($seckill);
if (!$redisSeckill->is_start) {
throw new \Exception('秒杀未开始'); // 抛出异常
}
} catch (\Exception $e) {
return responseJson(402, $e->getMessage()); // 返回错误响应
}
// 开始抢购逻辑,检查队列中是否还有可抢购的商品
if (is_null(Redis::lpop($seckill->getRedisQueueKey()))) {
return responseJson(403, '已经抢购完了'); // 返回错误响应
}
DB::beginTransaction(); // 开始数据库事务
try {
$product = $redisSeckill->product; // 获取秒杀商品
if (is_null($product)) {
return responseJson(400, '商品已下架'); // 返回错误响应
}
// 验证用户的收货地址是否有效
$user = auth()->user();
$address = Address::query()->where('user_id', $user->id)->find($request->input('address_id'));
if (is_null($address)) {
return responseJson(400, '无效的收货地址'); // 返回错误响应
}
// 创建秒杀主表订单和明细表订单,默认数量为 1
$masterOrder = ($orderUtil = new OrderUtil([['product' => $product]]))->make($user->id, $address);
$masterOrder->type = OrderTypeEnum::SEC_KILL; // 设置订单类型为秒杀
$masterOrder->amount = $redisSeckill->price; // 设置订单金额
$masterOrder->save(); // 保存订单
// 创建订单明细
$details = $orderUtil->getDetails();
data_set($details, '*.order_id', $masterOrder->id); // 设置订单 ID
OrderDetail::query()->insert($details); // 保存订单明细
// 设置未支付订单的自动取消时间
$setting = new SettingKeyEnum(SettingKeyEnum::UN_PAY_CANCEL_TIME);
$delay = Carbon::now()->addMinute(setting($setting, 30)); // 设置延迟时间
CancelUnPayOrder::dispatch($masterOrder)->delay($delay); // 调度取消任务
// 生成支付信息
$form = $this->buildPayForm($masterOrder, (new Agent)->isMobile())->getContent();
} catch (\Exception $e) {
DB::rollBack(); // 回滚事务
// 回滚秒杀数量
Redis::lpush($seckill->getRedisQueueKey(), 9);
// 从 Redis 中删除当前用户的抢购记录,允许用户重新抢购
Redis::del($seckill->getUsersKey($userId));
return responseJson(403, $e->getMessage()); // 返回错误响应
}
DB::commit(); // 提交事务
// 更新秒杀活动的剩余数量
$redisSeckill->sale_count += 1; // 增加销售数量
$redisSeckill->number -= 1; // 减少库存数量
Redis::set($seckill->getRedisModelKey(), json_encode($redisSeckill)); // 更新 Redis 中的秒杀活动数据
// 存储抢购成功的用户名
$user = auth()->user();
Redis::hset($seckill->getUsersKey($userId), 'name', $user->hidden_name);
return responseJson(200, '抢购成功', compact('form')); // 返回成功响应
}
/**
* 获取参与秒杀活动的用户
*
* @param int $id 秒杀活动的 ID
* @return \Illuminate\Http\JsonResponse
*/
public function getSeckillUsers($id)
{
$seckill = new Seckill(compact('id')); // 创建秒杀活动实例
$keys = Redis::keys($seckill->getUsersKey('*')); // 获取所有参与用户的键
$users = collect(); // 初始化用户集合
foreach ($keys as $key) {
$users->push(Redis::hget($key, 'name')); // 获取用户名称并添加到集合
}
return responseJson(200, 'success', $users); // 返回成功响应
}
/**
* 从 Redis 中获取秒杀活动的数据
*
* @param Seckill $seckill 秒杀活动实例
* @return mixed
*/
protected function getSeckill(Seckill $seckill)
{
/**
* @var $product Product
*/
$json = Redis::get($seckill->getRedisModelKey()); // 从 Redis 获取秒杀活动数据
$redisSeckill = json_decode($json); // 解析 JSON 数据
if (is_null($redisSeckill)) {
abort(403, "没有这个秒杀活动"); // 返回 403 错误
}
// 获取当前时间和秒杀结束时间
$now = Carbon::now();
$endAt = Carbon::make($redisSeckill->end_at);
if ($now->gt($endAt)) {
abort(403, "秒杀已经结束"); // 返回 403 错误
}
// 检查秒杀是否已经开始
$startAt = Carbon::make($redisSeckill->start_at);
$redisSeckill->is_start = $now->gt($startAt); // 设置活动是否开始
// 计算倒计时
$redisSeckill->diff_time = $startAt->getTimestamp() - time();
return $redisSeckill; // 返回秒杀活动数据
}
/**
* 重写父类的构建订单明细
*
* @param Product $product 产品实例
* @param int $number 数量
* @return array 订单明细数组
*/
protected function buildOrderDetail(Product $product, $number)
{
$attribute = [
'product_id' => $product->id, // 设置产品 ID
'number' => $number // 设置数量
];
// 价格为秒杀的价格,直接从 Redis 中读取
$attribute['price'] = ceilTwoPrice($this->redisSeckill->price); // 设置价格
$attribute['total'] = ceilTwoPrice($attribute['price'] * $attribute['number']); // 计算总价
return $attribute; // 返回订单明细数组
}
}