LotteryChanceService.php 42 KB

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