<?php namespace addons\shopro\model; use think\Model; use addons\shopro\exception\Exception; use addons\shopro\library\traits\ActivityCache; use addons\shopro\model\GoodsSku; use addons\shopro\model\GoodsSkuPrice; use think\Db; use traits\model\SoftDelete; /** * 商品模型 */ class Goods extends Model { use SoftDelete, ActivityCache; // 表名,不含前缀 protected $name = 'shopro_goods'; // 自动写入时间戳字段 protected $autoWriteTimestamp = 'int'; // 定义时间戳字段名 protected $createTime = 'createtime'; protected $updateTime = 'updatetime'; protected $deleteTime = 'deletetime'; protected $hidden = ['createtime', 'updatetime', 'status']; //列表动态隐藏字段 protected static $list_hidden = ['content', 'params', 'images', 'service_ids']; // 追加属性 protected $append = [ 'dispatch_type_arr' ]; /** * params 请求参数 * is_page 是否分页 */ public static function getGoodsList($params, $is_page = true) { extract($params); $where = [ 'status' => 'up', ]; //排序字段 if (isset($order) && $order !== '') { $order = self::getGoodsListOrder($order); }else{ $order = 'weigh desc'; } if (isset($keywords) && $keywords !== '') { $where['title|subtitle'] = ['like', "%$keywords%"]; } if (isset($goods_ids) && $goods_ids !== '') { $goodsIdsArray = explode(',', $goods_ids); $where['id'] = ['in', $goodsIdsArray]; } $category_ids = []; if (isset($category_id) && $category_id != 0) { // 查询分类所有子分类,包括自己 $category_ids = Category::getCategoryIds($category_id); } $goods = self::where($where)->where(function ($query) use ($category_ids) { // 所有子分类使用 find_in_set or 匹配,亲测速度并不慢 foreach($category_ids as $key => $category_id) { $query->whereOrRaw("find_in_set($category_id, category_ids)"); } }); // 过滤有活动的商品 if (isset($no_activity) && $no_activity) { $goods = $goods->whereNotExists(function ($query) use ($where) { $activityTableName = (new Activity())->getQuery()->getTable(); $goodsTableName = (new self())->getQuery()->getTable(); $query->table($activityTableName)->where("find_in_set(" . $goodsTableName . ".id, goods_ids)")->where('deletetime', 'null'); // 必须手动加上 deletetime = null }); } $goods = $goods->orderRaw($order); $cacheKey = 'goodslist-' . ($is_page ? 'page' : 'all') . '-' . md5(json_encode($params)); // 判断缓存 $goodsCache = cache($cacheKey); if ($goodsCache) { // 存在缓存直接 返回 $goodsCache = json_decode($goodsCache, true); return $goodsCache ? : []; } if ($is_page) { $goods = $goods->paginate($per_page ?? 10); $goodsData = $goods->items(); } else { $goods = $goodsData = $goods->select(); } $data = []; if ($goodsData) { $collection = collection($goodsData); $data = $collection->hidden(self::$list_hidden); // 处理活动 // load_relation($data, 'skuPrice'); // 只针对数组 $data->load('skuPrice'); // 延迟预加载 // if (!isset($no_activity) || !$no_activity) { // 没有 传入 no_activity 或者 no_activity = false // 默认查询活动, no_activity 的时候这里也要执行一下,这里计算了销量规格等信息 foreach ($data as $key => $g) { $data[$key] = self::operActivitySkuPrice($g, $g['sku_price']); } // } } if ($is_page) { $goods->data = $data; } else { $goods = $data; // 目前只缓存不分页的请求 cache($cacheKey, json_encode($goods), (600 + mt_rand(0, 300))); } return $goods; } public static function getGoodsListByIds($goodsIds) { $goodsIdsArray = explode(',', $goodsIds); $where = [ 'status' => 'up', 'deletetime' => null, 'id' => ['in', $goodsIdsArray] ]; $goods = self::where($where)->paginate(10); if ($goods->items()) { $collection = collection($goods->items()); $data = $collection->hidden(self::$list_hidden); // 处理活动 // load_relation($data, 'skuPrice'); // 只针对数组 $data->load('skuPrice'); // 延迟预加载 foreach ($data as $key => $g) { $data[$key] = self::operActivitySkuPrice($g, $g['sku_price']); } $goods->data = $data; } return $goods; } public static function getFavoriteGoodsList($type = 'normal', $status = 'up') { $where = [ 'type' => $type, 'status' => $status, 'deletetime' => null, ]; $goods = self::where($where)->paginate(10); if ($goods->items()) { $collection = collection($goods->items()); $data = $collection->hidden(self::$list_hidden); $goods->data = $data; } return $goods; } // 首页秒杀列表 public static function indexSeckillGoodsList() { $current_endtime = [ // 距本场结束时间 'hour' => 0, 'minute' => 0, 'second' => 0, ]; $soon_starttime = [ 'hour' => 0, 'minute' => 0, ]; // 即将开抢时间 $tomorrow_start = strtotime(date('Y-m-d',strtotime('+1 day'))); //明日开始时间 $tomorrow_end = strtotime(date('Y-m-d',strtotime('+1 day'))) + 86400; //明日结束时间 $where['type'] = 'seckill'; $where['starttime'] = ['<', time()]; $where['endtime'] = ['>', time()]; $activity = Activity::where($where)->order('starttime')->find(); if($activity){ //本场 $type = 'ing'; // 本场倒计时 $lefttime = $activity['endtime'] - time(); $current_endtime = [ 'hour' => date('H', $lefttime) - 1,// 倒计时剩余的小时数 'minute' => date('i', $lefttime) - 1,// 倒计时剩余的分钟数 'second' => date('s', $lefttime) - 1,// 倒计时剩余的秒数 ]; }else{ //下一场 $where['starttime'] = ['>', time()]; $where['endtime'] = ['<', $tomorrow_start]; $activity = Activity::where($where)->order('starttime')->find(); if($activity){ $type = 'nostart'; $soon_starttime = [ 'hour' => date('H', $activity['starttime']), 'minute' => date('i', $activity['starttime']), ]; }else{ //明日预告 $where['starttime'] = ['>', $tomorrow_start]; $where['endtime'] = ['<', $tomorrow_end]; $activity = Activity::where($where)->order('starttime')->find(); $type = 'tomorrow'; } } $goodsList = []; if($activity && $activity['goods_ids']){ $goodsList = self::getGoodsListByIds($activity['goods_ids']); $goodsList = array_slice(collection($goodsList)->toArray()['data'],0,4); } return empty($goodsList) ? [] : compact('type','current_endtime','soon_starttime','goodsList'); } // 首页拼团列表 public static function indexGrouponGoodsList() { $current_endtime = [ 'hour' => 0, 'minute' => 0, 'second' => 0, ]; // 距本场结束时间 $soon_starttime = [ 'hour' => 0, 'minute' => 0, ]; // 即将开抢时间 $tomorrow_start = strtotime(date('Y-m-d',strtotime('+1 day'))); //明日开始时间 $tomorrow_end = strtotime(date('Y-m-d',strtotime('+1 day'))) + 86400; //明日结束时间 $where['type'] = 'groupon'; $where['starttime'] = ['<', time()]; $where['endtime'] = ['>', time()]; $activity = Activity::where($where)->order('starttime')->find(); if($activity){ //本场 $type = 'ing'; // 本场倒计时 $lefttime = $activity['endtime'] - time(); $current_endtime = [ 'hour' => date('H', $lefttime) - 1,// 倒计时剩余的小时数 'minute' => date('i', $lefttime) - 1,// 倒计时剩余的分钟数 'second' => date('s', $lefttime) - 1,// 倒计时剩余的秒数 ]; }else{ //下一场 $where['starttime'] = ['>', time()]; $where['endtime'] = ['<', $tomorrow_start]; $activity = Activity::where($where)->order('starttime')->find(); if($activity){ $type = 'nostart'; $soon_starttime = [ 'hour' => date('H', $activity['starttime']), 'minute' => date('i', $activity['starttime']), ]; }else{ //明日预告 $where['starttime'] = ['>', $tomorrow_start]; $where['endtime'] = ['<', $tomorrow_end]; $activity = Activity::where($where)->order('starttime')->find(); $type = 'tomorrow'; } } $goodsList = []; if($activity && $activity['goods_ids']){ $goodsList = self::getGoodsListByIds($activity['goods_ids']); $goodsList = array_slice(collection($goodsList)->toArray()['data'],0,4); } return empty($goodsList) ? [] : compact('type','current_endtime','soon_starttime','goodsList'); } // 获取秒杀商品列表 public static function getSeckillGoodsList($params) { extract($params); $type = $type ?? 'all'; if ((new self)->hasRedis()) { // 如果有redis,读取 redis $activityList = (new self)->getActivityList('seckill', $type); } else { $where = [ 'type' => 'seckill' ]; $tomorrow_start = strtotime(date('Y-m-d',strtotime('+1 day'))); //明日开始时间 $tomorrow_end = strtotime(date('Y-m-d',strtotime('+1 day'))) + 86400; //明日结束时间 if ($type == 'ing') { $where['starttime'] = ['<', time()]; $where['endtime'] = ['>', time()]; } else if ($type == 'nostart') { $where['starttime'] = ['>', time()]; $where['endtime'] = ['<', $tomorrow_start]; } else if ($type == 'ended') { $where['endtime'] = ['<', time()]; } else if ($type == 'tomorrow') { $where['starttime'] = ['>', $tomorrow_start]; $where['endtime'] = ['<', $tomorrow_end]; } $activityList = Activity::where($where)->select(); } // 获取所有商品 id $goodsIds = ''; foreach ($activityList as $key => $activity) { $goodsIds .= ',' . $activity['goods_ids']; } if ($goodsIds) { $goodsIds = trim($goodsIds, ','); } $goodsList = self::getGoodsListByIds($goodsIds); return $goodsList; } // 获取拼团商品列表 public static function getGrouponGoodsList($params) { extract($params); $type = $type ?? 'all'; if ((new self)->hasRedis()) { // 如果有redis,读取 redis $activityList = (new self)->getActivityList('groupon', $type); } else { $where = [ 'type' => 'groupon' ]; $tomorrow_start = strtotime(date('Y-m-d',strtotime('+1 day'))); //明日开始时间 $tomorrow_end = strtotime(date('Y-m-d',strtotime('+1 day'))) + 86400; //明日结束时间 if ($type == 'ing') { $where['starttime'] = ['<', time()]; $where['endtime'] = ['>', time()]; } else if ($type == 'nostart') { $where['starttime'] = ['>', time()]; $where['endtime'] = ['<', $tomorrow_start]; } else if ($type == 'ended') { $where['endtime'] = ['<', time()]; } else if ($type == 'tomorrow') { $where['starttime'] = ['>', $tomorrow_start]; $where['endtime'] = ['<', $tomorrow_end]; } $activityList = Activity::where($where)->select(); } // 获取所有商品 id $goodsIds = ''; foreach ($activityList as $key => $activity) { $goodsIds .= ',' . $activity['goods_ids']; } if ($goodsIds) { $goodsIds = trim($goodsIds, ','); } $goodsList = self::getGoodsListByIds($goodsIds); return $goodsList; } public static function getGoodsDetail($id) { $user = User::info(); $detail = (new self)->where('id', $id)->with(['favorite' => function ($query) use ($user) { $user_id = empty($user) ? 0 : $user->id; return $query->where(['user_id' => $user_id]); }])->find(); if (!$detail || $detail->status === 'down') { throw new Exception('商品不存在或已下架'); } $detail = $detail->append(['service', 'sku', 'coupons']); // 处理活动规格 $detail = self::operActivitySkuPrice($detail, $detail->sku_price); return $detail; } /** * 获取自提点 */ public static function getGoodsStore($params) { $user = User::info(); $id = $params['id'] ?? 0; $latitude = $params['latitude'] ?? 0; $longitude = $params['longitude'] ?? 0; $detail = (new self)->where('id', $id)->find(); $selfetch = []; if ($detail && strpos($detail['dispatch_type'], 'selfetch') !== false) { // 商品支持自提,查询自提模板 $dispatch = Dispatch::where('type', 'selfetch')->where('id', 'in', $detail['dispatch_ids'])->find(); if ($dispatch) { // 查询自提点模板 $dispatchSelfetch = DispatchSelfetch::where('id', 'in', $dispatch['type_ids']) ->order('id', 'asc')->find(); if ($dispatchSelfetch) { // 查询自提点 $selfetch = Store::where('selfetch', 1)->where('id', 'in', $dispatchSelfetch['store_ids']); if ($latitude && $longitude) { $selfetch = $selfetch->field('*, ' . getDistanceBuilder($latitude, $longitude))->order('distance', 'asc'); } $selfetch = $selfetch->select(); } } } return $selfetch; } // 处理活动规格 public static function operActivitySkuPrice($detail, $sku_price) { $activity = (new self)->getActivity($detail['id']); if (!empty($activity)) { switch ($activity['type']) { case 'seckill': $activity_goods_sku_price = $activity['activity_goods_sku_price']; $new_sku_price = []; foreach ($sku_price as $s => $k) { $new_sku_price[$s] = $k; $new_sku_price[$s]['stock'] = 0; $new_sku_price[$s]['sales'] = 0; foreach ($activity_goods_sku_price as $c) { if ($k['id'] == $c['sku_price_id']) { // 采用活动的 规格内容 $new_sku_price[$s]['stock'] = $c['stock']; $new_sku_price[$s]['sales'] = $c['sales']; $new_sku_price[$s]['price'] = $c['price']; $new_sku_price[$s]['status'] = $c['status']; // 采用活动的上下架 // 记录相关活动类型 $new_sku_price[$s]['activity_type'] = $activity['type']; $new_sku_price[$s]['activity_id'] = $activity['id']; // 记录对应活动的规格的记录 $new_sku_price[$s]['item_goods_sku_price'] = $c; break; } } } $sku_price = $new_sku_price; break; case 'groupon': $activity_goods_sku_price = $activity['activity_goods_sku_price']; $new_sku_price = []; foreach ($sku_price as $s => $k) { $new_sku_price[$s] = $k; $new_sku_price[$s]['stock'] = 0; $new_sku_price[$s]['sales'] = 0; foreach ($activity_goods_sku_price as $c) { if ($k['id'] == $c['sku_price_id']) { // 采用活动的 规格内容 $new_sku_price[$s]['stock'] = $c['stock']; $new_sku_price[$s]['sales'] = $c['sales']; $new_sku_price[$s]['groupon_price'] = $c['price']; // 不覆盖原来规格价格,用作单独购买,讲活动的价格设置为新的拼团价格 $new_sku_price[$s]['status'] = $c['status']; // 采用活动的上下架 // 记录相关活动类型 $new_sku_price[$s]['activity_type'] = $activity['type']; $new_sku_price[$s]['activity_id'] = $activity['id']; // 记录对应活动的规格的记录(不要了,减小响应包体积, 还得要,下单的时候需要存活动 的 sku_id) $new_sku_price[$s]['item_goods_sku_price'] = $c; break; } } } $sku_price = $new_sku_price; break; } // 减小响应包体积 unset($activity['activity_goods_sku_price']); } // 商品参与的活动 // 所有的都需要设置一下, 要不然找不到类的属性,如果不存在活动,则都是 null $detail->activity = $activity ? : null; $detail->activity_type = $activity['type'] ?? null; // 移除下架的规格 foreach ($sku_price as $key => $sku) { if ($sku['status'] != 'up') { unset($sku_price[$key]); } } if ($activity) { $prices = array_column($sku_price, 'price'); $detail['price'] = $prices ? min($prices) : 0; // min 里面不能是空数组 if ($activity['type'] == 'groupon') { $grouponPrices = array_column($sku_price, 'groupon_price'); $detail['groupon_price'] = $grouponPrices ? min($grouponPrices) : 0; } $detail['sales'] = array_sum(array_column($sku_price, 'sales')); } else { // 正常商品加上显示销量 $detail['sales'] += $detail['show_sales']; } $detail['sku_price'] = array_values($sku_price); $detail['stock'] = array_sum(array_column($sku_price, 'stock')); return $detail; } public function getActivity($goods_id) { if ($this->hasRedis()) { // 如果有活动,读取 redis $activity = $this->getGoodsActivity($goods_id); return $activity; } // 没有配置 redis $activity = Activity::where('find_in_set(:id,goods_ids)', ['id' => $goods_id]) ->with(['activityGoodsSkuPrice' => function ($query) use ($goods_id) { $query->where('goods_id', $goods_id) ->where('status', 'up'); }]) ->where([ 'endtime' => ['>',time()], 'deletetime' => null, ])->find(); return $activity; } public function getCouponsAttr($value, $data) { $goods_id = $data['id']; $coupons = Coupons::where(function ($query) use ($goods_id) { $query->where('find_in_set('. $goods_id .',goods_ids)') ->whereOr('goods_ids', 0); })->select(); return $coupons; } protected function getSkuAttr($value, $data) { $sku = GoodsSku::all([ 'goods_id'=>$data['id'], 'pid' => 0, ]); foreach ($sku as $s => &$k) { $sku[$s]['content'] = GoodsSku::all([ 'goods_id' => $data['id'], 'pid' => $k['id'] ]); } return $sku; } private static function getSkuPrice($value, $data) { return GoodsSkuPrice::all([ 'goods_id' => $data['id'], 'status' => 'up', 'deletetime' => null ]); } public function getParamsAttr($value, $data) { return $value ? json_decode($value, true) : []; } public function getServiceAttr($value, $data) { $value = $data['service_ids']; $serviceData = []; if (!empty($value)) { $serviceArray = explode(',', $value); $serviceData = []; foreach ($serviceArray as $v) { $serviceData[] = \addons\shopro\model\GoodsService::get($v); } } return $serviceData; } public function getImageAttr($value, $data) { if (!empty($value)) return cdnurl($value, true); } public function getImagesAttr($value, $data) { $imagesArray = []; if (!empty($value)) { $imagesArray = explode(',', $value); foreach ($imagesArray as &$v) { $v = cdnurl($v, true); } return $imagesArray; } return $imagesArray; } public function getContentAttr($value, $data) { $content = $data['content']; $content = str_replace("<img src=\"/uploads", "<img style=\"width: 100%;!important\" src=\"" . request()->domain() . "/uploads", $content); $content = str_replace("<video src=\"/uploads", "<video style=\"width: 100%;!important\" src=\"" . request()->domain() . "/uploads", $content); return $content; } public function getDispatchTypeArrAttr($value, $data) { return array_filter(explode(',', $data['dispatch_type'])); } public function favorite() { return $this->hasOne(\addons\shopro\model\UserFavorite::class, 'goods_id', 'id'); } public function scoreGoodsSkuPrice() { return $this->hasMany(\addons\shopro\model\scoreGoodsSkuPrice::class, 'goods_id', 'id') ->where('status', 'up')->order('id', 'asc'); } public function skuPrice() { return $this->hasMany(\addons\shopro\model\GoodsSkuPrice::class, 'goods_id', 'id') ->order('id', 'asc'); } //商品列表排序 private static function getGoodsListOrder($orderStr) { $order = 'weigh desc'; $orderList = json_decode(htmlspecialchars_decode($orderStr), true); extract($orderList); if (isset($defaultOrder) && $defaultOrder === 1) { $order = 'weigh desc'; } if (isset($priceOrder) && $priceOrder === 1) { $order = "convert(`price`, DECIMAL(10, 2)) asc"; }elseif (isset($priceOrder) && $priceOrder === 2) { $order = "convert(`price`, DECIMAL(10, 2)) desc"; } if (isset($salesOrder) && $salesOrder === 1) { $order = 'sales asc'; }elseif (isset($salesOrder) && $salesOrder === 2) { $order = 'sales desc'; } if (isset($newProdcutOrder) && $newProdcutOrder === 1){ $order = 'id desc'; } return $order; } }