LotteryChanceService.php 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174
  1. <?php
  2. namespace app\common\Service\Lottery;
  3. use app\common\model\lottery\LotteryActivity;
  4. use app\common\model\lottery\LotteryCondition;
  5. use app\common\model\lottery\LotteryUserChance;
  6. use app\common\model\lottery\LotteryUserChanceRecord;
  7. use app\common\model\User;
  8. use app\common\model\Order;
  9. use app\common\Enum\LotteryEnum;
  10. use think\Exception;
  11. use think\Db;
  12. /**
  13. * 抽奖机会服务类
  14. * 处理用户获得抽奖机会的逻辑
  15. *
  16. * 主要功能:
  17. * - 订单/充值后自动分发抽奖机会
  18. * - 条件验证和机会计算
  19. * - 用户机会管理
  20. * - 批量操作和统计
  21. */
  22. class LotteryChanceService
  23. {
  24. // ============ 常量定义 ============
  25. /** @var int 默认批量处理数量 */
  26. const DEFAULT_BATCH_SIZE = 100;
  27. /** @var int 最大批量处理数量 */
  28. const MAX_BATCH_SIZE = 1000;
  29. /**
  30. * 订单完成后检查并分发抽奖机会
  31. *
  32. * @param array $orderInfo 订单信息 ['id', 'total_amount', 'goods' => [['goods_id']]]
  33. * @param int $userId 用户ID
  34. * @return array 获得的抽奖机会信息
  35. * @throws Exception
  36. */
  37. public static function checkAndGrantChanceForOrder($orderInfo, $userId)
  38. {
  39. if (empty($orderInfo) || empty($userId)) {
  40. throw new Exception('订单信息或用户ID不能为空');
  41. }
  42. $grantedChances = [];
  43. try {
  44. // 获取所有正在进行的抽奖活动
  45. $activities = LotteryService::getRunningActivities();
  46. foreach ($activities as $activity) {
  47. try {
  48. $chances = static::processActivityForOrder($activity, $orderInfo, $userId);
  49. if ($chances > 0) {
  50. $grantedChances[] = [
  51. 'activity_id' => $activity->id,
  52. 'activity_name' => $activity->name,
  53. 'chances' => $chances,
  54. 'granted_time' => time()
  55. ];
  56. }
  57. } catch (Exception $e) {
  58. // 记录错误但不影响其他活动的处理
  59. trace("抽奖机会分发失败 - 活动ID: {$activity->id}, 用户ID: {$userId}, 错误: " . $e->getMessage(), 'error');
  60. }
  61. }
  62. } catch (Exception $e) {
  63. trace("订单抽奖机会分发失败 - 用户ID: {$userId}, 错误: " . $e->getMessage(), 'error');
  64. throw $e;
  65. }
  66. return $grantedChances;
  67. }
  68. /**
  69. * 充值完成后检查并分发抽奖机会
  70. *
  71. * @param array $rechargeInfo 充值信息 ['amount', 'type' => 'recharge']
  72. * @param int $userId 用户ID
  73. * @return array 获得的抽奖机会信息
  74. * @throws Exception
  75. */
  76. public static function checkAndGrantChanceForRecharge($rechargeInfo, $userId)
  77. {
  78. if (empty($rechargeInfo) || empty($userId)) {
  79. throw new Exception('充值信息或用户ID不能为空');
  80. }
  81. $grantedChances = [];
  82. try {
  83. // 获取所有正在进行的抽奖活动
  84. $activities = LotteryService::getRunningActivities();
  85. foreach ($activities as $activity) {
  86. try {
  87. $chances = static::processActivityForRecharge($activity, $rechargeInfo, $userId);
  88. if ($chances > 0) {
  89. $grantedChances[] = [
  90. 'activity_id' => $activity->id,
  91. 'activity_name' => $activity->name,
  92. 'chances' => $chances,
  93. 'granted_time' => time()
  94. ];
  95. }
  96. } catch (Exception $e) {
  97. // 记录错误但不影响其他活动的处理
  98. trace("充值抽奖机会分发失败 - 活动ID: {$activity->id}, 用户ID: {$userId}, 错误: " . $e->getMessage(), 'error');
  99. }
  100. }
  101. } catch (Exception $e) {
  102. trace("充值抽奖机会分发失败 - 用户ID: {$userId}, 错误: " . $e->getMessage(), 'error');
  103. throw $e;
  104. }
  105. return $grantedChances;
  106. }
  107. /**
  108. * 手动给用户增加抽奖机会(管理员操作) *
  109. * @param int $activityId 活动ID
  110. * @param int $userId 用户ID
  111. * @param int $chances 机会次数
  112. * @param string $reason 原因
  113. * @param int $adminId 管理员ID
  114. * @return bool
  115. * @throws Exception
  116. */
  117. public static function manualGrantChance($activityId, $userId, $chances, $reason = '', $adminId = 0)
  118. {
  119. if ($chances <= 0) {
  120. throw new Exception('抽奖机会数量必须大于0');
  121. }
  122. $activity = LotteryActivity::find($activityId);
  123. if (!$activity) {
  124. throw new Exception('活动不存在');
  125. }
  126. $user = User::find($userId);
  127. if (!$user) {
  128. throw new Exception('用户不存在');
  129. }
  130. $detail = [
  131. 'reason' => $reason,
  132. 'admin_id' => $adminId,
  133. 'granted_time' => time()
  134. ];
  135. return static::grantChanceToUser($activityId, $userId, $chances, $detail);
  136. }
  137. /**
  138. * 批量初始化用户抽奖机会(活动开始时)
  139. *
  140. * @param int $activityId 活动ID
  141. * @param array $userIds 用户ID数组
  142. * @param int $initChances 初始机会数
  143. * @param int $batchSize 批量处理大小
  144. * @return array 处理结果
  145. * @throws Exception
  146. */
  147. public static function batchInitChances($activityId, $userIds, $initChances = 1, $batchSize = null)
  148. {
  149. if (empty($userIds)) {
  150. throw new Exception('用户ID列表不能为空');
  151. }
  152. $activity = LotteryActivity::find($activityId);
  153. if (!$activity) {
  154. throw new Exception('活动不存在');
  155. }
  156. $batchSize = $batchSize ?: static::DEFAULT_BATCH_SIZE;
  157. $batchSize = min($batchSize, static::MAX_BATCH_SIZE);
  158. $result = [
  159. 'total_users' => count($userIds),
  160. 'success_count' => 0,
  161. 'failed_count' => 0,
  162. 'errors' => []
  163. ];
  164. // 分批处理
  165. $batches = array_chunk($userIds, $batchSize);
  166. foreach ($batches as $batch) {
  167. try {
  168. $success = static::batchCreateChances($activityId, $batch, $initChances);
  169. if ($success) {
  170. $result['success_count'] += count($batch);
  171. } else {
  172. $result['failed_count'] += count($batch);
  173. $result['errors'][] = "批次处理失败: " . implode(',', $batch);
  174. }
  175. } catch (Exception $e) {
  176. $result['failed_count'] += count($batch);
  177. $result['errors'][] = "批次处理异常: " . $e->getMessage();
  178. }
  179. }
  180. return $result;
  181. }
  182. // ============ 私有处理方法 ============
  183. /**
  184. * 处理订单相关的活动
  185. *
  186. * @param LotteryActivity $activity 活动对象
  187. * @param array $orderInfo 订单信息
  188. * @param int $userId 用户ID
  189. * @return int 获得的抽奖机会数量
  190. */
  191. private static function processActivityForOrder($activity, $orderInfo, $userId)
  192. {
  193. // 检查用户是否符合活动参与资格
  194. if (!static::checkUserQualification($activity, $userId)) {
  195. return 0;
  196. }
  197. // 获取活动的参与条件
  198. $conditions = static::getValidConditions($activity->id);
  199. $totalChances = 0;
  200. $chanceDetails = [];
  201. foreach ($conditions as $condition) {
  202. // 跳过充值条件
  203. if ($condition->type == LotteryEnum::CONDITION_TYPE_RECHARGE_AMOUNT) {
  204. continue;
  205. }
  206. $result = static::processConditionForOrder($condition, $orderInfo, $userId);
  207. if ($result['chances'] > 0) {
  208. $totalChances += $result['chances'];
  209. $result['detail']['chances'] = $result['chances'];
  210. $chanceDetails[] = $result['detail'];
  211. }
  212. }
  213. // 如果获得了抽奖机会,记录到数据库
  214. if ($totalChances > 0) {
  215. // 为每个条件分别记录
  216. foreach ($chanceDetails as $detail) {
  217. static::grantChanceToUser($activity->id, $userId, $detail['chances'] ?? 1, [
  218. 'condition_id' => $detail['condition_id'],
  219. 'condition_type' => $detail['condition_type'],
  220. 'condition_value' => $detail['condition_value'],
  221. 'order_id' => $orderInfo['id'] ?? 0,
  222. 'granted_time' => time()
  223. ]);
  224. }
  225. }
  226. return $totalChances;
  227. }
  228. /**
  229. * 处理充值相关的活动
  230. *
  231. * @param LotteryActivity $activity 活动对象
  232. * @param array $rechargeInfo 充值信息
  233. * @param int $userId 用户ID
  234. * @return int 获得的抽奖机会数量
  235. */
  236. private static function processActivityForRecharge($activity, $rechargeInfo, $userId)
  237. {
  238. // 检查用户是否符合活动参与资格
  239. if (!static::checkUserQualification($activity, $userId)) {
  240. return 0;
  241. }
  242. // 获取活动的参与条件
  243. $conditions = static::getValidConditions($activity->id);
  244. $totalChances = 0;
  245. $chanceDetails = [];
  246. foreach ($conditions as $condition) {
  247. // 只处理充值条件
  248. if ($condition->type != LotteryEnum::CONDITION_TYPE_RECHARGE_AMOUNT) {
  249. continue;
  250. }
  251. $result = static::processConditionForRecharge($condition, $rechargeInfo, $userId);
  252. if ($result['chances'] > 0) {
  253. $totalChances += $result['chances'];
  254. $result['detail']['chances'] = $result['chances'];
  255. $chanceDetails[] = $result['detail'];
  256. }
  257. }
  258. // 如果获得了抽奖机会,记录到数据库
  259. if ($totalChances > 0) {
  260. // 为每个条件分别记录
  261. foreach ($chanceDetails as $detail) {
  262. static::grantChanceToUser($activity->id, $userId, $detail['chances'] ?? 1, [
  263. 'condition_id' => $detail['condition_id'],
  264. 'condition_type' => $detail['condition_type'],
  265. 'condition_value' => $detail['condition_value'],
  266. 'recharge_amount' => $rechargeInfo['amount'] ?? 0,
  267. 'granted_time' => time()
  268. ]);
  269. }
  270. }
  271. return $totalChances;
  272. }
  273. /**
  274. * 处理订单条件
  275. *
  276. * @param LotteryCondition $condition 条件对象
  277. * @param array $orderInfo 订单信息
  278. * @param int $userId 用户ID
  279. * @return array 获得的抽奖机会信息 ['chances' => int, 'detail' => array]
  280. */
  281. private static function processConditionForOrder($condition, $orderInfo, $userId)
  282. {
  283. $chances = 0;
  284. $detail = [
  285. 'condition_id' => $condition->id,
  286. 'condition_type' => $condition->type,
  287. 'condition_value' => $condition->condition_value,
  288. ];
  289. switch ($condition->type) {
  290. case LotteryEnum::CONDITION_TYPE_BUY_GOODS:
  291. if (static::validateGoodsCondition($condition, $orderInfo)) {
  292. $chances = static::getRewardTimes($condition);
  293. }
  294. break;
  295. case LotteryEnum::CONDITION_TYPE_ORDER_AMOUNT:
  296. if (static::validateOrderAmountCondition($condition, $orderInfo)) {
  297. $chances = static::getRewardTimes($condition);
  298. // 如果可重复获得,根据金额倍数计算次数
  299. if (static::canRepeat($condition)) {
  300. $multiple = floor($orderInfo['total_amount'] / $condition->condition_value);
  301. $chances *= $multiple;
  302. }
  303. }
  304. break;
  305. case LotteryEnum::CONDITION_TYPE_TOTAL_AMOUNT:
  306. if (static::validateAccumulateCondition($condition, $userId, $condition->activity_id)) {
  307. // 检查是否已经因为累计消费获得过机会
  308. if (!static::hasGrantedForAccumulate($condition->activity_id, $userId)) {
  309. $chances = static::getRewardTimes($condition);
  310. }
  311. }
  312. break;
  313. }
  314. return [
  315. 'chances' => $chances,
  316. 'detail' => $detail
  317. ];
  318. }
  319. /**
  320. * 处理充值条件
  321. *
  322. * @param LotteryCondition $condition 条件对象
  323. * @param array $rechargeInfo 充值信息
  324. * @param int $userId 用户ID
  325. * @return array 获得的抽奖机会信息 ['chances' => int, 'detail' => array]
  326. */
  327. private static function processConditionForRecharge($condition, $rechargeInfo, $userId)
  328. {
  329. $chances = 0;
  330. $detail = [
  331. 'condition_id' => $condition->id,
  332. 'condition_type' => $condition->type,
  333. 'condition_value' => $condition->condition_value,
  334. ];
  335. if (static::validateRechargeCondition($condition, ['type' => 'recharge', 'amount' => $rechargeInfo['amount']])) {
  336. $chances = static::getRewardTimes($condition);
  337. // 如果可重复获得,根据金额倍数计算次数
  338. if (static::canRepeat($condition)) {
  339. $multiple = floor($rechargeInfo['amount'] / $condition->condition_value);
  340. $chances *= $multiple;
  341. }
  342. }
  343. return [
  344. 'chances' => $chances,
  345. 'detail' => $detail
  346. ];
  347. }
  348. // ============ 用户资格检查方法 ============
  349. /**
  350. * 检查用户资格
  351. *
  352. * @param LotteryActivity $activity 活动对象
  353. * @param int $userId 用户ID
  354. * @return bool
  355. */
  356. private static function checkUserQualification($activity, $userId)
  357. {
  358. // 检查用户群体限制
  359. switch ($activity->user_limit_type) {
  360. case LotteryEnum::USER_LIMIT_ALL:
  361. return true;
  362. case LotteryEnum::USER_LIMIT_LEVEL:
  363. return static::checkUserLevel($userId, $activity->user_limit_value);
  364. case LotteryEnum::USER_LIMIT_TAG:
  365. return static::checkUserTag($userId, $activity->user_limit_value);
  366. default:
  367. return false;
  368. }
  369. }
  370. /**
  371. * 检查用户等级
  372. *
  373. * @param int $userId 用户ID
  374. * @param mixed $limitValue 限制值
  375. * @return bool
  376. */
  377. private static function checkUserLevel($userId, $limitValue)
  378. {
  379. if (empty($limitValue)) {
  380. return true;
  381. }
  382. $user = User::find($userId);
  383. if (!$user) {
  384. return false;
  385. }
  386. $limitLevels = is_array($limitValue) ? $limitValue : [$limitValue];
  387. return in_array($user->level, $limitLevels);
  388. }
  389. /**
  390. * 检查用户标签
  391. *
  392. * @param int $userId 用户ID
  393. * @param mixed $limitValue 限制值
  394. * @return bool
  395. */
  396. private static function checkUserTag($userId, $limitValue)
  397. {
  398. if (empty($limitValue)) {
  399. return true;
  400. }
  401. // TODO: 根据实际的用户标签系统实现
  402. // 这里需要根据具体的用户标签表结构来实现
  403. // 暂时返回true,避免影响现有功能
  404. return true;
  405. }
  406. /**
  407. * 检查是否已因累计消费获得过机会
  408. *
  409. * @param int $activityId 活动ID
  410. * @param int $userId 用户ID
  411. * @return bool
  412. */
  413. private static function hasGrantedForAccumulate($activityId, $userId)
  414. {
  415. $record = LotteryUserChanceRecord::where('activity_id', $activityId)
  416. ->where('user_id', $userId)
  417. ->where('get_type', LotteryEnum::CHANCE_GET_TYPE_TOTAL_AMOUNT)
  418. ->find();
  419. return $record ? true : false;
  420. }
  421. // ============ 条件验证方法 ============
  422. /**
  423. * 验证订单是否满足条件
  424. *
  425. * @param LotteryCondition $condition 条件对象
  426. * @param array $orderInfo 订单信息
  427. * @return bool
  428. */
  429. public static function validateOrder(LotteryCondition $condition, $orderInfo)
  430. {
  431. switch ($condition->type) {
  432. case LotteryEnum::CONDITION_TYPE_BUY_GOODS:
  433. return static::validateGoodsCondition($condition, $orderInfo);
  434. case LotteryEnum::CONDITION_TYPE_ORDER_AMOUNT:
  435. return static::validateOrderAmountCondition($condition, $orderInfo);
  436. case LotteryEnum::CONDITION_TYPE_RECHARGE_AMOUNT:
  437. return static::validateRechargeCondition($condition, $orderInfo);
  438. default:
  439. return false;
  440. }
  441. }
  442. /**
  443. * 验证商品条件
  444. *
  445. * @param LotteryCondition $condition 条件对象
  446. * @param array $orderInfo 订单信息
  447. * @return bool
  448. */
  449. public static function validateGoodsCondition(LotteryCondition $condition, $orderInfo)
  450. {
  451. if (empty($orderInfo['goods']) || empty($condition->goods_ids_list)) {
  452. return false;
  453. }
  454. $orderGoodsIds = array_column($orderInfo['goods'], 'goods_id');
  455. $conditionGoodsIds = $condition->goods_ids_list;
  456. $intersection = array_intersect($orderGoodsIds, $conditionGoodsIds);
  457. if ($condition->goods_rule == LotteryEnum::GOODS_RULE_INCLUDE) {
  458. // 指定商品参与:订单中必须包含指定商品
  459. return !empty($intersection);
  460. } else {
  461. // 指定商品不可参与:订单中不能包含指定商品
  462. return empty($intersection);
  463. }
  464. }
  465. /**
  466. * 验证订单金额条件
  467. *
  468. * @param LotteryCondition $condition 条件对象
  469. * @param array $orderInfo 订单信息
  470. * @return bool
  471. */
  472. public static function validateOrderAmountCondition(LotteryCondition $condition, $orderInfo)
  473. {
  474. $orderAmount = $orderInfo['total_amount'] ?? 0;
  475. return $orderAmount >= $condition->condition_value;
  476. }
  477. /**
  478. * 验证充值条件
  479. *
  480. * @param LotteryCondition $condition 条件对象
  481. * @param array $orderInfo 订单信息
  482. * @return bool
  483. */
  484. public static function validateRechargeCondition(LotteryCondition $condition, $orderInfo)
  485. {
  486. if (($orderInfo['type'] ?? '') !== 'recharge') {
  487. return false;
  488. }
  489. $rechargeAmount = $orderInfo['amount'] ?? 0;
  490. return $rechargeAmount >= $condition->condition_value;
  491. }
  492. /**
  493. * 验证累计消费条件
  494. *
  495. * @param LotteryCondition $condition 条件对象
  496. * @param int $userId 用户ID
  497. * @param int $activityId 活动ID
  498. * @return bool
  499. */
  500. public static function validateAccumulateCondition(LotteryCondition $condition, $userId, $activityId)
  501. {
  502. $activity = LotteryActivity::find($activityId);
  503. if (!$activity) {
  504. return false;
  505. }
  506. // 计算活动期间用户累计消费
  507. $totalAmount = Order::where('user_id', $userId)
  508. ->where('status', 'paid')
  509. ->where('createtime', '>=', $activity->start_time)
  510. ->where('createtime', '<=', $activity->end_time)
  511. ->sum('total_amount');
  512. return $totalAmount >= $condition->condition_value;
  513. }
  514. // ============ 条件管理方法 ============
  515. /**
  516. * 获取活动的有效条件
  517. *
  518. * @param int $activityId 活动ID
  519. * @return array
  520. */
  521. public static function getValidConditions($activityId)
  522. {
  523. return LotteryCondition::where('activity_id', $activityId)
  524. ->where('status', 1)
  525. ->order('id', 'asc')
  526. ->select();
  527. }
  528. /**
  529. * 检查条件是否可重复获得奖励
  530. *
  531. * @param LotteryCondition $condition 条件对象
  532. * @return bool
  533. */
  534. public static function canRepeat(LotteryCondition $condition)
  535. {
  536. return $condition->is_repeatable == 1;
  537. }
  538. /**
  539. * 获取奖励次数
  540. *
  541. * @param LotteryCondition $condition 条件对象
  542. * @return int
  543. */
  544. public static function getRewardTimes(LotteryCondition $condition)
  545. {
  546. return $condition->reward_times ?: 1;
  547. }
  548. /**
  549. * 批量验证订单条件
  550. *
  551. * @param int $activityId 活动ID
  552. * @param array $orderInfo 订单信息
  553. * @param int $userId 用户ID
  554. * @return array 满足的条件列表
  555. */
  556. public static function batchValidateConditions($activityId, $orderInfo, $userId)
  557. {
  558. $conditions = static::getValidConditions($activityId);
  559. $validConditions = [];
  560. foreach ($conditions as $condition) {
  561. if (static::validateOrder($condition, $orderInfo)) {
  562. $validConditions[] = [
  563. 'condition_id' => $condition->id,
  564. 'condition_name' => $condition->name,
  565. 'condition_type' => $condition->type,
  566. 'condition_type_text' => $condition->type_text,
  567. 'reward_times' => static::getRewardTimes($condition),
  568. 'can_repeat' => static::canRepeat($condition)
  569. ];
  570. }
  571. }
  572. return $validConditions;
  573. }
  574. /**
  575. * 获取条件统计信息
  576. *
  577. * @param int $activityId 活动ID
  578. * @return array
  579. */
  580. public static function getConditionStatistics($activityId)
  581. {
  582. $conditions = static::getValidConditions($activityId);
  583. $statistics = [
  584. 'total_conditions' => count($conditions),
  585. 'goods_conditions' => 0,
  586. 'amount_conditions' => 0,
  587. 'recharge_conditions' => 0,
  588. 'accumulate_conditions' => 0
  589. ];
  590. foreach ($conditions as $condition) {
  591. switch ($condition->type) {
  592. case LotteryEnum::CONDITION_TYPE_BUY_GOODS:
  593. $statistics['goods_conditions']++;
  594. break;
  595. case LotteryEnum::CONDITION_TYPE_ORDER_AMOUNT:
  596. $statistics['amount_conditions']++;
  597. break;
  598. case LotteryEnum::CONDITION_TYPE_RECHARGE_AMOUNT:
  599. $statistics['recharge_conditions']++;
  600. break;
  601. case LotteryEnum::CONDITION_TYPE_TOTAL_AMOUNT:
  602. $statistics['accumulate_conditions']++;
  603. break;
  604. }
  605. }
  606. return $statistics;
  607. }
  608. // ============ 用户机会管理方法 ============
  609. /**
  610. * 获取用户抽奖机会
  611. *
  612. * @param int $activityId 活动ID
  613. * @param int $userId 用户ID
  614. * @return LotteryUserChance|null
  615. */
  616. public static function getUserChance($activityId, $userId)
  617. {
  618. return LotteryUserChance::where('activity_id', $activityId)
  619. ->where('user_id', $userId)
  620. ->find();
  621. }
  622. /**
  623. * 根据详情确定机会获取类型
  624. *
  625. * @param array $detail 获得详情
  626. * @return int 获取类型
  627. */
  628. private static function getChanceGetTypeFromDetail($detail)
  629. {
  630. // 如果有条件类型,直接使用条件类型对应的获取类型
  631. if (isset($detail['condition_type'])) {
  632. switch ($detail['condition_type']) {
  633. case LotteryEnum::CONDITION_TYPE_BUY_GOODS:
  634. return LotteryEnum::CHANCE_GET_TYPE_BUY_GOODS;
  635. case LotteryEnum::CONDITION_TYPE_ORDER_AMOUNT:
  636. return LotteryEnum::CHANCE_GET_TYPE_ORDER_AMOUNT;
  637. case LotteryEnum::CONDITION_TYPE_RECHARGE_AMOUNT:
  638. return LotteryEnum::CHANCE_GET_TYPE_RECHARGE;
  639. case LotteryEnum::CONDITION_TYPE_TOTAL_AMOUNT:
  640. return LotteryEnum::CHANCE_GET_TYPE_TOTAL_AMOUNT;
  641. }
  642. }
  643. // 如果没有条件类型,检查是否是管理员赠送
  644. if (isset($detail['admin_id']) && $detail['admin_id'] > 0) {
  645. return LotteryEnum::CHANCE_GET_TYPE_ADMIN_GRANT;
  646. }
  647. // 默认返回管理员赠送
  648. return LotteryEnum::CHANCE_GET_TYPE_ADMIN_GRANT;
  649. }
  650. /**
  651. * 获取用户在指定活动中的抽奖机会详情
  652. *
  653. * @param int $activityId 活动ID
  654. * @param int $userId 用户ID
  655. * @return array
  656. */
  657. public static function getUserChanceDetail($activityId, $userId)
  658. {
  659. $userChance = static::getUserChance($activityId, $userId);
  660. if (!$userChance) {
  661. return [
  662. 'total_chances' => 0,
  663. 'used_chances' => 0,
  664. 'remain_chances' => 0,
  665. 'get_records' => []
  666. ];
  667. }
  668. // 获取机会获得记录
  669. $getRecords = LotteryUserChanceRecord::getUserChanceRecords($activityId, $userId);
  670. return [
  671. 'total_chances' => $userChance->total_chances,
  672. 'used_chances' => $userChance->used_chances,
  673. 'remain_chances' => $userChance->remain_chances,
  674. 'last_get_time' => $userChance->last_get_time,
  675. 'last_use_time' => $userChance->last_use_time,
  676. 'get_records' => $getRecords
  677. ];
  678. }
  679. /**
  680. * 给用户分发抽奖机会
  681. *
  682. * @param int $activityId 活动ID
  683. * @param int $userId 用户ID
  684. * @param int $chances 机会次数
  685. * @param array $detail 获得详情
  686. * @return LotteryUserChance|bool
  687. */
  688. private static function grantChanceToUser($activityId, $userId, $chances, $detail)
  689. {
  690. return static::addChance($activityId, $userId, $chances, $detail);
  691. }
  692. /**
  693. * 增加抽奖机会
  694. *
  695. * @param int $activityId 活动ID
  696. * @param int $userId 用户ID
  697. * @param int $times 机会次数
  698. * @param array $detail 获得详情
  699. * @return LotteryUserChance|bool
  700. */
  701. public static function addChance($activityId, $userId, $times = 1, $detail = [])
  702. {
  703. if ($times <= 0) {
  704. return false;
  705. }
  706. try {
  707. Db::startTrans();
  708. $chance = static::getUserChance($activityId, $userId);
  709. if (!$chance) {
  710. // 创建新记录
  711. $data = [
  712. 'activity_id' => $activityId,
  713. 'user_id' => $userId,
  714. 'total_chances' => $times,
  715. 'used_chances' => 0,
  716. 'remain_chances' => $times,
  717. 'last_get_time' => time(),
  718. 'get_detail' => json_encode([]) // 保留字段但不使用
  719. ];
  720. $chance = LotteryUserChance::create($data);
  721. } else {
  722. // 更新现有记录
  723. $chance->total_chances += $times;
  724. $chance->remain_chances += $times;
  725. $chance->last_get_time = time();
  726. $chance->save();
  727. }
  728. // 创建获得记录
  729. $recordData = [
  730. 'activity_id' => $activityId,
  731. 'user_id' => $userId,
  732. 'get_type' => static::getChanceGetTypeFromDetail($detail),
  733. 'chances' => $times,
  734. 'condition_id' => $detail['condition_id'] ?? null,
  735. 'condition_value' => $detail['condition_value'] ?? null,
  736. 'order_id' => $detail['order_id'] ?? null,
  737. 'recharge_amount' => $detail['recharge_amount'] ?? null,
  738. 'admin_id' => $detail['admin_id'] ?? null,
  739. 'reason' => $detail['reason'] ?? null,
  740. 'remark' => $detail['remark'] ?? null,
  741. 'get_time' => time()
  742. ];
  743. $validation = LotteryUserChanceRecord::validateRecord($recordData);
  744. if ($validation !== true) {
  745. throw new Exception($validation);
  746. }
  747. LotteryUserChanceRecord::create($recordData);
  748. Db::commit();
  749. return $chance;
  750. } catch (Exception $e) {
  751. Db::rollback();
  752. throw $e;
  753. }
  754. }
  755. /**
  756. * 使用抽奖机会
  757. *
  758. * @param LotteryUserChance $userChance 用户机会对象
  759. * @param int $times 使用次数
  760. * @return bool
  761. */
  762. public static function useChance(LotteryUserChance $userChance, $times = 1)
  763. {
  764. if ($userChance->remain_chances < $times) {
  765. return false;
  766. }
  767. $userChance->used_chances += $times;
  768. $userChance->remain_chances -= $times;
  769. $userChance->last_use_time = time();
  770. return $userChance->save();
  771. }
  772. /**
  773. * 检查是否有剩余机会
  774. *
  775. * @param LotteryUserChance $userChance 用户机会对象
  776. * @param int $times 需要的机会次数
  777. * @return bool
  778. */
  779. public static function hasChance(LotteryUserChance $userChance, $times = 1)
  780. {
  781. return $userChance->remain_chances >= $times;
  782. }
  783. /**
  784. * 重置抽奖机会(用于测试或特殊情况)
  785. *
  786. * @param LotteryUserChance $userChance 用户机会对象
  787. * @return bool
  788. */
  789. public static function resetChance(LotteryUserChance $userChance)
  790. {
  791. $userChance->used_chances = 0;
  792. $userChance->remain_chances = $userChance->total_chances;
  793. return $userChance->save();
  794. }
  795. // ============ 统计和查询方法 ============
  796. /**
  797. * 获取活动总参与人数
  798. *
  799. * @param int $activityId 活动ID
  800. * @return int
  801. */
  802. public static function getActivityParticipants($activityId)
  803. {
  804. return LotteryUserChance::where('activity_id', $activityId)->count();
  805. }
  806. /**
  807. * 获取用户在多个活动中的机会统计
  808. *
  809. * @param int $userId 用户ID
  810. * @param array $activityIds 活动ID数组
  811. * @return array
  812. */
  813. public static function getUserChancesStats($userId, $activityIds = [])
  814. {
  815. $query = LotteryUserChance::where('user_id', $userId);
  816. if (!empty($activityIds)) {
  817. $query->where('activity_id', 'in', $activityIds);
  818. }
  819. return $query->field([
  820. 'activity_id',
  821. 'total_chances',
  822. 'used_chances',
  823. 'remain_chances'
  824. ])
  825. ->select();
  826. }
  827. /**
  828. * 获取用户在所有活动中的机会概览
  829. *
  830. * @param int $userId 用户ID
  831. * @return array
  832. */
  833. public static function getUserAllChancesOverview($userId)
  834. {
  835. $chances = LotteryUserChance::where('user_id', $userId)
  836. ->with(['activity'])
  837. ->select();
  838. $overview = [
  839. 'total_activities' => count($chances),
  840. 'total_chances' => 0,
  841. 'total_used' => 0,
  842. 'total_remain' => 0,
  843. 'activities' => []
  844. ];
  845. foreach ($chances as $chance) {
  846. $overview['total_chances'] += $chance->total_chances;
  847. $overview['total_used'] += $chance->used_chances;
  848. $overview['total_remain'] += $chance->remain_chances;
  849. $overview['activities'][] = [
  850. 'activity_id' => $chance->activity_id,
  851. 'activity_name' => $chance->activity->name ?? '',
  852. 'total_chances' => $chance->total_chances,
  853. 'used_chances' => $chance->used_chances,
  854. 'remain_chances' => $chance->remain_chances,
  855. 'last_get_time' => $chance->last_get_time,
  856. 'last_use_time' => $chance->last_use_time
  857. ];
  858. }
  859. return $overview;
  860. }
  861. /**
  862. * 检查用户是否可以参与活动
  863. *
  864. * @param int $activityId 活动ID
  865. * @param int $userId 用户ID
  866. * @return bool
  867. */
  868. public static function canUserParticipate($activityId, $userId)
  869. {
  870. $userChance = static::getUserChance($activityId, $userId);
  871. return $userChance && static::hasChance($userChance, 1);
  872. }
  873. /**
  874. * 获取活动参与用户列表
  875. *
  876. * @param int $activityId 活动ID
  877. * @param int $page 页码
  878. * @param int $limit 每页数量
  879. * @return array
  880. */
  881. public static function getActivityParticipantsList($activityId, $page = 1, $limit = 20)
  882. {
  883. return LotteryUserChance::where('activity_id', $activityId)
  884. ->with(['user'])
  885. ->order('createtime', 'desc')
  886. ->page($page, $limit)
  887. ->select();
  888. }
  889. /**
  890. * 获取活动参与统计
  891. *
  892. * @param int $activityId 活动ID
  893. * @return array
  894. */
  895. public static function getActivityParticipationStats($activityId)
  896. {
  897. $stats = LotteryUserChance::where('activity_id', $activityId)
  898. ->field([
  899. 'COUNT(*) as total_participants',
  900. 'SUM(total_chances) as total_chances_granted',
  901. 'SUM(used_chances) as total_chances_used',
  902. 'SUM(remain_chances) as total_chances_remain'
  903. ])
  904. ->find();
  905. return [
  906. 'total_participants' => $stats['total_participants'] ?? 0,
  907. 'total_chances_granted' => $stats['total_chances_granted'] ?? 0,
  908. 'total_chances_used' => $stats['total_chances_used'] ?? 0,
  909. 'total_chances_remain' => $stats['total_chances_remain'] ?? 0,
  910. 'usage_rate' => $stats['total_chances_granted'] > 0 ?
  911. round(($stats['total_chances_used'] / $stats['total_chances_granted']) * 100, 2) : 0
  912. ];
  913. }
  914. // ============ 批量操作方法 ============
  915. /**
  916. * 批量创建用户机会(用于活动启动时)
  917. *
  918. * @param int $activityId 活动ID
  919. * @param array $userIds 用户ID数组
  920. * @param int $times 机会次数
  921. * @return bool
  922. */
  923. public static function batchCreateChances($activityId, $userIds, $times = 1)
  924. {
  925. if (empty($userIds)) {
  926. return false;
  927. }
  928. $data = [];
  929. $now = time();
  930. foreach ($userIds as $userId) {
  931. $data[] = [
  932. 'activity_id' => $activityId,
  933. 'user_id' => $userId,
  934. 'total_chances' => $times,
  935. 'used_chances' => 0,
  936. 'remain_chances' => $times,
  937. 'last_get_time' => $now,
  938. 'createtime' => $now,
  939. 'updatetime' => $now
  940. ];
  941. }
  942. return LotteryUserChance::insertAll($data);
  943. }
  944. /**
  945. * 批量检查用户资格
  946. *
  947. * @param int $activityId 活动ID
  948. * @param array $userIds 用户ID数组
  949. * @return array 符合条件的用户ID数组
  950. */
  951. public static function batchCheckUserQualification($activityId, $userIds)
  952. {
  953. $activity = LotteryActivity::find($activityId);
  954. if (!$activity) {
  955. return [];
  956. }
  957. $qualifiedUsers = [];
  958. foreach ($userIds as $userId) {
  959. if (static::checkUserQualification($activity, $userId)) {
  960. $qualifiedUsers[] = $userId;
  961. }
  962. }
  963. return $qualifiedUsers;
  964. }
  965. /**
  966. * 批量验证条件
  967. *
  968. * @param int $activityId 活动ID
  969. * @param array $orderInfo 订单信息
  970. * @param array $userIds 用户ID数组
  971. * @return array 每个用户满足的条件
  972. */
  973. public static function batchValidateConditionsForUsers($activityId, $orderInfo, $userIds)
  974. {
  975. $results = [];
  976. foreach ($userIds as $userId) {
  977. $results[$userId] = static::batchValidateConditions($activityId, $orderInfo, $userId);
  978. }
  979. return $results;
  980. }
  981. // ============ 机会获取记录管理方法 ============
  982. /**
  983. * 获取用户机会获取记录
  984. *
  985. * @param int $activityId 活动ID
  986. * @param int $userId 用户ID
  987. * @param int $page 页码
  988. * @param int $limit 每页数量
  989. * @return array
  990. */
  991. public static function getUserChanceRecords($activityId, $userId, $page = 1, $limit = 20)
  992. {
  993. return LotteryUserChanceRecord::getUserChanceRecords($activityId, $userId, $page, $limit);
  994. }
  995. /**
  996. * 获取活动机会获取统计
  997. *
  998. * @param int $activityId 活动ID
  999. * @return array
  1000. */
  1001. public static function getActivityChanceRecordStats($activityId)
  1002. {
  1003. return LotteryUserChanceRecord::getActivityChanceStats($activityId);
  1004. }
  1005. /**
  1006. * 获取用户机会获取统计
  1007. *
  1008. * @param int $userId 用户ID
  1009. * @param int $activityId 活动ID(可选)
  1010. * @return array
  1011. */
  1012. public static function getUserChanceRecordStats($userId, $activityId = null)
  1013. {
  1014. return LotteryUserChanceRecord::getUserChanceStats($userId, $activityId);
  1015. }
  1016. /**
  1017. * 批量创建机会获取记录
  1018. *
  1019. * @param array $records 记录数组
  1020. * @return bool
  1021. */
  1022. public static function batchCreateChanceRecords($records)
  1023. {
  1024. return LotteryUserChanceRecord::batchCreateRecords($records);
  1025. }
  1026. /**
  1027. * 验证机会获取记录数据
  1028. *
  1029. * @param array $data 记录数据
  1030. * @return bool|string
  1031. */
  1032. public static function validateChanceRecord($data)
  1033. {
  1034. return LotteryUserChanceRecord::validateRecord($data);
  1035. }
  1036. }