LotteryService.php 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114
  1. <?php
  2. namespace app\common\Service\Lottery;
  3. use app\common\model\lottery\LotteryActivity;
  4. use app\common\model\lottery\LotteryPrize;
  5. use app\common\Enum\LotteryEnum;
  6. use app\common\exception\BusinessException;
  7. use app\common\Enum\ErrorCodeEnum;
  8. use think\Db;
  9. use think\Exception;
  10. use think\Cache;
  11. use app\common\model\lottery\LotteryDrawRecord;
  12. use app\common\model\User;
  13. /**
  14. * 抽奖服务类
  15. * 核心抽奖逻辑处理
  16. */
  17. class LotteryService
  18. {
  19. // TODO: 分布式锁相关功能(暂时移除,后续实现)
  20. /**
  21. * 当前进程持有的锁信息
  22. * @var array
  23. */
  24. // private static $locks = [];
  25. /**
  26. * 执行抽奖
  27. * @param int $activityId 活动ID
  28. * @param int $userId 用户ID
  29. * @param int $triggerType 触发类型
  30. * @param int $triggerOrderId 触发订单ID
  31. * @param float $triggerAmount 触发金额
  32. * @return array 抽奖结果
  33. * @throws Exception
  34. */
  35. public static function drawLottery($activityId, $userId, $triggerType = 1, $triggerOrderId = null, $triggerAmount = null)
  36. {
  37. // 1. 验证活动有效性
  38. $activity = LotteryActivity::find($activityId);
  39. if (!$activity || !self::isActivityRunning($activity)) {
  40. throw new BusinessException('活动不存在或未开始', ErrorCodeEnum::LOTTERY_ACTIVITY_NOT_FOUND);
  41. }
  42. // 2. 验证开奖时间(仅对按时间开奖有效)
  43. if ($activity->lottery_type == LotteryEnum::LOTTERY_TYPE_TIME) {
  44. $now = time();
  45. if ($activity->lottery_time && $now >= $activity->lottery_time) {
  46. throw new BusinessException('开奖时间已过,无法参与', ErrorCodeEnum::LOTTERY_NOT_IN_TIME);
  47. }
  48. }
  49. // 3. 验证参与人数限制(仅对按人数开奖有效)
  50. if ($activity->lottery_type == LotteryEnum::LOTTERY_TYPE_PEOPLE) {
  51. $drawStats = LotteryRecordService::getActivityDrawStats($activityId);
  52. if ($drawStats['total_draw'] >= $activity->lottery_people_num) {
  53. throw new BusinessException('参与人数已满,无法参与', ErrorCodeEnum::LOTTERY_REACH_LIMIT);
  54. }
  55. }
  56. // 4. 验证用户资格
  57. if (!static::validateUserQualification($activity, $userId)) {
  58. throw new BusinessException('用户不符合参与条件', ErrorCodeEnum::LOTTERY_USER_NOT_QUALIFIED);
  59. }
  60. // 5. 检查用户抽奖机会
  61. $userChance = LotteryChanceService::getUserChance($activityId, $userId);
  62. if (!$userChance || !LotteryChanceService::hasChance($userChance)) {
  63. throw new BusinessException('没有抽奖机会', ErrorCodeEnum::LOTTERY_NO_CHANCE);
  64. }
  65. // 6. 检查用户参与次数限制
  66. if (!static::checkUserDrawLimit($activity, $userId)) {
  67. throw new BusinessException('已达到参与次数上限', ErrorCodeEnum::LOTTERY_REACH_LIMIT);
  68. }
  69. // 7. 防重复抽奖检查(基于订单)
  70. if ($triggerOrderId && static::hasDrawnForOrder($activityId, $userId, $triggerOrderId)) {
  71. throw new BusinessException('该订单已参与过抽奖', ErrorCodeEnum::LOTTERY_ORDER_ALREADY_DRAWN);
  72. }
  73. // TODO: 8. 使用分布式锁防止并发(暂时移除,后续实现)
  74. // $lockKey = "lottery_lock_{$activityId}_{$userId}";
  75. // $lockAcquired = static::acquireLock($lockKey, 10);
  76. // if (!$lockAcquired) {
  77. // throw new Exception('操作太频繁,请稍后再试');
  78. // }
  79. // try {
  80. // 8. 根据开奖方式处理抽奖流程
  81. return static::handleLotteryByType($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount);
  82. // } finally {
  83. // // 释放锁
  84. // static::releaseLock($lockKey);
  85. // }
  86. }
  87. /**
  88. * 根据开奖方式处理抽奖
  89. */
  90. private static function handleLotteryByType($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount)
  91. {
  92. // 第一步:统一创建参与记录
  93. $drawRecord = static::createParticipationRecord($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount);
  94. switch ($activity->lottery_type) {
  95. case LotteryEnum::LOTTERY_TYPE_INSTANT:
  96. // 即抽即中:立即执行抽奖并更新记录
  97. return static::executeInstantDraw($drawRecord, $activity);
  98. case LotteryEnum::LOTTERY_TYPE_TIME:
  99. // 按时间开奖:记录参与,等待定时任务开奖
  100. return static::handleTimeLottery($drawRecord, $activity);
  101. case LotteryEnum::LOTTERY_TYPE_PEOPLE:
  102. // 按人数开奖:记录参与,检查是否达到开奖人数
  103. return static::handlePeopleLottery($drawRecord, $activity);
  104. default:
  105. throw new BusinessException('不支持的开奖方式', ErrorCodeEnum::LOTTERY_ACTIVITY_NOT_FOUND);
  106. }
  107. }
  108. /**
  109. * 创建参与记录(统一入口)
  110. */
  111. private static function createParticipationRecord($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount)
  112. {
  113. // 开启事务
  114. Db::startTrans();
  115. try {
  116. // 1. 消耗用户抽奖机会
  117. $userChance = LotteryChanceService::getUserChance($activity->id, $userId);
  118. if (!LotteryChanceService::useChance($userChance)) {
  119. throw new BusinessException('抽奖机会使用失败');
  120. }
  121. // 2. 创建参与记录(状态为已参与)
  122. $drawRecord = new LotteryDrawRecord([
  123. 'activity_id' => $activity->id,
  124. 'user_id' => $userId,
  125. 'prize_id' => 0, // 暂时没有奖品ID
  126. 'status' => LotteryEnum::DRAW_STATUS_PARTICIPATED, // 状态:已参与
  127. 'is_win' => 0, // 未开奖
  128. 'trigger_type' => $triggerType,
  129. 'trigger_order_id' => $triggerOrderId,
  130. 'trigger_amount' => $triggerAmount,
  131. 'win_info' => json_encode([]), // 暂时没有中奖信息
  132. 'draw_ip' => request()->ip(),
  133. 'draw_time' => time(),
  134. 'device_info' => request()->header('user-agent', ''),
  135. ]);
  136. if (!$drawRecord->save()) {
  137. throw new Exception('创建抽奖记录失败');
  138. }
  139. // 提交事务
  140. Db::commit();
  141. return $drawRecord;
  142. } catch (Exception $e) {
  143. Db::rollback();
  144. throw $e;
  145. }
  146. }
  147. /**
  148. * 执行即抽即中的抽奖逻辑
  149. */
  150. private static function executeInstantDraw($drawRecord, $activity)
  151. {
  152. try {
  153. // 1. 获取可用奖品
  154. $prizes = static::getAvailablePrizes($activity);
  155. if (empty($prizes)) {
  156. throw new BusinessException('暂无可抽取的奖品');
  157. }
  158. // 2. 执行抽奖算法
  159. $selectedPrize = static::executeLotteryAlgorithm($prizes);
  160. // 3. 减少库存
  161. if (!static::decreasePrizeStock($selectedPrize)) {
  162. throw new BusinessException('奖品库存不足');
  163. }
  164. // 4. 更新抽奖记录
  165. $isWin = $selectedPrize->type != LotteryEnum::PRIZE_TYPE_NO_PRIZE;
  166. $winInfo = $isWin ? static::buildWinInfo($selectedPrize) : [];
  167. $drawRecord->prize_id = $selectedPrize->id;
  168. $drawRecord->status = $isWin ? LotteryEnum::DRAW_STATUS_WIN : LotteryEnum::DRAW_STATUS_NO_WIN;
  169. $drawRecord->is_win = $isWin ? 1 : 0;
  170. $drawRecord->win_info = json_encode($winInfo);
  171. $drawRecord->save();
  172. // 5. 如果中奖,创建中奖记录
  173. $winRecord = null;
  174. if ($isWin) {
  175. $winRecord = new \app\common\model\lottery\LotteryWinRecord([
  176. 'draw_record_id' => $drawRecord->id,
  177. 'activity_id' => $activity->id,
  178. 'user_id' => $drawRecord->user_id,
  179. 'prize_id' => $selectedPrize->id,
  180. 'prize_name' => $selectedPrize->name,
  181. 'prize_type' => $selectedPrize->type,
  182. 'prize_value' => json_encode(static::buildPrizeValue($selectedPrize)),
  183. 'deliver_status' => LotteryEnum::DELIVER_STATUS_PENDING,
  184. ]);
  185. if (!$winRecord->save()) {
  186. throw new Exception('创建中奖记录失败');
  187. }
  188. // 自动发放奖品
  189. if ($selectedPrize->deliver_type == LotteryEnum::DELIVER_TYPE_AUTO) {
  190. static::autoDeliverPrize($winRecord, $selectedPrize);
  191. }
  192. }
  193. // 6. 更新活动统计
  194. static::updateActivityStats($activity, $isWin);
  195. // 7. 返回抽奖结果
  196. return static::buildDrawResult($drawRecord, $selectedPrize, $winRecord);
  197. } catch (Exception $e) {
  198. // 如果发生错误,将状态改为未中奖
  199. $drawRecord->status = LotteryEnum::DRAW_STATUS_NO_WIN;
  200. $drawRecord->is_win = 0;
  201. $drawRecord->save();
  202. throw new BusinessException('抽奖失败');
  203. }
  204. }
  205. /**
  206. * 处理定时抽奖
  207. */
  208. private static function handleTimeLottery($drawRecord, $activity)
  209. {
  210. // 验证开奖时间
  211. if (isset($activity->lottery_time) && $activity->lottery_time < time()) {
  212. throw new BusinessException('活动开奖时间已过,无法参与');
  213. }
  214. return [
  215. 'draw_id' => $drawRecord->id,
  216. 'is_win' => 0,
  217. 'status' => LotteryEnum::DRAW_STATUS_PARTICIPATED, // 等待开奖
  218. 'lottery_type' => $activity->lottery_type,
  219. 'lottery_time' => $activity->lottery_time ?? 0,
  220. 'message' => isset($activity->lottery_time) ?
  221. '参与成功,等待' . date('Y-m-d H:i:s', $activity->lottery_time) . '开奖' :
  222. '参与成功,等待开奖'
  223. ];
  224. }
  225. /**
  226. * 处理按人数抽奖
  227. */
  228. private static function handlePeopleLottery($drawRecord, $activity)
  229. {
  230. // 检查是否达到开奖人数
  231. $participantCount = LotteryDrawRecord::where('activity_id', $activity->id)
  232. ->where('status', LotteryEnum::DRAW_STATUS_PARTICIPATED)
  233. ->count();
  234. $result = [
  235. 'draw_id' => $drawRecord->id,
  236. 'is_win' => 0,
  237. 'status' => 'waiting', // 等待开奖
  238. 'lottery_type' => $activity->lottery_type,
  239. 'current_participants' => $participantCount,
  240. 'required_participants' => $activity->lottery_people_num ?? 100
  241. ];
  242. if ($participantCount >= ($activity->lottery_people_num ?? 100)) {
  243. // 达到人数,触发开奖(这里可以异步处理)
  244. // TODO: 触发按人数开奖的处理逻辑
  245. $result['message'] = '参与成功,已达到开奖人数,正在开奖中...';
  246. } else {
  247. $remainingCount = ($activity->lottery_people_num ?? 100) - $participantCount;
  248. $result['message'] = "参与成功,还需要{$remainingCount}人参与即可开奖";
  249. }
  250. return $result;
  251. }
  252. /**
  253. * 处理抽奖核心逻辑
  254. */
  255. private static function processDrawLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount)
  256. {
  257. // 开启事务
  258. Db::startTrans();
  259. try {
  260. // 1. 获取可用奖品
  261. $prizes = static::getAvailablePrizes($activity);
  262. if (empty($prizes)) {
  263. throw new Exception('暂无可抽取的奖品');
  264. }
  265. // 2. 执行抽奖算法
  266. $selectedPrize = static::executeLotteryAlgorithm($prizes);
  267. // 3. 减少库存
  268. if (!static::decreasePrizeStock($selectedPrize)) {
  269. throw new Exception('奖品库存不足');
  270. }
  271. // 4. 消耗用户抽奖机会
  272. $userChance = LotteryChanceService::getUserChance($activity->id, $userId);
  273. if (!LotteryChanceService::useChance($userChance)) {
  274. throw new Exception('抽奖机会使用失败');
  275. }
  276. // 5. 创建抽奖记录
  277. $isWin = $selectedPrize->type != LotteryEnum::PRIZE_TYPE_NO_PRIZE;
  278. $winInfo = $isWin ? static::buildWinInfo($selectedPrize) : [];
  279. $drawRecord = LotteryRecordService::createDrawRecord(
  280. $activity->id,
  281. $userId,
  282. $selectedPrize->id,
  283. $isWin,
  284. $triggerType,
  285. $triggerOrderId,
  286. $triggerAmount,
  287. $winInfo
  288. );
  289. // 6. 如果中奖,创建中奖记录
  290. $winRecord = null;
  291. if ($isWin) {
  292. $winRecord = LotteryRecordService::createWinRecord(
  293. $drawRecord->id,
  294. $activity->id,
  295. $userId,
  296. $selectedPrize->id,
  297. $selectedPrize->name,
  298. $selectedPrize->type,
  299. static::buildPrizeValue($selectedPrize)
  300. );
  301. // 自动发放奖品
  302. if ($selectedPrize->deliver_type == LotteryEnum::DELIVER_TYPE_AUTO) {
  303. LotteryRecordService::autoDeliverPrize($winRecord, $selectedPrize);
  304. }
  305. }
  306. // 7. 更新活动统计
  307. static::updateActivityStats($activity, $isWin);
  308. // 提交事务
  309. Db::commit();
  310. // 8. 返回抽奖结果
  311. return static::buildDrawResult($drawRecord, $selectedPrize, $winRecord);
  312. } catch (Exception $e) {
  313. Db::rollback();
  314. throw $e;
  315. }
  316. }
  317. /**
  318. * 记录参与(用于按时间开奖和按人数开奖)
  319. */
  320. private static function recordParticipation($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount)
  321. {
  322. // 开启事务
  323. Db::startTrans();
  324. try {
  325. // 1. 消耗用户抽奖机会
  326. $userChance = LotteryChanceService::getUserChance($activity->id, $userId);
  327. if (!LotteryChanceService::useChance($userChance)) {
  328. throw new Exception('抽奖机会使用失败');
  329. }
  330. // 2. 创建参与记录(未开奖状态)
  331. $drawRecord = LotteryRecordService::createDrawRecord(
  332. $activity->id,
  333. $userId,
  334. 0, // 暂时没有奖品ID
  335. 0, // 未开奖
  336. $triggerType,
  337. $triggerOrderId,
  338. $triggerAmount,
  339. [] // 暂时没有中奖信息
  340. );
  341. // 提交事务
  342. Db::commit();
  343. // 3. 返回参与结果
  344. return [
  345. 'draw_id' => $drawRecord->id,
  346. 'is_win' => 0,
  347. 'status' => 'waiting', // 等待开奖
  348. 'lottery_type' => $activity->lottery_type,
  349. 'lottery_time' => $activity->lottery_time ?? 0,
  350. 'message' => $activity->lottery_type == LotteryEnum::LOTTERY_TYPE_TIME ?
  351. '参与成功,等待' . date('Y-m-d H:i:s', $activity->lottery_time) . '开奖' :
  352. '参与成功,等待开奖'
  353. ];
  354. } catch (Exception $e) {
  355. Db::rollback();
  356. throw new BusinessException('创建参与记录失败');
  357. }
  358. }
  359. /**
  360. * 定时任务:处理定时开奖活动
  361. * 用于定时任务调用,批量处理到时间开奖的活动
  362. */
  363. public static function processScheduledLotteries()
  364. {
  365. // 获取所有到达开奖时间的活动
  366. $activities = LotteryActivity::where('lottery_type', LotteryEnum::LOTTERY_TYPE_TIME)
  367. ->where('status', LotteryEnum::STATUS_ONGOING)
  368. ->where('lottery_time', '<=', time())
  369. ->select();
  370. $processedCount = 0;
  371. foreach ($activities as $activity) {
  372. try {
  373. $count = static::executeScheduledDraw($activity);
  374. $processedCount += $count;
  375. // 更新活动状态为已结束
  376. $activity->status = LotteryEnum::STATUS_ENDED;
  377. $activity->save();
  378. } catch (Exception $e) {
  379. trace("定时开奖失败 - 活动ID: {$activity->id}, 错误: " . $e->getMessage(), 'error');
  380. }
  381. }
  382. return $processedCount;
  383. }
  384. /**
  385. * 执行定时开奖
  386. */
  387. private static function executeScheduledDraw($activity)
  388. {
  389. // 获取所有参与记录
  390. $participantRecords = LotteryDrawRecord::where('activity_id', $activity->id)
  391. ->where('status', LotteryEnum::DRAW_STATUS_PARTICIPATED)
  392. ->select();
  393. if (empty($participantRecords)) {
  394. return 0;
  395. }
  396. $processedCount = 0;
  397. foreach ($participantRecords as $drawRecord) {
  398. try {
  399. // 为每个参与记录执行抽奖
  400. static::executeDrawForRecord($drawRecord, $activity);
  401. $processedCount++;
  402. } catch (Exception $e) {
  403. trace("用户开奖失败 - 记录ID: {$drawRecord->id}, 错误: " . $e->getMessage(), 'error');
  404. // 设置为未中奖
  405. $drawRecord->status = LotteryEnum::DRAW_STATUS_NO_WIN;
  406. $drawRecord->is_win = 0;
  407. $drawRecord->save();
  408. }
  409. }
  410. return $processedCount;
  411. }
  412. /**
  413. * 为单个抽奖记录执行开奖
  414. */
  415. private static function executeDrawForRecord($drawRecord, $activity)
  416. {
  417. // 获取可用奖品
  418. $prizes = static::getAvailablePrizes($activity);
  419. if (empty($prizes)) {
  420. // 没有奖品,设置为未中奖
  421. $drawRecord->status = LotteryEnum::DRAW_STATUS_NO_WIN;
  422. $drawRecord->is_win = 0;
  423. $drawRecord->save();
  424. return;
  425. }
  426. // 执行抽奖算法
  427. $selectedPrize = static::executeLotteryAlgorithm($prizes);
  428. // 减少库存
  429. if (!static::decreasePrizeStock($selectedPrize)) {
  430. // 库存不足,设置为未中奖
  431. $drawRecord->status = LotteryEnum::DRAW_STATUS_NO_WIN;
  432. $drawRecord->is_win = 0;
  433. $drawRecord->save();
  434. return;
  435. }
  436. // 更新抽奖记录
  437. $isWin = $selectedPrize->type != LotteryEnum::PRIZE_TYPE_NO_PRIZE;
  438. $winInfo = $isWin ? static::buildWinInfo($selectedPrize) : [];
  439. $drawRecord->prize_id = $selectedPrize->id;
  440. $drawRecord->status = $isWin ? LotteryEnum::DRAW_STATUS_WIN : LotteryEnum::DRAW_STATUS_NO_WIN;
  441. $drawRecord->is_win = $isWin ? 1 : 0;
  442. $drawRecord->win_info = json_encode($winInfo);
  443. $drawRecord->save();
  444. // 如果中奖,创建中奖记录
  445. if ($isWin) {
  446. $winRecord = new \app\common\model\lottery\LotteryWinRecord([
  447. 'draw_record_id' => $drawRecord->id,
  448. 'activity_id' => $activity->id,
  449. 'user_id' => $drawRecord->user_id,
  450. 'prize_id' => $selectedPrize->id,
  451. 'prize_name' => $selectedPrize->name,
  452. 'prize_type' => $selectedPrize->type,
  453. 'prize_value' => json_encode(static::buildPrizeValue($selectedPrize)),
  454. 'deliver_status' => LotteryEnum::DELIVER_STATUS_PENDING,
  455. ]);
  456. $winRecord->save();
  457. // 自动发放奖品
  458. if ($selectedPrize->deliver_type == LotteryEnum::DELIVER_TYPE_AUTO) {
  459. static::autoDeliverPrize($winRecord, $selectedPrize);
  460. }
  461. }
  462. // 更新活动统计
  463. static::updateActivityStats($activity, $isWin);
  464. }
  465. /**
  466. * 处理按人数开奖
  467. */
  468. private static function handlePeopleBasedLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount)
  469. {
  470. // 先记录参与
  471. $result = static::recordParticipation($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount);
  472. // 检查是否达到开奖人数
  473. $drawStats = LotteryRecordService::getActivityDrawStats($activity->id);
  474. $participantCount = $drawStats['total_draw'];
  475. if ($participantCount >= $activity->lottery_people_num) {
  476. // 达到人数,触发开奖(这里可以异步处理)
  477. // TODO: 触发按人数开奖的处理逻辑
  478. $result['message'] = '参与成功,已达到开奖人数,正在开奖中...';
  479. } else {
  480. $remainingCount = $activity->lottery_people_num - $participantCount;
  481. $result['message'] = "参与成功,还需要{$remainingCount}人参与即可开奖";
  482. }
  483. return $result;
  484. }
  485. /**
  486. * 获取可用奖品列表
  487. */
  488. private static function getAvailablePrizes($activity)
  489. {
  490. $prizes = static::getValidPrizes($activity->id);
  491. // 如果开启按人数解锁功能
  492. if ($activity->unlock_by_people) {
  493. $currentPeopleCount = LotteryChanceService::getActivityParticipants($activity->id);
  494. $prizes = $prizes->filter(function($prize) use ($currentPeopleCount) {
  495. return static::isPrizeUnlocked($prize, $currentPeopleCount);
  496. });
  497. }
  498. return $prizes;
  499. }
  500. /**
  501. * 执行抽奖算法(基于概率权重)
  502. */
  503. private static function executeLotteryAlgorithm($prizes)
  504. {
  505. // 检查是否有奖品
  506. if (empty($prizes)) {
  507. return null;
  508. }
  509. // 将奖品转换为数组(如果是集合对象)
  510. $prizeArray = is_array($prizes) ? $prizes : $prizes->toArray();
  511. // 使用BC函数计算总概率
  512. $totalProbability = '0';
  513. foreach ($prizeArray as $prize) {
  514. $totalProbability = bcadd($totalProbability, $prize['probability'] ?? '0', 4);
  515. }
  516. // 如果总概率为0,返回未中奖奖品
  517. if (bccomp($totalProbability, '0', 4) == 0) {
  518. return static::findNoPrizePrize($prizeArray);
  519. }
  520. // 生成随机数(0-总概率之间)
  521. $randomNumber = bcdiv(mt_rand(0, bcmul($totalProbability, '10000', 4)), '10000', 4);
  522. // 按概率选择奖品
  523. $currentProbability = '0';
  524. foreach ($prizeArray as $prize) {
  525. $currentProbability = bcadd($currentProbability, $prize['probability'] ?? '0', 4);
  526. if (bccomp($randomNumber, $currentProbability, 4) <= 0) {
  527. return $prize;
  528. }
  529. }
  530. // 保底返回未中奖类型的奖品
  531. return static::findNoPrizePrize($prizeArray);
  532. }
  533. /**
  534. * 查找未中奖类型的奖品
  535. */
  536. private static function findNoPrizePrize($prizeArray)
  537. {
  538. // 优先查找未中奖类型的奖品
  539. foreach ($prizeArray as $prize) {
  540. if (($prize['type'] ?? 0) == LotteryEnum::PRIZE_TYPE_NO_PRIZE) {
  541. return $prize;
  542. }
  543. }
  544. // 如果没找到未中奖类型,返回第一个奖品作为兜底
  545. return isset($prizeArray[0]) ? $prizeArray[0] : null;
  546. }
  547. /**
  548. * 验证用户资格
  549. */
  550. private static function validateUserQualification($activity, $userId)
  551. {
  552. // 检查用户群体限制
  553. switch ($activity->user_limit_type) {
  554. case LotteryEnum::USER_LIMIT_ALL:
  555. return true;
  556. case LotteryEnum::USER_LIMIT_LEVEL:
  557. return static::checkUserLevel($userId, $activity->user_limit_value);
  558. case LotteryEnum::USER_LIMIT_TAG:
  559. return static::checkUserTag($userId, $activity->user_limit_value);
  560. default:
  561. return false;
  562. }
  563. }
  564. /**
  565. * 检查用户等级
  566. */
  567. private static function checkUserLevel($userId, $limitValue)
  568. {
  569. if (empty($limitValue)) {
  570. return true;
  571. }
  572. $user = User::find($userId);
  573. return $user && in_array($user->level, (array)$limitValue);
  574. }
  575. /**
  576. * 检查用户标签
  577. */
  578. private static function checkUserTag($userId, $limitValue)
  579. {
  580. if (empty($limitValue)) {
  581. return true;
  582. }
  583. // 这里需要根据实际的用户标签系统实现
  584. // 暂时返回true
  585. return true;
  586. }
  587. /**
  588. * 检查用户抽奖次数限制
  589. */
  590. private static function checkUserDrawLimit($activity, $userId)
  591. {
  592. if (!$activity->person_limit_num) {
  593. return true;
  594. }
  595. $drawCount = LotteryRecordService::getUserDrawCount($activity->id, $userId);
  596. return $drawCount < $activity->person_limit_num;
  597. }
  598. /**
  599. * 检查订单是否已抽奖
  600. */
  601. private static function hasDrawnForOrder($activityId, $userId, $orderId)
  602. {
  603. return LotteryRecordService::hasUserDrawnForOrder($activityId, $userId, $orderId);
  604. }
  605. /**
  606. * 构建中奖信息
  607. */
  608. private static function buildWinInfo($prize)
  609. {
  610. return [
  611. 'prize_id' => $prize->id,
  612. 'prize_name' => $prize->name,
  613. 'prize_type' => $prize->type,
  614. 'prize_image' => $prize->image,
  615. 'win_prompt' => $prize->win_prompt
  616. ];
  617. }
  618. /**
  619. * 构建奖品价值信息
  620. */
  621. private static function buildPrizeValue($prize)
  622. {
  623. $prizeValue = [
  624. 'type' => $prize->type,
  625. 'name' => $prize->name,
  626. 'image' => $prize->image
  627. ];
  628. switch ($prize->type) {
  629. case LotteryEnum::PRIZE_TYPE_COUPON:
  630. $prizeValue['coupon_id'] = $prize->coupon_id;
  631. break;
  632. case LotteryEnum::PRIZE_TYPE_REDPACK:
  633. $prizeValue['amount'] = $prize->amount;
  634. break;
  635. case LotteryEnum::PRIZE_TYPE_SHOP_GOODS:
  636. $prizeValue['goods_id'] = $prize->goods_id;
  637. $prizeValue['goods_sku_id'] = $prize->goods_sku_id;
  638. break;
  639. case LotteryEnum::PRIZE_TYPE_CODE:
  640. $prizeValue['exchange_code'] = static::getAvailableExchangeCode($prize);
  641. break;
  642. }
  643. return $prizeValue;
  644. }
  645. /**
  646. * 自动发放奖品
  647. */
  648. private static function autoDeliverPrize($winRecord, $prize)
  649. {
  650. try {
  651. switch ($prize->type) {
  652. case LotteryEnum::PRIZE_TYPE_COUPON:
  653. static::deliverCoupon($winRecord, $prize);
  654. break;
  655. case LotteryEnum::PRIZE_TYPE_REDPACK:
  656. static::deliverRedPacket($winRecord, $prize);
  657. break;
  658. case LotteryEnum::PRIZE_TYPE_CODE:
  659. static::deliverExchangeCode($winRecord, $prize);
  660. break;
  661. case LotteryEnum::PRIZE_TYPE_SHOP_GOODS:
  662. static::deliverGoods($winRecord, $prize);
  663. break;
  664. }
  665. } catch (Exception $e) {
  666. LotteryRecordService::markWinRecordAsFailed($winRecord, $e->getMessage());
  667. }
  668. }
  669. /**
  670. * 发放优惠券
  671. */
  672. private static function deliverCoupon($winRecord, $prize)
  673. {
  674. // 这里调用优惠券发放接口
  675. // 示例代码,需要根据实际优惠券系统实现
  676. LotteryRecordService::markWinRecordAsDelivered($winRecord, ['coupon_id' => $prize->coupon_id]);
  677. }
  678. /**
  679. * 发放红包
  680. */
  681. private static function deliverRedPacket($winRecord, $prize)
  682. {
  683. // 这里调用红包发放接口
  684. // 示例代码,需要根据实际红包系统实现
  685. LotteryRecordService::markWinRecordAsDelivered($winRecord, ['amount' => $prize->amount]);
  686. }
  687. /**
  688. * 发放兑换码
  689. */
  690. private static function deliverExchangeCode($winRecord, $prize)
  691. {
  692. $code = static::getAvailableExchangeCode($prize);
  693. if ($code) {
  694. LotteryRecordService::setWinRecordExchangeCode($winRecord, $code);
  695. static::markExchangeCodeUsed($prize, $code);
  696. LotteryRecordService::markWinRecordAsDelivered($winRecord, ['exchange_code' => $code]);
  697. } else {
  698. throw new Exception('兑换码已用完');
  699. }
  700. }
  701. /**
  702. * 发放商城商品
  703. */
  704. private static function deliverGoods($winRecord, $prize)
  705. {
  706. // 这里可以自动加入购物车或创建订单
  707. // 示例代码,需要根据实际商城系统实现
  708. LotteryRecordService::markWinRecordAsDelivered($winRecord, [
  709. 'goods_id' => $prize->goods_id,
  710. 'goods_sku_id' => $prize->goods_sku_id
  711. ]);
  712. }
  713. /**
  714. * 更新活动统计
  715. */
  716. private static function updateActivityStats($activity, $isWin)
  717. {
  718. $activity->total_draw_count += 1;
  719. if ($isWin) {
  720. $activity->total_win_count += 1;
  721. }
  722. $activity->save();
  723. }
  724. /**
  725. * 构建即抽即中的抽奖结果
  726. */
  727. private static function buildDrawResult($drawRecord, $prize, $winRecord = null)
  728. {
  729. $result = [
  730. 'draw_id' => $drawRecord->id,
  731. 'is_win' => $drawRecord->is_win,
  732. 'prize' => [
  733. 'id' => $prize->id,
  734. 'name' => $prize->name,
  735. 'type' => $prize->type,
  736. 'type_text' => $prize->type_text,
  737. 'image' => cdnurl($prize->image),
  738. 'win_prompt' => $prize->win_prompt
  739. ],
  740. 'draw_time' => $drawRecord->draw_time
  741. ];
  742. if ($winRecord) {
  743. $result['win_record_id'] = $winRecord->id;
  744. $result['deliver_status'] = $winRecord->deliver_status;
  745. // 只有兑换码类型的奖品才记录兑换码信息
  746. if ($prize->type == LotteryEnum::PRIZE_TYPE_CODE) {
  747. $result['exchange_code'] = $winRecord->exchange_code;
  748. }
  749. }
  750. return $result;
  751. }
  752. /**
  753. * 获取用户抽奖机会
  754. * @param int $activityId 活动ID
  755. * @param int $userId 用户ID
  756. * @return array
  757. */
  758. public static function getUserChances($activityId, $userId)
  759. {
  760. $userChance = LotteryChanceService::getUserChance($activityId, $userId);
  761. if (!$userChance) {
  762. return [
  763. 'total_chances' => 0,
  764. 'used_chances' => 0,
  765. 'remain_chances' => 0
  766. ];
  767. }
  768. return [
  769. 'total_chances' => $userChance->total_chances,
  770. 'used_chances' => $userChance->used_chances,
  771. 'remain_chances' => $userChance->remain_chances,
  772. 'last_get_time' => $userChance->last_get_time,
  773. 'last_use_time' => $userChance->last_use_time
  774. ];
  775. }
  776. // ============ 从LotteryPrize模型移过来的业务逻辑方法 ============
  777. /**
  778. * 检查奖品库存是否充足
  779. */
  780. public static function hasPrizeStock(LotteryPrize $prize, $quantity = 1)
  781. {
  782. return $prize->remain_stock >= $quantity;
  783. }
  784. /**
  785. * 减少奖品库存
  786. */
  787. public static function decreasePrizeStock(LotteryPrize $prize, $quantity = 1)
  788. {
  789. if (!static::hasPrizeStock($prize, $quantity)) {
  790. return false;
  791. }
  792. $prize->remain_stock -= $quantity;
  793. $prize->win_count += $quantity;
  794. return $prize->save();
  795. }
  796. /**
  797. * 获取可用的兑换码
  798. */
  799. public static function getAvailableExchangeCode(LotteryPrize $prize)
  800. {
  801. if ($prize->type != LotteryEnum::PRIZE_TYPE_CODE) {
  802. return null;
  803. }
  804. $allCodes = $prize->exchange_codes_list;
  805. $usedCodes = $prize->used_codes_list;
  806. $availableCodes = array_diff($allCodes, $usedCodes);
  807. if (empty($availableCodes)) {
  808. return null;
  809. }
  810. return array_shift($availableCodes);
  811. }
  812. /**
  813. * 标记兑换码为已使用
  814. */
  815. public static function markExchangeCodeUsed(LotteryPrize $prize, $code)
  816. {
  817. $usedCodes = $prize->used_codes_list;
  818. if (!in_array($code, $usedCodes)) {
  819. $usedCodes[] = $code;
  820. $prize->used_codes = json_encode($usedCodes);
  821. return $prize->save();
  822. }
  823. return true;
  824. }
  825. /**
  826. * 获取有效奖品(库存大于0且状态正常)
  827. */
  828. public static function getValidPrizes($activityId)
  829. {
  830. return LotteryPrize::where('activity_id', $activityId)
  831. ->where('status', 1)
  832. ->where('remain_stock', '>', 0)
  833. ->order('sort_order', 'asc')
  834. ->select();
  835. }
  836. /**
  837. * 检查奖品是否已解锁(按人数解锁功能)
  838. */
  839. public static function isPrizeUnlocked(LotteryPrize $prize, $currentPeopleCount)
  840. {
  841. if (empty($prize->unlock_people_num)) {
  842. return true;
  843. }
  844. return $currentPeopleCount >= $prize->unlock_people_num;
  845. }
  846. /**
  847. * 检查活动是否正在进行
  848. */
  849. public static function isActivityRunning(LotteryActivity $activity)
  850. {
  851. $now = time();
  852. return $activity->status == LotteryEnum::STATUS_ONGOING
  853. && $activity->start_time <= $now
  854. && $activity->end_time >= $now;
  855. }
  856. /**
  857. * 检查活动是否已结束
  858. */
  859. public static function isActivityEnded(LotteryActivity $activity)
  860. {
  861. $now = time();
  862. return $activity->status == LotteryEnum::STATUS_ENDED
  863. || $activity->end_time < $now;
  864. }
  865. /**
  866. * 检查活动是否未开始
  867. */
  868. public static function isActivityNotStarted(LotteryActivity $activity)
  869. {
  870. $now = time();
  871. return $activity->status == LotteryEnum::STATUS_NOT_STARTED
  872. && $activity->start_time > $now;
  873. }
  874. /**
  875. * 检查活动是否已暂停
  876. */
  877. public static function isActivitySuspended(LotteryActivity $activity)
  878. {
  879. return $activity->status == LotteryEnum::STATUS_SUSPENDED;
  880. }
  881. /**
  882. * 检查活动是否已取消
  883. */
  884. public static function isActivityCancelled(LotteryActivity $activity)
  885. {
  886. return $activity->status == LotteryEnum::STATUS_CANCELLED;
  887. }
  888. // TODO: 分布式锁相关方法(暂时移除,后续实现)
  889. /*
  890. private static function acquireLock($lockKey, $expireTime = 10)
  891. {
  892. // Redis锁获取逻辑
  893. }
  894. private static function releaseLock($lockKey)
  895. {
  896. // Redis锁释放逻辑
  897. }
  898. private static function acquireFileLock($lockKey, $expireTime = 10)
  899. {
  900. // 文件锁获取逻辑
  901. }
  902. private static function releaseFileLock($lockKey)
  903. {
  904. // 文件锁释放逻辑
  905. }
  906. */
  907. /**
  908. * 获取正在进行的活动
  909. */
  910. public static function getRunningActivities()
  911. {
  912. $now = time();
  913. return LotteryActivity::where('status', LotteryEnum::STATUS_ONGOING)
  914. ->where('start_time', '<=', $now)
  915. ->where('end_time', '>=', $now)
  916. ->select();
  917. }
  918. /**
  919. * 获取未开始的活动
  920. */
  921. public static function getNotStartedActivities()
  922. {
  923. $now = time();
  924. return LotteryActivity::where('status', LotteryEnum::STATUS_NOT_STARTED)
  925. ->where('start_time', '>', $now)
  926. ->select();
  927. }
  928. /**
  929. * 获取已结束的活动
  930. */
  931. public static function getEndedActivities()
  932. {
  933. $now = time();
  934. return LotteryActivity::where('status', LotteryEnum::STATUS_ENDED)
  935. ->orWhere('end_time', '<', $now)
  936. ->select();
  937. }
  938. /**
  939. * 获取可显示的活动(排除逻辑状态)
  940. */
  941. public static function getDisplayableActivities()
  942. {
  943. $displayableStatuses = array_keys(LotteryEnum::getActivityStatusMap());
  944. return LotteryActivity::whereIn('status', $displayableStatuses)->select();
  945. }
  946. /**
  947. * 验证活动状态是否有效
  948. */
  949. public static function isValidActivityStatus(LotteryActivity $activity)
  950. {
  951. return LotteryEnum::isValidActivityStatus($activity->status);
  952. }
  953. /**
  954. * 验证开奖方式是否有效
  955. */
  956. public static function isValidLotteryType(LotteryActivity $activity)
  957. {
  958. return LotteryEnum::isValidLotteryType($activity->lottery_type);
  959. }
  960. }