LotteryRecordService.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. <?php
  2. namespace app\common\Service\Lottery;
  3. use app\common\model\lottery\LotteryDrawRecord;
  4. use app\common\model\lottery\LotteryWinRecord;
  5. use app\common\model\lottery\LotteryPrize;
  6. use app\common\Enum\LotteryEnum;
  7. use think\Exception;
  8. /**
  9. * 抽奖记录服务类
  10. * 专门处理抽奖记录和中奖记录的业务逻辑
  11. */
  12. class LotteryRecordService
  13. {
  14. // ============ 抽奖记录相关方法 ============
  15. /**
  16. * 创建抽奖记录
  17. */
  18. public static function createDrawRecord($activityId, $userId, $prizeId, $isWin, $triggerType, $triggerOrderId = null, $triggerAmount = null, $winInfo = [])
  19. {
  20. $data = [
  21. 'activity_id' => $activityId,
  22. 'user_id' => $userId,
  23. 'prize_id' => $prizeId,
  24. 'is_win' => $isWin ? 1 : 0,
  25. 'trigger_type' => $triggerType,
  26. 'trigger_order_id' => $triggerOrderId,
  27. 'trigger_amount' => $triggerAmount,
  28. 'win_info' => $winInfo ? json_encode($winInfo) : '',
  29. 'draw_ip' => request()->ip(),
  30. 'draw_time' => time(),
  31. 'device_info' => request()->header('user-agent', '')
  32. ];
  33. return LotteryDrawRecord::create($data);
  34. }
  35. /**
  36. * 获取用户抽奖次数
  37. */
  38. public static function getUserDrawCount($activityId, $userId, $timeRange = null)
  39. {
  40. $query = LotteryDrawRecord::where('activity_id', $activityId)
  41. ->where('user_id', $userId);
  42. if ($timeRange) {
  43. if (isset($timeRange['start'])) {
  44. $query->where('draw_time', '>=', $timeRange['start']);
  45. }
  46. if (isset($timeRange['end'])) {
  47. $query->where('draw_time', '<=', $timeRange['end']);
  48. }
  49. }
  50. return $query->count();
  51. }
  52. /**
  53. * 获取用户中奖次数
  54. */
  55. public static function getUserWinCount($activityId, $userId, $timeRange = null)
  56. {
  57. $query = LotteryDrawRecord::where('activity_id', $activityId)
  58. ->where('user_id', $userId)
  59. ->where('is_win', 1);
  60. if ($timeRange) {
  61. if (isset($timeRange['start'])) {
  62. $query->where('draw_time', '>=', $timeRange['start']);
  63. }
  64. if (isset($timeRange['end'])) {
  65. $query->where('draw_time', '<=', $timeRange['end']);
  66. }
  67. }
  68. return $query->count();
  69. }
  70. /**
  71. * 获取活动抽奖统计
  72. */
  73. public static function getActivityDrawStats($activityId, $timeRange = null)
  74. {
  75. $query = LotteryDrawRecord::where('activity_id', $activityId);
  76. if ($timeRange) {
  77. if (isset($timeRange['start'])) {
  78. $query->where('draw_time', '>=', $timeRange['start']);
  79. }
  80. if (isset($timeRange['end'])) {
  81. $query->where('draw_time', '<=', $timeRange['end']);
  82. }
  83. }
  84. return [
  85. 'total_draw' => $query->count(),
  86. 'total_win' => $query->where('is_win', 1)->count(),
  87. 'total_people' => $query->distinct('user_id')->count()
  88. ];
  89. }
  90. /**
  91. * 获取用户抽奖记录列表
  92. */
  93. public static function getUserDrawRecords($userId = 0, $activityId = null, $page = 1, $pageSize = 20)
  94. {
  95. $query = LotteryDrawRecord::where('user_id', $userId);
  96. if ($activityId) {
  97. $query->where('activity_id', $activityId);
  98. }
  99. // 1. 先分页查询抽奖记录
  100. $records = $query->field('id,activity_id,prize_id,is_win,trigger_type,trigger_order_id,trigger_amount,draw_time')
  101. ->order('draw_time', 'desc')
  102. ->paginate($pageSize, false, ['page' => $page]);
  103. if (empty($records) || $records->isEmpty()) {
  104. return $records;
  105. }
  106. // 2. 收集关联ID
  107. $activityIds = [];
  108. $prizeIds = [];
  109. $drawRecordIds = [];
  110. foreach ($records as $record) {
  111. $activityIds[] = $record->activity_id;
  112. $prizeIds[] = $record->prize_id;
  113. if ($record->is_win) {
  114. $drawRecordIds[] = $record->id;
  115. }
  116. }
  117. // 去重
  118. $activityIds = array_unique($activityIds);
  119. $prizeIds = array_unique($prizeIds);
  120. $drawRecordIds = array_unique($drawRecordIds);
  121. // 3. 批量查询关联数据
  122. $activities = [];
  123. if (!empty($activityIds)) {
  124. $activityList = \app\common\model\lottery\LotteryActivity::whereIn('id', $activityIds)
  125. ->field('id,name,status,lottery_type')
  126. ->select();
  127. foreach ($activityList as $activity) {
  128. $activities[$activity->id] = $activity;
  129. }
  130. }
  131. $prizes = [];
  132. if (!empty($prizeIds)) {
  133. $prizeList = \app\common\model\lottery\LotteryPrize::whereIn('id', $prizeIds)
  134. ->field('id,name,image,activity_id,type')
  135. ->select();
  136. foreach ($prizeList as $prize) {
  137. $prizes[$prize->id] = $prize;
  138. }
  139. }
  140. $winRecords = [];
  141. if (!empty($drawRecordIds)) {
  142. $winRecordList = \app\common\model\lottery\LotteryWinRecord::whereIn('draw_record_id', $drawRecordIds)
  143. ->field('id,draw_record_id,deliver_status,deliver_time,exchange_code,receiver_name,receiver_mobile,receiver_address,express_company,express_number,exchange_code')
  144. ->select();
  145. foreach ($winRecordList as $winRecord) {
  146. $winRecords[$winRecord->draw_record_id] = $winRecord;
  147. }
  148. }
  149. // 4. 将关联数据作为字段附加到记录中
  150. foreach ($records as $record) {
  151. // 附加活动信息字段
  152. $activity = $activities[$record->activity_id] ?? null;
  153. $record->activity_name = $activity ? $activity->name : '';
  154. $record->activity_status = $activity ? $activity->status : 0;
  155. $record->activity_lottery_type = $activity ? $activity->lottery_type : 0;
  156. // 附加奖品信息字段
  157. $prize = $prizes[$record->prize_id] ?? null;
  158. $record->prize_name = $prize ? $prize->name : '';
  159. $record->prize_image = $prize ? cdnurl($prize->image) : '';
  160. $record->prize_type = $prize ? $prize->type : 0;
  161. // 附加中奖记录信息字段(仅限中奖记录)
  162. if ($record->is_win) {
  163. $winRecord = $winRecords[$record->id] ?? null;
  164. $record->win_record_id = $winRecord ? $winRecord->id : 0;
  165. $record->deliver_status = $winRecord ? $winRecord->deliver_status : 0;
  166. $record->deliver_time = $winRecord ? $winRecord->deliver_time : 0;
  167. $record->exchange_code = $winRecord ? $winRecord->exchange_code : '';
  168. $record->receiver_name = $winRecord ? $winRecord->receiver_name : '';
  169. $record->receiver_mobile = $winRecord ? $winRecord->receiver_mobile : '';
  170. $record->receiver_address = $winRecord ? $winRecord->receiver_address : '';
  171. $record->express_company = $winRecord ? $winRecord->express_company : '';
  172. $record->express_number = $winRecord ? $winRecord->express_number : '';
  173. } else {
  174. $record->win_record_id = 0;
  175. $record->deliver_status = 0;
  176. $record->deliver_time = 0;
  177. $record->exchange_code = '';
  178. $record->receiver_name = '';
  179. $record->receiver_mobile = '';
  180. $record->receiver_address = '';
  181. $record->express_company = '';
  182. $record->express_number = '';
  183. }
  184. // 添加触发类型文本
  185. $record->trigger_type_text = static::getTriggerTypeText($record->trigger_type);
  186. }
  187. return $records;
  188. }
  189. /**
  190. * 获取触发类型文本
  191. */
  192. private static function getTriggerTypeText($triggerType)
  193. {
  194. $map = [
  195. 1 => '购买商品',
  196. 2 => '订单消费',
  197. 3 => '充值',
  198. 4 => '累计消费'
  199. ];
  200. return $map[$triggerType] ?? '未知';
  201. }
  202. /**
  203. * 获取活动抽奖记录列表
  204. */
  205. public static function getActivityDrawRecords($activityId, $page = 1, $limit = 20, $filters = [])
  206. {
  207. $query = LotteryDrawRecord::where('activity_id', $activityId);
  208. // 应用过滤器
  209. if (isset($filters['is_win'])) {
  210. $query->where('is_win', $filters['is_win']);
  211. }
  212. if (isset($filters['trigger_type'])) {
  213. $query->where('trigger_type', $filters['trigger_type']);
  214. }
  215. if (isset($filters['start_time'])) {
  216. $query->where('draw_time', '>=', $filters['start_time']);
  217. }
  218. if (isset($filters['end_time'])) {
  219. $query->where('draw_time', '<=', $filters['end_time']);
  220. }
  221. return $query->with(['user', 'prize', 'winRecord'])
  222. ->order('draw_time', 'desc')
  223. ->page($page, $limit)
  224. ->select();
  225. }
  226. /**
  227. * 获取抽奖记录详情
  228. */
  229. public static function getDrawRecordDetail($drawRecordId)
  230. {
  231. return LotteryDrawRecord::where('id', $drawRecordId)
  232. ->with(['activity', 'user', 'prize', 'winRecord', 'order'])
  233. ->find();
  234. }
  235. /**
  236. * 检查用户是否已为指定订单抽奖
  237. */
  238. public static function hasUserDrawnForOrder($activityId, $userId, $orderId)
  239. {
  240. return LotteryDrawRecord::where('activity_id', $activityId)
  241. ->where('user_id', $userId)
  242. ->where('trigger_order_id', $orderId)
  243. ->count() > 0;
  244. }
  245. /**
  246. * 获取用户抽奖历史统计
  247. */
  248. public static function getUserDrawHistoryStats($userId)
  249. {
  250. $stats = LotteryDrawRecord::where('user_id', $userId)
  251. ->field([
  252. 'COUNT(*) as total_draws',
  253. 'SUM(is_win) as total_wins',
  254. 'COUNT(DISTINCT activity_id) as total_activities'
  255. ])
  256. ->find();
  257. return [
  258. 'total_draws' => $stats['total_draws'] ?? 0,
  259. 'total_wins' => $stats['total_wins'] ?? 0,
  260. 'total_activities' => $stats['total_activities'] ?? 0,
  261. 'win_rate' => $stats['total_draws'] > 0 ?
  262. round(($stats['total_wins'] / $stats['total_draws']) * 100, 2) : 0
  263. ];
  264. }
  265. /**
  266. * 获取活动抽奖排行榜
  267. */
  268. public static function getActivityDrawRanking($activityId, $limit = 50)
  269. {
  270. return LotteryDrawRecord::where('activity_id', $activityId)
  271. ->field('user_id, COUNT(*) as draw_count, SUM(is_win) as win_count')
  272. ->with('user')
  273. ->group('user_id')
  274. ->order('draw_count desc, win_count desc')
  275. ->limit($limit)
  276. ->select();
  277. }
  278. /**
  279. * 获取活动抽奖时间分布统计
  280. */
  281. public static function getActivityDrawTimeDistribution($activityId, $dateRange = null)
  282. {
  283. $query = LotteryDrawRecord::where('activity_id', $activityId);
  284. if ($dateRange) {
  285. if (isset($dateRange['start'])) {
  286. $query->where('draw_time', '>=', $dateRange['start']);
  287. }
  288. if (isset($dateRange['end'])) {
  289. $query->where('draw_time', '<=', $dateRange['end']);
  290. }
  291. }
  292. return $query->field('DATE(FROM_UNIXTIME(draw_time)) as date, COUNT(*) as draw_count, SUM(is_win) as win_count')
  293. ->group('date')
  294. ->order('date asc')
  295. ->select();
  296. }
  297. // ============ 中奖记录相关方法 ============
  298. /**
  299. * 创建中奖记录
  300. */
  301. public static function createWinRecord($drawRecordId, $activityId, $userId, $prizeId, $prizeName, $prizeType, $prizeValue = [])
  302. {
  303. $data = [
  304. 'draw_record_id' => $drawRecordId,
  305. 'activity_id' => $activityId,
  306. 'user_id' => $userId,
  307. 'prize_id' => $prizeId,
  308. 'prize_name' => $prizeName,
  309. 'prize_type' => $prizeType,
  310. 'prize_value' => is_array($prizeValue) ? json_encode($prizeValue) : $prizeValue,
  311. 'deliver_status' => LotteryEnum::DELIVER_STATUS_PENDING
  312. ];
  313. return LotteryWinRecord::create($data);
  314. }
  315. /**
  316. * 标记中奖记录为发放成功
  317. */
  318. public static function markWinRecordAsDelivered(LotteryWinRecord $winRecord, $deliverInfo = [])
  319. {
  320. $winRecord->deliver_status = LotteryEnum::DELIVER_STATUS_SUCCESS;
  321. $winRecord->deliver_time = time();
  322. if ($deliverInfo) {
  323. $winRecord->deliver_info = json_encode($deliverInfo);
  324. }
  325. return $winRecord->save();
  326. }
  327. /**
  328. * 标记中奖记录为发放失败
  329. */
  330. public static function markWinRecordAsFailed(LotteryWinRecord $winRecord, $reason = '')
  331. {
  332. $winRecord->deliver_status = LotteryEnum::DELIVER_STATUS_FAILED;
  333. $winRecord->fail_reason = $reason;
  334. return $winRecord->save();
  335. }
  336. /**
  337. * 设置中奖记录收货地址
  338. */
  339. public static function setWinRecordDeliveryAddress(LotteryWinRecord $winRecord, $name, $mobile, $address)
  340. {
  341. $winRecord->receiver_name = $name;
  342. $winRecord->receiver_mobile = $mobile;
  343. $winRecord->receiver_address = $address;
  344. return $winRecord->save();
  345. }
  346. /**
  347. * 设置中奖记录快递信息
  348. */
  349. public static function setWinRecordExpressInfo(LotteryWinRecord $winRecord, $company, $number)
  350. {
  351. $winRecord->express_company = $company;
  352. $winRecord->express_number = $number;
  353. return $winRecord->save();
  354. }
  355. /**
  356. * 设置中奖记录兑换码
  357. */
  358. public static function setWinRecordExchangeCode(LotteryWinRecord $winRecord, $code)
  359. {
  360. $winRecord->exchange_code = $code;
  361. return $winRecord->save();
  362. }
  363. /**
  364. * 标记中奖记录兑换码已使用
  365. */
  366. public static function markWinRecordCodeUsed(LotteryWinRecord $winRecord)
  367. {
  368. $winRecord->code_used_time = time();
  369. return $winRecord->save();
  370. }
  371. /**
  372. * 获取待发放的中奖记录
  373. */
  374. public static function getPendingWinRecords($limit = 100)
  375. {
  376. return LotteryWinRecord::where('deliver_status', LotteryEnum::DELIVER_STATUS_PENDING)
  377. ->order('createtime', 'asc')
  378. ->limit($limit)
  379. ->select();
  380. }
  381. /**
  382. * 获取用户中奖记录
  383. */
  384. public static function getUserWinRecords($userId, $page = 1, $limit = 20)
  385. {
  386. return LotteryWinRecord::where('user_id', $userId)
  387. ->with(['activity', 'prize'])
  388. ->order('createtime', 'desc')
  389. ->page($page, $limit)
  390. ->select();
  391. }
  392. /**
  393. * 获取活动中奖统计
  394. */
  395. public static function getActivityWinStats($activityId)
  396. {
  397. return [
  398. 'total_win' => LotteryWinRecord::where('activity_id', $activityId)->count(),
  399. 'delivered' => LotteryWinRecord::where('activity_id', $activityId)
  400. ->where('deliver_status', LotteryEnum::DELIVER_STATUS_SUCCESS)
  401. ->count(),
  402. 'pending' => LotteryWinRecord::where('activity_id', $activityId)
  403. ->where('deliver_status', LotteryEnum::DELIVER_STATUS_PENDING)
  404. ->count(),
  405. 'failed' => LotteryWinRecord::where('activity_id', $activityId)
  406. ->where('deliver_status', LotteryEnum::DELIVER_STATUS_FAILED)
  407. ->count()
  408. ];
  409. }
  410. /**
  411. * 批量处理待发放的中奖记录
  412. */
  413. public static function batchProcessPendingWinRecords($limit = 50)
  414. {
  415. $pendingRecords = static::getPendingWinRecords($limit);
  416. $processedCount = 0;
  417. foreach ($pendingRecords as $winRecord) {
  418. try {
  419. $prize = LotteryPrize::find($winRecord->prize_id);
  420. if ($prize && $prize->deliver_type == LotteryEnum::DELIVER_TYPE_AUTO) {
  421. static::autoDeliverPrize($winRecord, $prize);
  422. $processedCount++;
  423. }
  424. } catch (Exception $e) {
  425. static::markWinRecordAsFailed($winRecord, $e->getMessage());
  426. trace('自动发放奖品失败: ' . $e->getMessage(), 'error');
  427. }
  428. }
  429. return $processedCount;
  430. }
  431. /**
  432. * 手动发放奖品(管理员操作)
  433. */
  434. public static function manualDeliverPrize($winRecordId, $deliverInfo = [], $adminId = 0)
  435. {
  436. $winRecord = LotteryWinRecord::find($winRecordId);
  437. if (!$winRecord) {
  438. throw new Exception('中奖记录不存在');
  439. }
  440. if ($winRecord->deliver_status != LotteryEnum::DELIVER_STATUS_PENDING) {
  441. throw new Exception('该记录已处理,无法重复发放');
  442. }
  443. $deliverInfo['admin_id'] = $adminId;
  444. $deliverInfo['manual_deliver_time'] = time();
  445. return static::markWinRecordAsDelivered($winRecord, $deliverInfo);
  446. }
  447. /**
  448. * 取消中奖记录
  449. */
  450. public static function cancelWinRecord($winRecordId, $reason = '', $adminId = 0)
  451. {
  452. $winRecord = LotteryWinRecord::find($winRecordId);
  453. if (!$winRecord) {
  454. throw new Exception('中奖记录不存在');
  455. }
  456. $winRecord->deliver_status = LotteryEnum::DELIVER_STATUS_CANCELLED;
  457. $winRecord->fail_reason = $reason;
  458. $winRecord->cancel_time = time();
  459. $winRecord->cancel_admin_id = $adminId;
  460. return $winRecord->save();
  461. }
  462. /**
  463. * 自动发放奖品
  464. */
  465. public static function autoDeliverPrize($winRecord, $prize)
  466. {
  467. try {
  468. switch ($prize->type) {
  469. case LotteryEnum::PRIZE_TYPE_COUPON:
  470. static::deliverCoupon($winRecord, $prize);
  471. break;
  472. case LotteryEnum::PRIZE_TYPE_REDPACK:
  473. static::deliverRedPacket($winRecord, $prize);
  474. break;
  475. case LotteryEnum::PRIZE_TYPE_CODE:
  476. static::deliverExchangeCode($winRecord, $prize);
  477. break;
  478. case LotteryEnum::PRIZE_TYPE_SHOP_GOODS:
  479. static::deliverGoods($winRecord, $prize);
  480. break;
  481. }
  482. } catch (Exception $e) {
  483. static::markWinRecordAsFailed($winRecord, $e->getMessage());
  484. }
  485. }
  486. /**
  487. * 发放优惠券
  488. */
  489. private static function deliverCoupon($winRecord, $prize)
  490. {
  491. // 这里调用优惠券发放接口
  492. // 示例代码,需要根据实际优惠券系统实现
  493. static::markWinRecordAsDelivered($winRecord, ['coupon_id' => $prize->coupon_id]);
  494. }
  495. /**
  496. * 发放红包
  497. */
  498. private static function deliverRedPacket($winRecord, $prize)
  499. {
  500. // 这里调用红包发放接口
  501. // 示例代码,需要根据实际红包系统实现
  502. static::markWinRecordAsDelivered($winRecord, ['amount' => $prize->amount]);
  503. }
  504. /**
  505. * 发放兑换码
  506. */
  507. private static function deliverExchangeCode($winRecord, $prize)
  508. {
  509. $code = LotteryService::getAvailableExchangeCode($prize);
  510. if ($code) {
  511. static::setWinRecordExchangeCode($winRecord, $code);
  512. LotteryService::markExchangeCodeUsed($prize, $code);
  513. static::markWinRecordAsDelivered($winRecord, ['exchange_code' => $code]);
  514. } else {
  515. throw new Exception('兑换码已用完');
  516. }
  517. }
  518. /**
  519. * 发放商城商品
  520. */
  521. private static function deliverGoods($winRecord, $prize)
  522. {
  523. // 这里可以自动加入购物车或创建订单
  524. // 示例代码,需要根据实际商城系统实现
  525. static::markWinRecordAsDelivered($winRecord, [
  526. 'goods_id' => $prize->goods_id,
  527. 'goods_sku_id' => $prize->goods_sku_id
  528. ]);
  529. }
  530. }