diff --git a/app/Admin/Controllers/Store/ProductController.php b/app/Admin/Controllers/Store/ProductController.php index 101024e6..412206d9 100644 --- a/app/Admin/Controllers/Store/ProductController.php +++ b/app/Admin/Controllers/Store/ProductController.php @@ -118,14 +118,17 @@ class ProductController extends AdminController $form = Form::make(StoreProductSku::with(['productSku'])); $form->select('store_id')->options(Store::pluck('title', 'id')); $form->select('product_sku_id')->options(ProductSku::class)->ajax('api/product-skus'); + $form->hidden('product_spu_id'); $form->switch('status', '状态')->default(1); $form->saving(function (Form $form) { if ($form->isCreating()) { $store_id = $form->input('store_id'); $product_sku_id = $form->input('product_sku_id'); + $sku = ProductSku::findOrFail($product_sku_id); if (StoreProductSku::where(compact('store_id', 'product_sku_id'))->exists()) { return $form->response()->error('该商品已经存在'); } + $form->product_spu_id = $sku->spu_id; } }); return $form; diff --git a/app/Admin/Imports/StoreProduct.php b/app/Admin/Imports/StoreProduct.php index 6d5a1028..8c443823 100644 --- a/app/Admin/Imports/StoreProduct.php +++ b/app/Admin/Imports/StoreProduct.php @@ -137,7 +137,7 @@ class StoreProduct $skuList = $product->skus()->get(); $skus = []; foreach($skuList as $sku) { - array_push($skus, ['amount' => $stock, 'product_sku_id' => $sku->id, 'status' => 1, 'store_id' => $store->id]); + array_push($skus, ['amount' => $stock, 'product_sku_id' => $sku->id, 'product_spu_id' => $product->id, 'status' => 1, 'store_id' => $store->id]); } StoreProductSku::insert($skus); } diff --git a/app/Endpoint/Api/Filters/ProductSpuFilter.php b/app/Endpoint/Api/Filters/ProductSpuFilter.php new file mode 100644 index 00000000..59b22b7b --- /dev/null +++ b/app/Endpoint/Api/Filters/ProductSpuFilter.php @@ -0,0 +1,61 @@ +find($id); + + if ($category === null) { + return $this->whereRaw('1=0'); + } + + $ids = $category->descendants()->showable()->pluck('id'); + + if ($ids->isEmpty()) { + $this->where('category_id', $id); + } else { + $this->whereIn('category_id', $ids); + } + } + + /** + * 关键字搜索商品 + * + * @param string $keyword + */ + public function keyword($keyword) + { + $this->where(function ($q) use ($keyword) { + $q->whereLike('name', $keyword)->orWhere('subtitle', 'like', '%'.$keyword.'%'); + }); + } + + /** + * 商品排序 + * + * @param string $sort + */ + public function sort($sort) + { + $column = str_ireplace('-', '', $sort); + + if (in_array($column, ['id', 'price', 'sales', 'release_at'])) { + if ($column === 'price') { + $column = 'sell_price'; + } + + $this->orderBy($column, strpos($sort, '-') === 0 ? 'desc' : 'asc'); + } + } +} diff --git a/app/Endpoint/Api/Http/Controllers/Order/OrderPreController.php b/app/Endpoint/Api/Http/Controllers/Order/OrderPreController.php index d4d6ad18..1bed5210 100644 --- a/app/Endpoint/Api/Http/Controllers/Order/OrderPreController.php +++ b/app/Endpoint/Api/Http/Controllers/Order/OrderPreController.php @@ -9,7 +9,7 @@ use Illuminate\Http\Request; use App\Models\{Order, OrderPre, ProductSku}; use App\Models\Store\Store; use App\Services\OrderService; -use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\{DB, Storage}; use App\Exceptions\BizException; use Throwable; use EasyWeChat\Kernel\Http\StreamResponse; @@ -75,8 +75,16 @@ class OrderPreController extends Controller ] ]); - $app = $this->getWechatApp(); + if (config('app.env') == 'local') { + $url = 'https://ui-avatars.com/api/?name=pdkj'; + $order_pre->update(['qrcode' => $url]); + return response()->json([ + 'order_pre' => $order_pre->id, + 'image' => $url, + ]); + } + $app = $this->getWechatApp(); $scene = http_build_query([ 'o' => $order_pre->id, @@ -92,9 +100,18 @@ class OrderPreController extends Controller ]); if ($response instanceof StreamResponse) { + // 保存小程序码 + $disk = Storage::disk('public'); + $filepath = 'order-pre'; + $filename = $order_pre->id . '.png'; + $response->saveAs($disk->path($filepath), $filename); + + $url = $disk->url($filepath . '/' . $filename); + + $order_pre->update(['qrcode' => $url]); return response()->json([ 'order_pre' => $order_pre->id, - 'image' => 'data:image/png;base64,'.base64_encode($response->getBody()), + 'image' => $url, ]); } diff --git a/app/Endpoint/Api/Http/Controllers/StoreController.php b/app/Endpoint/Api/Http/Controllers/StoreController.php index 06c3c621..09ce6e7d 100644 --- a/app/Endpoint/Api/Http/Controllers/StoreController.php +++ b/app/Endpoint/Api/Http/Controllers/StoreController.php @@ -4,9 +4,11 @@ namespace App\Endpoint\Api\Http\Controllers; use Illuminate\Http\Request; use App\Models\Store\Store; -use App\Models\ProductSku; +use App\Models\{ProductSku, ProductSpu}; use App\Helpers\Paginator; -use App\Endpoint\Api\Http\Resources\{StoreResource, StoreProductResource}; +use App\Endpoint\Api\Http\Resources\{StoreResource, StoreProductSkuResource, StoreProductSpuResource}; +use App\Endpoint\Api\Http\Resources\ProductFeatureResource; +use App\Events\ProductSkuViewed; class StoreController extends Controller { @@ -29,7 +31,7 @@ class StoreController extends Controller return StoreResource::make($info); } - public function products($id, Request $request) + public function productSpus($id, Request $request) { $store = Store::findOrFail($id); @@ -39,15 +41,109 @@ class StoreController extends Controller $input['sort'] = '-id'; } - $fields = ['product_skus.id', 'product_skus.name', 'product_skus.cover', 'product_skus.sell_price', 'product_skus.vip_price', 'product_skus.market_price', 'store_product_skus.amount']; + $fields = ['product_spus.id', 'product_spus.name', 'product_spus.cover', 'product_spus.sell_price', 'product_spus.vip_price', 'product_spus.market_price', 'store_product_skus.amount']; - $skus = $store->productSkus() + $list = $store->productSpus() ->select($fields) + ->distinct('store_product_skus.product_spu_id') ->filter($input) // ->online() ->wherePivot('status', 1) ->simplePaginate(Paginator::resolvePerPage('per_page', 20, 50)); - return StoreProductResource::collection($skus); + return StoreProductSpuResource::collection($list); + } + + public function productSpu($store, $id, Request $request) + { + $store = Store::findOrFail($store); + + $user = $request->user(); + $spu = $store->productSpus()->with(['specs', 'features'])->findOrFail($id); + + $sku = $store->productSkus()->where('product_spu_id', $spu->id)->first(); + if (!$sku) { + throw new BizException('商品sku未上架'); + } + // 主商品的规格 + $spuSpecs = []; + + if (count($original = (array) $sku->specs) > 0) { + $skus = $spu->skus()->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['name']; + + $key = implode('_', $jSpecs); + $mapSku = $mapSkus->get($key); + if ($mapSku) { + $spuSpecItems[] = [ + 'name' => $value['name'], + 'selected' => $sku->is($mapSku), + 'sku_id' => (int) $mapSku?->id, + 'sku_stock' => (int) $mapSku?->saleable_stock, + ]; + } + + } + + $spuSpecs[] = [ + 'name' => $spec->name, + 'items' => $spuSpecItems, + ]; + } + } + + ProductSkuViewed::dispatch($user, $sku, now()); + + // 是否收藏商品 + $isCollected = (bool) $user?->skuFavorites()->where('sku_id', $sku->id)->exists(); + + return response()->json([ + 'spu_specs' => $spuSpecs, + 'sku' => StoreProductSkuResource::make($sku), + 'is_collected' => $isCollected, + 'features' => ProductFeatureResource::collection($spu->features), + 'is_pre_sale' => !!$spu->is_pre_sale + ]); + } + + public function productSkus($id, Request $request) + { + $store = Store::findOrFail($id); + $query = $store->productSkus() + ->select(['product_skus.id', 'product_skus.specs', 'product_skus.name', 'product_skus.cover', 'product_skus.sell_price', 'product_skus.vip_price', 'product_skus.market_price', 'store_product_skus.amount']) + ->filter($request->all()) + ->when($request->filled('id'), function ($q) use ($request) { + $id = explode(',', $request->input('id')); + $q->whereIn('product_skus.id', $id); + }) + ->wherePivot('status', 1); + + if ($request->input('per_page') == -1) { + $list = $query->get(); + } else { + $list = $query->simplePaginate(Paginator::resolvePerPage('per_page', 20, 50)); + } + return StoreProductSkuResource::collection($list); + } + + public function productSku($store, $id, Request $request) + { + $store = Store::findOrFail($store); + $sku = $store->productSkus()->findOrFail($id); + + return StoreProductSkuResource::make($sku); } } diff --git a/app/Endpoint/Api/Http/Resources/OrderPreResource.php b/app/Endpoint/Api/Http/Resources/OrderPreResource.php index c8f6b529..c2c8ba6c 100644 --- a/app/Endpoint/Api/Http/Resources/OrderPreResource.php +++ b/app/Endpoint/Api/Http/Resources/OrderPreResource.php @@ -18,6 +18,7 @@ class OrderPreResource extends JsonResource 'id' => $this->id, 'store_id' => $this->store_id, 'user_id' => $this->user_id, + 'qrcode' => $this->qrcode, 'products' => $this->products, 'others' => $this->others, 'created_at' => $this->created_at->timestamp, diff --git a/app/Endpoint/Api/Http/Resources/StoreProductSkuResource.php b/app/Endpoint/Api/Http/Resources/StoreProductSkuResource.php new file mode 100644 index 00000000..fc66258d --- /dev/null +++ b/app/Endpoint/Api/Http/Resources/StoreProductSkuResource.php @@ -0,0 +1,24 @@ + $this->id, + 'spu_id' => $this->spu_id, + 'name' => $this->name, + 'cover' => (string) $this->cover, + 'sell_price' => (string) $this->sell_price_format, + 'vip_price' => (string) $this->vip_price_format, + 'market_price' => (string) $this->market_price_format, + 'amount' => (int) $this->pivot->amount, + 'stock' => (int) $this->pivot->amount, + 'specs' => (array) $this->specs, + ]; + } +} diff --git a/app/Endpoint/Api/Http/Resources/StoreProductResource.php b/app/Endpoint/Api/Http/Resources/StoreProductSpuResource.php similarity index 85% rename from app/Endpoint/Api/Http/Resources/StoreProductResource.php rename to app/Endpoint/Api/Http/Resources/StoreProductSpuResource.php index f0dcf64e..0dd67792 100644 --- a/app/Endpoint/Api/Http/Resources/StoreProductResource.php +++ b/app/Endpoint/Api/Http/Resources/StoreProductSpuResource.php @@ -4,7 +4,7 @@ namespace App\Endpoint\Api\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; -class StoreProductResource extends JsonResource +class StoreProductSpuResource extends JsonResource { public function toArray($request) { @@ -16,6 +16,7 @@ class StoreProductResource extends JsonResource 'vip_price' => (string) $this->vip_price_format, 'market_price' => (string) $this->market_price_format, 'amount' => (int) $this->pivot->amount, + 'stock' => (int) $this->pivot->amount, 'specs' => (array) $this->specs, ]; } diff --git a/app/Endpoint/Api/routes.php b/app/Endpoint/Api/routes.php index 160e830f..057b58ef 100644 --- a/app/Endpoint/Api/routes.php +++ b/app/Endpoint/Api/routes.php @@ -227,7 +227,10 @@ Route::group([ }); // 门店提货 - Route::get('store/{id}/products', [StoreController::class, 'products']); + Route::get('store/{id}/product-spus', [StoreController::class, 'productSpus']); + Route::get('store/{id}/product-skus', [StoreController::class, 'productSkus']); + Route::get('store/{store}/product-spu/{id}', [StoreController::class, 'productSpu']); + Route::get('store/{store}/product-sku/{id}', [StoreController::class, 'productSku']); Route::apiResource('store', StoreController::class)->only(['index', 'show']); // 门店下单 diff --git a/app/Models/OrderPre.php b/app/Models/OrderPre.php index 2ed7603c..ca7f209d 100644 --- a/app/Models/OrderPre.php +++ b/app/Models/OrderPre.php @@ -10,7 +10,7 @@ class OrderPre extends Model { use HasFactory, HasDateTimeFormatter; - protected $fillable = ['id', 'store_id', 'user_id', 'remarks', 'products', 'others']; + protected $fillable = ['id', 'store_id', 'user_id', 'remarks', 'products', 'others', 'qrcode']; protected $casts = [ 'products' => 'array', diff --git a/app/Models/Store/ProductSku.php b/app/Models/Store/ProductSku.php index b62daeed..73fa8759 100644 --- a/app/Models/Store/ProductSku.php +++ b/app/Models/Store/ProductSku.php @@ -8,7 +8,7 @@ class ProductSku extends Model { protected $table = 'store_product_skus'; - protected $fillable = ['amount', 'product_sku_id', 'status', 'store_id']; + protected $fillable = ['amount', 'product_sku_id', 'status', 'store_id', 'product_spu_id']; protected $attributes = [ 'status' => 0, @@ -26,4 +26,9 @@ class ProductSku extends Model { return $this->belongsTo(\App\Models\ProductSku::class, 'product_sku_id'); } + + public function productSpu() + { + return $this->belongsTo(\App\Models\ProductSpu::class, 'product_spu_id'); + } } diff --git a/app/Models/Store/Store.php b/app/Models/Store/Store.php index 4ad63770..c3eb1484 100644 --- a/app/Models/Store/Store.php +++ b/app/Models/Store/Store.php @@ -39,6 +39,11 @@ class Store extends Model return $this->belongsToMany(\App\Models\ProductSku::class, 'store_product_skus', 'store_id', 'product_sku_id')->withPivot('amount', 'status'); } + public function productSpus() + { + return $this->belongsToMany(\App\Models\ProductSpu::class, 'store_product_skus', 'store_id', 'product_spu_id')->withPivot('amount', 'status'); + } + public function stockLogs() { return $this->hasMany(StockLog::class, 'store_id'); diff --git a/database/migrations/2023_02_10_104133_add_spu_id_to_store_products.php b/database/migrations/2023_02_10_104133_add_spu_id_to_store_products.php new file mode 100644 index 00000000..d4f23456 --- /dev/null +++ b/database/migrations/2023_02_10_104133_add_spu_id_to_store_products.php @@ -0,0 +1,35 @@ +unsignedBigInteger('product_spu_id'); + }); + + DB::statement('update `store_product_skus` set `product_spu_id` = (select `spu_id` from `product_skus` where `product_skus`.`id` = `store_product_skus`.`product_sku_id`);'); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('store_product_skus', function (Blueprint $table) { + $table->dropColumn(['product_spu_id']); + }); + } +} diff --git a/database/migrations/2023_02_10_144100_add_qrcode_to_order_pres.php b/database/migrations/2023_02_10_144100_add_qrcode_to_order_pres.php new file mode 100644 index 00000000..5b2a480b --- /dev/null +++ b/database/migrations/2023_02_10_144100_add_qrcode_to_order_pres.php @@ -0,0 +1,32 @@ +string('qrcode')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('order_pres', function (Blueprint $table) { + $table->dropColumn(['qrcode']); + }); + } +}