LotteryChanceService.php 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287
  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. echo '<pre>';
  301. print_r($chanceDetails);
  302. echo '</pre>';
  303. die();
  304. // 如果获得了抽奖机会,记录到数据库
  305. if ($totalChances > 0) {
  306. // 为每个条件分别记录
  307. foreach ($chanceDetails as $detail) {
  308. static::grantChanceToUser($activity->id, $userId, $detail['chances'] ?? 1, [
  309. 'condition_id' => $detail['condition_id'],
  310. 'condition_type' => $detail['condition_type'],
  311. 'condition_value' => $detail['condition_value'],
  312. 'order_id' => $orderInfo['id'] ?? 0,
  313. 'granted_time' => time()
  314. ]);
  315. }
  316. }
  317. return $totalChances;
  318. }
  319. /**
  320. * 处理充值相关的活动
  321. *
  322. * @param LotteryActivity $activity 活动对象
  323. * @param array $rechargeInfo 充值信息
  324. * @param int $userId 用户ID
  325. * @return int 获得的抽奖机会数量
  326. */
  327. private static function processActivityForRecharge($activity, $rechargeInfo, $userId)
  328. {
  329. // 检查用户是否符合活动参与资格
  330. if (!static::checkUserQualification($activity, $userId)) {
  331. return 0;
  332. }
  333. // 获取活动的参与条件
  334. $conditions = static::getValidConditions($activity->id);
  335. $totalChances = 0;
  336. $chanceDetails = [];
  337. foreach ($conditions as $condition) {
  338. // 只处理充值条件
  339. if ($condition->type != LotteryEnum::CONDITION_TYPE_RECHARGE_AMOUNT) {
  340. continue;
  341. }
  342. $result = static::processConditionForRecharge($condition, $rechargeInfo, $userId);
  343. if ($result['chances'] > 0) {
  344. $totalChances += $result['chances'];
  345. $result['detail']['chances'] = $result['chances'];
  346. $chanceDetails[] = $result['detail'];
  347. }
  348. }
  349. // 如果获得了抽奖机会,记录到数据库
  350. if ($totalChances > 0) {
  351. // 为每个条件分别记录
  352. foreach ($chanceDetails as $detail) {
  353. static::grantChanceToUser($activity->id, $userId, $detail['chances'] ?? 1, [
  354. 'condition_id' => $detail['condition_id'],
  355. 'condition_type' => $detail['condition_type'],
  356. 'condition_value' => $detail['condition_value'],
  357. 'recharge_amount' => $rechargeInfo['amount'] ?? 0,
  358. 'granted_time' => time()
  359. ]);
  360. }
  361. }
  362. return $totalChances;
  363. }
  364. /**
  365. * 处理订单条件
  366. *
  367. * @param LotteryCondition $condition 条件对象
  368. * @param array $orderInfo 订单信息
  369. * @param int $userId 用户ID
  370. * @return array 获得的抽奖机会信息 ['chances' => int, 'detail' => array]
  371. */
  372. private static function processConditionForOrder($condition, $orderInfo, $userId)
  373. {
  374. $chances = 0;
  375. $detail = [
  376. 'condition_id' => $condition->id,
  377. 'condition_type' => $condition->type,
  378. 'condition_value' => $condition->condition_value,
  379. ];
  380. switch ($condition->type) {
  381. case LotteryEnum::CONDITION_TYPE_BUY_GOODS:
  382. if (static::validateGoodsCondition($condition, $orderInfo)) {
  383. $chances = static::getRewardTimes($condition);
  384. }
  385. break;
  386. case LotteryEnum::CONDITION_TYPE_ORDER_AMOUNT:
  387. if (static::validateOrderAmountCondition($condition, $orderInfo)) {
  388. $chances = static::getRewardTimes($condition);
  389. // 如果可重复获得,根据金额倍数计算次数
  390. if (static::canRepeat($condition)) {
  391. // 使用bcdiv进行精确的金额除法运算,然后取整
  392. $multiple = floor(bcdiv($orderInfo['total_amount'], $condition->condition_value, 2));
  393. $chances *= $multiple;
  394. }
  395. }
  396. break;
  397. case LotteryEnum::CONDITION_TYPE_TOTAL_AMOUNT:
  398. if (static::validateAccumulateCondition($condition, $userId, $condition->activity_id)) {
  399. // 检查是否已经因为累计消费获得过机会
  400. if (!static::hasGrantedForAccumulate($condition->activity_id, $userId)) {
  401. $chances = static::getRewardTimes($condition);
  402. }
  403. }
  404. break;
  405. }
  406. return [
  407. 'chances' => $chances,
  408. 'detail' => $detail
  409. ];
  410. }
  411. /**
  412. * 处理充值条件
  413. *
  414. * @param LotteryCondition $condition 条件对象
  415. * @param array $rechargeInfo 充值信息
  416. * @param int $userId 用户ID
  417. * @return array 获得的抽奖机会信息 ['chances' => int, 'detail' => array]
  418. */
  419. private static function processConditionForRecharge($condition, $rechargeInfo, $userId)
  420. {
  421. $chances = 0;
  422. $detail = [
  423. 'condition_id' => $condition->id,
  424. 'condition_type' => $condition->type,
  425. 'condition_value' => $condition->condition_value,
  426. ];
  427. if (static::validateRechargeCondition($condition, ['type' => 'recharge', 'amount' => $rechargeInfo['amount']])) {
  428. $chances = static::getRewardTimes($condition);
  429. // 如果可重复获得,根据金额倍数计算次数
  430. if (static::canRepeat($condition)) {
  431. // 使用bcdiv进行精确的金额除法运算,然后取整
  432. $multiple = floor(bcdiv($rechargeInfo['amount'], $condition->condition_value, 2));
  433. $chances *= $multiple;
  434. }
  435. }
  436. return [
  437. 'chances' => $chances,
  438. 'detail' => $detail
  439. ];
  440. }
  441. // ============ 用户资格检查方法 ============
  442. /**
  443. * 检查用户资格
  444. *
  445. * @param LotteryActivity $activity 活动对象
  446. * @param int $userId 用户ID
  447. * @return bool
  448. */
  449. private static function checkUserQualification($activity, $userId)
  450. {
  451. // 检查用户群体限制
  452. switch ($activity->user_limit_type) {
  453. case LotteryEnum::USER_LIMIT_ALL:
  454. return true;
  455. case LotteryEnum::USER_LIMIT_LEVEL:
  456. return static::checkUserLevel($userId, $activity->user_limit_value);
  457. case LotteryEnum::USER_LIMIT_TAG:
  458. return static::checkUserTag($userId, $activity->user_limit_value);
  459. default:
  460. return false;
  461. }
  462. }
  463. /**
  464. * 检查用户等级
  465. *
  466. * @param int $userId 用户ID
  467. * @param mixed $limitValue 限制值
  468. * @return bool
  469. */
  470. private static function checkUserLevel($userId, $limitValue)
  471. {
  472. if (empty($limitValue)) {
  473. return true;
  474. }
  475. $user = User::find($userId);
  476. if (!$user) {
  477. return false;
  478. }
  479. $limitLevels = is_array($limitValue) ? $limitValue : [$limitValue];
  480. return in_array($user->level, $limitLevels);
  481. }
  482. /**
  483. * 检查用户标签
  484. *
  485. * @param int $userId 用户ID
  486. * @param mixed $limitValue 限制值
  487. * @return bool
  488. */
  489. private static function checkUserTag($userId, $limitValue)
  490. {
  491. if (empty($limitValue)) {
  492. return true;
  493. }
  494. // TODO: 根据实际的用户标签系统实现
  495. // 这里需要根据具体的用户标签表结构来实现
  496. // 暂时返回true,避免影响现有功能
  497. return true;
  498. }
  499. /**
  500. * 检查是否已因累计消费获得过机会
  501. *
  502. * @param int $activityId 活动ID
  503. * @param int $userId 用户ID
  504. * @return bool
  505. */
  506. private static function hasGrantedForAccumulate($activityId, $userId)
  507. {
  508. $record = LotteryUserChanceRecord::where('activity_id', $activityId)
  509. ->where('user_id', $userId)
  510. ->where('get_type', LotteryEnum::CHANCE_GET_TYPE_TOTAL_AMOUNT)
  511. ->find();
  512. return $record ? true : false;
  513. }
  514. // ============ 条件验证方法 ============
  515. /**
  516. * 验证订单是否满足条件
  517. *
  518. * @param LotteryCondition $condition 条件对象
  519. * @param array $orderInfo 订单信息
  520. * @return bool
  521. */
  522. public static function validateOrder(LotteryCondition $condition, $orderInfo)
  523. {
  524. switch ($condition->type) {
  525. case LotteryEnum::CONDITION_TYPE_BUY_GOODS:
  526. return static::validateGoodsCondition($condition, $orderInfo);
  527. case LotteryEnum::CONDITION_TYPE_ORDER_AMOUNT:
  528. return static::validateOrderAmountCondition($condition, $orderInfo);
  529. case LotteryEnum::CONDITION_TYPE_RECHARGE_AMOUNT:
  530. return static::validateRechargeCondition($condition, $orderInfo);
  531. default:
  532. return false;
  533. }
  534. }
  535. /**
  536. * 验证商品条件
  537. *
  538. * @param LotteryCondition $condition 条件对象
  539. * @param array $orderInfo 订单信息
  540. * @return bool
  541. */
  542. public static function validateGoodsCondition(LotteryCondition $condition, $orderInfo)
  543. {
  544. if (empty($orderInfo['goods']) || empty($condition->goods_ids_list)) {
  545. return false;
  546. }
  547. $orderGoodsIds = array_column($orderInfo['goods'], 'goods_id');
  548. $conditionGoodsIds = $condition->goods_ids_list;
  549. $intersection = array_intersect($orderGoodsIds, $conditionGoodsIds);
  550. if ($condition->goods_rule == LotteryEnum::GOODS_RULE_INCLUDE) {
  551. // 指定商品参与:订单中必须包含指定商品
  552. return !empty($intersection);
  553. } else {
  554. // 指定商品不可参与:订单中不能包含指定商品
  555. return empty($intersection);
  556. }
  557. }
  558. /**
  559. * 验证订单金额条件
  560. *
  561. * @param LotteryCondition $condition 条件对象
  562. * @param array $orderInfo 订单信息
  563. * @return bool
  564. */
  565. public static function validateOrderAmountCondition(LotteryCondition $condition, $orderInfo)
  566. {
  567. $orderAmount = $orderInfo['total_amount'] ?? 0;
  568. // 使用bccomp进行精确的金额比较,返回值:-1(小于)、0(等于)、1(大于)
  569. return bccomp($orderAmount, $condition->condition_value, 2) >= 0;
  570. }
  571. /**
  572. * 验证充值条件
  573. *
  574. * @param LotteryCondition $condition 条件对象
  575. * @param array $orderInfo 订单信息
  576. * @return bool
  577. */
  578. public static function validateRechargeCondition(LotteryCondition $condition, $orderInfo)
  579. {
  580. if (($orderInfo['type'] ?? '') !== 'recharge') {
  581. return false;
  582. }
  583. $rechargeAmount = $orderInfo['amount'] ?? 0;
  584. // 使用bccomp进行精确的金额比较
  585. return bccomp($rechargeAmount, $condition->condition_value, 2) >= 0;
  586. }
  587. /**
  588. * 验证累计消费条件
  589. *
  590. * @param LotteryCondition $condition 条件对象
  591. * @param int $userId 用户ID
  592. * @param int $activityId 活动ID
  593. * @return bool
  594. */
  595. public static function validateAccumulateCondition(LotteryCondition $condition, $userId, $activityId)
  596. {
  597. $activity = LotteryActivity::find($activityId);
  598. if (!$activity) {
  599. return false;
  600. }
  601. // 计算活动期间用户累计消费
  602. $totalAmount = Order::where('user_id', $userId)
  603. ->where('order_status', OrderEnum::STATUS_PAY)
  604. ->where('pay_time', '>=', $activity->start_time)
  605. ->where('pay_time', '<=', $activity->end_time)
  606. ->sum('amount');
  607. // 使用bccomp进行精确的金额比较
  608. return bccomp($totalAmount, $condition->condition_value, 2) >= 0;
  609. }
  610. // ============ 条件管理方法 ============
  611. /**
  612. * 获取活动的有效条件
  613. *
  614. * @param int $activityId 活动ID
  615. * @return array
  616. */
  617. public static function getValidConditions($activityId)
  618. {
  619. return LotteryCondition::where('activity_id', $activityId)
  620. ->where('status', 1)
  621. ->order('id', 'asc')
  622. ->select();
  623. }
  624. /**
  625. * 检查条件是否可重复获得奖励
  626. *
  627. * @param LotteryCondition $condition 条件对象
  628. * @return bool
  629. */
  630. public static function canRepeat(LotteryCondition $condition)
  631. {
  632. return $condition->is_repeatable == 1;
  633. }
  634. /**
  635. * 获取奖励次数
  636. *
  637. * @param LotteryCondition $condition 条件对象
  638. * @return int
  639. */
  640. public static function getRewardTimes(LotteryCondition $condition)
  641. {
  642. return $condition->reward_times ?: 1;
  643. }
  644. /**
  645. * 批量验证订单条件
  646. *
  647. * @param int $activityId 活动ID
  648. * @param array $orderInfo 订单信息
  649. * @param int $userId 用户ID
  650. * @return array 满足的条件列表
  651. */
  652. public static function batchValidateConditions($activityId, $orderInfo, $userId)
  653. {
  654. $conditions = static::getValidConditions($activityId);
  655. $validConditions = [];
  656. foreach ($conditions as $condition) {
  657. if (static::validateOrder($condition, $orderInfo)) {
  658. $validConditions[] = [
  659. 'condition_id' => $condition->id,
  660. 'condition_name' => $condition->name,
  661. 'condition_type' => $condition->type,
  662. 'condition_type_text' => $condition->type_text,
  663. 'reward_times' => static::getRewardTimes($condition),
  664. 'can_repeat' => static::canRepeat($condition)
  665. ];
  666. }
  667. }
  668. return $validConditions;
  669. }
  670. /**
  671. * 获取条件统计信息
  672. *
  673. * @param int $activityId 活动ID
  674. * @return array
  675. */
  676. public static function getConditionStatistics($activityId)
  677. {
  678. $conditions = static::getValidConditions($activityId);
  679. $statistics = [
  680. 'total_conditions' => count($conditions),
  681. 'goods_conditions' => 0,
  682. 'amount_conditions' => 0,
  683. 'recharge_conditions' => 0,
  684. 'accumulate_conditions' => 0
  685. ];
  686. foreach ($conditions as $condition) {
  687. switch ($condition->type) {
  688. case LotteryEnum::CONDITION_TYPE_BUY_GOODS:
  689. $statistics['goods_conditions']++;
  690. break;
  691. case LotteryEnum::CONDITION_TYPE_ORDER_AMOUNT:
  692. $statistics['amount_conditions']++;
  693. break;
  694. case LotteryEnum::CONDITION_TYPE_RECHARGE_AMOUNT:
  695. $statistics['recharge_conditions']++;
  696. break;
  697. case LotteryEnum::CONDITION_TYPE_TOTAL_AMOUNT:
  698. $statistics['accumulate_conditions']++;
  699. break;
  700. }
  701. }
  702. return $statistics;
  703. }
  704. // ============ 用户机会管理方法 ============
  705. /**
  706. * 获取用户抽奖机会
  707. *
  708. * @param int $activityId 活动ID
  709. * @param int $userId 用户ID
  710. * @return LotteryUserChance|null
  711. */
  712. public static function getUserChance($activityId, $userId)
  713. {
  714. return LotteryUserChance::where('activity_id', $activityId)
  715. ->where('user_id', $userId)
  716. ->find();
  717. }
  718. /**
  719. * 根据详情确定机会获取类型
  720. *
  721. * @param array $detail 获得详情
  722. * @return int 获取类型
  723. */
  724. private static function getChanceGetTypeFromDetail($detail)
  725. {
  726. // 如果有条件类型,直接使用条件类型对应的获取类型
  727. if (isset($detail['condition_type'])) {
  728. switch ($detail['condition_type']) {
  729. case LotteryEnum::CONDITION_TYPE_BUY_GOODS:
  730. return LotteryEnum::CHANCE_GET_TYPE_BUY_GOODS;
  731. case LotteryEnum::CONDITION_TYPE_ORDER_AMOUNT:
  732. return LotteryEnum::CHANCE_GET_TYPE_ORDER_AMOUNT;
  733. case LotteryEnum::CONDITION_TYPE_RECHARGE_AMOUNT:
  734. return LotteryEnum::CHANCE_GET_TYPE_RECHARGE;
  735. case LotteryEnum::CONDITION_TYPE_TOTAL_AMOUNT:
  736. return LotteryEnum::CHANCE_GET_TYPE_TOTAL_AMOUNT;
  737. }
  738. }
  739. // 如果没有条件类型,检查是否是管理员赠送
  740. if (isset($detail['admin_id']) && $detail['admin_id'] > 0) {
  741. return LotteryEnum::CHANCE_GET_TYPE_ADMIN_GRANT;
  742. }
  743. // 默认返回管理员赠送
  744. return LotteryEnum::CHANCE_GET_TYPE_ADMIN_GRANT;
  745. }
  746. /**
  747. * 获取用户在指定活动中的抽奖机会详情
  748. *
  749. * @param int $activityId 活动ID
  750. * @param int $userId 用户ID
  751. * @return array
  752. */
  753. public static function getUserChanceDetail($activityId, $userId)
  754. {
  755. $userChance = static::getUserChance($activityId, $userId);
  756. if (!$userChance) {
  757. return [
  758. 'total_chances' => 0,
  759. 'used_chances' => 0,
  760. 'remain_chances' => 0,
  761. 'get_records' => []
  762. ];
  763. }
  764. // 获取机会获得记录
  765. $getRecords = LotteryUserChanceRecord::getUserChanceRecords($activityId, $userId);
  766. return [
  767. 'total_chances' => $userChance->total_chances,
  768. 'used_chances' => $userChance->used_chances,
  769. 'remain_chances' => $userChance->remain_chances,
  770. 'last_get_time' => $userChance->last_get_time,
  771. 'last_use_time' => $userChance->last_use_time,
  772. 'get_records' => $getRecords
  773. ];
  774. }
  775. /**
  776. * 给用户分发抽奖机会
  777. *
  778. * @param int $activityId 活动ID
  779. * @param int $userId 用户ID
  780. * @param int $chances 机会次数
  781. * @param array $detail 获得详情
  782. * @return LotteryUserChance|bool
  783. */
  784. private static function grantChanceToUser($activityId, $userId, $chances, $detail)
  785. {
  786. return static::addChance($activityId, $userId, $chances, $detail);
  787. }
  788. /**
  789. * 增加抽奖机会
  790. *
  791. * @param int $activityId 活动ID
  792. * @param int $userId 用户ID
  793. * @param int $times 机会次数
  794. * @param array $detail 获得详情
  795. * @return LotteryUserChance|bool
  796. */
  797. public static function addChance($activityId, $userId, $times = 1, $detail = [])
  798. {
  799. if ($times <= 0) {
  800. return false;
  801. }
  802. try {
  803. Db::startTrans();
  804. $chance = static::getUserChance($activityId, $userId);
  805. if (!$chance) {
  806. // 创建新记录
  807. $data = [
  808. 'activity_id' => $activityId,
  809. 'user_id' => $userId,
  810. 'total_chances' => $times,
  811. 'used_chances' => 0,
  812. 'remain_chances' => $times,
  813. 'last_get_time' => time(),
  814. 'get_detail' => json_encode([]) // 保留字段但不使用
  815. ];
  816. $chance = LotteryUserChance::create($data);
  817. } else {
  818. // 更新现有记录
  819. $chance->total_chances += $times;
  820. $chance->remain_chances += $times;
  821. $chance->last_get_time = time();
  822. $chance->save();
  823. }
  824. // 创建获得记录
  825. $recordData = [
  826. 'activity_id' => $activityId,
  827. 'user_id' => $userId,
  828. 'get_type' => static::getChanceGetTypeFromDetail($detail),
  829. 'chances' => $times,
  830. 'condition_id' => $detail['condition_id'] ?? null,
  831. 'condition_value' => $detail['condition_value'] ?? null,
  832. 'order_id' => $detail['order_id'] ?? null,
  833. 'recharge_amount' => $detail['recharge_amount'] ?? null,
  834. 'admin_id' => $detail['admin_id'] ?? null,
  835. 'reason' => $detail['reason'] ?? null,
  836. 'remark' => $detail['remark'] ?? null,
  837. 'get_time' => time()
  838. ];
  839. $validation = LotteryUserChanceRecord::validateRecord($recordData);
  840. if ($validation !== true) {
  841. throw new Exception($validation);
  842. }
  843. LotteryUserChanceRecord::create($recordData);
  844. Db::commit();
  845. return $chance;
  846. } catch (Exception $e) {
  847. Db::rollback();
  848. throw $e;
  849. }
  850. }
  851. /**
  852. * 使用抽奖机会
  853. *
  854. * @param LotteryUserChance $userChance 用户机会对象
  855. * @param int $times 使用次数
  856. * @return bool
  857. */
  858. public static function useChance(LotteryUserChance $userChance, $times = 1)
  859. {
  860. if ($userChance->remain_chances < $times) {
  861. return false;
  862. }
  863. $userChance->used_chances += $times;
  864. $userChance->remain_chances -= $times;
  865. $userChance->last_use_time = time();
  866. return $userChance->save();
  867. }
  868. /**
  869. * 检查是否有剩余机会
  870. *
  871. * @param LotteryUserChance $userChance 用户机会对象
  872. * @param int $times 需要的机会次数
  873. * @return bool
  874. */
  875. public static function hasChance(LotteryUserChance $userChance, $times = 1)
  876. {
  877. return $userChance->remain_chances >= $times;
  878. }
  879. /**
  880. * 重置抽奖机会(用于测试或特殊情况)
  881. *
  882. * @param LotteryUserChance $userChance 用户机会对象
  883. * @return bool
  884. */
  885. public static function resetChance(LotteryUserChance $userChance)
  886. {
  887. $userChance->used_chances = 0;
  888. $userChance->remain_chances = $userChance->total_chances;
  889. return $userChance->save();
  890. }
  891. // ============ 统计和查询方法 ============
  892. /**
  893. * 获取活动总参与人数
  894. *
  895. * @param int $activityId 活动ID
  896. * @return int
  897. */
  898. public static function getActivityParticipants($activityId)
  899. {
  900. return LotteryUserChance::where('activity_id', $activityId)->count();
  901. }
  902. /**
  903. * 获取用户在多个活动中的机会统计
  904. *
  905. * @param int $userId 用户ID
  906. * @param array $activityIds 活动ID数组
  907. * @return array
  908. */
  909. public static function getUserChancesStats($userId, $activityIds = [])
  910. {
  911. $query = LotteryUserChance::where('user_id', $userId);
  912. if (!empty($activityIds)) {
  913. $query->where('activity_id', 'in', $activityIds);
  914. }
  915. return $query->field([
  916. 'activity_id',
  917. 'total_chances',
  918. 'used_chances',
  919. 'remain_chances'
  920. ])
  921. ->select();
  922. }
  923. /**
  924. * 获取用户在所有活动中的机会概览
  925. *
  926. * @param int $userId 用户ID
  927. * @return array
  928. */
  929. public static function getUserAllChancesOverview($userId)
  930. {
  931. $chances = LotteryUserChance::where('user_id', $userId)
  932. ->with(['activity'])
  933. ->select();
  934. $overview = [
  935. 'total_activities' => count($chances),
  936. 'total_chances' => 0,
  937. 'total_used' => 0,
  938. 'total_remain' => 0,
  939. 'activities' => []
  940. ];
  941. foreach ($chances as $chance) {
  942. $overview['total_chances'] += $chance->total_chances;
  943. $overview['total_used'] += $chance->used_chances;
  944. $overview['total_remain'] += $chance->remain_chances;
  945. $overview['activities'][] = [
  946. 'activity_id' => $chance->activity_id,
  947. 'activity_name' => $chance->activity->name ?? '',
  948. 'total_chances' => $chance->total_chances,
  949. 'used_chances' => $chance->used_chances,
  950. 'remain_chances' => $chance->remain_chances,
  951. 'last_get_time' => $chance->last_get_time,
  952. 'last_use_time' => $chance->last_use_time
  953. ];
  954. }
  955. return $overview;
  956. }
  957. /**
  958. * 检查用户是否可以参与活动
  959. *
  960. * @param int $activityId 活动ID
  961. * @param int $userId 用户ID
  962. * @return bool
  963. */
  964. public static function canUserParticipate($activityId, $userId)
  965. {
  966. $userChance = static::getUserChance($activityId, $userId);
  967. return $userChance && static::hasChance($userChance, 1);
  968. }
  969. /**
  970. * 获取活动参与用户列表
  971. *
  972. * @param int $activityId 活动ID
  973. * @param int $page 页码
  974. * @param int $limit 每页数量
  975. * @return array
  976. */
  977. public static function getActivityParticipantsList($activityId, $page = 1, $limit = 20)
  978. {
  979. return LotteryUserChance::where('activity_id', $activityId)
  980. ->with(['user'])
  981. ->order('createtime', 'desc')
  982. ->page($page, $limit)
  983. ->select();
  984. }
  985. /**
  986. * 获取活动参与统计
  987. *
  988. * @param int $activityId 活动ID
  989. * @return array
  990. */
  991. public static function getActivityParticipationStats($activityId)
  992. {
  993. $stats = LotteryUserChance::where('activity_id', $activityId)
  994. ->field([
  995. 'COUNT(*) as total_participants',
  996. 'SUM(total_chances) as total_chances_granted',
  997. 'SUM(used_chances) as total_chances_used',
  998. 'SUM(remain_chances) as total_chances_remain'
  999. ])
  1000. ->find();
  1001. return [
  1002. 'total_participants' => $stats['total_participants'] ?? 0,
  1003. 'total_chances_granted' => $stats['total_chances_granted'] ?? 0,
  1004. 'total_chances_used' => $stats['total_chances_used'] ?? 0,
  1005. 'total_chances_remain' => $stats['total_chances_remain'] ?? 0,
  1006. 'usage_rate' => $stats['total_chances_granted'] > 0 ?
  1007. round(($stats['total_chances_used'] / $stats['total_chances_granted']) * 100, 2) : 0
  1008. ];
  1009. }
  1010. // ============ 批量操作方法 ============
  1011. /**
  1012. * 批量创建用户机会(用于活动启动时)
  1013. *
  1014. * @param int $activityId 活动ID
  1015. * @param array $userIds 用户ID数组
  1016. * @param int $times 机会次数
  1017. * @return bool
  1018. */
  1019. public static function batchCreateChances($activityId, $userIds, $times = 1)
  1020. {
  1021. if (empty($userIds)) {
  1022. return false;
  1023. }
  1024. $data = [];
  1025. $now = time();
  1026. foreach ($userIds as $userId) {
  1027. $data[] = [
  1028. 'activity_id' => $activityId,
  1029. 'user_id' => $userId,
  1030. 'total_chances' => $times,
  1031. 'used_chances' => 0,
  1032. 'remain_chances' => $times,
  1033. 'last_get_time' => $now,
  1034. 'createtime' => $now,
  1035. 'updatetime' => $now
  1036. ];
  1037. }
  1038. return LotteryUserChance::insertAll($data);
  1039. }
  1040. /**
  1041. * 批量检查用户资格
  1042. *
  1043. * @param int $activityId 活动ID
  1044. * @param array $userIds 用户ID数组
  1045. * @return array 符合条件的用户ID数组
  1046. */
  1047. public static function batchCheckUserQualification($activityId, $userIds)
  1048. {
  1049. $activity = LotteryActivity::find($activityId);
  1050. if (!$activity) {
  1051. return [];
  1052. }
  1053. $qualifiedUsers = [];
  1054. foreach ($userIds as $userId) {
  1055. if (static::checkUserQualification($activity, $userId)) {
  1056. $qualifiedUsers[] = $userId;
  1057. }
  1058. }
  1059. return $qualifiedUsers;
  1060. }
  1061. /**
  1062. * 批量验证条件
  1063. *
  1064. * @param int $activityId 活动ID
  1065. * @param array $orderInfo 订单信息
  1066. * @param array $userIds 用户ID数组
  1067. * @return array 每个用户满足的条件
  1068. */
  1069. public static function batchValidateConditionsForUsers($activityId, $orderInfo, $userIds)
  1070. {
  1071. $results = [];
  1072. foreach ($userIds as $userId) {
  1073. $results[$userId] = static::batchValidateConditions($activityId, $orderInfo, $userId);
  1074. }
  1075. return $results;
  1076. }
  1077. // ============ 机会获取记录管理方法 ============
  1078. /**
  1079. * 获取用户机会获取记录
  1080. *
  1081. * @param int $activityId 活动ID
  1082. * @param int $userId 用户ID
  1083. * @param int $page 页码
  1084. * @param int $limit 每页数量
  1085. * @return array
  1086. */
  1087. public static function getUserChanceRecords($activityId, $userId, $page = 1, $limit = 20)
  1088. {
  1089. return LotteryUserChanceRecord::getUserChanceRecords($activityId, $userId, $page, $limit);
  1090. }
  1091. /**
  1092. * 获取活动机会获取统计
  1093. *
  1094. * @param int $activityId 活动ID
  1095. * @return array
  1096. */
  1097. public static function getActivityChanceRecordStats($activityId)
  1098. {
  1099. return LotteryUserChanceRecord::getActivityChanceStats($activityId);
  1100. }
  1101. /**
  1102. * 获取用户机会获取统计
  1103. *
  1104. * @param int $userId 用户ID
  1105. * @param int $activityId 活动ID(可选)
  1106. * @return array
  1107. */
  1108. public static function getUserChanceRecordStats($userId, $activityId = null)
  1109. {
  1110. return LotteryUserChanceRecord::getUserChanceStats($userId, $activityId);
  1111. }
  1112. /**
  1113. * 批量创建机会获取记录
  1114. *
  1115. * @param array $records 记录数组
  1116. * @return bool
  1117. */
  1118. public static function batchCreateChanceRecords($records)
  1119. {
  1120. return LotteryUserChanceRecord::batchCreateRecords($records);
  1121. }
  1122. /**
  1123. * 验证机会获取记录数据
  1124. *
  1125. * @param array $data 记录数据
  1126. * @return bool|string
  1127. */
  1128. public static function validateChanceRecord($data)
  1129. {
  1130. return LotteryUserChanceRecord::validateRecord($data);
  1131. }
  1132. }