Groupon.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. <?php
  2. namespace addons\shopro\library\activity\traits;
  3. use addons\shopro\facade\Redis;
  4. use addons\shopro\facade\ActivityRedis;
  5. use app\admin\model\shopro\activity\Activity;
  6. use app\admin\model\shopro\activity\Groupon as ActivityGroupon;
  7. use app\admin\model\shopro\activity\GrouponLog;
  8. use app\admin\model\shopro\order\Order;
  9. use app\admin\model\shopro\order\OrderItem;
  10. use app\admin\model\shopro\data\FakeUser;
  11. use addons\shopro\service\order\OrderRefund;
  12. use addons\shopro\service\order\OrderOper;
  13. /**
  14. * 拼团 (普通拼团,阶梯拼团,幸运拼团)
  15. */
  16. trait Groupon
  17. {
  18. /**
  19. * *、redis 没有存团完整信息,只存了团当前人数,团成员(当前人数,团成员均没有存虚拟用户)
  20. * *、redis userList 没有存这个人的购买状态
  21. * *、团 解散,成团,(因为直接修改了数据库,参团判断,先判断的数据库后判断的 redis)
  22. * *、虚拟成团时将虚拟人数存入 redis userList 中,因为团中有虚拟人时,redis 实际人数 和 团需要人数 都没有计算虚拟人,导致团可以超员
  23. */
  24. /**
  25. * 判断加入旧拼团
  26. */
  27. protected function checkAndGetJoinGroupon($buyInfo, $user, $groupon_id)
  28. {
  29. $goods = $buyInfo['goods'];
  30. $activity = $goods['activity'];
  31. // 获取团信息
  32. $activityGroupon = ActivityGroupon::where('id', $groupon_id)->find();
  33. if (!$activityGroupon) {
  34. error_stop('要参与的团不存在');
  35. }
  36. // 判断团所属活动是否正常
  37. if ($activityGroupon->activity_id != $activity['id']) { // 修复,后台手动将活动删除,然后又立即给这个商品创建新的拼团活动,导致参与新活动的旧团错乱问题
  38. error_stop('要参与的活动已结束');
  39. }
  40. if ($activityGroupon['status'] != 'ing') {
  41. error_stop('要参与的团已成团,请选择其它团或自己开团');
  42. }
  43. if ($activityGroupon['current_num'] >= $activityGroupon['num']) {
  44. error_stop('该团已满,请参与其它团或自己开团');
  45. }
  46. if (!has_redis()) {
  47. // 没有 redis 直接判断数据库团信息,因为 current_num 支付成功才会累加,故无法保证超员,
  48. $isJoin = GrouponLog::where('user_id', $user['id'])->where('groupon_id', $activityGroupon->id)->where('is_fictitious', 0)->count();
  49. if ($isJoin) {
  50. error_stop('您已参与该团,请不要重复参团');
  51. }
  52. // 该团可加入
  53. return $activityGroupon;
  54. }
  55. $keys = ActivityRedis::keysActivity([
  56. 'groupon_id' => $activityGroupon['id'],
  57. 'goods_id' => $activityGroupon['goods_id'],
  58. ], [
  59. 'activity_id' => $activity['id'],
  60. 'activity_type' => $activity['type'],
  61. ]);
  62. extract($keys);
  63. $current_num = Redis::HGET($keyActivity, $keyGrouponNum);
  64. if ($current_num >= $activityGroupon['num']) {
  65. error_stop('该团已满,请参与其它团或自己开团');
  66. }
  67. // 将用户加入拼团缓存,用来判断同一个人在一个团,多次下单,订单失效时删除缓存
  68. $userList = Redis::HGET($keyActivity, $keyGrouponUserlist);
  69. $userList = json_decode($userList, true);
  70. $userIds = array_column($userList, 'user_id');
  71. if (in_array($user['id'], $userIds)) {
  72. error_stop('您已参与该团,请不要重复参团');
  73. }
  74. return $activityGroupon;
  75. }
  76. /**
  77. * 增加拼团预成员人数
  78. */
  79. protected function grouponCacheForwardNum($activityGroupon, $activity, $user, $payed = 'nopay')
  80. {
  81. if (!has_redis()) {
  82. return true;
  83. }
  84. $keys = ActivityRedis::keysActivity([
  85. 'groupon_id' => $activityGroupon['id'],
  86. 'goods_id' => $activityGroupon['goods_id'],
  87. ], [
  88. 'activity_id' => $activity['id'],
  89. 'activity_type' => $activity['type'],
  90. ]);
  91. extract($keys);
  92. // 当前团人数 grouponNumKey 如果不存在,自动创建
  93. $current_num = Redis::HINCRBY($keyActivity, $keyGrouponNum, 1);
  94. if ($current_num > $activityGroupon['num']) {
  95. // 再把刚加上的减回来
  96. $current_num = Redis::HINCRBY($keyActivity, $keyGrouponNum, -1);
  97. error_stop('该团已满,请参与其它团或自己开团');
  98. }
  99. // 将用户加入拼团缓存,用来判断同一个人在一个团,多次下单,取消失效订单时删除缓存
  100. $userList = Redis::HGET($keyActivity, $keyGrouponUserlist);
  101. $userList = json_decode($userList, true);
  102. $userList = $userList ?: [];
  103. $userList[] = [
  104. 'user_id' => $user['id'],
  105. // 'status' => $payed // 太复杂,先不做
  106. ];
  107. Redis::HSET($keyActivity, $keyGrouponUserlist, json_encode($userList));
  108. }
  109. // 拼团团成员预成员退回
  110. protected function grouponCacheBackNum($order, $type)
  111. {
  112. if (!has_redis()) {
  113. return true;
  114. }
  115. // 查询拼团商品
  116. $item = OrderItem::where('order_id', $order['id'])->find(); // 拼团订单只有一个商品
  117. // 扩展字段
  118. $order_ext = $order['ext'];
  119. // 团 id
  120. $groupon_id = $order_ext['groupon_id'] ?? 0;
  121. if (!$groupon_id) {
  122. return true; // 商品独立购买,未参团,或者开新团
  123. }
  124. // 查询拼团,必须是拼团中才处理(已结束的(完成或者解散的没意义了)),redis 中没有存 团信息和状态
  125. $groupon = ActivityGroupon::ing()->lock(true)->find($groupon_id);
  126. if (!$groupon) {
  127. return true;
  128. }
  129. // if ($type == 'refund') { // 退款这里不删除拼团记录,当成正常团成员处理
  130. // // 退款,真实删除拼团记录,并减少参团人数
  131. // $this->delGrouponLog($order, $groupon);
  132. // }
  133. $keys = ActivityRedis::keysActivity([
  134. 'groupon_id' => $groupon_id,
  135. 'goods_id' => $item['goods_id'],
  136. 'goods_sku_price_id' => $item['goods_sku_price_id'],
  137. ], [
  138. 'activity_id' => $item['activity_id'],
  139. 'activity_type' => $item['activity_type'],
  140. ]);
  141. extract($keys);
  142. if (!Redis::EXISTS($keyActivity)) {
  143. // redis 不存在,可能活动已删除,不处理
  144. return true;
  145. }
  146. // 扣除预参团成员
  147. if (Redis::HEXISTS($keyActivity, $keyGrouponNum)) {
  148. $groupon_num = Redis::HINCRBY($keyActivity, $keyGrouponNum, -1);
  149. }
  150. $userList = Redis::HGET($keyActivity, $keyGrouponUserlist);
  151. $userList = json_decode($userList, true);
  152. $userList = $userList ?: [];
  153. foreach ($userList as $key => $user) {
  154. if ($user['user_id'] == $item['user_id']) {
  155. unset($userList[$key]);
  156. }
  157. }
  158. $userList = array_values($userList);
  159. Redis::HSET($keyActivity, $keyGrouponUserlist, json_encode($userList));
  160. }
  161. /**
  162. * 支付成功真实加入团
  163. */
  164. protected function joinGroupon($order, $user, \Closure $grouponCb = null)
  165. {
  166. $items = $order->items;
  167. $item = $items[0]; // 拼团只能单独购买
  168. // 扩展字段
  169. $order_ext = $order['ext'];
  170. // 团 id
  171. $groupon_id = $order_ext['groupon_id'] ?? 0;
  172. $buy_type = $order_ext['buy_type'] ?? 'groupon';
  173. // 单独购买,不加入团
  174. if ($buy_type == 'alone') {
  175. return true;
  176. }
  177. if ($groupon_id) {
  178. // 加入旧团,查询团
  179. $activityGroupon = ActivityGroupon::find($groupon_id);
  180. } else {
  181. // 加入新团,创建团
  182. $activityGroupon = $this->joinNewGroupon($order, $user, $item, $grouponCb);
  183. }
  184. // 添加参团记录
  185. $activityGrouponLog = $this->addGrouponLog($order, $user, $item, $activityGroupon);
  186. return $this->checkGrouponStatus($activityGroupon);
  187. }
  188. /**
  189. * 支付成功开启新拼团
  190. */
  191. protected function joinNewGroupon($order, $user, $item, \Closure $grouponCb = null)
  192. {
  193. // 获取活动
  194. $activity = Activity::where('id', $item['activity_id'])->find();
  195. $rules = $activity['rules'];
  196. // 小于 0 不限结束时间单位小时
  197. $expire_time = 0;
  198. if (isset($rules['valid_time']) && $rules['valid_time'] > 0) {
  199. // 转为 秒
  200. $expire_time = $rules['valid_time'] * 3600;
  201. }
  202. // 小于 0 不限结束时间单位小时
  203. $fictitious_time = 0;
  204. if (isset($rules['is_fictitious']) && $rules['is_fictitious'] && isset($rules['fictitious_time']) && $rules['fictitious_time'] > 0) {
  205. // 转为 秒
  206. $fictitious_time = $rules['fictitious_time'] * 3600;
  207. }
  208. if ($grouponCb) {
  209. // team_num
  210. extract($grouponCb($rules, $item['ext']));
  211. }
  212. // 开新团
  213. $activityGroupon = new ActivityGroupon();
  214. $activityGroupon->user_id = $user['id'];
  215. $activityGroupon->goods_id = $item['goods_id'];
  216. $activityGroupon->activity_id = $item['activity_id'];
  217. $activityGroupon->num = $team_num ?? 1; // 避免活动找不到
  218. $activityGroupon->current_num = 0; // 真实团成员等支付完成之后再增加
  219. $activityGroupon->status = 'ing';
  220. $activityGroupon->expire_time = $expire_time > 0 ? (time() + $expire_time) : 0;
  221. $activityGroupon->save();
  222. // 记录团 id
  223. //订单表冗余groupon_id
  224. $order->groupon_id = $activityGroupon->id;
  225. $order->ext = array_merge($order->ext, ['groupon_id' => $activityGroupon->id]);
  226. $order->save();
  227. // 将团信息存入缓存,增加缓存中当前团人数
  228. $this->grouponCacheForwardNum($activityGroupon, $activity, $user, 'payed');
  229. if ($expire_time > 0) {
  230. // 增加自动关闭拼团队列(如果有虚拟成团,会判断虚拟成团)
  231. \think\Queue::later($expire_time, '\addons\shopro\job\GrouponAutoOper@expire', [
  232. 'activity' => $activity,
  233. 'activity_groupon_id' => $activityGroupon->id
  234. ], 'shopro');
  235. }
  236. if ($fictitious_time > 0) {
  237. // 自动虚拟成团时间(提前自动虚拟成团,让虚拟成团更加真实一点,避免在团结束那一刻突然成团了)应小于自动过期时间
  238. \think\Queue::later($fictitious_time, '\addons\shopro\job\GrouponAutoOper@fictitious', [
  239. 'activity' => $activity,
  240. 'activity_groupon_id' => $activityGroupon->id
  241. ], 'shopro');
  242. }
  243. return $activityGroupon;
  244. }
  245. /**
  246. * 增加团成员记录
  247. */
  248. protected function addGrouponLog($order, $user, $item, $activityGroupon)
  249. {
  250. if (!$activityGroupon) {
  251. \think\Log::error('groupon-notfund: order_id: ' . $order['id']);
  252. return null;
  253. }
  254. // 增加团成员数量
  255. $activityGroupon->setInc('current_num', 1);
  256. // 增加参团记录
  257. $activityGrouponLog = new GrouponLog();
  258. $activityGrouponLog->user_id = $user['id'];
  259. $activityGrouponLog->nickname = $user['nickname'];
  260. $activityGrouponLog->avatar = $user['avatar'];
  261. $activityGrouponLog->groupon_id = $activityGroupon['id'] ?? 0;
  262. $activityGrouponLog->goods_id = $item['goods_id'];
  263. $activityGrouponLog->goods_sku_price_id = $item['goods_sku_price_id'];
  264. $activityGrouponLog->activity_id = $item['activity_id'];
  265. $activityGrouponLog->is_leader = ($activityGroupon['user_id'] == $user['id']) ? 1 : 0;
  266. $activityGrouponLog->is_fictitious = 0;
  267. $activityGrouponLog->order_id = $order['id'];
  268. $activityGrouponLog->save();
  269. return $activityGrouponLog;
  270. }
  271. /**
  272. * 【此方法即将废除,加入团之后,不删除参团记录】,删除团成员记录(退款:已经真实加入团了,这里扣除)()
  273. */
  274. protected function delGrouponLog($order, $groupon)
  275. {
  276. $activityGrouponLog = GrouponLog::where('user_id', $order->user_id)
  277. ->where('groupon_id', $groupon->id)
  278. ->where('order_id', $order->id)
  279. ->find();
  280. if ($activityGrouponLog) {
  281. $activityGrouponLog->delete();
  282. // 扣除参团人数
  283. $groupon->setDec('current_num', 1);
  284. }
  285. }
  286. /**
  287. * 订单退款时标记拼团记录为已退款(主动退款和拼团失败退款)
  288. *
  289. * @param \think\Model $order
  290. * @return void
  291. */
  292. protected function refundGrouponLog($order)
  293. {
  294. $order_ext = $order['ext'];
  295. $groupon_id = $order_ext['groupon_id'] ?? 0;
  296. if (!$groupon_id) {
  297. return true; // 商品独立购买,未参团,或者开新团
  298. }
  299. $activityGrouponLog = GrouponLog::where('user_id', $order->user_id)
  300. ->where('groupon_id', $groupon_id)
  301. ->where('order_id', $order->id)
  302. ->find();
  303. if ($activityGrouponLog) {
  304. // 修改 logs 为已退款
  305. $activityGrouponLog->is_refund = 1;
  306. $activityGrouponLog->save();
  307. }
  308. }
  309. // 虚拟成团,增加虚拟成员,并判断是否完成,然后将团状态改为,虚拟成团成功
  310. protected function finishFictitiousGroupon($activity, $activityGroupon, $invalid = true, $num = 0, $users = [])
  311. {
  312. // 拼团剩余人数
  313. $surplus_num = $activityGroupon['num'] - $activityGroupon['current_num'];
  314. // 团已经满员
  315. if ($surplus_num <= 0) {
  316. if ($activityGroupon['status'] == 'ing') {
  317. // 已满员但还是进行中状态,检测并完成团,起到纠正作用
  318. return $this->checkGrouponStatus($activityGroupon);
  319. }
  320. return true;
  321. }
  322. // 本次虚拟人数, 如果传入 num 则使用 num 和 surplus_num 中最小值, 如果没有传入,默认剩余人数全部虚拟
  323. $fictitious_num = $num ? ($num > $surplus_num ? $surplus_num : $num) : $surplus_num;
  324. $fakeUsers = FakeUser::orderRaw('rand()')->limit($fictitious_num)->select();
  325. if (count($fakeUsers) < $fictitious_num && $num == 0) {
  326. if ($invalid) {
  327. // 虚拟用户不足,并且是自动虚拟成团进程,自动解散团
  328. return $this->invalidRefundGroupon($activityGroupon);
  329. }
  330. return false;
  331. }
  332. // 增加团人数
  333. $activityGroupon->setInc('current_num', $fictitious_num);
  334. if (has_redis()) {
  335. // redis 参数
  336. $keys = ActivityRedis::keysActivity([
  337. 'groupon_id' => $activityGroupon['id'],
  338. 'goods_id' => $activityGroupon['goods_id'],
  339. ], [
  340. 'activity_id' => $activity['id'],
  341. 'activity_type' => $activity['type'],
  342. ]);
  343. extract($keys);
  344. Redis::HINCRBY($keyActivity, $keyGrouponNum, $fictitious_num); // 增加 redis 参团人数
  345. // 将用户加入拼团缓存,用来判断同一个人在一个团,多次下单,取消失效订单时删除缓存
  346. $userList = Redis::HGET($keyActivity, $keyGrouponUserlist);
  347. $userList = json_decode($userList, true);
  348. $userList = $userList ?: [];
  349. for ($i =0; $i < $fictitious_num; $i++) {
  350. $userList[] = [
  351. 'user_id' => 'fictitiou_' . time() . mt_rand(1000, 9999),
  352. ];
  353. }
  354. Redis::HSET($keyActivity, $keyGrouponUserlist, json_encode($userList));
  355. }
  356. for ($i = 0; $i < $fictitious_num; $i++) {
  357. // 先用传过来的
  358. $avatar = isset($users[$i]['avatar']) ? $users[$i]['avatar'] : '';
  359. $nickname = isset($users[$i]['nickname']) ? $users[$i]['nickname'] : '';
  360. // 如果没有,用查的虚拟的
  361. $avatar = $avatar ?: $fakeUsers[$i]['avatar'];
  362. $nickname = $nickname ?: $fakeUsers[$i]['nickname'];
  363. // 增加参团记录
  364. $activityGrouponLog = new GrouponLog();
  365. $activityGrouponLog->user_id = 0;
  366. $activityGrouponLog->nickname = $nickname;
  367. $activityGrouponLog->avatar = $avatar;
  368. $activityGrouponLog->groupon_id = $activityGroupon['id'] ?? 0;
  369. $activityGrouponLog->goods_id = $activityGroupon['goods_id'];
  370. $activityGrouponLog->goods_sku_price_id = 0; // 没有订单,所以也就没有 goods_sku_price_id
  371. $activityGrouponLog->activity_id = $activityGroupon['activity_id'];
  372. $activityGrouponLog->is_leader = 0; // 不是团长
  373. $activityGrouponLog->is_fictitious = 1; // 虚拟用户
  374. $activityGrouponLog->order_id = 0; // 虚拟成员没有订单
  375. $activityGrouponLog->save();
  376. }
  377. return $this->checkGrouponStatus($activityGroupon);
  378. }
  379. /**
  380. * 团过期退款,或者后台手动解散退款
  381. */
  382. protected function invalidRefundGroupon($activityGroupon, $user = null)
  383. {
  384. $activityGroupon->status = 'invalid'; // 拼团失败
  385. $activityGroupon->save();
  386. // 查询参团真人
  387. $logs = GrouponLog::with(['order'])->where('groupon_id', $activityGroupon['id'])->where('is_fictitious', 0)->select();
  388. foreach ($logs as $key => $log) {
  389. $order = $log->order;
  390. if ($order && in_array($order->status, [Order::STATUS_PAID, Order::STATUS_COMPLETED])) {
  391. $refundNum = OrderItem::where('order_id', $order->id)->where('refund_status', '<>', OrderItem::REFUND_STATUS_NOREFUND)->count();
  392. if (!$refundNum) {
  393. // 无条件全额退款
  394. $refund = new OrderRefund($order);
  395. $refund->fullRefund($user, [
  396. 'remark' => '拼团失败退款'
  397. ]);
  398. }
  399. } else if ($order && $order->isOffline($order)) {
  400. $orderOper = new OrderOper();
  401. $orderOper->cancel($order, null, 'system', '拼团失败,系统自动取消订单');
  402. }
  403. }
  404. // 触发拼团失败行为
  405. $data = ['groupon' => $activityGroupon];
  406. \think\Hook::listen('activity_groupon_fail', $data);
  407. return true;
  408. }
  409. /**
  410. * 检查团状态
  411. */
  412. protected function checkGrouponStatus($activityGroupon)
  413. {
  414. if (!$activityGroupon) {
  415. return true;
  416. }
  417. // 重新获取团信息
  418. $activityGroupon = ActivityGroupon::where('id', $activityGroupon['id'])->find();
  419. if ($activityGroupon['current_num'] >= $activityGroupon['num'] && !in_array($activityGroupon['status'], ['finish', 'finish_fictitious'])) {
  420. // 查询是否有虚拟团成员
  421. $fictitiousCount = GrouponLog::where('groupon_id', $activityGroupon['id'])->where('is_fictitious', 1)->count();
  422. // 将团设置为已完成
  423. $activityGroupon->status = $fictitiousCount ? 'finish_fictitious' : 'finish';
  424. $activityGroupon->finish_time = time();
  425. $activityGroupon->save();
  426. // 触发成团行为
  427. $data = ['groupon' => $activityGroupon];
  428. \think\Hook::listen('activity_groupon_finish', $data);
  429. }
  430. return true;
  431. }
  432. }