LotteryService.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812
  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. /**
  12. * 抽奖服务类
  13. * 核心抽奖逻辑处理
  14. */
  15. class LotteryService
  16. {
  17. // TODO: 分布式锁相关功能(暂时移除,后续实现)
  18. /**
  19. * 当前进程持有的锁信息
  20. * @var array
  21. */
  22. // private static $locks = [];
  23. /**
  24. * 执行抽奖
  25. * @param int $activityId 活动ID
  26. * @param int $userId 用户ID
  27. * @param int $triggerType 触发类型
  28. * @param int $triggerOrderId 触发订单ID
  29. * @param float $triggerAmount 触发金额
  30. * @return array 抽奖结果
  31. * @throws Exception
  32. */
  33. public static function drawLottery($activityId, $userId, $triggerType = 1, $triggerOrderId = null, $triggerAmount = null)
  34. {
  35. // 1. 验证活动有效性
  36. $activity = LotteryActivity::find($activityId);
  37. if (!$activity || !self::isActivityRunning($activity)) {
  38. throw new BusinessException('活动不存在或未开始', ErrorCodeEnum::LOTTERY_ACTIVITY_NOT_FOUND);
  39. }
  40. // 2. 验证开奖时间(仅对按时间开奖有效)
  41. if ($activity->lottery_type == LotteryEnum::LOTTERY_TYPE_TIME) {
  42. $now = time();
  43. if ($activity->lottery_time && $now >= $activity->lottery_time) {
  44. throw new BusinessException('开奖时间已过,无法参与', ErrorCodeEnum::LOTTERY_NOT_IN_TIME);
  45. }
  46. }
  47. // 3. 验证参与人数限制(仅对按人数开奖有效)
  48. if ($activity->lottery_type == LotteryEnum::LOTTERY_TYPE_PEOPLE) {
  49. $drawStats = LotteryRecordService::getActivityDrawStats($activityId);
  50. if ($drawStats['total_draw'] >= $activity->lottery_people_num) {
  51. throw new BusinessException('参与人数已满,无法参与', ErrorCodeEnum::LOTTERY_REACH_LIMIT);
  52. }
  53. }
  54. // 4. 验证用户资格
  55. if (!static::validateUserQualification($activity, $userId)) {
  56. throw new BusinessException('用户不符合参与条件', ErrorCodeEnum::LOTTERY_USER_NOT_QUALIFIED);
  57. }
  58. // 5. 检查用户抽奖机会
  59. $userChance = LotteryChanceService::getUserChance($activityId, $userId);
  60. if (!$userChance || !LotteryChanceService::hasChance($userChance)) {
  61. throw new BusinessException('没有抽奖机会', ErrorCodeEnum::LOTTERY_NO_CHANCE);
  62. }
  63. // 6. 检查用户参与次数限制
  64. if (!static::checkUserDrawLimit($activity, $userId)) {
  65. throw new BusinessException('已达到参与次数上限', ErrorCodeEnum::LOTTERY_REACH_LIMIT);
  66. }
  67. // 7. 防重复抽奖检查(基于订单)
  68. if ($triggerOrderId && static::hasDrawnForOrder($activityId, $userId, $triggerOrderId)) {
  69. throw new BusinessException('该订单已参与过抽奖', ErrorCodeEnum::LOTTERY_ORDER_ALREADY_DRAWN);
  70. }
  71. // TODO: 8. 使用分布式锁防止并发(暂时移除,后续实现)
  72. // $lockKey = "lottery_lock_{$activityId}_{$userId}";
  73. // $lockAcquired = static::acquireLock($lockKey, 10);
  74. // if (!$lockAcquired) {
  75. // throw new Exception('操作太频繁,请稍后再试');
  76. // }
  77. // try {
  78. // 8. 根据开奖方式处理抽奖流程
  79. return static::handleLotteryByType($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount);
  80. // } finally {
  81. // // 释放锁
  82. // static::releaseLock($lockKey);
  83. // }
  84. }
  85. /**
  86. * 根据开奖方式处理抽奖
  87. */
  88. private static function handleLotteryByType($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount)
  89. {
  90. switch ($activity->lottery_type) {
  91. case LotteryEnum::LOTTERY_TYPE_INSTANT:
  92. // 即抽即中:直接执行抽奖
  93. return static::processDrawLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount);
  94. case LotteryEnum::LOTTERY_TYPE_TIME:
  95. // 按时间开奖:记录参与,等待定时任务开奖
  96. return static::recordParticipation($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount);
  97. case LotteryEnum::LOTTERY_TYPE_PEOPLE:
  98. // 按人数开奖:记录参与,检查是否达到开奖人数
  99. return static::handlePeopleBasedLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount);
  100. default:
  101. throw new BusinessException('不支持的开奖方式', ErrorCodeEnum::LOTTERY_ACTIVITY_NOT_FOUND);
  102. }
  103. }
  104. /**
  105. * 处理抽奖核心逻辑
  106. */
  107. private static function processDrawLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount)
  108. {
  109. // 开启事务
  110. Db::startTrans();
  111. try {
  112. // 1. 获取可用奖品
  113. $prizes = static::getAvailablePrizes($activity);
  114. if (empty($prizes)) {
  115. throw new Exception('暂无可抽取的奖品');
  116. }
  117. // 2. 执行抽奖算法
  118. $selectedPrize = static::executeLotteryAlgorithm($prizes);
  119. // 3. 减少库存
  120. if (!static::decreasePrizeStock($selectedPrize)) {
  121. throw new Exception('奖品库存不足');
  122. }
  123. // 4. 消耗用户抽奖机会
  124. $userChance = LotteryChanceService::getUserChance($activity->id, $userId);
  125. if (!LotteryChanceService::useChance($userChance)) {
  126. throw new Exception('抽奖机会使用失败');
  127. }
  128. // 5. 创建抽奖记录
  129. $isWin = $selectedPrize->type != LotteryEnum::PRIZE_TYPE_NO_PRIZE;
  130. $winInfo = $isWin ? static::buildWinInfo($selectedPrize) : [];
  131. $drawRecord = LotteryRecordService::createDrawRecord(
  132. $activity->id,
  133. $userId,
  134. $selectedPrize->id,
  135. $isWin,
  136. $triggerType,
  137. $triggerOrderId,
  138. $triggerAmount,
  139. $winInfo
  140. );
  141. // 6. 如果中奖,创建中奖记录
  142. $winRecord = null;
  143. if ($isWin) {
  144. $winRecord = LotteryRecordService::createWinRecord(
  145. $drawRecord->id,
  146. $activity->id,
  147. $userId,
  148. $selectedPrize->id,
  149. $selectedPrize->name,
  150. $selectedPrize->type,
  151. static::buildPrizeValue($selectedPrize)
  152. );
  153. // 自动发放奖品
  154. if ($selectedPrize->deliver_type == LotteryEnum::DELIVER_TYPE_AUTO) {
  155. LotteryRecordService::autoDeliverPrize($winRecord, $selectedPrize);
  156. }
  157. }
  158. // 7. 更新活动统计
  159. static::updateActivityStats($activity, $isWin);
  160. // 提交事务
  161. Db::commit();
  162. // 8. 返回抽奖结果
  163. return static::buildDrawResult($drawRecord, $selectedPrize, $winRecord);
  164. } catch (Exception $e) {
  165. Db::rollback();
  166. throw $e;
  167. }
  168. }
  169. /**
  170. * 记录参与(用于按时间开奖和按人数开奖)
  171. */
  172. private static function recordParticipation($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount)
  173. {
  174. // 开启事务
  175. Db::startTrans();
  176. try {
  177. // 1. 消耗用户抽奖机会
  178. $userChance = LotteryChanceService::getUserChance($activity->id, $userId);
  179. if (!LotteryChanceService::useChance($userChance)) {
  180. throw new Exception('抽奖机会使用失败');
  181. }
  182. // 2. 创建参与记录(未开奖状态)
  183. $drawRecord = LotteryRecordService::createDrawRecord(
  184. $activity->id,
  185. $userId,
  186. 0, // 暂时没有奖品ID
  187. 0, // 未开奖
  188. $triggerType,
  189. $triggerOrderId,
  190. $triggerAmount,
  191. [] // 暂时没有中奖信息
  192. );
  193. // 提交事务
  194. Db::commit();
  195. // 3. 返回参与结果
  196. return [
  197. 'draw_id' => $drawRecord->id,
  198. 'is_win' => 0,
  199. 'status' => 'waiting', // 等待开奖
  200. 'lottery_type' => $activity->lottery_type,
  201. 'lottery_time' => $activity->lottery_time ?? 0,
  202. 'message' => $activity->lottery_type == LotteryEnum::LOTTERY_TYPE_TIME ?
  203. '参与成功,等待' . date('Y-m-d H:i:s', $activity->lottery_time) . '开奖' :
  204. '参与成功,等待开奖'
  205. ];
  206. } catch (Exception $e) {
  207. Db::rollback();
  208. throw $e;
  209. }
  210. }
  211. /**
  212. * 处理按人数开奖
  213. */
  214. private static function handlePeopleBasedLottery($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount)
  215. {
  216. // 先记录参与
  217. $result = static::recordParticipation($activity, $userId, $triggerType, $triggerOrderId, $triggerAmount);
  218. // 检查是否达到开奖人数
  219. $drawStats = LotteryRecordService::getActivityDrawStats($activity->id);
  220. $participantCount = $drawStats['total_draw'];
  221. if ($participantCount >= $activity->lottery_people_num) {
  222. // 达到人数,触发开奖(这里可以异步处理)
  223. // TODO: 触发按人数开奖的处理逻辑
  224. $result['message'] = '参与成功,已达到开奖人数,正在开奖中...';
  225. } else {
  226. $remainingCount = $activity->lottery_people_num - $participantCount;
  227. $result['message'] = "参与成功,还需要{$remainingCount}人参与即可开奖";
  228. }
  229. return $result;
  230. }
  231. /**
  232. * 获取可用奖品列表
  233. */
  234. private static function getAvailablePrizes($activity)
  235. {
  236. $prizes = static::getValidPrizes($activity->id);
  237. // 如果开启按人数解锁功能
  238. if ($activity->unlock_by_people) {
  239. $currentPeopleCount = LotteryChanceService::getActivityParticipants($activity->id);
  240. $prizes = $prizes->filter(function($prize) use ($currentPeopleCount) {
  241. return static::isPrizeUnlocked($prize, $currentPeopleCount);
  242. });
  243. }
  244. return $prizes;
  245. }
  246. /**
  247. * 执行抽奖算法(基于概率权重)
  248. */
  249. private static function executeLotteryAlgorithm($prizes)
  250. {
  251. // 检查是否有奖品
  252. if (empty($prizes)) {
  253. return null;
  254. }
  255. // 将奖品转换为数组(如果是集合对象)
  256. $prizeArray = is_array($prizes) ? $prizes : $prizes->toArray();
  257. // 使用BC函数计算总概率
  258. $totalProbability = '0';
  259. foreach ($prizeArray as $prize) {
  260. $totalProbability = bcadd($totalProbability, $prize['probability'] ?? '0', 4);
  261. }
  262. // 如果总概率为0,返回未中奖奖品
  263. if (bccomp($totalProbability, '0', 4) == 0) {
  264. return static::findNoPrizePrize($prizeArray);
  265. }
  266. // 生成随机数(0-总概率之间)
  267. $randomNumber = bcdiv(mt_rand(0, bcmul($totalProbability, '10000', 4)), '10000', 4);
  268. // 按概率选择奖品
  269. $currentProbability = '0';
  270. foreach ($prizeArray as $prize) {
  271. $currentProbability = bcadd($currentProbability, $prize['probability'] ?? '0', 4);
  272. if (bccomp($randomNumber, $currentProbability, 4) <= 0) {
  273. return $prize;
  274. }
  275. }
  276. // 保底返回未中奖类型的奖品
  277. return static::findNoPrizePrize($prizeArray);
  278. }
  279. /**
  280. * 查找未中奖类型的奖品
  281. */
  282. private static function findNoPrizePrize($prizeArray)
  283. {
  284. // 优先查找未中奖类型的奖品
  285. foreach ($prizeArray as $prize) {
  286. if (($prize['type'] ?? 0) == LotteryEnum::PRIZE_TYPE_NO_PRIZE) {
  287. return $prize;
  288. }
  289. }
  290. // 如果没找到未中奖类型,返回第一个奖品作为兜底
  291. return isset($prizeArray[0]) ? $prizeArray[0] : null;
  292. }
  293. /**
  294. * 验证用户资格
  295. */
  296. private static function validateUserQualification($activity, $userId)
  297. {
  298. // 检查用户群体限制
  299. switch ($activity->user_limit_type) {
  300. case LotteryEnum::USER_LIMIT_ALL:
  301. return true;
  302. case LotteryEnum::USER_LIMIT_LEVEL:
  303. return static::checkUserLevel($userId, $activity->user_limit_value);
  304. case LotteryEnum::USER_LIMIT_TAG:
  305. return static::checkUserTag($userId, $activity->user_limit_value);
  306. default:
  307. return false;
  308. }
  309. }
  310. /**
  311. * 检查用户等级
  312. */
  313. private static function checkUserLevel($userId, $limitValue)
  314. {
  315. if (empty($limitValue)) {
  316. return true;
  317. }
  318. $user = \app\common\model\User::find($userId);
  319. return $user && in_array($user->level, (array)$limitValue);
  320. }
  321. /**
  322. * 检查用户标签
  323. */
  324. private static function checkUserTag($userId, $limitValue)
  325. {
  326. if (empty($limitValue)) {
  327. return true;
  328. }
  329. // 这里需要根据实际的用户标签系统实现
  330. // 暂时返回true
  331. return true;
  332. }
  333. /**
  334. * 检查用户抽奖次数限制
  335. */
  336. private static function checkUserDrawLimit($activity, $userId)
  337. {
  338. if (!$activity->person_limit_num) {
  339. return true;
  340. }
  341. $drawCount = LotteryRecordService::getUserDrawCount($activity->id, $userId);
  342. return $drawCount < $activity->person_limit_num;
  343. }
  344. /**
  345. * 检查订单是否已抽奖
  346. */
  347. private static function hasDrawnForOrder($activityId, $userId, $orderId)
  348. {
  349. return LotteryRecordService::hasUserDrawnForOrder($activityId, $userId, $orderId);
  350. }
  351. /**
  352. * 构建中奖信息
  353. */
  354. private static function buildWinInfo($prize)
  355. {
  356. return [
  357. 'prize_id' => $prize->id,
  358. 'prize_name' => $prize->name,
  359. 'prize_type' => $prize->type,
  360. 'prize_image' => $prize->image,
  361. 'win_prompt' => $prize->win_prompt
  362. ];
  363. }
  364. /**
  365. * 构建奖品价值信息
  366. */
  367. private static function buildPrizeValue($prize)
  368. {
  369. $prizeValue = [
  370. 'type' => $prize->type,
  371. 'name' => $prize->name,
  372. 'image' => $prize->image
  373. ];
  374. switch ($prize->type) {
  375. case LotteryEnum::PRIZE_TYPE_COUPON:
  376. $prizeValue['coupon_id'] = $prize->coupon_id;
  377. break;
  378. case LotteryEnum::PRIZE_TYPE_REDPACK:
  379. $prizeValue['amount'] = $prize->amount;
  380. break;
  381. case LotteryEnum::PRIZE_TYPE_SHOP_GOODS:
  382. $prizeValue['goods_id'] = $prize->goods_id;
  383. $prizeValue['goods_sku_id'] = $prize->goods_sku_id;
  384. break;
  385. case LotteryEnum::PRIZE_TYPE_CODE:
  386. $prizeValue['exchange_code'] = static::getAvailableExchangeCode($prize);
  387. break;
  388. }
  389. return $prizeValue;
  390. }
  391. /**
  392. * 自动发放奖品
  393. */
  394. private static function autoDeliverPrize($winRecord, $prize)
  395. {
  396. try {
  397. switch ($prize->type) {
  398. case LotteryEnum::PRIZE_TYPE_COUPON:
  399. static::deliverCoupon($winRecord, $prize);
  400. break;
  401. case LotteryEnum::PRIZE_TYPE_REDPACK:
  402. static::deliverRedPacket($winRecord, $prize);
  403. break;
  404. case LotteryEnum::PRIZE_TYPE_CODE:
  405. static::deliverExchangeCode($winRecord, $prize);
  406. break;
  407. case LotteryEnum::PRIZE_TYPE_SHOP_GOODS:
  408. static::deliverGoods($winRecord, $prize);
  409. break;
  410. }
  411. } catch (Exception $e) {
  412. LotteryRecordService::markWinRecordAsFailed($winRecord, $e->getMessage());
  413. }
  414. }
  415. /**
  416. * 发放优惠券
  417. */
  418. private static function deliverCoupon($winRecord, $prize)
  419. {
  420. // 这里调用优惠券发放接口
  421. // 示例代码,需要根据实际优惠券系统实现
  422. LotteryRecordService::markWinRecordAsDelivered($winRecord, ['coupon_id' => $prize->coupon_id]);
  423. }
  424. /**
  425. * 发放红包
  426. */
  427. private static function deliverRedPacket($winRecord, $prize)
  428. {
  429. // 这里调用红包发放接口
  430. // 示例代码,需要根据实际红包系统实现
  431. LotteryRecordService::markWinRecordAsDelivered($winRecord, ['amount' => $prize->amount]);
  432. }
  433. /**
  434. * 发放兑换码
  435. */
  436. private static function deliverExchangeCode($winRecord, $prize)
  437. {
  438. $code = static::getAvailableExchangeCode($prize);
  439. if ($code) {
  440. LotteryRecordService::setWinRecordExchangeCode($winRecord, $code);
  441. static::markExchangeCodeUsed($prize, $code);
  442. LotteryRecordService::markWinRecordAsDelivered($winRecord, ['exchange_code' => $code]);
  443. } else {
  444. throw new Exception('兑换码已用完');
  445. }
  446. }
  447. /**
  448. * 发放商城商品
  449. */
  450. private static function deliverGoods($winRecord, $prize)
  451. {
  452. // 这里可以自动加入购物车或创建订单
  453. // 示例代码,需要根据实际商城系统实现
  454. LotteryRecordService::markWinRecordAsDelivered($winRecord, [
  455. 'goods_id' => $prize->goods_id,
  456. 'goods_sku_id' => $prize->goods_sku_id
  457. ]);
  458. }
  459. /**
  460. * 更新活动统计
  461. */
  462. private static function updateActivityStats($activity, $isWin)
  463. {
  464. $activity->total_draw_count += 1;
  465. if ($isWin) {
  466. $activity->total_win_count += 1;
  467. }
  468. $activity->save();
  469. }
  470. /**
  471. * 构建抽奖结果
  472. */
  473. private static function buildDrawResult($drawRecord, $prize, $winRecord = null)
  474. {
  475. $result = [
  476. 'draw_id' => $drawRecord->id,
  477. 'is_win' => $drawRecord->is_win,
  478. 'prize' => [
  479. 'id' => $prize->id,
  480. 'name' => $prize->name,
  481. 'type' => $prize->type,
  482. 'type_text' => $prize->type_text,
  483. 'image' => $prize->image,
  484. 'win_prompt' => $prize->win_prompt
  485. ],
  486. 'draw_time' => $drawRecord->draw_time
  487. ];
  488. if ($winRecord) {
  489. $result['win_record_id'] = $winRecord->id;
  490. $result['deliver_status'] = $winRecord->deliver_status;
  491. // 只有兑换码类型的奖品才记录兑换码信息
  492. if ($prize->type == LotteryEnum::PRIZE_TYPE_CODE) {
  493. $result['exchange_code'] = $winRecord->exchange_code;
  494. }
  495. }
  496. return $result;
  497. }
  498. /**
  499. * 获取用户抽奖机会
  500. * @param int $activityId 活动ID
  501. * @param int $userId 用户ID
  502. * @return array
  503. */
  504. public static function getUserChances($activityId, $userId)
  505. {
  506. $userChance = LotteryChanceService::getUserChance($activityId, $userId);
  507. if (!$userChance) {
  508. return [
  509. 'total_chances' => 0,
  510. 'used_chances' => 0,
  511. 'remain_chances' => 0
  512. ];
  513. }
  514. return [
  515. 'total_chances' => $userChance->total_chances,
  516. 'used_chances' => $userChance->used_chances,
  517. 'remain_chances' => $userChance->remain_chances,
  518. 'last_get_time' => $userChance->last_get_time,
  519. 'last_use_time' => $userChance->last_use_time
  520. ];
  521. }
  522. // ============ 从LotteryPrize模型移过来的业务逻辑方法 ============
  523. /**
  524. * 检查奖品库存是否充足
  525. */
  526. public static function hasPrizeStock(LotteryPrize $prize, $quantity = 1)
  527. {
  528. return $prize->remain_stock >= $quantity;
  529. }
  530. /**
  531. * 减少奖品库存
  532. */
  533. public static function decreasePrizeStock(LotteryPrize $prize, $quantity = 1)
  534. {
  535. if (!static::hasPrizeStock($prize, $quantity)) {
  536. return false;
  537. }
  538. $prize->remain_stock -= $quantity;
  539. $prize->win_count += $quantity;
  540. return $prize->save();
  541. }
  542. /**
  543. * 获取可用的兑换码
  544. */
  545. public static function getAvailableExchangeCode(LotteryPrize $prize)
  546. {
  547. if ($prize->type != LotteryEnum::PRIZE_TYPE_CODE) {
  548. return null;
  549. }
  550. $allCodes = $prize->exchange_codes_list;
  551. $usedCodes = $prize->used_codes_list;
  552. $availableCodes = array_diff($allCodes, $usedCodes);
  553. if (empty($availableCodes)) {
  554. return null;
  555. }
  556. return array_shift($availableCodes);
  557. }
  558. /**
  559. * 标记兑换码为已使用
  560. */
  561. public static function markExchangeCodeUsed(LotteryPrize $prize, $code)
  562. {
  563. $usedCodes = $prize->used_codes_list;
  564. if (!in_array($code, $usedCodes)) {
  565. $usedCodes[] = $code;
  566. $prize->used_codes = json_encode($usedCodes);
  567. return $prize->save();
  568. }
  569. return true;
  570. }
  571. /**
  572. * 获取有效奖品(库存大于0且状态正常)
  573. */
  574. public static function getValidPrizes($activityId)
  575. {
  576. return LotteryPrize::where('activity_id', $activityId)
  577. ->where('status', 1)
  578. ->where('remain_stock', '>', 0)
  579. ->order('sort_order', 'asc')
  580. ->select();
  581. }
  582. /**
  583. * 检查奖品是否已解锁(按人数解锁功能)
  584. */
  585. public static function isPrizeUnlocked(LotteryPrize $prize, $currentPeopleCount)
  586. {
  587. if (empty($prize->unlock_people_num)) {
  588. return true;
  589. }
  590. return $currentPeopleCount >= $prize->unlock_people_num;
  591. }
  592. /**
  593. * 检查活动是否正在进行
  594. */
  595. public static function isActivityRunning(LotteryActivity $activity)
  596. {
  597. $now = time();
  598. return $activity->status == LotteryEnum::STATUS_ONGOING
  599. && $activity->start_time <= $now
  600. && $activity->end_time >= $now;
  601. }
  602. /**
  603. * 检查活动是否已结束
  604. */
  605. public static function isActivityEnded(LotteryActivity $activity)
  606. {
  607. $now = time();
  608. return $activity->status == LotteryEnum::STATUS_ENDED
  609. || $activity->end_time < $now;
  610. }
  611. /**
  612. * 检查活动是否未开始
  613. */
  614. public static function isActivityNotStarted(LotteryActivity $activity)
  615. {
  616. $now = time();
  617. return $activity->status == LotteryEnum::STATUS_NOT_STARTED
  618. && $activity->start_time > $now;
  619. }
  620. /**
  621. * 检查活动是否已暂停
  622. */
  623. public static function isActivitySuspended(LotteryActivity $activity)
  624. {
  625. return $activity->status == LotteryEnum::STATUS_SUSPENDED;
  626. }
  627. /**
  628. * 检查活动是否已取消
  629. */
  630. public static function isActivityCancelled(LotteryActivity $activity)
  631. {
  632. return $activity->status == LotteryEnum::STATUS_CANCELLED;
  633. }
  634. // TODO: 分布式锁相关方法(暂时移除,后续实现)
  635. /*
  636. private static function acquireLock($lockKey, $expireTime = 10)
  637. {
  638. // Redis锁获取逻辑
  639. }
  640. private static function releaseLock($lockKey)
  641. {
  642. // Redis锁释放逻辑
  643. }
  644. private static function acquireFileLock($lockKey, $expireTime = 10)
  645. {
  646. // 文件锁获取逻辑
  647. }
  648. private static function releaseFileLock($lockKey)
  649. {
  650. // 文件锁释放逻辑
  651. }
  652. */
  653. /**
  654. * 获取正在进行的活动
  655. */
  656. public static function getRunningActivities()
  657. {
  658. $now = time();
  659. return LotteryActivity::where('status', LotteryEnum::STATUS_ONGOING)
  660. ->where('start_time', '<=', $now)
  661. ->where('end_time', '>=', $now)
  662. ->select();
  663. }
  664. /**
  665. * 获取未开始的活动
  666. */
  667. public static function getNotStartedActivities()
  668. {
  669. $now = time();
  670. return LotteryActivity::where('status', LotteryEnum::STATUS_NOT_STARTED)
  671. ->where('start_time', '>', $now)
  672. ->select();
  673. }
  674. /**
  675. * 获取已结束的活动
  676. */
  677. public static function getEndedActivities()
  678. {
  679. $now = time();
  680. return LotteryActivity::where('status', LotteryEnum::STATUS_ENDED)
  681. ->orWhere('end_time', '<', $now)
  682. ->select();
  683. }
  684. /**
  685. * 获取可显示的活动(排除逻辑状态)
  686. */
  687. public static function getDisplayableActivities()
  688. {
  689. $displayableStatuses = array_keys(LotteryEnum::getActivityStatusMap());
  690. return LotteryActivity::whereIn('status', $displayableStatuses)->select();
  691. }
  692. /**
  693. * 验证活动状态是否有效
  694. */
  695. public static function isValidActivityStatus(LotteryActivity $activity)
  696. {
  697. return LotteryEnum::isValidActivityStatus($activity->status);
  698. }
  699. /**
  700. * 验证开奖方式是否有效
  701. */
  702. public static function isValidLotteryType(LotteryActivity $activity)
  703. {
  704. return LotteryEnum::isValidLotteryType($activity->lottery_type);
  705. }
  706. }