LotteryService.php 37 KB

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