123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- <?php
- namespace app\common\Service;
- use app\common\model\lottery\LotteryActivity;
- use app\common\model\lottery\LotteryPrize;
- use app\common\model\lottery\LotteryCondition;
- use app\common\model\lottery\LotteryDrawRecord;
- use app\common\model\lottery\LotteryWinRecord;
- use app\common\model\lottery\LotteryUserChance;
- use think\Db;
- use think\Exception;
- use think\Cache;
- /**
- * 抽奖服务类
- * 核心抽奖逻辑处理
- */
- class LotteryService
- {
- /**
- * 执行抽奖
- * @param int $activityId 活动ID
- * @param int $userId 用户ID
- * @param int $triggerType 触发类型
- * @param int $triggerOrderId 触发订单ID
- * @param float $triggerAmount 触发金额
- * @return array 抽奖结果
- * @throws Exception
- */
- public static function drawLottery($activityId, $userId, $triggerType = 1, $triggerOrderId = null, $triggerAmount = null)
- {
- // 1. 验证活动有效性
- $activity = LotteryActivity::find($activityId);
- if (!$activity || !$activity->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
- ];
- }
- }
|