|
@@ -10,7 +10,8 @@ use app\common\Enum\ErrorCodeEnum;
|
|
|
use think\Db;
|
|
|
use think\Exception;
|
|
|
use think\Cache;
|
|
|
-
|
|
|
+use app\common\model\lottery\LotteryDrawRecord;
|
|
|
+use app\common\model\User;
|
|
|
/**
|
|
|
* 抽奖服务类
|
|
|
* 核心抽奖逻辑处理
|
|
@@ -98,18 +99,21 @@ class LotteryService
|
|
|
*/
|
|
|
private static function handleLotteryByType($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount)
|
|
|
{
|
|
|
+ // 第一步:统一创建参与记录
|
|
|
+ $drawRecord = static::createParticipationRecord($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount);
|
|
|
+
|
|
|
switch ($activity->lottery_type) {
|
|
|
case LotteryEnum::LOTTERY_TYPE_INSTANT:
|
|
|
- // 即抽即中:直接执行抽奖
|
|
|
- return static::processDrawLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount);
|
|
|
+ // 即抽即中:立即执行抽奖并更新记录
|
|
|
+ return static::executeInstantDraw($drawRecord, $activity);
|
|
|
|
|
|
case LotteryEnum::LOTTERY_TYPE_TIME:
|
|
|
// 按时间开奖:记录参与,等待定时任务开奖
|
|
|
- return static::recordParticipation($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount);
|
|
|
+ return static::handleTimeLottery($drawRecord, $activity);
|
|
|
|
|
|
case LotteryEnum::LOTTERY_TYPE_PEOPLE:
|
|
|
// 按人数开奖:记录参与,检查是否达到开奖人数
|
|
|
- return static::handlePeopleBasedLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount);
|
|
|
+ return static::handlePeopleLottery($drawRecord, $activity);
|
|
|
|
|
|
default:
|
|
|
throw new BusinessException('不支持的开奖方式', ErrorCodeEnum::LOTTERY_ACTIVITY_NOT_FOUND);
|
|
@@ -117,6 +121,175 @@ class LotteryService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 创建参与记录(统一入口)
|
|
|
+ */
|
|
|
+ private static function createParticipationRecord($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount)
|
|
|
+ {
|
|
|
+ // 开启事务
|
|
|
+ Db::startTrans();
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 1. 消耗用户抽奖机会
|
|
|
+ $userChance = LotteryChanceService::getUserChance($activity->id, $userId);
|
|
|
+ if (!LotteryChanceService::useChance($userChance)) {
|
|
|
+ throw new BusinessException('抽奖机会使用失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 创建参与记录(状态为已参与)
|
|
|
+ $drawRecord = new LotteryDrawRecord([
|
|
|
+ 'activity_id' => $activity->id,
|
|
|
+ 'user_id' => $userId,
|
|
|
+ 'prize_id' => 0, // 暂时没有奖品ID
|
|
|
+ 'status' => LotteryEnum::DRAW_STATUS_PARTICIPATED, // 状态:已参与
|
|
|
+ 'is_win' => 0, // 未开奖
|
|
|
+ 'trigger_type' => $triggerType,
|
|
|
+ 'trigger_order_id' => $triggerOrderId,
|
|
|
+ 'trigger_amount' => $triggerAmount,
|
|
|
+ 'win_info' => json_encode([]), // 暂时没有中奖信息
|
|
|
+ 'draw_ip' => request()->ip(),
|
|
|
+ 'draw_time' => time(),
|
|
|
+ 'device_info' => request()->header('user-agent', ''),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if (!$drawRecord->save()) {
|
|
|
+ throw new Exception('创建抽奖记录失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提交事务
|
|
|
+ Db::commit();
|
|
|
+
|
|
|
+ return $drawRecord;
|
|
|
+
|
|
|
+ } catch (Exception $e) {
|
|
|
+ Db::rollback();
|
|
|
+ throw $e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行即抽即中的抽奖逻辑
|
|
|
+ */
|
|
|
+ private static function executeInstantDraw($drawRecord, $activity)
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ // 1. 获取可用奖品
|
|
|
+ $prizes = static::getAvailablePrizes($activity);
|
|
|
+
|
|
|
+ if (empty($prizes)) {
|
|
|
+ throw new BusinessException('暂无可抽取的奖品');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 执行抽奖算法
|
|
|
+ $selectedPrize = static::executeLotteryAlgorithm($prizes);
|
|
|
+
|
|
|
+ // 3. 减少库存
|
|
|
+ if (!static::decreasePrizeStock($selectedPrize)) {
|
|
|
+ throw new BusinessException('奖品库存不足');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 更新抽奖记录
|
|
|
+ $isWin = $selectedPrize->type != LotteryEnum::PRIZE_TYPE_NO_PRIZE;
|
|
|
+ $winInfo = $isWin ? static::buildWinInfo($selectedPrize) : [];
|
|
|
+
|
|
|
+ $drawRecord->prize_id = $selectedPrize->id;
|
|
|
+ $drawRecord->status = $isWin ? LotteryEnum::DRAW_STATUS_WIN : LotteryEnum::DRAW_STATUS_NO_WIN;
|
|
|
+ $drawRecord->is_win = $isWin ? 1 : 0;
|
|
|
+ $drawRecord->win_info = json_encode($winInfo);
|
|
|
+ $drawRecord->save();
|
|
|
+
|
|
|
+ // 5. 如果中奖,创建中奖记录
|
|
|
+ $winRecord = null;
|
|
|
+ if ($isWin) {
|
|
|
+ $winRecord = new \app\common\model\lottery\LotteryWinRecord([
|
|
|
+ 'draw_record_id' => $drawRecord->id,
|
|
|
+ 'activity_id' => $activity->id,
|
|
|
+ 'user_id' => $drawRecord->user_id,
|
|
|
+ 'prize_id' => $selectedPrize->id,
|
|
|
+ 'prize_name' => $selectedPrize->name,
|
|
|
+ 'prize_type' => $selectedPrize->type,
|
|
|
+ 'prize_value' => json_encode(static::buildPrizeValue($selectedPrize)),
|
|
|
+ 'deliver_status' => LotteryEnum::DELIVER_STATUS_PENDING,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if (!$winRecord->save()) {
|
|
|
+ throw new Exception('创建中奖记录失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 自动发放奖品
|
|
|
+ if ($selectedPrize->deliver_type == LotteryEnum::DELIVER_TYPE_AUTO) {
|
|
|
+ static::autoDeliverPrize($winRecord, $selectedPrize);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 更新活动统计
|
|
|
+ static::updateActivityStats($activity, $isWin);
|
|
|
+
|
|
|
+ // 7. 返回抽奖结果
|
|
|
+ return static::buildDrawResult($drawRecord, $selectedPrize, $winRecord);
|
|
|
+
|
|
|
+ } catch (Exception $e) {
|
|
|
+ // 如果发生错误,将状态改为未中奖
|
|
|
+ $drawRecord->status = LotteryEnum::DRAW_STATUS_NO_WIN;
|
|
|
+ $drawRecord->is_win = 0;
|
|
|
+ $drawRecord->save();
|
|
|
+ throw new BusinessException('抽奖失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理定时抽奖
|
|
|
+ */
|
|
|
+ private static function handleTimeLottery($drawRecord, $activity)
|
|
|
+ {
|
|
|
+ // 验证开奖时间
|
|
|
+ if (isset($activity->lottery_time) && $activity->lottery_time < time()) {
|
|
|
+ throw new BusinessException('活动开奖时间已过,无法参与');
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'draw_id' => $drawRecord->id,
|
|
|
+ 'is_win' => 0,
|
|
|
+ 'status' => LotteryEnum::DRAW_STATUS_PARTICIPATED, // 等待开奖
|
|
|
+ 'lottery_type' => $activity->lottery_type,
|
|
|
+ 'lottery_time' => $activity->lottery_time ?? 0,
|
|
|
+ 'message' => isset($activity->lottery_time) ?
|
|
|
+ '参与成功,等待' . date('Y-m-d H:i:s', $activity->lottery_time) . '开奖' :
|
|
|
+ '参与成功,等待开奖'
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理按人数抽奖
|
|
|
+ */
|
|
|
+ private static function handlePeopleLottery($drawRecord, $activity)
|
|
|
+ {
|
|
|
+ // 检查是否达到开奖人数
|
|
|
+ $participantCount = LotteryDrawRecord::where('activity_id', $activity->id)
|
|
|
+ ->where('status', LotteryEnum::DRAW_STATUS_PARTICIPATED)
|
|
|
+ ->count();
|
|
|
+
|
|
|
+ $result = [
|
|
|
+ 'draw_id' => $drawRecord->id,
|
|
|
+ 'is_win' => 0,
|
|
|
+ 'status' => 'waiting', // 等待开奖
|
|
|
+ 'lottery_type' => $activity->lottery_type,
|
|
|
+ 'current_participants' => $participantCount,
|
|
|
+ 'required_participants' => $activity->lottery_people_num ?? 100
|
|
|
+ ];
|
|
|
+
|
|
|
+ if ($participantCount >= ($activity->lottery_people_num ?? 100)) {
|
|
|
+ // 达到人数,触发开奖(这里可以异步处理)
|
|
|
+ // TODO: 触发按人数开奖的处理逻辑
|
|
|
+ $result['message'] = '参与成功,已达到开奖人数,正在开奖中...';
|
|
|
+ } else {
|
|
|
+ $remainingCount = ($activity->lottery_people_num ?? 100) - $participantCount;
|
|
|
+ $result['message'] = "参与成功,还需要{$remainingCount}人参与即可开奖";
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 处理抽奖核心逻辑
|
|
|
*/
|
|
|
private static function processDrawLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount)
|
|
@@ -240,8 +413,137 @@ class LotteryService
|
|
|
|
|
|
} catch (Exception $e) {
|
|
|
Db::rollback();
|
|
|
- throw $e;
|
|
|
+ throw new BusinessException('创建参与记录失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 定时任务:处理定时开奖活动
|
|
|
+ * 用于定时任务调用,批量处理到时间开奖的活动
|
|
|
+ */
|
|
|
+ public static function processScheduledLotteries()
|
|
|
+ {
|
|
|
+ // 获取所有到达开奖时间的活动
|
|
|
+ $activities = LotteryActivity::where('lottery_type', LotteryEnum::LOTTERY_TYPE_TIME)
|
|
|
+ ->where('status', LotteryEnum::STATUS_ONGOING)
|
|
|
+ ->where('lottery_time', '<=', time())
|
|
|
+ ->select();
|
|
|
+
|
|
|
+ $processedCount = 0;
|
|
|
+
|
|
|
+ foreach ($activities as $activity) {
|
|
|
+ try {
|
|
|
+ $count = static::executeScheduledDraw($activity);
|
|
|
+ $processedCount += $count;
|
|
|
+
|
|
|
+ // 更新活动状态为已结束
|
|
|
+ $activity->status = LotteryEnum::STATUS_ENDED;
|
|
|
+ $activity->save();
|
|
|
+
|
|
|
+ } catch (Exception $e) {
|
|
|
+ trace("定时开奖失败 - 活动ID: {$activity->id}, 错误: " . $e->getMessage(), 'error');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $processedCount;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行定时开奖
|
|
|
+ */
|
|
|
+ private static function executeScheduledDraw($activity)
|
|
|
+ {
|
|
|
+ // 获取所有参与记录
|
|
|
+ $participantRecords = LotteryDrawRecord::where('activity_id', $activity->id)
|
|
|
+ ->where('status', LotteryEnum::DRAW_STATUS_PARTICIPATED)
|
|
|
+ ->select();
|
|
|
+
|
|
|
+ if (empty($participantRecords)) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ $processedCount = 0;
|
|
|
+
|
|
|
+ foreach ($participantRecords as $drawRecord) {
|
|
|
+ try {
|
|
|
+ // 为每个参与记录执行抽奖
|
|
|
+ static::executeDrawForRecord($drawRecord, $activity);
|
|
|
+ $processedCount++;
|
|
|
+
|
|
|
+ } catch (Exception $e) {
|
|
|
+ trace("用户开奖失败 - 记录ID: {$drawRecord->id}, 错误: " . $e->getMessage(), 'error');
|
|
|
+
|
|
|
+ // 设置为未中奖
|
|
|
+ $drawRecord->status = LotteryEnum::DRAW_STATUS_NO_WIN;
|
|
|
+ $drawRecord->is_win = 0;
|
|
|
+ $drawRecord->save();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $processedCount;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 为单个抽奖记录执行开奖
|
|
|
+ */
|
|
|
+ private static function executeDrawForRecord($drawRecord, $activity)
|
|
|
+ {
|
|
|
+ // 获取可用奖品
|
|
|
+ $prizes = static::getAvailablePrizes($activity);
|
|
|
+
|
|
|
+ if (empty($prizes)) {
|
|
|
+ // 没有奖品,设置为未中奖
|
|
|
+ $drawRecord->status = LotteryEnum::DRAW_STATUS_NO_WIN;
|
|
|
+ $drawRecord->is_win = 0;
|
|
|
+ $drawRecord->save();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 执行抽奖算法
|
|
|
+ $selectedPrize = static::executeLotteryAlgorithm($prizes);
|
|
|
+
|
|
|
+ // 减少库存
|
|
|
+ if (!static::decreasePrizeStock($selectedPrize)) {
|
|
|
+ // 库存不足,设置为未中奖
|
|
|
+ $drawRecord->status = LotteryEnum::DRAW_STATUS_NO_WIN;
|
|
|
+ $drawRecord->is_win = 0;
|
|
|
+ $drawRecord->save();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新抽奖记录
|
|
|
+ $isWin = $selectedPrize->type != LotteryEnum::PRIZE_TYPE_NO_PRIZE;
|
|
|
+ $winInfo = $isWin ? static::buildWinInfo($selectedPrize) : [];
|
|
|
+
|
|
|
+ $drawRecord->prize_id = $selectedPrize->id;
|
|
|
+ $drawRecord->status = $isWin ? LotteryEnum::DRAW_STATUS_WIN : LotteryEnum::DRAW_STATUS_NO_WIN;
|
|
|
+ $drawRecord->is_win = $isWin ? 1 : 0;
|
|
|
+ $drawRecord->win_info = json_encode($winInfo);
|
|
|
+ $drawRecord->save();
|
|
|
+
|
|
|
+ // 如果中奖,创建中奖记录
|
|
|
+ if ($isWin) {
|
|
|
+ $winRecord = new \app\common\model\lottery\LotteryWinRecord([
|
|
|
+ 'draw_record_id' => $drawRecord->id,
|
|
|
+ 'activity_id' => $activity->id,
|
|
|
+ 'user_id' => $drawRecord->user_id,
|
|
|
+ 'prize_id' => $selectedPrize->id,
|
|
|
+ 'prize_name' => $selectedPrize->name,
|
|
|
+ 'prize_type' => $selectedPrize->type,
|
|
|
+ 'prize_value' => json_encode(static::buildPrizeValue($selectedPrize)),
|
|
|
+ 'deliver_status' => LotteryEnum::DELIVER_STATUS_PENDING,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $winRecord->save();
|
|
|
+
|
|
|
+ // 自动发放奖品
|
|
|
+ if ($selectedPrize->deliver_type == LotteryEnum::DELIVER_TYPE_AUTO) {
|
|
|
+ static::autoDeliverPrize($winRecord, $selectedPrize);
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ // 更新活动统计
|
|
|
+ static::updateActivityStats($activity, $isWin);
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -368,7 +670,7 @@ class LotteryService
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- $user = \app\common\model\User::find($userId);
|
|
|
+ $user = User::find($userId);
|
|
|
return $user && in_array($user->level, (array)$limitValue);
|
|
|
}
|
|
|
|
|
@@ -537,7 +839,7 @@ class LotteryService
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 构建抽奖结果
|
|
|
+ * 构建即抽即中的抽奖结果
|
|
|
*/
|
|
|
private static function buildDrawResult($drawRecord, $prize, $winRecord = null)
|
|
|
{
|
|
@@ -549,7 +851,7 @@ class LotteryService
|
|
|
'name' => $prize->name,
|
|
|
'type' => $prize->type,
|
|
|
'type_text' => $prize->type_text,
|
|
|
- 'image' => $prize->image,
|
|
|
+ 'image' => cdnurl($prize->image),
|
|
|
'win_prompt' => $prize->win_prompt
|
|
|
],
|
|
|
'draw_time' => $drawRecord->draw_time
|