LotteryChanceService.php 38 KB

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