isRunning()) { throw new Exception('活动不存在或未开始'); } // 2. 验证抽奖时间 if (!$activity->isInDrawTime()) { throw new Exception('不在抽奖时间内'); } // 3. 验证用户资格 if (!static::validateUserQualification($activity, $userId)) { throw new Exception('用户不符合参与条件'); } // 4. 检查用户抽奖机会 $userChance = LotteryUserChance::getUserChance($activityId, $userId); if (!$userChance || !$userChance->hasChance()) { throw new Exception('没有抽奖机会'); } // 5. 检查用户参与次数限制 if (!static::checkUserDrawLimit($activity, $userId)) { throw new Exception('已达到参与次数上限'); } // 6. 防重复抽奖检查(基于订单) if ($triggerOrderId && static::hasDrawnForOrder($activityId, $userId, $triggerOrderId)) { throw new Exception('该订单已参与过抽奖'); } // 7. 使用Redis锁防止并发 $lockKey = "lottery_lock_{$activityId}_{$userId}"; $lock = Cache::store('redis')->handler()->set($lockKey, 1, 'NX', 'EX', 10); if (!$lock) { throw new Exception('操作太频繁,请稍后再试'); } try { // 8. 开始抽奖流程 return static::processDrawLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount); } finally { // 释放锁 Cache::store('redis')->handler()->del($lockKey); } } /** * 处理抽奖核心逻辑 */ private static function processDrawLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount) { // 开启事务 Db::startTrans(); try { // 1. 获取可用奖品 $prizes = static::getAvailablePrizes($activity); if (empty($prizes)) { throw new Exception('暂无可抽取的奖品'); } // 2. 执行抽奖算法 $selectedPrize = static::executeLotteryAlgorithm($prizes); // 3. 减少库存 if (!$selectedPrize->decreaseStock()) { throw new Exception('奖品库存不足'); } // 4. 消耗用户抽奖机会 $userChance = LotteryUserChance::getUserChance($activity->id, $userId); if (!$userChance->useChance()) { throw new Exception('抽奖机会使用失败'); } // 5. 创建抽奖记录 $isWin = $selectedPrize->type != LotteryPrize::TYPE_NO_PRIZE; $winInfo = $isWin ? static::buildWinInfo($selectedPrize) : []; $drawRecord = LotteryDrawRecord::createRecord( $activity->id, $userId, $selectedPrize->id, $isWin, $triggerType, $triggerOrderId, $triggerAmount, $winInfo ); // 6. 如果中奖,创建中奖记录 $winRecord = null; if ($isWin) { $winRecord = LotteryWinRecord::createWinRecord( $drawRecord->id, $activity->id, $userId, $selectedPrize->id, $selectedPrize->name, $selectedPrize->type, static::buildPrizeValue($selectedPrize) ); // 自动发放奖品 if ($selectedPrize->deliver_type == LotteryPrize::DELIVER_AUTO) { static::autoDeliverPrize($winRecord, $selectedPrize); } } // 7. 更新活动统计 static::updateActivityStats($activity, $isWin); // 提交事务 Db::commit(); // 8. 返回抽奖结果 return static::buildDrawResult($drawRecord, $selectedPrize, $winRecord); } catch (Exception $e) { Db::rollback(); throw $e; } } /** * 获取可用奖品列表 */ private static function getAvailablePrizes($activity) { $prizes = LotteryPrize::getValidPrizes($activity->id); // 如果开启按人数解锁功能 if ($activity->unlock_by_people) { $currentPeopleCount = LotteryUserChance::getActivityParticipants($activity->id); $prizes = $prizes->filter(function($prize) use ($currentPeopleCount) { return $prize->isUnlocked($currentPeopleCount); }); } return $prizes; } /** * 执行抽奖算法(基于概率权重) */ private static function executeLotteryAlgorithm($prizes) { // 计算总概率 $totalProbability = $prizes->sum('probability'); // 生成随机数 $randomNumber = mt_rand(1, $totalProbability * 100) / 100; // 按概率选择奖品 $currentProbability = 0; foreach ($prizes as $prize) { $currentProbability += $prize->probability; if ($randomNumber <= $currentProbability) { return $prize; } } // 保底返回第一个奖品(通常是未中奖) return $prizes->first(); } /** * 验证用户资格 */ private static function validateUserQualification($activity, $userId) { // 检查用户群体限制 switch ($activity->user_limit_type) { case LotteryActivity::USER_LIMIT_ALL: return true; case LotteryActivity::USER_LIMIT_LEVEL: return static::checkUserLevel($userId, $activity->user_limit_value); case LotteryActivity::USER_LIMIT_TAG: return static::checkUserTag($userId, $activity->user_limit_value); default: return false; } } /** * 检查用户等级 */ private static function checkUserLevel($userId, $limitValue) { if (empty($limitValue)) { return true; } $user = \app\common\model\User::find($userId); return $user && in_array($user->level, (array)$limitValue); } /** * 检查用户标签 */ private static function checkUserTag($userId, $limitValue) { if (empty($limitValue)) { return true; } // 这里需要根据实际的用户标签系统实现 // 暂时返回true return true; } /** * 检查用户抽奖次数限制 */ private static function checkUserDrawLimit($activity, $userId) { if (!$activity->person_limit_num) { return true; } $drawCount = LotteryDrawRecord::getUserDrawCount($activity->id, $userId); return $drawCount < $activity->person_limit_num; } /** * 检查订单是否已抽奖 */ private static function hasDrawnForOrder($activityId, $userId, $orderId) { return LotteryDrawRecord::where('activity_id', $activityId) ->where('user_id', $userId) ->where('trigger_order_id', $orderId) ->count() > 0; } /** * 构建中奖信息 */ private static function buildWinInfo($prize) { return [ 'prize_id' => $prize->id, 'prize_name' => $prize->name, 'prize_type' => $prize->type, 'prize_image' => $prize->image, 'win_prompt' => $prize->win_prompt ]; } /** * 构建奖品价值信息 */ private static function buildPrizeValue($prize) { $prizeValue = [ 'type' => $prize->type, 'name' => $prize->name, 'image' => $prize->image ]; switch ($prize->type) { case LotteryPrize::TYPE_COUPON: $prizeValue['coupon_id'] = $prize->coupon_id; break; case LotteryPrize::TYPE_RED_PACKET: $prizeValue['amount'] = $prize->amount; break; case LotteryPrize::TYPE_GOODS: $prizeValue['goods_id'] = $prize->goods_id; $prizeValue['goods_sku_id'] = $prize->goods_sku_id; break; case LotteryPrize::TYPE_EXCHANGE_CODE: $prizeValue['exchange_code'] = $prize->getAvailableExchangeCode(); break; } return $prizeValue; } /** * 自动发放奖品 */ private static function autoDeliverPrize($winRecord, $prize) { try { switch ($prize->type) { case LotteryPrize::TYPE_COUPON: static::deliverCoupon($winRecord, $prize); break; case LotteryPrize::TYPE_RED_PACKET: static::deliverRedPacket($winRecord, $prize); break; case LotteryPrize::TYPE_EXCHANGE_CODE: static::deliverExchangeCode($winRecord, $prize); break; case LotteryPrize::TYPE_GOODS: static::deliverGoods($winRecord, $prize); break; } } catch (Exception $e) { $winRecord->markAsFailed($e->getMessage()); } } /** * 发放优惠券 */ private static function deliverCoupon($winRecord, $prize) { // 这里调用优惠券发放接口 // 示例代码,需要根据实际优惠券系统实现 $winRecord->markAsDelivered(['coupon_id' => $prize->coupon_id]); } /** * 发放红包 */ private static function deliverRedPacket($winRecord, $prize) { // 这里调用红包发放接口 // 示例代码,需要根据实际红包系统实现 $winRecord->markAsDelivered(['amount' => $prize->amount]); } /** * 发放兑换码 */ private static function deliverExchangeCode($winRecord, $prize) { $code = $prize->getAvailableExchangeCode(); if ($code) { $winRecord->setExchangeCode($code); $prize->markExchangeCodeUsed($code); $winRecord->markAsDelivered(['exchange_code' => $code]); } else { throw new Exception('兑换码已用完'); } } /** * 发放商城商品 */ private static function deliverGoods($winRecord, $prize) { // 这里可以自动加入购物车或创建订单 // 示例代码,需要根据实际商城系统实现 $winRecord->markAsDelivered([ 'goods_id' => $prize->goods_id, 'goods_sku_id' => $prize->goods_sku_id ]); } /** * 更新活动统计 */ private static function updateActivityStats($activity, $isWin) { $activity->total_draw_count += 1; if ($isWin) { $activity->total_win_count += 1; } $activity->save(); } /** * 构建抽奖结果 */ private static function buildDrawResult($drawRecord, $prize, $winRecord = null) { $result = [ 'draw_id' => $drawRecord->id, 'is_win' => $drawRecord->is_win, 'prize' => [ 'id' => $prize->id, 'name' => $prize->name, 'type' => $prize->type, 'type_text' => $prize->type_text, 'image' => $prize->image, 'win_prompt' => $prize->win_prompt ], 'draw_time' => $drawRecord->draw_time ]; if ($winRecord) { $result['win_record_id'] = $winRecord->id; $result['deliver_status'] = $winRecord->deliver_status; $result['exchange_code'] = $winRecord->exchange_code; } return $result; } /** * 获取用户抽奖机会 * @param int $activityId 活动ID * @param int $userId 用户ID * @return array */ public static function getUserChances($activityId, $userId) { $userChance = LotteryUserChance::getUserChance($activityId, $userId); if (!$userChance) { return [ 'total_chances' => 0, 'used_chances' => 0, 'remain_chances' => 0 ]; } return [ 'total_chances' => $userChance->total_chances, 'used_chances' => $userChance->used_chances, 'remain_chances' => $userChance->remain_chances, 'last_get_time' => $userChance->last_get_time, 'last_use_time' => $userChance->last_use_time ]; } }