diff --git a/app/Endpoint/Api/Filters/ProductSkuFilter.php b/app/Endpoint/Api/Filters/ProductSkuFilter.php index 11aac483..9ced0662 100644 --- a/app/Endpoint/Api/Filters/ProductSkuFilter.php +++ b/app/Endpoint/Api/Filters/ProductSkuFilter.php @@ -48,14 +48,12 @@ class ProductSkuFilter extends ModelFilter { $column = str_ireplace('-', '', $sort); - if (in_array($column, ['price', 'sales', 'release_at'])) { + if (in_array($column, ['id', 'price', 'sales', 'release_at'])) { if ($column === 'price') { $column = 'sell_price'; } - return $this->orderBy($column, strpos($sort, '-') === 0 ? 'desc' : 'asc'); + $this->orderBy($column, strpos($sort, '-') === 0 ? 'desc' : 'asc'); } - - $this->orderBy($column, 'desc'); } } diff --git a/app/Endpoint/Api/Http/Controllers/Product/HotController.php b/app/Endpoint/Api/Http/Controllers/Product/HotController.php index daf44726..a9936be8 100644 --- a/app/Endpoint/Api/Http/Controllers/Product/HotController.php +++ b/app/Endpoint/Api/Http/Controllers/Product/HotController.php @@ -15,11 +15,11 @@ class HotController extends Controller */ public function __invoke() { - $skus = ProductSku::isRelease() - ->whereRelation('category', 'is_show', true) - ->latest('sales') - ->limit(20) - ->get(); + $skus = ProductSku::online() + ->whereRelation('category', 'is_show', true) + ->latest('sales') + ->limit(20) + ->get(); return ProductSkuSimpleResource::collection($skus); } diff --git a/app/Endpoint/Api/Http/Controllers/Product/ProductController.php b/app/Endpoint/Api/Http/Controllers/Product/ProductController.php index 85fda6ff..9a1726d3 100644 --- a/app/Endpoint/Api/Http/Controllers/Product/ProductController.php +++ b/app/Endpoint/Api/Http/Controllers/Product/ProductController.php @@ -3,11 +3,13 @@ namespace App\Endpoint\Api\Http\Controllers\Product; use App\Endpoint\Api\Http\Controllers\Controller; +use App\Endpoint\Api\Http\Resources\ProductSku\ProduckSkuResource; use App\Endpoint\Api\Http\Resources\ProductSku\ProductSkuSimpleResource; use App\Helpers\Paginator as PaginatorHelper; use App\Models\ProductPart; use App\Models\ProductPartSku; use App\Models\ProductSku; +use App\Models\ProductSpu; use Illuminate\Http\Request; use Illuminate\Pagination\Paginator; @@ -28,6 +30,83 @@ class ProductController extends Controller return ProductSkuSimpleResource::collection($skus); } + /** + * 商品详情 + * + * @param int $id + * @return \Illuminate\Http\JsonResponse + */ + public function show($id) + { + $sku = ProductSku::findOrFail($id); + + if (! $sku->isOnline()) { + return response()->json([ + 'spu_specs' => [], + 'sku' => array_merge(ProduckSkuResource::make($sku)->resolve(), [ + 'status' => ProductSku::STATUS_OFFLINE, + ]), + ]); + } + + $spu = ProductSpu::with('specs')->findOrFail($sku->spu_id); + + // 如果商品已失效 + if (! $spu->isValidProductSku($sku)) { + return response()->json([ + 'spu_specs' => [], + 'sku' => array_merge(ProduckSkuResource::make($sku)->resolve(), [ + 'status' => ProductSku::STATUS_INVALID, + ]), + ]); + } + + // 主商品的规格 + $spuSpecs = []; + + if (count($original = (array) $sku->specs) > 0) { + $skus = $spu->skus()->online()->get(['id', 'specs', 'stock', 'release_at']); + + $mapSkus = $skus->mapWithKeys(function ($item) { + $key = implode('_', $item->specs) ?: $item->id; + + return [$key => $item]; + }); + + foreach ($spu->specs as $spec) { + $spuSpecItems = []; + + foreach ($spec->items as $value) { + // 根据当前 SKU 的规格,组装可能出现的其它规格组合 + $jSpecs = $original; + $jSpecs[$spec->id] = $value; + + $key = implode('_', $jSpecs); + $mapSku = $mapSkus->get($key); + + $spuSpecItems[] = [ + 'name' => $value, + 'selected' => $sku->is($mapSku), + 'sku_id' => (int) $mapSku?->id, + 'sku_stock' => (int) $mapSku?->stock, + ]; + } + + $spuSpecs[] = [ + 'name' => $spec->name, + 'items' => $spuSpecItems, + ]; + } + } + + return response()->json([ + 'spu_specs' => $spuSpecs, + 'sku' => array_merge(ProduckSkuResource::make($sku)->resolve(), [ + 'status' => ProductSku::STATUS_ONLINE, + ]), + ]); + } + /** * 过滤商品 * @@ -44,7 +123,7 @@ class ProductController extends Controller return ProductSku::select(['id', 'name', 'cover', 'sell_price', 'vip_price', 'sales']) ->filter($input) - ->isRelease() + ->online() ->whereRelation('category', 'is_show', true) ->simplePaginate(PaginatorHelper::resolvePerPage('per_page', 20, 50)); } @@ -65,7 +144,7 @@ class ProductController extends Controller $paginator = ProductPartSku::with('sku:id,name,cover,sell_price,vip_price,sales') ->whereHas('sku', function ($query) { - $query->isRelease()->whereRelation('category', 'is_show', true); + $query->online()->whereRelation('category', 'is_show', true); }) ->where('part_id', $productPart->id) ->latest('sort') diff --git a/app/Endpoint/Api/Http/Resources/ProductSku/ProduckSkuResource.php b/app/Endpoint/Api/Http/Resources/ProductSku/ProduckSkuResource.php new file mode 100644 index 00000000..3e7d303f --- /dev/null +++ b/app/Endpoint/Api/Http/Resources/ProductSku/ProduckSkuResource.php @@ -0,0 +1,33 @@ + $this->id, + 'name' => $this->name, + 'subtitle' => (string) $this->subtitle, + 'cover' => (string) $this->cover, + 'media' => (string) $this->media, + 'images' => $this->images, + 'sell_price' => $this->sell_price, + 'vip_price' => (string) $this->vip_price, + 'sales' => $this->sales, + 'description' => (string) $this->description, + 'attrs' => $this->attrs, + 'stock' => $this->saleable_stock, + 'weight' => (int) $this->weight, + ]; + } +} diff --git a/app/Endpoint/Api/routes.php b/app/Endpoint/Api/routes.php index c2c778f9..0d144245 100644 --- a/app/Endpoint/Api/routes.php +++ b/app/Endpoint/Api/routes.php @@ -30,7 +30,9 @@ Route::group([ Route::prefix('product')->group(function () { Route::get('categories', [CategoryController::class, 'index']); + Route::get('hot', HotController::class); Route::get('products', [ProductController::class, 'index']); + Route::get('products/{product}', [ProductController::class, 'show']); }); }); diff --git a/app/Models/ProductSku.php b/app/Models/ProductSku.php index 96cf2827..d07cf80b 100644 --- a/app/Models/ProductSku.php +++ b/app/Models/ProductSku.php @@ -13,6 +13,10 @@ class ProductSku extends Model use Filterable; use HasDateTimeFormatter; + public const STATUS_INVALID = -1; // 无效的 + public const STATUS_ONLINE = 1; // 已上架 + public const STATUS_OFFLINE = 2; // 已下架 + /** * @var array */ @@ -56,11 +60,19 @@ class ProductSku extends Model * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ - public function scopeIsRelease($query) + public function scopeOnline($query) { return $query->whereNotNull('release_at'); } + /** + * 此商品所属的 SPU + */ + public function spu() + { + return $this->belongsTo(ProductSpu::class, 'spu_id'); + } + /** * 此商品所属的分类 */ @@ -76,4 +88,28 @@ class ProductSku extends Model { return $this->belongsToMany(ProductPart::class, ProductPartSku::class, 'sku_id', 'part_id'); } + + /** + * 确认此商品是否已上架 + * + * @return bool + */ + public function isOnline(): bool + { + return $this->release_at !== null; + } + + /** + * 获取此商品的可售库存 + * + * @return int + */ + public function getSaleableStockAttribute(): int + { + if ($this->isOnline()) { + return $this->attributes['stock']; + } + + return 0; + } } diff --git a/app/Models/ProductSpu.php b/app/Models/ProductSpu.php index ed7be744..ebfa47a8 100644 --- a/app/Models/ProductSpu.php +++ b/app/Models/ProductSpu.php @@ -42,6 +42,11 @@ class ProductSpu extends Model 'release_at', ]; + public function skus() + { + return $this->hasMany(ProductSku::class, 'spu_id'); + } + public function specs() { return $this->hasMany(ProductSpuSpec::class, 'product_spu_id'); @@ -52,9 +57,40 @@ class ProductSpu extends Model return $this->belongsToMany(ProductFeature::class, 'product_spu_features', 'feature_id', 'spu_id'); } - public function hasSku() + /** + * 确认此主商品是否有 sku + * + * @return bool + */ + public function hasSku(): bool { - // dd($this->hasMany(ProductSku::class, 'spu_id')->exists()); - return $this->hasMany(ProductSku::class, 'spu_id')->exists(); + return $this->skus()->exists(); + } + + /** + * 确认给定的 SKU 是否有效 + * + * @param \App\Models\ProductSku $sku + * @return bool + */ + public function isValidProductSku(ProductSku $sku): bool + { + if ($this->getKey() !== $sku->spu_id) { + return false; + } + + if (count($specs = (array) $sku->specs) === $this->specs->count()) { + foreach ($this->specs as $spec) { + $key = $spec->getKey(); + + if (! array_key_exists($key, $specs) || ! in_array($specs[$key], $spec->items)) { + return false; + } + } + + return true; + } + + return false; } } diff --git a/resources/lang/zh_CN/models.php b/resources/lang/zh_CN/models.php index afd366a3..5272c27b 100644 --- a/resources/lang/zh_CN/models.php +++ b/resources/lang/zh_CN/models.php @@ -1,5 +1,9 @@ '用户', + ProductSpu::class => '商品', + ProductSku::class => '商品', ];