Groupon.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. <?php
  2. namespace addons\shopro\library\traits;
  3. use addons\shopro\exception\Exception;
  4. use addons\shopro\model\Activity;
  5. use addons\shopro\model\ActivityGroupon;
  6. use addons\shopro\model\ActivityGrouponLog;
  7. use addons\shopro\model\Order;
  8. use addons\shopro\model\OrderItem;
  9. use addons\shopro\model\UserFake;
  10. /**
  11. * 拼团
  12. */
  13. trait Groupon
  14. {
  15. /**
  16. * *、redis 没有存团完整信息,只存了团当前人数,团成员(当前人数,团成员均没有存虚拟用户)
  17. * *、redis userList 没有存这个人的购买状态
  18. * *、团 解散,成团,虚拟成团,没有修改 redis 团信息(因为直接修改了数据库,参团判断,先判断的数据库后判断的 redis)
  19. */
  20. /**
  21. * 增加拼团预成员人数
  22. */
  23. protected function grouponCacheForwardNum($activityGroupon, $activity, $user, $payed = 'nopay')
  24. {
  25. if (!$this->hasRedis()) {
  26. return true;
  27. }
  28. $keys = $this->getKeys([
  29. 'groupon_id' => $activityGroupon['id'],
  30. 'goods_id' => $activityGroupon['goods_id'],
  31. ], [
  32. 'activity_id' => $activity['id'],
  33. 'activity_type' => $activity['type'],
  34. ]);
  35. extract($keys);
  36. $redis = $this->getRedis();
  37. // 将拼团团信息存入 redis 没有用,还得维护团状态,先不存
  38. // $redis->HSET($activityHashKey, $grouponKey, json_encode($activityGroupon));
  39. // 当前团人数 grouponNumKey 如果不存在,自动创建
  40. $current_num = $redis->HINCRBY($activityHashKey, $grouponNumKey, 1);
  41. if ($current_num > $activityGroupon['num']) {
  42. // 再把刚加上的减回来
  43. $current_num = $redis->HINCRBY($activityHashKey, $grouponNumKey, -1);
  44. new Exception('该团已满,请参与其它团或自己开团');
  45. }
  46. // 将用户加入拼团缓存,用来判断同一个人在一个团,多次下单,取消订单删除
  47. $userList = $redis->HGET($activityHashKey, $grouponUserlistKey);
  48. $userList = json_decode($userList, true);
  49. $userList = $userList ? : [];
  50. $userList[] = [
  51. 'user_id' => $user['id'],
  52. // 'status' => $payed // 太复杂,先不做
  53. ];
  54. $redis->HSET($activityHashKey, $grouponUserlistKey, json_encode($userList));
  55. }
  56. // 拼团团成员预成员退回
  57. protected function grouponCacheBackNum($order)
  58. {
  59. $items = OrderItem::where('order_id', $order['id'])->select();
  60. foreach ($items as $key => $item) {
  61. // 不是拼团,或者 没有配置 redis
  62. if (strpos($item['activity_type'], 'groupon') === false || !$this->hasRedis()) {
  63. continue;
  64. }
  65. // 扩展字段
  66. $order_ext = $order['ext_arr'];
  67. // 团 id
  68. $groupon_id = $order_ext['groupon_id'] ?? 0;
  69. if (!$groupon_id) {
  70. continue; // 商品独立购买,未参团,或者开新团
  71. }
  72. // 实例化 redis
  73. $redis = $this->getRedis();
  74. $keys = $this->getKeys([
  75. 'groupon_id' => $groupon_id,
  76. 'goods_id' => $item['goods_id'],
  77. 'goods_sku_price_id' => $item['goods_sku_price_id'],
  78. ], [
  79. 'activity_id' => $item['activity_id'],
  80. 'activity_type' => $item['activity_type'],
  81. ]);
  82. extract($keys);
  83. // 扣除预参团成员
  84. if ($redis->EXISTS($activityHashKey) && $redis->HEXISTS($activityHashKey, $grouponNumKey)) {
  85. $sale = $redis->HINCRBY($activityHashKey, $grouponNumKey, -1);
  86. }
  87. $userList = $redis->HGET($activityHashKey, $grouponUserlistKey);
  88. $userList = json_decode($userList, true);
  89. $userList = $userList ?: [];
  90. foreach($userList as $key => $user) {
  91. if ($user['user_id'] == $item['user_id']) {
  92. unset($userList[$key]);
  93. }
  94. }
  95. $redis->HSET($activityHashKey, $grouponUserlistKey, json_encode($userList));
  96. }
  97. }
  98. /**
  99. * 判断加入旧拼团
  100. */
  101. protected function checkJoinGroupon($goods_info, $user, $groupon_id)
  102. {
  103. $goods = $goods_info['detail'];
  104. $activity = $goods['activity'];
  105. $rules = $activity['rules'];
  106. // 获取团信息
  107. $activityGroupon = ActivityGroupon::with('activity')->where('id', $groupon_id)->find();
  108. if (!$activityGroupon) {
  109. new Exception('要参与的团不存在');
  110. }
  111. // 判断团所属活动是否正常
  112. if (!$activityGroupon->activity || $activityGroupon->activity['id'] != $activity['id']) { // 修复,后台手动将活动删除,然后又立即给这个商品创建新的拼团活动,导致参与新活动的旧团错乱问题
  113. new Exception('要参与的活动已结束');
  114. }
  115. if ($activityGroupon['status'] != 'ing') {
  116. new Exception('要参与的团已成团,请选择其它团或自己开团');
  117. }
  118. if ($activityGroupon['current_num'] >= $activityGroupon['num']) {
  119. new Exception('该团已满,请参与其它团或自己开团');
  120. }
  121. if (!$this->hasRedis()) {
  122. // 没有 redis 直接判断数据库团信息,因为 current_num 支付成功才会累加,故无法保证超员,
  123. // 该团可加入
  124. return $activityGroupon;
  125. }
  126. $keys = $this->getKeys([
  127. 'groupon_id' => $activityGroupon['id'],
  128. 'goods_id' => $activityGroupon['goods_id'],
  129. ], [
  130. 'activity_id' => $activity['id'],
  131. 'activity_type' => $activity['type'],
  132. ]);
  133. extract($keys);
  134. $redis = $this->getRedis();
  135. $current_num = $redis->HGET($activityHashKey, $grouponNumKey);
  136. if ($current_num >= $activityGroupon['num']) {
  137. new Exception('该团已满,请参与其它团或自己开团');
  138. }
  139. // 将用户加入拼团缓存,用来判断同一个人在一个团,多次下单,订单失效删除
  140. $userList = $redis->HGET($activityHashKey, $grouponUserlistKey);
  141. $userList = json_decode($userList, true);
  142. $userIds = array_column($userList, 'user_id');
  143. if (in_array($user['id'], $userIds)) {
  144. new Exception('您已参与该团,请不要重复参团');
  145. }
  146. return $activityGroupon;
  147. }
  148. /**
  149. * 支付成功真实加入团
  150. */
  151. protected function joinGroupon($order, $user) {
  152. $item = $order->item;
  153. $goods_item = $item[0]; // 拼团只能单独购买
  154. // 扩展字段
  155. $order_ext = $order['ext_arr'];
  156. // 团 id
  157. $groupon_id = $order_ext['groupon_id'] ?? 0;
  158. $buy_type = $order_ext['buy_type'] ?? 0;
  159. // 不是拼团购买,比如拼团单独购买
  160. if ($buy_type != 'groupon') {
  161. return true;
  162. }
  163. if ($groupon_id) {
  164. // 加入旧团,查询团
  165. $activityGroupon = ActivityGroupon::where('id', $groupon_id)->find();
  166. } else {
  167. // 加入新团,创建团
  168. $activityGroupon = $this->joinNewGroupon($order, $user);
  169. }
  170. // 添加参团记录
  171. $activityGrouponLog = $this->addGrouponLog($order, $user, $activityGroupon);
  172. return $this->checkGrouponStatus($activityGroupon);
  173. }
  174. /**
  175. * 支付成功开启新拼团
  176. */
  177. protected function joinNewGroupon($order, $user)
  178. {
  179. $item = $order->item;
  180. $goodsItem = $item[0]; // 拼团只能单独购买
  181. // 获取活动
  182. $activity = Activity::where('id', $goodsItem['activity_id'])->find();
  183. $rules = $activity['rules'];
  184. // 小于 0 不限结束时间单位小时
  185. $expiretime = 0;
  186. if (isset($rules['valid_time']) && $rules['valid_time'] > 0) {
  187. // 转为 秒
  188. $expiretime = $rules['valid_time'] * 3600;
  189. }
  190. // 开团
  191. $activityGroupon = new ActivityGroupon();
  192. $activityGroupon->user_id = $user['id'];
  193. $activityGroupon->goods_id = $goodsItem['goods_id'];
  194. $activityGroupon->activity_id = $goodsItem['activity_id'];
  195. $activityGroupon->num = $rules['team_num'] ?? 1; // 避免活动找不到
  196. $activityGroupon->current_num = 0; // 真实团成员等支付完成之后再增加
  197. $activityGroupon->status = 'ing';
  198. $activityGroupon->expiretime = $expiretime > 0 ? (time() + $expiretime) : 0;
  199. $activityGroupon->save();
  200. // 记录团 id
  201. $order->ext = json_encode($order->setExt($order, ['groupon_id' => $activityGroupon->id])); // 团 id
  202. $order->save();
  203. // 将团信息存入缓存,增加缓存中当前团人数
  204. $this->grouponCacheForwardNum($activityGroupon, $activity, $user, 'payed');
  205. if ($expiretime > 0) {
  206. // 增加自动关闭拼团队列(如果有虚拟成团,会判断虚拟成团)
  207. \think\Queue::later($expiretime, '\addons\shopro\job\ActivityGrouponAutoOper@expire', [
  208. 'activity' => $activity,
  209. 'activity_groupon_id' => $activityGroupon->id
  210. ], 'shopro');
  211. }
  212. return $activityGroupon;
  213. }
  214. /**
  215. * 增加团成员记录
  216. */
  217. protected function addGrouponLog($order, $user, $activityGroupon) {
  218. if (!$activityGroupon) {
  219. \think\Log::write('groupon-notfund: order_id: ' . $order['id']);
  220. return null;
  221. }
  222. $item = $order->item;
  223. $goodsItem = $item[0]; // 拼团只能单独购买
  224. // 增加团成员数量
  225. $activityGroupon->setInc('current_num', 1);
  226. // 增加参团记录
  227. $activityGrouponLog = new ActivityGrouponLog();
  228. $activityGrouponLog->user_id = $user['id'];
  229. $activityGrouponLog->user_nickname = $user['nickname'];
  230. $activityGrouponLog->user_avatar = $user['avatar'];
  231. $activityGrouponLog->groupon_id = $activityGroupon['id'] ?? 0;
  232. $activityGrouponLog->goods_id = $goodsItem['goods_id'];
  233. $activityGrouponLog->goods_sku_price_id = $goodsItem['goods_sku_price_id'];
  234. $activityGrouponLog->activity_id = $goodsItem['activity_id'];
  235. $activityGrouponLog->is_leader = ($activityGroupon['user_id'] == $user['id']) ? 1 : 0;
  236. $activityGrouponLog->is_fictitious = 0;
  237. $activityGrouponLog->order_id = $order['id'];
  238. $activityGrouponLog->save();
  239. return $activityGrouponLog;
  240. }
  241. // 虚拟成团,增加虚拟成员,并判断是否完成,然后将团状态改为,虚拟成团成功
  242. protected function finishFictitiousGroupon($activityGroupon, $num = 0, $users = []) {
  243. // 拼团剩余人数
  244. $surplus_num = $activityGroupon['num'] - $activityGroupon['current_num'];
  245. // 团已经满员
  246. if ($surplus_num <= 0) {
  247. if ($activityGroupon['status'] == 'ing') {
  248. // 已满员但还是进行中状态,检测并完成团,起到纠正作用
  249. return $this->checkGrouponStatus($activityGroupon);
  250. }
  251. return true;
  252. }
  253. // 本次虚拟人数, 如果传入 num 则使用 num 和 surplus_num 中最小值, 如果没有传入,默认剩余人数全部虚拟
  254. $fictitious_num = $num ? ($num > $surplus_num ? $surplus_num : $num) : $surplus_num;
  255. // 查询虚拟用户
  256. $userFakes = UserFake::orderRaw('rand()')->limit($fictitious_num)->select();
  257. if (count($userFakes) < $fictitious_num && $num == 0) {
  258. // 虚拟用户不足,并且是自动虚拟成团进程,自动解散团
  259. return $this->invalidRefundGroupon($activityGroupon);
  260. }
  261. // 增加团人数
  262. $activityGroupon->setInc('current_num', $fictitious_num);
  263. for ($i = 0; $i < $fictitious_num; $i ++) {
  264. // 先用传过来的
  265. $avatar = isset($users[$i]['avatar']) ? $users[$i]['avatar'] : '';
  266. $nickname = isset($users[$i]['nickname']) ? $users[$i]['nickname'] : '';
  267. // 如果没有,用查的虚拟的
  268. $avatar = $avatar ? : $userFakes[$i]['avatar'];
  269. $nickname = $nickname ? : $userFakes[$i]['nickname'];
  270. // 增加参团记录
  271. $activityGrouponLog = new ActivityGrouponLog();
  272. $activityGrouponLog->user_id = 0;
  273. $activityGrouponLog->user_nickname = $nickname;
  274. $activityGrouponLog->user_avatar = $avatar;
  275. $activityGrouponLog->groupon_id = $activityGroupon['id'] ?? 0;
  276. $activityGrouponLog->goods_id = $activityGroupon['goods_id'];
  277. $activityGrouponLog->goods_sku_price_id = 0; // 没有订单,所以也就没有 goods_sku_price_id
  278. $activityGrouponLog->activity_id = $activityGroupon['activity_id'];
  279. $activityGrouponLog->is_leader = 0; // 不是团长
  280. $activityGrouponLog->is_fictitious = 1; // 虚拟用户
  281. $activityGrouponLog->order_id = 0; // 虚拟成员没有订单
  282. $activityGrouponLog->save();
  283. }
  284. return $this->checkGrouponStatus($activityGroupon);
  285. }
  286. /**
  287. * 团过期退款,或者后台手动解散退款
  288. */
  289. protected function invalidRefundGroupon($activityGroupon, $user = null) {
  290. $activityGroupon->status = 'invalid'; // 拼团失败
  291. $activityGroupon->save();
  292. // 查询参团真人
  293. $logs = ActivityGrouponLog::with('order')->where('groupon_id', $activityGroupon['id'])->where('is_fictitious', 0)->select();
  294. foreach ($logs as $key => $log) {
  295. $order = $log['order'];
  296. if ($order && $order['status'] > 0) {
  297. // 退款,只能有一个 item
  298. $item = $order['item'][0];
  299. if ($item && in_array($item['refund_status'], [OrderItem::REFUND_STATUS_NOREFUND, OrderItem::REFUND_STATUS_ING])) {
  300. // 未申请退款,或者退款中,直接全额退款
  301. Order::startRefund($order, $order['item'][0], $order['pay_fee'], $user, '拼团失败退款');
  302. }
  303. }
  304. // 修改 logs 为已退款
  305. $log->is_refund = 1;
  306. $log->save();
  307. }
  308. // 触发拼团失败行为
  309. $data = ['groupon' => $activityGroupon];
  310. \think\Hook::listen('activity_groupon_fail', $data);
  311. return true;
  312. }
  313. /**
  314. * 检查团状态
  315. */
  316. protected function checkGrouponStatus($activityGroupon) {
  317. if (!$activityGroupon) {
  318. return true;
  319. }
  320. // 重新获取团信息
  321. $activityGroupon = ActivityGroupon::where('id', $activityGroupon['id'])->find();
  322. if ($activityGroupon['current_num'] >= $activityGroupon['num'] && !in_array($activityGroupon['status'], ['finish', 'finish-fictitious'])) {
  323. // 查询是否有虚拟团成员
  324. $fictitiousCount = ActivityGrouponLog::where('groupon_id', $activityGroupon['id'])->where('is_fictitious', 1)->count();
  325. // 将团设置为已完成
  326. $activityGroupon->status = $fictitiousCount ? 'finish-fictitious' : 'finish';
  327. $activityGroupon->finishtime = time();
  328. $activityGroupon->save();
  329. // 触发成团行为
  330. $data = ['groupon' => $activityGroupon];
  331. \think\Hook::listen('activity_groupon_finish', $data);
  332. }
  333. return true;
  334. }
  335. }