LotteryRecordService.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  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. return $query->with(['activity', 'prize', 'winRecord'])
  100. ->order('draw_time', 'desc')
  101. ->paginate($pageSize, false, ['page' => $page]);
  102. }
  103. /**
  104. * 获取活动抽奖记录列表
  105. */
  106. public static function getActivityDrawRecords($activityId, $page = 1, $limit = 20, $filters = [])
  107. {
  108. $query = LotteryDrawRecord::where('activity_id', $activityId);
  109. // 应用过滤器
  110. if (isset($filters['is_win'])) {
  111. $query->where('is_win', $filters['is_win']);
  112. }
  113. if (isset($filters['trigger_type'])) {
  114. $query->where('trigger_type', $filters['trigger_type']);
  115. }
  116. if (isset($filters['start_time'])) {
  117. $query->where('draw_time', '>=', $filters['start_time']);
  118. }
  119. if (isset($filters['end_time'])) {
  120. $query->where('draw_time', '<=', $filters['end_time']);
  121. }
  122. return $query->with(['user', 'prize', 'winRecord'])
  123. ->order('draw_time', 'desc')
  124. ->page($page, $limit)
  125. ->select();
  126. }
  127. /**
  128. * 获取抽奖记录详情
  129. */
  130. public static function getDrawRecordDetail($drawRecordId)
  131. {
  132. return LotteryDrawRecord::where('id', $drawRecordId)
  133. ->with(['activity', 'user', 'prize', 'winRecord', 'order'])
  134. ->find();
  135. }
  136. /**
  137. * 检查用户是否已为指定订单抽奖
  138. */
  139. public static function hasUserDrawnForOrder($activityId, $userId, $orderId)
  140. {
  141. return LotteryDrawRecord::where('activity_id', $activityId)
  142. ->where('user_id', $userId)
  143. ->where('trigger_order_id', $orderId)
  144. ->count() > 0;
  145. }
  146. /**
  147. * 获取用户抽奖历史统计
  148. */
  149. public static function getUserDrawHistoryStats($userId)
  150. {
  151. $stats = LotteryDrawRecord::where('user_id', $userId)
  152. ->field([
  153. 'COUNT(*) as total_draws',
  154. 'SUM(is_win) as total_wins',
  155. 'COUNT(DISTINCT activity_id) as total_activities'
  156. ])
  157. ->find();
  158. return [
  159. 'total_draws' => $stats['total_draws'] ?? 0,
  160. 'total_wins' => $stats['total_wins'] ?? 0,
  161. 'total_activities' => $stats['total_activities'] ?? 0,
  162. 'win_rate' => $stats['total_draws'] > 0 ?
  163. round(($stats['total_wins'] / $stats['total_draws']) * 100, 2) : 0
  164. ];
  165. }
  166. /**
  167. * 获取活动抽奖排行榜
  168. */
  169. public static function getActivityDrawRanking($activityId, $limit = 50)
  170. {
  171. return LotteryDrawRecord::where('activity_id', $activityId)
  172. ->field('user_id, COUNT(*) as draw_count, SUM(is_win) as win_count')
  173. ->with('user')
  174. ->group('user_id')
  175. ->order('draw_count desc, win_count desc')
  176. ->limit($limit)
  177. ->select();
  178. }
  179. /**
  180. * 获取活动抽奖时间分布统计
  181. */
  182. public static function getActivityDrawTimeDistribution($activityId, $dateRange = null)
  183. {
  184. $query = LotteryDrawRecord::where('activity_id', $activityId);
  185. if ($dateRange) {
  186. if (isset($dateRange['start'])) {
  187. $query->where('draw_time', '>=', $dateRange['start']);
  188. }
  189. if (isset($dateRange['end'])) {
  190. $query->where('draw_time', '<=', $dateRange['end']);
  191. }
  192. }
  193. return $query->field('DATE(FROM_UNIXTIME(draw_time)) as date, COUNT(*) as draw_count, SUM(is_win) as win_count')
  194. ->group('date')
  195. ->order('date asc')
  196. ->select();
  197. }
  198. // ============ 中奖记录相关方法 ============
  199. /**
  200. * 创建中奖记录
  201. */
  202. public static function createWinRecord($drawRecordId, $activityId, $userId, $prizeId, $prizeName, $prizeType, $prizeValue = [])
  203. {
  204. $data = [
  205. 'draw_record_id' => $drawRecordId,
  206. 'activity_id' => $activityId,
  207. 'user_id' => $userId,
  208. 'prize_id' => $prizeId,
  209. 'prize_name' => $prizeName,
  210. 'prize_type' => $prizeType,
  211. 'prize_value' => is_array($prizeValue) ? json_encode($prizeValue) : $prizeValue,
  212. 'deliver_status' => LotteryEnum::DELIVER_STATUS_PENDING
  213. ];
  214. return LotteryWinRecord::create($data);
  215. }
  216. /**
  217. * 标记中奖记录为发放成功
  218. */
  219. public static function markWinRecordAsDelivered(LotteryWinRecord $winRecord, $deliverInfo = [])
  220. {
  221. $winRecord->deliver_status = LotteryEnum::DELIVER_STATUS_SUCCESS;
  222. $winRecord->deliver_time = time();
  223. if ($deliverInfo) {
  224. $winRecord->deliver_info = json_encode($deliverInfo);
  225. }
  226. return $winRecord->save();
  227. }
  228. /**
  229. * 标记中奖记录为发放失败
  230. */
  231. public static function markWinRecordAsFailed(LotteryWinRecord $winRecord, $reason = '')
  232. {
  233. $winRecord->deliver_status = LotteryEnum::DELIVER_STATUS_FAILED;
  234. $winRecord->fail_reason = $reason;
  235. return $winRecord->save();
  236. }
  237. /**
  238. * 设置中奖记录收货地址
  239. */
  240. public static function setWinRecordDeliveryAddress(LotteryWinRecord $winRecord, $name, $mobile, $address)
  241. {
  242. $winRecord->receiver_name = $name;
  243. $winRecord->receiver_mobile = $mobile;
  244. $winRecord->receiver_address = $address;
  245. return $winRecord->save();
  246. }
  247. /**
  248. * 设置中奖记录快递信息
  249. */
  250. public static function setWinRecordExpressInfo(LotteryWinRecord $winRecord, $company, $number)
  251. {
  252. $winRecord->express_company = $company;
  253. $winRecord->express_number = $number;
  254. return $winRecord->save();
  255. }
  256. /**
  257. * 设置中奖记录兑换码
  258. */
  259. public static function setWinRecordExchangeCode(LotteryWinRecord $winRecord, $code)
  260. {
  261. $winRecord->exchange_code = $code;
  262. return $winRecord->save();
  263. }
  264. /**
  265. * 标记中奖记录兑换码已使用
  266. */
  267. public static function markWinRecordCodeUsed(LotteryWinRecord $winRecord)
  268. {
  269. $winRecord->code_used_time = time();
  270. return $winRecord->save();
  271. }
  272. /**
  273. * 获取待发放的中奖记录
  274. */
  275. public static function getPendingWinRecords($limit = 100)
  276. {
  277. return LotteryWinRecord::where('deliver_status', LotteryEnum::DELIVER_STATUS_PENDING)
  278. ->order('createtime', 'asc')
  279. ->limit($limit)
  280. ->select();
  281. }
  282. /**
  283. * 获取用户中奖记录
  284. */
  285. public static function getUserWinRecords($userId, $page = 1, $limit = 20)
  286. {
  287. return LotteryWinRecord::where('user_id', $userId)
  288. ->with(['activity', 'prize'])
  289. ->order('createtime', 'desc')
  290. ->page($page, $limit)
  291. ->select();
  292. }
  293. /**
  294. * 获取活动中奖统计
  295. */
  296. public static function getActivityWinStats($activityId)
  297. {
  298. return [
  299. 'total_win' => LotteryWinRecord::where('activity_id', $activityId)->count(),
  300. 'delivered' => LotteryWinRecord::where('activity_id', $activityId)
  301. ->where('deliver_status', LotteryEnum::DELIVER_STATUS_SUCCESS)
  302. ->count(),
  303. 'pending' => LotteryWinRecord::where('activity_id', $activityId)
  304. ->where('deliver_status', LotteryEnum::DELIVER_STATUS_PENDING)
  305. ->count(),
  306. 'failed' => LotteryWinRecord::where('activity_id', $activityId)
  307. ->where('deliver_status', LotteryEnum::DELIVER_STATUS_FAILED)
  308. ->count()
  309. ];
  310. }
  311. /**
  312. * 批量处理待发放的中奖记录
  313. */
  314. public static function batchProcessPendingWinRecords($limit = 50)
  315. {
  316. $pendingRecords = static::getPendingWinRecords($limit);
  317. $processedCount = 0;
  318. foreach ($pendingRecords as $winRecord) {
  319. try {
  320. $prize = LotteryPrize::find($winRecord->prize_id);
  321. if ($prize && $prize->deliver_type == LotteryEnum::DELIVER_TYPE_AUTO) {
  322. static::autoDeliverPrize($winRecord, $prize);
  323. $processedCount++;
  324. }
  325. } catch (Exception $e) {
  326. static::markWinRecordAsFailed($winRecord, $e->getMessage());
  327. trace('自动发放奖品失败: ' . $e->getMessage(), 'error');
  328. }
  329. }
  330. return $processedCount;
  331. }
  332. /**
  333. * 手动发放奖品(管理员操作)
  334. */
  335. public static function manualDeliverPrize($winRecordId, $deliverInfo = [], $adminId = 0)
  336. {
  337. $winRecord = LotteryWinRecord::find($winRecordId);
  338. if (!$winRecord) {
  339. throw new Exception('中奖记录不存在');
  340. }
  341. if ($winRecord->deliver_status != LotteryEnum::DELIVER_STATUS_PENDING) {
  342. throw new Exception('该记录已处理,无法重复发放');
  343. }
  344. $deliverInfo['admin_id'] = $adminId;
  345. $deliverInfo['manual_deliver_time'] = time();
  346. return static::markWinRecordAsDelivered($winRecord, $deliverInfo);
  347. }
  348. /**
  349. * 取消中奖记录
  350. */
  351. public static function cancelWinRecord($winRecordId, $reason = '', $adminId = 0)
  352. {
  353. $winRecord = LotteryWinRecord::find($winRecordId);
  354. if (!$winRecord) {
  355. throw new Exception('中奖记录不存在');
  356. }
  357. $winRecord->deliver_status = LotteryEnum::DELIVER_STATUS_CANCELLED;
  358. $winRecord->fail_reason = $reason;
  359. $winRecord->cancel_time = time();
  360. $winRecord->cancel_admin_id = $adminId;
  361. return $winRecord->save();
  362. }
  363. /**
  364. * 自动发放奖品
  365. */
  366. public static function autoDeliverPrize($winRecord, $prize)
  367. {
  368. try {
  369. switch ($prize->type) {
  370. case LotteryEnum::PRIZE_TYPE_COUPON:
  371. static::deliverCoupon($winRecord, $prize);
  372. break;
  373. case LotteryEnum::PRIZE_TYPE_REDPACK:
  374. static::deliverRedPacket($winRecord, $prize);
  375. break;
  376. case LotteryEnum::PRIZE_TYPE_CODE:
  377. static::deliverExchangeCode($winRecord, $prize);
  378. break;
  379. case LotteryEnum::PRIZE_TYPE_SHOP_GOODS:
  380. static::deliverGoods($winRecord, $prize);
  381. break;
  382. }
  383. } catch (Exception $e) {
  384. static::markWinRecordAsFailed($winRecord, $e->getMessage());
  385. }
  386. }
  387. /**
  388. * 发放优惠券
  389. */
  390. private static function deliverCoupon($winRecord, $prize)
  391. {
  392. // 这里调用优惠券发放接口
  393. // 示例代码,需要根据实际优惠券系统实现
  394. static::markWinRecordAsDelivered($winRecord, ['coupon_id' => $prize->coupon_id]);
  395. }
  396. /**
  397. * 发放红包
  398. */
  399. private static function deliverRedPacket($winRecord, $prize)
  400. {
  401. // 这里调用红包发放接口
  402. // 示例代码,需要根据实际红包系统实现
  403. static::markWinRecordAsDelivered($winRecord, ['amount' => $prize->amount]);
  404. }
  405. /**
  406. * 发放兑换码
  407. */
  408. private static function deliverExchangeCode($winRecord, $prize)
  409. {
  410. $code = LotteryService::getAvailableExchangeCode($prize);
  411. if ($code) {
  412. static::setWinRecordExchangeCode($winRecord, $code);
  413. LotteryService::markExchangeCodeUsed($prize, $code);
  414. static::markWinRecordAsDelivered($winRecord, ['exchange_code' => $code]);
  415. } else {
  416. throw new Exception('兑换码已用完');
  417. }
  418. }
  419. /**
  420. * 发放商城商品
  421. */
  422. private static function deliverGoods($winRecord, $prize)
  423. {
  424. // 这里可以自动加入购物车或创建订单
  425. // 示例代码,需要根据实际商城系统实现
  426. static::markWinRecordAsDelivered($winRecord, [
  427. 'goods_id' => $prize->goods_id,
  428. 'goods_sku_id' => $prize->goods_sku_id
  429. ]);
  430. }
  431. }