GrouponLadder.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. <?php
  2. namespace addons\shopro\library\activity\provider;
  3. use addons\shopro\library\activity\traits\Groupon as GrouponTrait;
  4. use addons\shopro\service\StockSale;
  5. use addons\shopro\exception\ShoproException;
  6. /**
  7. * 阶梯拼团
  8. */
  9. class GrouponLadder extends Base
  10. {
  11. use GrouponTrait;
  12. protected $rules = [
  13. "is_commission" => "require|boolean",
  14. "is_free_shipping" => "require|boolean",
  15. "sales_show_type" => "require",
  16. "ladders" => "require|array",
  17. "is_alone" => "require|boolean",
  18. "is_fictitious" => "require|boolean",
  19. "fictitious_time" => "requireIf:is_fictitious,1|float|egt:0",
  20. "is_team_card" => "require|boolean",
  21. "is_leader_discount" => "require|boolean",
  22. "valid_time" => "require|float|egt:0",
  23. "limit_num" => "number|egt:0",
  24. "refund_type" => "require", // 退款方式 back=原路退回|money=退回到余额
  25. "order_auto_close" => "float|egt:0",
  26. ];
  27. protected $message = [
  28. 'ladders.require' => '请填写拼团阶梯',
  29. 'ladders.array' => '请填写拼团阶梯',
  30. ];
  31. protected $default = [
  32. "is_commission" => 0, // 是否参与分销
  33. "is_free_shipping" => 0, // 是否包邮
  34. "sales_show_type" => "real", // real=真实活动销量|goods=商品总销量(包含虚拟销量)
  35. "ladders" => [], // {ladder_one:2,ladder_two:2,ladder_three:3}
  36. "is_alone" => 0, // 是否允许单独购买
  37. "is_fictitious" => 0, // 是否允许虚拟成团
  38. "fictitious_num" => 0, // 最多虚拟人数 0:不允许虚拟 '' 不限制
  39. "fictitious_time" => 0, // 开团多长时间自动虚拟成团
  40. "is_team_card" => 0, // 参团卡显示
  41. "is_leader_discount" => 0, // 团长优惠
  42. "valid_time" => 0, // 组团有效时间, 0:一直有效
  43. "limit_num" => 0, // 每人限购数量 0:不限购
  44. "refund_type" => "back", // 退款方式 back=原路退回|money=退回到余额
  45. "order_auto_close" => 0, // 订单自动关闭时间,如果为 0 将使用系统级订单自动关闭时间
  46. ];
  47. public function check($params, $activity_id = 0)
  48. {
  49. // 数据验证
  50. $params = parent::check($params);
  51. // 验证添加的活动商品是否至少设置了一个活动规格
  52. $this->checkActivitySkuPrice($params['goods_list']);
  53. // 检测活动之间是否存在冲突
  54. $this->checkActivityConflict($params, $params['goods_list'], $activity_id);
  55. return $params;
  56. }
  57. public function save($activity, $params = [])
  58. {
  59. $goodsList = $params['goods_list'];
  60. $this->saveSkuPrice($goodsList, $activity, function ($skuPrice) use ($activity) {
  61. // 处理 阶梯价格,团长优惠
  62. $rules = $activity->rules;
  63. $is_leader_discount = $rules['is_leader_discount'] ?? 0;
  64. $ladders = $rules['ladders'] ?? 0;
  65. $ext = [
  66. 'is_leader_discount' => $is_leader_discount,
  67. 'ladders' => []
  68. ];
  69. foreach ($ladders as $ladder_level => $ladder) {
  70. $ladder_price = isset($skuPrice[$ladder_level]) ? number_format(floatval($skuPrice[$ladder_level]), 2, '.', '') : 0;
  71. $leader_ladder_price = (isset($skuPrice[$ladder_level . '_leader']) && $skuPrice[$ladder_level . '_leader'] > 0) ? number_format(floatval($skuPrice[$ladder_level . '_leader']), 2, '.', '') : $ladder_price; // 默认当前阶梯参团价
  72. $current = [
  73. 'ladder_level' => $ladder_level,
  74. 'ladder' => $ladder,
  75. 'ladder_price' => $ladder_price,
  76. 'leader_ladder_price' => $leader_ladder_price
  77. ];
  78. unset($skuPrice[$ladder_level], $skuPrice[$ladder_level . '_leader']);
  79. $ext['ladders'][] = $current;
  80. }
  81. $skuPrice['ext'] = $ext;
  82. return $skuPrice;
  83. });
  84. }
  85. public function showSkuPrice($skuPrice)
  86. {
  87. $ext = $skuPrice['ext'] ?? [];
  88. $ladders = $ext['ladders'] ?? [];
  89. if ($ladders) {
  90. foreach ($ladders as $ladder) {
  91. $ladder_level = $ladder['ladder_level'];
  92. $skuPrice[$ladder_level] = $ladder['ladder_price'];
  93. $skuPrice[$ladder_level . '_leader'] = $ladder['leader_ladder_price'];
  94. }
  95. } else {
  96. // 全部初始化为 0
  97. $skuPrice['ladder_one'] = 0;
  98. $skuPrice['ladder_two'] = 0;
  99. $skuPrice['ladder_three'] = 0;
  100. $skuPrice['ladder_one_leader'] = 0;
  101. $skuPrice['ladder_two_leader'] = 0;
  102. $skuPrice['ladder_three_leader'] = 0;
  103. }
  104. return $skuPrice;
  105. }
  106. public function recoverSkuPrices($goods, $activity)
  107. {
  108. $groupon_num = request()->param('groupon_num', 0); // 是否传了开团人数(这里不再使用阶梯,前端没反)
  109. $activitySkuPrices = $activity['activity_sku_prices'];
  110. $skuPrices = $goods->sku_prices;
  111. foreach ($skuPrices as $key => &$skuPrice) {
  112. $stock = $skuPrice->stock; // 下面要用
  113. $skuPrice->stock = 0;
  114. $skuPrice->sales = 0;
  115. foreach ($activitySkuPrices as $activitySkuPrice) {
  116. if ($skuPrice['id'] == $activitySkuPrice['goods_sku_price_id']) {
  117. // 采用活动的 规格内容
  118. $is_leader_discount = $activitySkuPrice['ext']['is_leader_discount'];
  119. $ladders = $activitySkuPrice['ext']['ladders'];
  120. $skuPrice->old_price = $skuPrice->price; // 保存原始普通商品规格的价格(计算活动的优惠)
  121. $skuPrice->stock = ($activitySkuPrice['stock'] > $stock) ? $stock : $activitySkuPrice['stock']; // 活动库存不能超过商品库存
  122. $skuPrice->sales = $activitySkuPrice['sales'];
  123. $skuPrice->is_leader_discount = $is_leader_discount; // 是否团长优惠
  124. $skuPrice->ladders = $ladders; // 阶梯价格,包含团长优惠
  125. $skuPrice->status = $activitySkuPrice['status']; // 采用活动的上下架
  126. $skuPrice->ext = $activitySkuPrice['ext']; // 活动规格 ext, order_item 保存备用
  127. $skuPrice->min_price = min(array_column($ladders, 'ladder_price')); // 当前活动规格最小价格,这里是阶梯最低拼团价(不要团长价)
  128. $skuPrice->max_price = max(array_column($ladders, 'ladder_price')); // 当前活动规格最大价格,这里是阶梯最低拼团价(不要团长价)
  129. $ladders = array_column($ladders, null, 'ladder');
  130. $currentLadder = $ladders[$groupon_num] ?? current($ladders);
  131. $skuPrice->ladder_price = $currentLadder['ladder_price']; // 当前阶梯价格(默认是 ladder_one)
  132. $skuPrice->leader_ladder_price = $currentLadder['leader_ladder_price']; // 当前阶梯团长价(默认是 ladder_one)
  133. $skuPrice->price = $is_leader_discount ? $skuPrice->leader_ladder_price : $skuPrice->ladder_price; // 默认是计算好的价格,团长价或者普通价
  134. // 记录相关活动类型
  135. $skuPrice->activity_type = $activity['type'];
  136. $skuPrice->activity_id = $activity['id'];
  137. // 下单的时候需要存活动 的 sku_price_id)
  138. $skuPrice->item_goods_sku_price = $activitySkuPrice;
  139. break;
  140. }
  141. }
  142. }
  143. return $skuPrices;
  144. }
  145. /**
  146. * 这里要使用 shoproException 抛出异常
  147. *
  148. * @param array $buyInfo
  149. * @param array $activity
  150. * @return array
  151. */
  152. public function buyCheck($buyInfo, $activity)
  153. {
  154. $buy_type = request()->param('buy_type', 'groupon');
  155. $groupon_id = request()->param('groupon_id', 0);
  156. $groupon_num = request()->param('groupon_num', 0);
  157. // 拼团
  158. $rules = $activity['rules'];
  159. $is_alone = $rules['is_alone'] ?? 1;
  160. $currentSkuPrice = $buyInfo['current_sku_price'];
  161. $is_leader_discount = $currentSkuPrice['is_leader_discount']; // 是否团长优惠
  162. $ladders = $currentSkuPrice['ladders']; // 阶梯数据
  163. $ladders = array_column($ladders, null, 'ladder');
  164. $currentLadder = $ladders[$groupon_num] ?? current($ladders); // 当前阶梯的 价格数据
  165. // 开新团,并且没有找到要参与的阶梯数据
  166. if (!$groupon_id && (!$currentLadder || $currentLadder['ladder'] <= 1)) {
  167. throw new ShoproException('请选择正确的开团阶梯');
  168. }
  169. $buyInfo['ladder'] = $currentLadder; // 存储当前购买的拼团阶梯 ladder
  170. // 额外需要的库存
  171. $need_add_num = 0;
  172. // 要单独购买
  173. if ($buy_type == 'alone') {
  174. // 不允许单独购买
  175. if (!$is_alone) {
  176. throw new ShoproException('该商品不允许单独购买');
  177. }
  178. } else {
  179. // 拼团,临时将拼团价设置为商品价格
  180. if (!$groupon_id && $is_leader_discount) {
  181. // 开新团,并且有团长优惠,使用优惠价格
  182. $buyInfo['current_sku_price']['price'] = $currentLadder['leader_ladder_price'];
  183. } else {
  184. // 参与团,或者没有团长优惠
  185. $buyInfo['current_sku_price']['price'] = $currentSkuPrice['ladder_price'];
  186. }
  187. }
  188. // 如果是开新团
  189. if (!$groupon_id && $buy_type == 'groupon') {
  190. // 成团人数
  191. $num = $currentLadder['ladder'] ?? 1;
  192. // 开团需要的最小库存
  193. $need_add_num = ($num - 1);
  194. }
  195. // 当前库存,小于要购买的数量
  196. $need_num = $buyInfo['goods_num'] + ($need_add_num ?? 0);
  197. if ($currentSkuPrice['stock'] < $need_num) {
  198. if ($need_add_num && $is_alone && !$groupon_id && $buy_type == 'groupon') {
  199. throw new ShoproException('商品库存不足以开团,请选择单独购买');
  200. } else if ($buy_type == 'alone') {
  201. throw new ShoproException('商品库存不足');
  202. } else {
  203. throw new ShoproException('商品库存不足以开团');
  204. }
  205. }
  206. $buyInfo['is_commission'] = $rules['is_commission'] ?? 0; // 是否参与分销
  207. return $buyInfo;
  208. }
  209. public function buy($buyInfo, $activity)
  210. {
  211. $user = auth_user();
  212. $buy_type = request()->param('buy_type', 'groupon');
  213. $groupon_id = request()->param('groupon_id', 0);
  214. // 参与现有团
  215. if ($buy_type != 'alone' && $groupon_id) {
  216. // 检测并获取要参与的团
  217. $activityGroupon = $this->checkAndGetJoinGroupon($buyInfo, $user, $groupon_id);
  218. }
  219. // 判断 并 增加 redis 销量
  220. $stockSale = new StockSale();
  221. $stockSale->cacheForwardSale($buyInfo);
  222. // (开新团不判断)参与旧团 增加预拼团人数,上面加入团的时候已经判断过一次了,所以这里 99.99% 会加入成功的
  223. if (isset($activityGroupon) && $activityGroupon) {
  224. // 增加拼团预成员人数
  225. $goods = $buyInfo['goods'];
  226. $activity = $goods['activity'];
  227. $this->grouponCacheForwardNum($activityGroupon, $activity, $user);
  228. }
  229. return $buyInfo;
  230. }
  231. public function buyOk($order, $user)
  232. {
  233. $this->joinGroupon($order, $user, function ($activityRules, $itemExt) {
  234. // 处理拼团特殊的数据
  235. $ladder = $itemExt['ladder'];
  236. $team_num = $ladder['ladder'];
  237. return compact('team_num');
  238. });
  239. }
  240. /**
  241. * 拼团购买失败
  242. *
  243. * @param \think\Model $order
  244. * @param string $type
  245. * @return void
  246. */
  247. public function buyFail($order, $type)
  248. {
  249. if ($type == 'invalid') {
  250. if ($order->pay_mode == 'offline') {
  251. // 肯定是已经货到付款的订单取消订单,这时候已经添加了参团记录
  252. $this->refundGrouponLog($order);
  253. } else {
  254. // 订单失效,扣除预拼团人数(只处理正在进行中的团)
  255. $this->grouponCacheBackNum($order, $type);
  256. }
  257. } else {
  258. // type = refund 退款订单将参团标记为已退款
  259. $this->refundGrouponLog($order);
  260. }
  261. // 判断扣除预销量 (活动信息还在 redis)
  262. $stockSale = new StockSale();
  263. $stockSale->cacheBackSale($order);
  264. }
  265. }