|
|
<?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; // 返回订单明细数组
|
|
|
}
|
|
|
}
|