lottery_type == LotteryEnum::LOTTERY_TYPE_TIME) { $now = time(); if ($activity->lottery_time && $now >= $activity->lottery_time) { throw new BusinessException('开奖时间已过,无法参与', ErrorCodeEnum::LOTTERY_NOT_IN_TIME); } } // 3. 验证参与人数限制(仅对按人数开奖有效) if ($activity->lottery_type == LotteryEnum::LOTTERY_TYPE_PEOPLE) { $drawStats = LotteryRecordService::getActivityDrawStats($activityId); if ($drawStats['total_draw'] >= $activity->lottery_people_num) { throw new BusinessException('参与人数已满,无法参与', ErrorCodeEnum::LOTTERY_REACH_LIMIT); } } // 4. 验证用户资格 if (!static::validateUserQualification($activity, $userId)) { throw new BusinessException('用户不符合参与条件', ErrorCodeEnum::LOTTERY_USER_NOT_QUALIFIED); } // 5. 检查用户抽奖机会 $userChance = LotteryChanceService::getUserChance($activityId, $userId); if (!$userChance || !LotteryChanceService::hasChance($userChance)) { throw new BusinessException('没有抽奖机会', ErrorCodeEnum::LOTTERY_NO_CHANCE); } // 6. 检查用户参与次数限制 if (!static::checkUserDrawLimit($activity, $userId)) { throw new BusinessException('已达到参与次数上限', ErrorCodeEnum::LOTTERY_REACH_LIMIT); } // 7. 防重复抽奖检查(基于订单) if ($triggerOrderId && static::hasDrawnForOrder($activityId, $userId, $triggerOrderId)) { throw new BusinessException('该订单已参与过抽奖', ErrorCodeEnum::LOTTERY_ORDER_ALREADY_DRAWN); } // TODO: 8. 使用分布式锁防止并发(暂时移除,后续实现) // $lockKey = "lottery_lock_{$activityId}_{$userId}"; // $lockAcquired = static::acquireLock($lockKey, 10); // if (!$lockAcquired) { // throw new Exception('操作太频繁,请稍后再试'); // } // try { // 8. 根据开奖方式处理抽奖流程 return static::handleLotteryByType($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount); // } finally { // // 释放锁 // static::releaseLock($lockKey); // } } /** * 根据开奖方式处理抽奖 */ private static function handleLotteryByType($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount) { switch ($activity->lottery_type) { case LotteryEnum::LOTTERY_TYPE_INSTANT: // 即抽即中:直接执行抽奖 return static::processDrawLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount); case LotteryEnum::LOTTERY_TYPE_TIME: // 按时间开奖:记录参与,等待定时任务开奖 return static::recordParticipation($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount); case LotteryEnum::LOTTERY_TYPE_PEOPLE: // 按人数开奖:记录参与,检查是否达到开奖人数 return static::handlePeopleBasedLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount); default: throw new BusinessException('不支持的开奖方式', ErrorCodeEnum::LOTTERY_ACTIVITY_NOT_FOUND); } } /** * 处理抽奖核心逻辑 */ 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 (!static::decreasePrizeStock($selectedPrize)) { throw new Exception('奖品库存不足'); } // 4. 消耗用户抽奖机会 $userChance = LotteryChanceService::getUserChance($activity->id, $userId); if (!LotteryChanceService::useChance($userChance)) { throw new Exception('抽奖机会使用失败'); } // 5. 创建抽奖记录 $isWin = $selectedPrize->type != LotteryEnum::PRIZE_TYPE_NO_PRIZE; $winInfo = $isWin ? static::buildWinInfo($selectedPrize) : []; $drawRecord = LotteryRecordService::createDrawRecord( $activity->id, $userId, $selectedPrize->id, $isWin, $triggerType, $triggerOrderId, $triggerAmount, $winInfo ); // 6. 如果中奖,创建中奖记录 $winRecord = null; if ($isWin) { $winRecord = LotteryRecordService::createWinRecord( $drawRecord->id, $activity->id, $userId, $selectedPrize->id, $selectedPrize->name, $selectedPrize->type, static::buildPrizeValue($selectedPrize) ); // 自动发放奖品 if ($selectedPrize->deliver_type == LotteryEnum::DELIVER_TYPE_AUTO) { LotteryRecordService::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 recordParticipation($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount) { // 开启事务 Db::startTrans(); try { // 1. 消耗用户抽奖机会 $userChance = LotteryChanceService::getUserChance($activity->id, $userId); if (!LotteryChanceService::useChance($userChance)) { throw new Exception('抽奖机会使用失败'); } // 2. 创建参与记录(未开奖状态) $drawRecord = LotteryRecordService::createDrawRecord( $activity->id, $userId, 0, // 暂时没有奖品ID 0, // 未开奖 $triggerType, $triggerOrderId, $triggerAmount, [] // 暂时没有中奖信息 ); // 提交事务 Db::commit(); // 3. 返回参与结果 return [ 'draw_id' => $drawRecord->id, 'is_win' => 0, 'status' => 'waiting', // 等待开奖 'lottery_type' => $activity->lottery_type, 'lottery_time' => $activity->lottery_time ?? 0, 'message' => $activity->lottery_type == LotteryEnum::LOTTERY_TYPE_TIME ? '参与成功,等待' . date('Y-m-d H:i:s', $activity->lottery_time) . '开奖' : '参与成功,等待开奖' ]; } catch (Exception $e) { Db::rollback(); throw $e; } } /** * 处理按人数开奖 */ private static function handlePeopleBasedLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount) { // 先记录参与 $result = static::recordParticipation($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount); // 检查是否达到开奖人数 $drawStats = LotteryRecordService::getActivityDrawStats($activity->id); $participantCount = $drawStats['total_draw']; if ($participantCount >= $activity->lottery_people_num) { // 达到人数,触发开奖(这里可以异步处理) // TODO: 触发按人数开奖的处理逻辑 $result['message'] = '参与成功,已达到开奖人数,正在开奖中...'; } else { $remainingCount = $activity->lottery_people_num - $participantCount; $result['message'] = "参与成功,还需要{$remainingCount}人参与即可开奖"; } return $result; } /** * 获取可用奖品列表 */ private static function getAvailablePrizes($activity) { $prizes = static::getValidPrizes($activity->id); // 如果开启按人数解锁功能 if ($activity->unlock_by_people) { $currentPeopleCount = LotteryChanceService::getActivityParticipants($activity->id); $prizes = $prizes->filter(function($prize) use ($currentPeopleCount) { return static::isPrizeUnlocked($prize, $currentPeopleCount); }); } return $prizes; } /** * 执行抽奖算法(基于概率权重) */ private static function executeLotteryAlgorithm($prizes) { // 检查是否有奖品 if (empty($prizes)) { return null; } // 将奖品转换为数组(如果是集合对象) $prizeArray = is_array($prizes) ? $prizes : $prizes->toArray(); // 使用BC函数计算总概率 $totalProbability = '0'; foreach ($prizeArray as $prize) { $totalProbability = bcadd($totalProbability, $prize['probability'] ?? '0', 4); } // 如果总概率为0,返回未中奖奖品 if (bccomp($totalProbability, '0', 4) == 0) { return static::findNoPrizePrize($prizeArray); } // 生成随机数(0-总概率之间) $randomNumber = bcdiv(mt_rand(0, bcmul($totalProbability, '10000', 4)), '10000', 4); // 按概率选择奖品 $currentProbability = '0'; foreach ($prizeArray as $prize) { $currentProbability = bcadd($currentProbability, $prize['probability'] ?? '0', 4); if (bccomp($randomNumber, $currentProbability, 4) <= 0) { return $prize; } } // 保底返回未中奖类型的奖品 return static::findNoPrizePrize($prizeArray); } /** * 查找未中奖类型的奖品 */ private static function findNoPrizePrize($prizeArray) { // 优先查找未中奖类型的奖品 foreach ($prizeArray as $prize) { if (($prize['type'] ?? 0) == LotteryEnum::PRIZE_TYPE_NO_PRIZE) { return $prize; } } // 如果没找到未中奖类型,返回第一个奖品作为兜底 return isset($prizeArray[0]) ? $prizeArray[0] : null; } /** * 验证用户资格 */ private static function validateUserQualification($activity, $userId) { // 检查用户群体限制 switch ($activity->user_limit_type) { case LotteryEnum::USER_LIMIT_ALL: return true; case LotteryEnum::USER_LIMIT_LEVEL: return static::checkUserLevel($userId, $activity->user_limit_value); case LotteryEnum::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 = LotteryRecordService::getUserDrawCount($activity->id, $userId); return $drawCount < $activity->person_limit_num; } /** * 检查订单是否已抽奖 */ private static function hasDrawnForOrder($activityId, $userId, $orderId) { return LotteryRecordService::hasUserDrawnForOrder($activityId, $userId, $orderId); } /** * 构建中奖信息 */ 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 LotteryEnum::PRIZE_TYPE_COUPON: $prizeValue['coupon_id'] = $prize->coupon_id; break; case LotteryEnum::PRIZE_TYPE_REDPACK: $prizeValue['amount'] = $prize->amount; break; case LotteryEnum::PRIZE_TYPE_SHOP_GOODS: $prizeValue['goods_id'] = $prize->goods_id; $prizeValue['goods_sku_id'] = $prize->goods_sku_id; break; case LotteryEnum::PRIZE_TYPE_CODE: $prizeValue['exchange_code'] = static::getAvailableExchangeCode($prize); break; } return $prizeValue; } /** * 自动发放奖品 */ private static function autoDeliverPrize($winRecord, $prize) { try { switch ($prize->type) { case LotteryEnum::PRIZE_TYPE_COUPON: static::deliverCoupon($winRecord, $prize); break; case LotteryEnum::PRIZE_TYPE_REDPACK: static::deliverRedPacket($winRecord, $prize); break; case LotteryEnum::PRIZE_TYPE_CODE: static::deliverExchangeCode($winRecord, $prize); break; case LotteryEnum::PRIZE_TYPE_SHOP_GOODS: static::deliverGoods($winRecord, $prize); break; } } catch (Exception $e) { LotteryRecordService::markWinRecordAsFailed($winRecord, $e->getMessage()); } } /** * 发放优惠券 */ private static function deliverCoupon($winRecord, $prize) { // 这里调用优惠券发放接口 // 示例代码,需要根据实际优惠券系统实现 LotteryRecordService::markWinRecordAsDelivered($winRecord, ['coupon_id' => $prize->coupon_id]); } /** * 发放红包 */ private static function deliverRedPacket($winRecord, $prize) { // 这里调用红包发放接口 // 示例代码,需要根据实际红包系统实现 LotteryRecordService::markWinRecordAsDelivered($winRecord, ['amount' => $prize->amount]); } /** * 发放兑换码 */ private static function deliverExchangeCode($winRecord, $prize) { $code = static::getAvailableExchangeCode($prize); if ($code) { LotteryRecordService::setWinRecordExchangeCode($winRecord, $code); static::markExchangeCodeUsed($prize, $code); LotteryRecordService::markWinRecordAsDelivered($winRecord, ['exchange_code' => $code]); } else { throw new Exception('兑换码已用完'); } } /** * 发放商城商品 */ private static function deliverGoods($winRecord, $prize) { // 这里可以自动加入购物车或创建订单 // 示例代码,需要根据实际商城系统实现 LotteryRecordService::markWinRecordAsDelivered($winRecord, [ '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; // 只有兑换码类型的奖品才记录兑换码信息 if ($prize->type == LotteryEnum::PRIZE_TYPE_CODE) { $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 = LotteryChanceService::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 ]; } // ============ 从LotteryPrize模型移过来的业务逻辑方法 ============ /** * 检查奖品库存是否充足 */ public static function hasPrizeStock(LotteryPrize $prize, $quantity = 1) { return $prize->remain_stock >= $quantity; } /** * 减少奖品库存 */ public static function decreasePrizeStock(LotteryPrize $prize, $quantity = 1) { if (!static::hasPrizeStock($prize, $quantity)) { return false; } $prize->remain_stock -= $quantity; $prize->win_count += $quantity; return $prize->save(); } /** * 获取可用的兑换码 */ public static function getAvailableExchangeCode(LotteryPrize $prize) { if ($prize->type != LotteryEnum::PRIZE_TYPE_CODE) { return null; } $allCodes = $prize->exchange_codes_list; $usedCodes = $prize->used_codes_list; $availableCodes = array_diff($allCodes, $usedCodes); if (empty($availableCodes)) { return null; } return array_shift($availableCodes); } /** * 标记兑换码为已使用 */ public static function markExchangeCodeUsed(LotteryPrize $prize, $code) { $usedCodes = $prize->used_codes_list; if (!in_array($code, $usedCodes)) { $usedCodes[] = $code; $prize->used_codes = json_encode($usedCodes); return $prize->save(); } return true; } /** * 获取有效奖品(库存大于0且状态正常) */ public static function getValidPrizes($activityId) { return LotteryPrize::where('activity_id', $activityId) ->where('status', 1) ->where('remain_stock', '>', 0) ->order('sort_order', 'asc') ->select(); } /** * 检查奖品是否已解锁(按人数解锁功能) */ public static function isPrizeUnlocked(LotteryPrize $prize, $currentPeopleCount) { if (empty($prize->unlock_people_num)) { return true; } return $currentPeopleCount >= $prize->unlock_people_num; } /** * 检查活动是否正在进行 */ public static function isActivityRunning(LotteryActivity $activity) { $now = time(); return $activity->status == LotteryEnum::STATUS_ONGOING && $activity->start_time <= $now && $activity->end_time >= $now; } /** * 检查活动是否已结束 */ public static function isActivityEnded(LotteryActivity $activity) { $now = time(); return $activity->status == LotteryEnum::STATUS_ENDED || $activity->end_time < $now; } /** * 检查活动是否未开始 */ public static function isActivityNotStarted(LotteryActivity $activity) { $now = time(); return $activity->status == LotteryEnum::STATUS_NOT_STARTED && $activity->start_time > $now; } /** * 检查活动是否已暂停 */ public static function isActivitySuspended(LotteryActivity $activity) { return $activity->status == LotteryEnum::STATUS_SUSPENDED; } /** * 检查活动是否已取消 */ public static function isActivityCancelled(LotteryActivity $activity) { return $activity->status == LotteryEnum::STATUS_CANCELLED; } // TODO: 分布式锁相关方法(暂时移除,后续实现) /* private static function acquireLock($lockKey, $expireTime = 10) { // Redis锁获取逻辑 } private static function releaseLock($lockKey) { // Redis锁释放逻辑 } private static function acquireFileLock($lockKey, $expireTime = 10) { // 文件锁获取逻辑 } private static function releaseFileLock($lockKey) { // 文件锁释放逻辑 } */ /** * 获取正在进行的活动 */ public static function getRunningActivities() { $now = time(); return LotteryActivity::where('status', LotteryEnum::STATUS_ONGOING) ->where('start_time', '<=', $now) ->where('end_time', '>=', $now) ->select(); } /** * 获取未开始的活动 */ public static function getNotStartedActivities() { $now = time(); return LotteryActivity::where('status', LotteryEnum::STATUS_NOT_STARTED) ->where('start_time', '>', $now) ->select(); } /** * 获取已结束的活动 */ public static function getEndedActivities() { $now = time(); return LotteryActivity::where('status', LotteryEnum::STATUS_ENDED) ->orWhere('end_time', '<', $now) ->select(); } /** * 获取可显示的活动(排除逻辑状态) */ public static function getDisplayableActivities() { $displayableStatuses = array_keys(LotteryEnum::getActivityStatusMap()); return LotteryActivity::whereIn('status', $displayableStatuses)->select(); } /** * 验证活动状态是否有效 */ public static function isValidActivityStatus(LotteryActivity $activity) { return LotteryEnum::isValidActivityStatus($activity->status); } /** * 验证开奖方式是否有效 */ public static function isValidLotteryType(LotteryActivity $activity) { return LotteryEnum::isValidLotteryType($activity->lottery_type); } }