You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

479 lines
18 KiB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. <?php
  2. namespace App\Service\v3\Implementations;
  3. use App\Model\v3\Coupon;
  4. use App\Model\v3\CouponRec;
  5. use App\Model\v3\CouponRecType;
  6. use App\Model\v3\GoodsActivity;
  7. use App\Service\CommonService;
  8. use App\Service\v3\Interfaces\CouponRecServiceInterface;
  9. use App\Service\v3\Interfaces\CouponServiceInterface;
  10. use App\Service\v3\Interfaces\ShopCartServiceInterface;
  11. use Hyperf\DbConnection\Db;
  12. use Hyperf\Redis\Redis;
  13. use Hyperf\Utils\ApplicationContext;
  14. use Hyperf\Di\Annotation\Inject;
  15. use App\Constants\v3\SsdbKeys;
  16. use App\TaskWorker\SSDBTask;
  17. class CouponRecService implements CouponRecServiceInterface
  18. {
  19. /**
  20. * @Inject
  21. * @var ShopCartServiceInterface
  22. */
  23. protected $shopCartService;
  24. /**
  25. * @Inject
  26. * @var CouponServiceInterface
  27. */
  28. protected $couponService;
  29. /**
  30. * @Inject
  31. * @var HelperService
  32. */
  33. protected $helperService;
  34. public function do()
  35. {
  36. // TODO: Implement do() method.
  37. }
  38. public function check()
  39. {
  40. // TODO: Implement check() method.
  41. }
  42. public function undo()
  43. {
  44. // TODO: Implement undo() method.
  45. }
  46. /**
  47. * 获取当前订单可使用的优惠券
  48. * @param $totalAmount
  49. * @param $userId
  50. * @param $marketId
  51. * @param $type
  52. * @param $storeTypeIds
  53. * @return array
  54. */
  55. public function allForOrderOlAvailable($totalAmount, $userId, $marketId, $type, $storeTypeIds = [])
  56. {
  57. // 用户今日使用过的优惠券
  58. $redis = ApplicationContext::getContainer()->get(Redis::class);
  59. $couponTodayUsedIds = $redis->sMembers('coupon_'.date('Ymd').'_used_'.$userId);
  60. $currentTime = time();
  61. $builder = Db::table('lanzu_coupon_receive as receive')
  62. ->join('lanzu_coupon as coupon', 'coupon.id', '=', 'receive.coupon_id', 'inner');
  63. if (is_array($couponTodayUsedIds)&&!empty($couponTodayUsedIds)) {
  64. $builder->whereNotIn('coupon.id', $couponTodayUsedIds);
  65. }
  66. foreach ($storeTypeIds as $key => &$item) {
  67. $item = (string)$item;
  68. }
  69. if (!empty($storeTypeIds)) {
  70. $builder->whereJsonContains('coupon.category_ids', $storeTypeIds);
  71. }
  72. $builder->where(['receive.user_id' => $userId])
  73. ->whereIn('receive.status', [0,1])
  74. ->where('receive.number_remain', '>', 0)
  75. ->whereIn('coupon.type', [1,$type])
  76. ->where('coupon.full_amount', '<=', $totalAmount)
  77. ->where('coupon.usable_start_time', '<=', $currentTime)
  78. ->where('coupon.usable_end_time', '>=', $currentTime)
  79. ->where('coupon.usable_number', '<=', Db::raw('receive.number_remain'));
  80. if ($marketId) {
  81. $builder->whereJsonContains('coupon.market_ids', [(string)$marketId]);
  82. }
  83. return $builder->orderByRaw('coupon.discounts DESC, coupon.full_amount DESC')
  84. ->get()
  85. ->toArray();
  86. }
  87. /**
  88. * 用户优惠券列表
  89. * @param $userId
  90. * @param $type
  91. * @param int $page
  92. * @param int $pagesize
  93. * @return array
  94. */
  95. public function getListByUser($userId,$type,$page = 1,$pagesize = 5)
  96. {
  97. //查询优惠券
  98. $builder = CouponRec::query()->join('lanzu_coupon', 'lanzu_coupon.id', '=', 'lanzu_coupon_receive.coupon_id')->with('coupon')
  99. ->where([
  100. ['lanzu_coupon_receive.user_id' ,'=', $userId],
  101. ]);
  102. /**
  103. * $type unused 未使用 used 已使用 expired 已失效
  104. */
  105. switch ($type){
  106. case 'unused':
  107. $builder = $builder->where([
  108. ['lanzu_coupon.usable_end_time' ,'>', time()],
  109. ['lanzu_coupon_receive.number_remain' ,'>', 0]
  110. ])
  111. ->whereIn('lanzu_coupon_receive.status',[0,1]);
  112. break;
  113. case 'used':
  114. $builder = $builder->whereIn('lanzu_coupon_receive.status',[1,2]);
  115. break;
  116. case 'expired':
  117. $builder = $builder->where(function ($query) {
  118. $query->where('lanzu_coupon.usable_end_time', '<', time());
  119. });
  120. break;
  121. }
  122. $builder = $builder->select(
  123. 'lanzu_coupon_receive.*'
  124. )
  125. ->orderBy('lanzu_coupon.weigh','desc');
  126. $paginate = $builder->paginate($pagesize);
  127. $couponList = $paginate->toArray();
  128. return ['has_more_pages' => $paginate->hasMorePages(), 'list' => $couponList['data']];
  129. }
  130. /**
  131. * 获取用户当前线上订单可以使用的优惠券
  132. * 1、所有未过期额优惠券,并且满足订单的金额要求
  133. * 2、筛选出其中当日使用过的优惠券
  134. * 3、筛选出其中活动商品不可用的优惠券(订单中有活动商品且活动商品不可使用优惠券)
  135. * 4、筛选出其中活动商品可用但商品的活动类型不符合优惠券活动使用类型的要求(订单中有活动商品可以用优惠券,但是活动类型type和优惠券中的available活动类型不可用)
  136. * @param $userId
  137. * @param $marketId
  138. * @param $shopcartIds
  139. * @return array
  140. */
  141. public function allForOnlineOrderAvailable($userId, $marketId, $shopcartIds = [])
  142. {
  143. // 获取购物车数据
  144. $carts = $this->shopCartService->allForUser($userId, $marketId, $shopcartIds);
  145. $totalAmount = $carts['total'];
  146. // 获取购物车中商品和店铺的类别
  147. $storeCategoryIds = []; // 商户类型
  148. $goodsCategoryIds = []; // 商品类型
  149. $hasActivityGoodsCannotUse = false; // 购物车中是否有不可使用优惠券的活动商品
  150. $goodsActivityTypes = []; // 活动商品的类型集合
  151. foreach ($carts['store_lists'] as $key => &$cart) {
  152. // 商户类型
  153. if (isset($cart['store']['category_id']) && $cart['store']['category_id']) {
  154. array_push($storeCategoryIds, $cart['store']['category_id']);
  155. }
  156. if (isset($cart['shopping_cart']) && is_array($cart['shopping_cart'])) {
  157. foreach ($cart['shopping_cart'] as $key2 => &$goods) {
  158. // 商品类型
  159. if (isset($goods['goods']['category_id']) && $goods['goods']['category_id']) {
  160. array_push($goodsCategoryIds, $goods['goods']['category_id']);
  161. }
  162. // 活动商品不可使用优惠券的情况
  163. if ($goods['activity_type'] == 2 && $goods['goods']['can_use_coupon'] != 1) {
  164. $hasActivityGoodsCannotUse = true;
  165. }
  166. // 活动商品类型集合
  167. if ($goods['activity_type'] == 2) {
  168. array_merge($goodsActivityTypes, [$goods['goods']['type']]);
  169. }
  170. }
  171. }
  172. }
  173. $categoryIds = array_merge($storeCategoryIds, $goodsCategoryIds);
  174. $coupon = ApplicationContext::getContainer()->get(Coupon::class);
  175. $couponRec = ApplicationContext::getContainer()->get(CouponRec::class);
  176. $couponTable = $coupon->getTable();
  177. $receiveTable = $couponRec->getTable();
  178. $currentTime = time();
  179. // 获取用户已领取的优惠券,同时是在使用期内的优惠券
  180. $builder = $couponRec->with('coupon')
  181. ->select("{$receiveTable}.*")
  182. ->join($couponTable, "{$couponTable}.id", "=", "{$receiveTable}.coupon_id")
  183. ->where("{$receiveTable}.user_id", $userId)
  184. ->where("{$receiveTable}.number_remain", ">", 0)
  185. ->whereIn("{$receiveTable}.status", [0,1])
  186. ->where("{$couponTable}.usable_start_time", "<=", $currentTime)
  187. ->where("{$couponTable}.usable_end_time", ">=", $currentTime);
  188. if (!empty($marketId)) { # 市场限制
  189. $builder->where(function ($query) use ($marketId, $couponTable) {
  190. return $query->whereJsonContains("{$couponTable}.market_ids", [(string)$marketId])
  191. ->orWhereJsonLength("{$couponTable}.market_ids", '=', 0);
  192. });
  193. }
  194. if (!empty($categoryIds)) { # 分类限制
  195. $builder->where(function ($query) use ($categoryIds, $couponTable) {
  196. return $query->whereJsonContains("{$couponTable}.category_ids", $categoryIds)
  197. ->orWhereJsonLength("{$couponTable}.category_ids", '=', 0);
  198. });
  199. }
  200. $couponReceives = $builder->orderByRaw("{$couponTable}.usable_end_time ASC")->get();
  201. $available = [];
  202. $notAvailable = [];
  203. // 有活动商品不可用优惠券,全部都不可用了
  204. if ($hasActivityGoodsCannotUse) {
  205. $notAvailable = $couponReceives;
  206. foreach ($notAvailable as $key => &$item) {
  207. $item['not_available_reason'] = '活动不可用';
  208. }
  209. $couponReceives = [];
  210. }
  211. // 循环摘出失效不可用的优惠券
  212. foreach ($couponReceives as $key => &$item) {
  213. // 不满订单金额
  214. if ($item['coupon']['full_amount'] > $totalAmount) {
  215. $item['not_available_reason'] = '差'.bcsub($item['coupon']['full_amount'], $totalAmount, 2).'元';
  216. array_push($notAvailable, $item);
  217. continue;
  218. }
  219. // 今日已使用
  220. $todayUsedCouponIds = $this->couponService->allTodayCouponUsed($userId);
  221. if (in_array($item['coupon']['id'], $todayUsedCouponIds)) {
  222. $item['not_available_reason'] = '今日已使用';
  223. array_push($notAvailable, $item);
  224. continue;
  225. }
  226. // 活动商品可用,校验对应的优惠券可使用活动类型
  227. $intersects = array_intersect($goodsActivityTypes, (array)$item['activity_available']); # 所有活动商品的活动类型和优惠券的可用活动类型求交集
  228. $canUseByTypes = array_diff($goodsActivityTypes, $intersects); # 所有活动商品的活动类型和上述交集求差集,如果为空则是可以用的,否则说明前者中有后者不能用的活动类型
  229. if (!empty($canUseByTypes)) {
  230. $item['not_available_reason'] = '活动不可用';
  231. array_push($notAvailable, $item);
  232. continue;
  233. }
  234. $item['not_available_reason'] = '';
  235. array_push($available, $item);
  236. }
  237. return ['available' => $available, 'not_available' => $notAvailable];
  238. }
  239. /**
  240. * @param $userId
  241. * @return mixed
  242. */
  243. public function statistics($userId)
  244. {
  245. //未使用
  246. $res['unused'] = 0;
  247. //已使用
  248. $res['used'] = 0;
  249. //已过期
  250. $res['expired'] = 0;
  251. $coupons = CouponRec::query()
  252. ->with('coupon')
  253. ->where('user_id',$userId)
  254. ->get();
  255. foreach ($coupons as $coupon){
  256. if($coupon->coupon->usable_end_time < time()){
  257. $res['expired'] += $coupon->number_remain;
  258. }else{
  259. $res['unused'] += $coupon->number_remain;
  260. }
  261. $res['used'] += $coupon->used_num;
  262. }
  263. return $res;
  264. }
  265. public function getAvailableList($userId,$receiveType)
  266. {
  267. /* 优惠券活动标志 2 */
  268. $ssdb = ApplicationContext::getContainer()->get(SSDBTask::class);
  269. $couponActivity = $ssdb->exec('hgetall', SsdbKeys::COUPON_REBATE_ACTIVITY);
  270. $activityType = $couponActivity === false ? 0 : $couponActivity['activity'];
  271. $result = [
  272. 'active_type' => 1,
  273. 'not_receive' => [],
  274. 'jump_data' => [
  275. 'src' => "/zh_cjdianc/pages/couponrebate/index?activity_type=".$activityType,
  276. 'src2' => "/zh_cjdianc/pages/couponrebate/index?activity_type=".$activityType,
  277. 'share_bg' => env('OSS_IMG_HOST').'/static/img/coupon_share.png',
  278. 'receive_bg' => env('OSS_IMG_HOST').'/static/img/coupon_bg.png',
  279. 'coupons' => []
  280. ]
  281. ];
  282. $nowTime = time();
  283. $c_ids = [];
  284. // 渠道开启,查询该渠道可以领取的优惠券ID
  285. // 渠道未开启,查询所有优惠券
  286. if (env('SUB_CHANNEL') == 1) {
  287. $c_ids = CouponRecType::join('lanzu_coupon','lanzu_coupon.id','lanzu_coupon_receive_type.coupon_id')
  288. ->where([
  289. ['lanzu_coupon_receive_type.receive_type','=',$receiveType],
  290. ['lanzu_coupon.end_time','>',$nowTime],
  291. ['lanzu_coupon.start_time','<=',$nowTime],
  292. ['lanzu_coupon.status','=',1]
  293. ])
  294. ->pluck('lanzu_coupon.id');
  295. } else {
  296. $c_ids = Coupon::where([
  297. ['end_time','>',$nowTime],
  298. ['start_time','<=',$nowTime],
  299. ['status','=',1]
  300. ])->pluck('id');
  301. }
  302. $couponReceive = CouponRec::where('user_id',$userId);
  303. // 渠道开启,查询该用户在此渠道领过的优惠券ID
  304. if (env('SUB_CHANNEL') == 1) {
  305. $couponReceive->where('receive_type', $receiveType);
  306. }
  307. $cr_ids = $couponReceive->pluck('coupon_id');
  308. // 可领取的券ID
  309. $c_ids = $c_ids->toArray();
  310. // 已经领取的券ID
  311. $cr_ids = $cr_ids->toArray();
  312. // 当前用户可领的优惠券ID
  313. $couponIds = array_diff($c_ids, $cr_ids);
  314. // 转发型优惠券
  315. $couponReceiveIds = ($couponActivity === false || $this->helperService->nonempty($couponActivity['forward']) )? [] : explode(',',$couponActivity['forward']);
  316. // 所有优惠券
  317. $couponIds = array_merge($couponIds,$couponReceiveIds);
  318. $whereC = [
  319. ['lanzu_coupon.end_time','>',$nowTime],
  320. ['lanzu_coupon.start_time','<=',$nowTime],
  321. ['lanzu_coupon.status','=',1]
  322. ];
  323. // 查询领取型1 和 转发型2
  324. $whereActiveType = [1,2];
  325. if (env('SUB_CHANNEL') == 1) {
  326. array_push($whereC, ['type.receive_type','=', $receiveType]);
  327. }
  328. $coupons = Coupon::join('lanzu_coupon_receive_type as type', 'lanzu_coupon.id', '=', 'type.coupon_id')
  329. ->whereIn('lanzu_coupon.id', $couponIds)
  330. ->whereIn('lanzu_coupon.active_type', $whereActiveType)
  331. ->where($whereC)
  332. ->whereRaw('lanzu_coupon.inventory_use < lanzu_coupon.inventory and lanzu_coupon.inventory-lanzu_coupon.inventory_use >= type.one_receive_number')
  333. ->select('lanzu_coupon.*','type.one_receive_number')
  334. ->orderBy('lanzu_coupon.weigh','desc')
  335. ->get();
  336. foreach ($coupons as $k => &$v){
  337. if($v->active_type == 1){
  338. $result['not_receive'][] = $v;
  339. }else if($v->active_type == 2 && in_array($v->id,$couponReceiveIds)){
  340. $result['jump_data']['coupons'][] = $v->id;
  341. }
  342. if($v->discount_type == 2){
  343. $v->discounts = floatval($v->discounts);
  344. }
  345. }
  346. $result['active_type'] = count($result['jump_data']['coupons']) > 0 ? 2 : $result['active_type'] ;
  347. return $result;
  348. }
  349. /**
  350. * @param $userId
  351. * @param $receiveType
  352. * @param $ids
  353. */
  354. public function receive($userId, $receiveType, $ids)
  355. {
  356. $ids = explode(',', $ids);
  357. $now = time();
  358. $success = [];
  359. $fail = [];
  360. // if ($this->empty($userId) || $this->empty($receiveType) || $this->empty($ids)) {
  361. // return [
  362. // 'success' => $success,
  363. // 'fail' => $fail,
  364. // ];
  365. // }
  366. Db::transaction(function () use ($ids,$receiveType,$userId,&$success,&$fail,$now) {
  367. //读写锁,完全控制,性能底
  368. $builder = Coupon::whereIn('id', $ids);
  369. if (env('SUB_CHANNEL') == 1) {
  370. $builder->where('receive_type',$receiveType);
  371. }
  372. $cps = $builder->lockForUpdate()->get();
  373. //写锁,可能在高并发下,读取到脏数据,写入可能有超发情况
  374. //$cps = Coupon::whereIn('id', $ids)->sharedLock()->get();
  375. foreach ($cps as $key => $cp) {
  376. $where = [
  377. 'coupon_id' => $cp->id,
  378. ];
  379. $oneReceiveNumber = CouponRecType::where($where)->value('one_receive_number');
  380. $couponInventory = $cp->inventory - $cp->inventory_use;
  381. if($couponInventory < $oneReceiveNumber){
  382. continue;
  383. }
  384. $couponReceiveExists = CouponRec::query()->where(['coupon_id'=>$cp->id,'user_id'=>$userId])->exists();
  385. if($couponReceiveExists){
  386. continue;
  387. }
  388. $cr = new CouponRec;
  389. $cr->user_id = $userId;
  390. $cr->coupon_id = $cp->id;
  391. $cr->order_main_id = 0;
  392. $cr->receive_time = $now;
  393. $cr->number = $oneReceiveNumber;
  394. $cr->number_remain = $oneReceiveNumber;
  395. $cr->status = 0;
  396. $cr->update_time = $now;
  397. $cr->receive_type = $receiveType;
  398. //如果优惠卷库存小于等于已领取的数量, 则返回领取失败的优惠券
  399. if ($cp->inventory<=$cp->inventory_use||$cp->inventory<($cp->inventory_use+$cr->number)){
  400. $fail[] = $cp;
  401. }else{
  402. $cp->inventory_use += $cr->number;//记录已领取的数量
  403. if ($cr->save()&&$cp->save()) {
  404. $success[] = $cp;
  405. } else {
  406. $fail[] = $cp;
  407. }
  408. }
  409. }
  410. });
  411. return [
  412. 'success' => $success,
  413. 'fail' => $fail,
  414. ];
  415. }
  416. }