GoodsLogic.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | likeshop100%开源免费商用商城系统
  4. // +----------------------------------------------------------------------
  5. // | 欢迎阅读学习系统程序代码,建议反馈是我们前进的动力
  6. // | 开源版本可自由商用,可去除界面版权logo
  7. // | 商业版本务必购买商业授权,以免引起法律纠纷
  8. // | 禁止对系统程序代码以任何目的,任何形式的再发布
  9. // | gitee下载:https://gitee.com/likeshop_gitee
  10. // | github下载:https://github.com/likeshop-github
  11. // | 访问官网:https://www.likeshop.cn
  12. // | 访问社区:https://home.likeshop.cn
  13. // | 访问手册:http://doc.likeshop.cn
  14. // | 微信公众号:likeshop技术社区
  15. // | likeshop团队 版权所有 拥有最终解释权
  16. // +----------------------------------------------------------------------
  17. // | author: likeshopTeam
  18. // +----------------------------------------------------------------------
  19. namespace app\shopapi\logic;
  20. use app\shopapi\logic\Order\FreightLogic;
  21. use app\common\{cache\HandleConcurrencyCache,
  22. enum\FootprintEnum,
  23. enum\OrderEnum,
  24. enum\PayEnum,
  25. logic\CommonPresellLogic,
  26. logic\GoodsActivityLogic,
  27. model\Cart,
  28. model\Distribution,
  29. model\DistributionConfig,
  30. model\DistributionGoods,
  31. model\DistributionLevel,
  32. model\FreeShipping,
  33. model\Goods,
  34. logic\BaseLogic,
  35. logic\DiscountLogic,
  36. model\GoodsCategoryIndex,
  37. model\GoodsItem,
  38. model\GoodsServiceGuarantee,
  39. model\GoodsVisit,
  40. model\GoodsCollect,
  41. model\OrderGoods,
  42. model\PresellGoods,
  43. model\SearchRecord,
  44. model\GoodsComment,
  45. enum\GoodsCommentEnum,
  46. model\User,
  47. model\UserAddress,
  48. service\FileService,
  49. model\GoodsCategory};
  50. use app\common\service\ConfigService;
  51. /**
  52. * 商品接口逻辑层
  53. * Class GoodsLogic
  54. * @package app\shopapi\logic
  55. */
  56. class GoodsLogic extends BaseLogic
  57. {
  58. /**
  59. * @notes 商品详情
  60. * @param $id
  61. * @return array|false
  62. * @throws \think\db\exception\DataNotFoundException
  63. * @throws \think\db\exception\DbException
  64. * @throws \think\db\exception\ModelNotFoundException
  65. * @author cjhao
  66. * @date 2021/8/2 19:54
  67. */
  68. public function detail($params)
  69. {
  70. $id = $params['id'];
  71. $userId = $params['user_id'];
  72. outFileLog($params,'goods_detail','params');
  73. $goods = Goods::with(['spec_value.spec_list','spec_value_list'])
  74. ->field('id,name,type,code,image,video,video_cover,total_stock,click_num,virtual_sales_num+sales_num as sales_num,unit_id,spec_type,content,poster,virtual_click_num,express_type,express_money,express_template_id,limit_type,limit_value,status,service_guarantee_ids')
  75. ->append(['goods_image'])
  76. ->find($id);
  77. if(empty($goods)){
  78. self::$error = '商品已下架!';
  79. return false;
  80. }
  81. // 服务保障
  82. $goods->service_guarantee = GoodsServiceGuarantee::getApiList($goods->service_guarantee_ids);
  83. // 判断是否需要统计浏览量
  84. if (isset($params['visit'])) {
  85. //记录点击量
  86. $goods->click_num = $goods->click_num + 1;
  87. $goods->save();
  88. // 浏览量
  89. $this->visit($id, $userId);
  90. }
  91. $stockShow = ConfigService::get('goods_set', 'is_show', 1);
  92. $showPrice = ConfigService::get('goods_set', 'show_price', 1);
  93. $goods->stock_show = true;
  94. if(0 == $stockShow){
  95. $goods->stock_show = false;
  96. }
  97. $goods->buy_num = 0;//已购买数量
  98. $goods->cart_goods_num = 0;
  99. if($userId) {
  100. // 地址
  101. $address_id = input('address_id/d', 0);
  102. $address = $address_id ? UserAddress::getAddressById($userId, $address_id) : UserAddress::getDefaultAddress($userId);
  103. if ($address) {
  104. $goods->address = $address;
  105. }
  106. // 汽泡足迹
  107. event('Footprint', ['type' => FootprintEnum::BROWSE_PRODUCT, 'user_id' => $userId, 'foreign_id'=>$id]);
  108. //是否收藏过
  109. $IsCollect = GoodsCollect::where(['goods_id' => $id, 'user_id' => $userId])->value('id');
  110. $goods->is_collect = $IsCollect ? 1 : 0;
  111. //会员价
  112. $userLevel = User::where(['id' => $userId])
  113. ->field('id,level')
  114. ->with('user_level')
  115. ->findOrEmpty()->toArray();
  116. $goodsDiscountPrice = DiscountLogic::getGoodsDiscount($userId, [$goods->id])[$goods->id] ?? [];
  117. foreach ($goods->spec_value_list as $key => $specValue) {
  118. $specValue['member_price'] = $goodsDiscountPrice[$specValue['id']]['discount_price'] ?? '';
  119. $specValue['member_level'] = [
  120. 'name' => $userLevel['name'] ?? '',
  121. ];
  122. }
  123. $goods->member_price = $goods->spec_value_list[0]->member_price;
  124. $goods->member_level = [
  125. 'name' => $userLevel['name'] ?? '',
  126. ];
  127. //用户下单数量
  128. $goods->buy_num = OrderGoods::alias('og')
  129. ->join('order o', 'o.id = og.order_id')
  130. ->where(['og.goods_id'=>$id,'o.order_status'=>[OrderEnum::STATUS_WAIT_PAY,OrderEnum::STATUS_WAIT_DELIVERY,OrderEnum::STATUS_WAIT_RECEIVE,OrderEnum::STATUS_FINISH],'o.user_id'=>$params['user_id'],'o.order_type'=>[OrderEnum::NORMAL_ORDER,OrderEnum::VIRTUAL_ORDER]])
  131. ->sum('og.goods_num');
  132. //购物车商品数量
  133. $goods->cart_goods_num = Cart::where(['goods_id'=>$id,'user_id'=>$params['user_id']])->sum('goods_num');
  134. }
  135. if(0 == $showPrice){
  136. foreach ($goods->spec_value_list as $key =>$specValue){
  137. $specValue['lineation_price'] = 0;
  138. }
  139. }
  140. $goods->sell_price = $goods->spec_value_list[0]->sell_price;
  141. $goods->lineation_price = $goods->spec_value_list[0]->lineation_price;
  142. $goods->goods_comment = $this->getComment($goods->id);
  143. $goods->goods_comment_count = count($this->getComment($goods->id));
  144. $goods->click_num += $goods->virtual_click_num;
  145. $goods->unit_name = '';
  146. if($goods->unit_id){
  147. $goods->unit_name = $goods->unit->name;
  148. }
  149. $goods->hidden(['unit_id','unit']);
  150. // 预估佣金
  151. $goods->distribution = self::getDistribution($id, $userId);
  152. // 包邮信息
  153. $goods->free_shipping_tips = self::getFreeShippingTips($userId, $goods);
  154. // 预售信息
  155. $goods = CommonPresellLogic::goodsDetailInfo($goods);
  156. return $goods->toArray();
  157. }
  158. public function getshopGoods(){
  159. $goods_cate = GoodsCategory::where(['is_show'=>1,'level'=>1])->field('id,name')->order('sort asc')->select()->toArray();
  160. $shop_goods =[];
  161. foreach($goods_cate as &$v){
  162. $cate_arr = [];
  163. array_push($cate_arr,$v['id']);
  164. $second = GoodsCategory::where(['is_show'=>1,'pid'=>$v['id']])->field('id,name')->order('sort asc')->select()->toArray();
  165. $second_arr = array_column($second,'id');
  166. $cate_arr = array_merge($cate_arr,$second_arr);
  167. foreach($second as $sv){
  168. $three = GoodsCategory::where(['is_show'=>1,'pid'=>$sv['id']])->field('id,name')->order('sort asc')->select()->toArray();
  169. $three_arr= array_column($three,'id');
  170. $cate_arr = array_merge($cate_arr,$three_arr);
  171. }
  172. // if($v['id'] == 18){
  173. // outFileLog($cate_arr,'cate_arr','$cate_arr');
  174. // }
  175. $good_id_list = GoodsCategoryIndex::where(['category_id'=>$cate_arr])->select()->toArray();
  176. $goods_list=[];
  177. if($good_id_list){
  178. $goods_id_arr = array_column($good_id_list,'goods_id');
  179. $goods_list = Goods::where(['id'=>$goods_id_arr,'status'=>1])->field('id,name,min_price,min_lineation_price,image')->order('sort desc')->limit(4)->select()->toArray();
  180. foreach($goods_list as &$gv){
  181. $goods_item_count = GoodsItem::where(['goods_id'=>$gv['id']])->count();
  182. if($goods_item_count > 1){
  183. $gv['is_multi_gauge'] = 1;
  184. }else{
  185. $gv['is_multi_gauge'] = 0;
  186. }
  187. }
  188. $v['goods_list'] = $goods_list;
  189. $shop_goods[] = $v;
  190. }
  191. $v['goods_list'] = $goods_list;
  192. }
  193. return ['goods_cate'=>$shop_goods];
  194. }
  195. /**
  196. * @notes 商品搜索记录
  197. * @param $userId
  198. * @param $limit
  199. * @return array
  200. * @author cjhao
  201. * @date 2021/8/11 17:12
  202. */
  203. public function searchRecord($userId,$limit){
  204. //读取缓存
  205. $HandleConcurrencyCache = new HandleConcurrencyCache();
  206. $recordList = $HandleConcurrencyCache->getCache($HandleConcurrencyCache->getGoodsSearchRecordKey($userId));
  207. if ($recordList !== false) {
  208. return $recordList;
  209. }
  210. $recordList = SearchRecord::where(['user_id'=>$userId])
  211. ->limit($limit)
  212. ->order('id desc')
  213. ->column('keyword');
  214. //设置缓存
  215. $HandleConcurrencyCache->setCache($HandleConcurrencyCache->getGoodsSearchRecordKey($userId),$recordList,3600);
  216. return $recordList;
  217. }
  218. /**
  219. * @notes 商品营销接口
  220. * @param int $goodsId
  221. * @param int $userId
  222. * @return array
  223. * @author cjhao
  224. * @date 2021/8/27 17:27
  225. */
  226. public function goodsMarketing(int $goodsId,int $userId):array
  227. {
  228. $coupon = CouponLogic::goodsCoupon($goodsId,$userId);
  229. $activityList = GoodsActivityLogic::activityInfo($goodsId)[$goodsId] ?? [];
  230. $marketing = [
  231. 'coupon' => $coupon,
  232. 'activity' => array_values($activityList),
  233. ];
  234. return $marketing;
  235. }
  236. /**
  237. * @notes 清空搜索记录
  238. * @param int $userId
  239. * @author cjhao
  240. * @date 2021/9/15 11:35
  241. */
  242. public function clearRecord(int $userId)
  243. {
  244. SearchRecord::where(['user_id'=>$userId])->delete();
  245. }
  246. /**
  247. * @notes 商品浏览记录
  248. * @param $goodsId
  249. * @param $userId
  250. * @return bool
  251. * @author Tab
  252. * @date 2021/9/15 14:04
  253. */
  254. public function visit($goodsId, $userId)
  255. {
  256. if (empty($userId)) {
  257. $userId = 0;
  258. }
  259. $ip = request()->ip();
  260. // 一个ip一个商品一个用户一天只生成一条记录
  261. $record = GoodsVisit::where([
  262. 'ip' => $ip,
  263. 'goods_id' => $goodsId,
  264. 'user_id' => $userId,
  265. ])->whereDay('create_time')->findOrEmpty();
  266. if (!$record->isEmpty()) {
  267. // 增加浏览量
  268. $record->visit += 1;
  269. $record->save();
  270. return true;
  271. }
  272. // 生成商品浏览记录
  273. GoodsVisit::create([
  274. 'ip' => $ip,
  275. 'goods_id' => $goodsId,
  276. 'user_id' => $userId,
  277. 'visit' => 1
  278. ]);
  279. }
  280. /**
  281. * @notes 获取最近的商品评价
  282. * @param $id
  283. * @param int $limit
  284. * @author cjhao
  285. * @date 2021/11/17 17:44
  286. */
  287. public static function getComment($id,$limit = 1){
  288. //商品评论
  289. $goodsComment = GoodsComment::with(['goods_comment_image','user'])
  290. ->where(['goods_id'=>$id,'status'=>GoodsCommentEnum::APPROVED])
  291. ->field('id,user_id,spec_value_str,comment,virtual')
  292. ->order('id desc')
  293. ->limit($limit)
  294. ->findOrEmpty();
  295. if(!$goodsComment->isEmpty()){
  296. $commentCount = GoodsComment::where(['goods_id'=>$id,'status'=>GoodsCommentEnum::APPROVED])->count();
  297. $goodsCommentCount = GoodsComment::where([['goods_id','=',$id],['goods_comment','>',3],['status', '=', GoodsCommentEnum::APPROVED]])->count();
  298. $goodsRate = $commentCount > 0 ? round(($goodsCommentCount/$commentCount)*100).'%' : '100%';
  299. $goodsComment->goods_rate = $goodsRate;
  300. $goodsComment->comment_image = array_column($goodsComment->goods_comment_image->toArray(),'uri');
  301. $goodsComment->hidden(['user_id','goods_comment_image']);
  302. if (!is_null($goodsComment->virtual)) {
  303. // 虚拟评价
  304. $vitual = json_decode($goodsComment->virtual, true);
  305. $goodsComment->nickname = $vitual['nickname'];
  306. $goodsComment->avatar = FileService::getFileUrl($vitual['avatar']);
  307. }
  308. //隐藏用户昵称
  309. $goodsComment->nickname = hide_substr($goodsComment->nickname);
  310. if(empty($goodsComment->comment)){
  311. $goodsComment->comment = '此用户没有填写评论';
  312. }
  313. }
  314. return $goodsComment->toArray();
  315. }
  316. /**
  317. * 计算最高可得预估佣金
  318. */
  319. public static function getDistribution($goodsId, $userId)
  320. {
  321. $earnings = 0;
  322. $ratio = 0;
  323. $rule = 0;
  324. $goods = Goods::findOrEmpty($goodsId)->toArray();
  325. // 用户分销等级
  326. $distribution_level_id = Distribution::where('user_id', $userId)->where('is_distribution', 1)->value('level_id', 0);
  327. $distribution_level = DistributionLevel::where('id', $distribution_level_id)->findOrEmpty()->toArray();
  328. // 分销等级
  329. $default_level = DistributionLevel::where('is_default', 1)->findOrEmpty()->toArray();
  330. $level = $distribution_level ? : $default_level;
  331. // 分销商品
  332. $distributionGoods = DistributionGoods::where('goods_id', $goodsId)->where('is_distribution', 1)->select()->toArray();
  333. $rule = $distributionGoods[0]['rule'] ?? $rule;
  334. // 按分销等级比例分佣
  335. if ($rule == 1) {
  336. $ratio = $level['first_ratio'] ?? 0;
  337. $earnings = bcdiv(($goods['max_price'] ?? 0) * $ratio , 100, 2);
  338. }
  339. // 单独设置分佣比例
  340. if ($rule == 2) {
  341. foreach ($distributionGoods as $distributionGood) {
  342. if ($distributionGood['level_id'] == $level['id']) {
  343. $ratio = $distributionGood['first_ratio'] ?? 0;
  344. $earnings = bcdiv(($goods['max_price'] ?? 0) * $ratio , 100, 2);
  345. break;
  346. }
  347. }
  348. }
  349. $dbConfig = DistributionConfig::column('value', 'key');
  350. // 分销总开关
  351. $switch = empty($dbConfig['switch']) ? 0 : 1;
  352. // 商品详情页是否显示佣金 0-不显示 1-显示
  353. if (!isset($dbConfig['is_show_earnings'])) {
  354. $isShowEarnings = 1;
  355. } else if(empty((int)$dbConfig['is_show_earnings'])) {
  356. $isShowEarnings = 0;
  357. } else {
  358. $isShowEarnings = 1;
  359. }
  360. // 详情页佣金可见用户 0-全部用户 1-分销商
  361. if (!isset($dbConfig['show_earnings_scope'])) {
  362. $showEarningsScope = 0;
  363. } else if(empty((int)$dbConfig['show_earnings_scope'])) {
  364. $showEarningsScope = 0;
  365. } else {
  366. $showEarningsScope = 1;
  367. }
  368. if (!$switch || empty($distributionGoods)) {
  369. $isShowEarnings = 0;
  370. }
  371. if ($isShowEarnings) {
  372. $user = Distribution::where(['user_id' => $userId])->findOrEmpty()->toArray();
  373. if ($showEarningsScope && empty($user['is_distribution'])) {
  374. $isShowEarnings = 0;
  375. }
  376. }
  377. return [
  378. 'is_show' => $isShowEarnings,
  379. 'earnings' => $earnings,
  380. 'ratio' => $ratio,
  381. 'rule' => $rule,
  382. ];
  383. }
  384. /**
  385. * @notes 自定义海报获取商品信息
  386. */
  387. public static function getGoodsByTypeId($type, $activityId, $goodsId, $userId) {
  388. // type 1普通通商品 2秒杀 3拼团 4砍价 5预售
  389. switch($type) {
  390. case 1:
  391. return Goods::field('name, image, min_lineation_price, min_price')->where('id', $goodsId)->findOrEmpty()->toArray();
  392. case 2:
  393. $data = SeckillLogic::detail([
  394. 'id' => $activityId,
  395. 'user_id' => $userId,
  396. ]);
  397. if (!is_array($data)) {
  398. return [];
  399. }
  400. return [
  401. 'name' => $data['name'],
  402. 'image' => $data['image'],
  403. 'min_lineation_price' => $data['min_price'],
  404. 'min_price' => $data['activity']['min_seckill_price'],
  405. ];
  406. case 3:
  407. $data = TeamLogic::detail($activityId, $userId);
  408. if (!is_array($data)) {
  409. return [];
  410. }
  411. return [
  412. 'name' => $data['name'],
  413. 'image' => $data['image'],
  414. 'min_lineation_price' => $data['min_price'],
  415. 'min_price' => $data['activity']['min_team_price'],
  416. ];
  417. case 4:
  418. $data = BargainLogic::detail([
  419. 'activity_id' => $activityId,
  420. 'goods_id' => $goodsId,
  421. ]);
  422. if (!is_array($data)) {
  423. return [];
  424. }
  425. return [
  426. 'name' => $data['goods_name'],
  427. 'image' => $data['image'],
  428. 'min_lineation_price' => $data['goods_max_price'],
  429. 'min_price' => $data['min_price'],
  430. ];
  431. case 5:
  432. $data = PresellGoods::where(['id'=>$goodsId])->findOrEmpty()->toArray();
  433. if (empty($data)) {
  434. return [];
  435. }
  436. $goods = Goods::where(['id'=>$data['goods_id']])->field('name,image')->findOrEmpty()->toArray();
  437. return [
  438. 'name' => $goods['name'],
  439. 'image' => $goods['image'],
  440. 'min_lineation_price' => '',
  441. 'min_price' => $data['min_price'],
  442. ];
  443. default:
  444. return [];
  445. }
  446. }
  447. public static function getFreeShippingTips($userId, $goods)
  448. {
  449. $expressTips = '';
  450. $activityTips = '';
  451. $address = $goods['address'] ?? [];
  452. // 用户未登录
  453. if (empty($userId) || empty($address['id'])) {
  454. return '免运费';
  455. }
  456. // 包邮活动
  457. $activity = FreeShipping::where([
  458. ['start_time', '<=', time()],
  459. ['end_time', '>', time()],
  460. ['status', '=', 1]
  461. ])->findOrEmpty()->toArray();
  462. if ($activity) {
  463. foreach($activity['region'] as $item) {
  464. if ($item->region_id == "100000") {
  465. // 全国区域
  466. if ($activity['condition_type'] == 1) {
  467. $activityTips = '订单满' . $item->threshold . '元包邮';
  468. } else {
  469. $activityTips = '订单满' . $item->threshold . '件包邮';
  470. }
  471. continue;
  472. }
  473. // 未设置地址
  474. if(empty($address)) {
  475. continue;
  476. }
  477. if (
  478. str_contains($item->region_id, $address['district_id'])
  479. ||
  480. str_contains($item->region_id, $address['city_id'])
  481. ||
  482. str_contains($item->region_id, $address['province_id'])
  483. ) {
  484. if ($activity['condition_type'] == 1) {
  485. $activityTips = '订单满' . $item->threshold . '元包邮';
  486. } else {
  487. $activityTips = '订单满' . (int)$item->threshold . '件包邮';
  488. }
  489. break;
  490. }
  491. }
  492. }
  493. switch ($goods['express_type']) {
  494. // 包邮
  495. case 1:
  496. $expressTips = '免运费';
  497. $activityTips = '';
  498. break;
  499. // 统一运费
  500. case 2:
  501. if ($goods['express_money'] > 0) {
  502. $expressTips = "¥{$goods['express_money']}";
  503. } else {
  504. $expressTips = '免运费';
  505. $activityTips = '';
  506. }
  507. break;
  508. // 运费模板
  509. case 3:
  510. $express = FreightLogic::calculateFreight([
  511. [
  512. 'weight' => $goods['spec_value_list'][0]['weight'] ?? 0,
  513. 'volume' => $goods['spec_value_list'][0]['volume'] ?? 0,
  514. 'goods_num' => 1,
  515. 'id' => $goods['id'],
  516. 'express_type' => $goods['express_type'],
  517. 'express_money' => $goods['express_money'],
  518. 'express_template_id' => $goods['express_template_id'],
  519. ]
  520. ], $address);
  521. $express_money = $express['express_price'] ?? 0;
  522. if ($express_money > 0) {
  523. $expressTips = "¥{$express_money}";
  524. } else {
  525. $expressTips = '免运费';
  526. $activityTips = '';
  527. }
  528. break;
  529. default:
  530. $expressTips = '免邮';
  531. $activityTips = '';
  532. break;
  533. }
  534. return "{$expressTips} {$activityTips}";
  535. }
  536. static function checkCanBuy($item_id, $num) : bool|string
  537. {
  538. $item = GoodsItem::findOrEmpty($item_id);
  539. $goods = Goods::findOrEmpty($item['goods_id'] ?? 0);
  540. if (empty($goods['id'])) {
  541. return '找不到商品';
  542. }
  543. if ($goods['status'] == 0) {
  544. return '商品不能购买';
  545. }
  546. if ($item['stock'] < $num) {
  547. return '商品库存不足';
  548. }
  549. return true;
  550. }
  551. }