购物车
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Mini;
|
||||
|
||||
use App\Exceptions\RepositoryException;
|
||||
use App\Http\Requests\Mini\MiniCartRequest;
|
||||
use App\Models\CartModel;
|
||||
use App\Models\ProductModel;
|
||||
use App\Models\ProductPriceModel;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\SystemTool\Models\SysFileModel;
|
||||
|
||||
/**
|
||||
* 小程序购物车(门店订货车:加购 / 列表 / 改数量 / 删项 / 清空)
|
||||
* 提交订货单复用 POST /mini/order,购物车仅作前置编辑容器
|
||||
*/
|
||||
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
|
||||
class CartController extends BaseMiniController
|
||||
{
|
||||
/** decimal(10,2) 上限 */
|
||||
private const MAX_QUANTITY = '99999999.99';
|
||||
|
||||
/**
|
||||
* 加购:商品上架 + 门店有等级价(与下单一致 fail-fast),同商品合并累加
|
||||
*/
|
||||
#[PostRoute('/cart', authorize: true)]
|
||||
public function store(MiniCartRequest $request): JsonResponse
|
||||
{
|
||||
$user = $this->currentUser($request);
|
||||
$store = $this->ensureStoreBound($user);
|
||||
if ($store->level_id <= 0) {
|
||||
throw new RepositoryException('门店未设置客户等级,无法加购,请联系客服');
|
||||
}
|
||||
|
||||
$productId = (int) $request->validated('product_id');
|
||||
// Eloquent 查询自带 SoftDeletes 全局作用域:软删除/下架一并在内
|
||||
$product = ProductModel::where('status', ProductModel::STATUS_ON)->find($productId);
|
||||
if ($product === null) {
|
||||
throw new RepositoryException('商品不存在或已下架,请刷新后重试');
|
||||
}
|
||||
$price = ProductPriceModel::query()
|
||||
->where('product_id', $productId)
|
||||
->where('level_id', $store->level_id)
|
||||
->value('price');
|
||||
if ($price === null) {
|
||||
throw new RepositoryException('商品「' . $product->name . '」未设置您所在等级的价格,无法加购');
|
||||
}
|
||||
|
||||
$quantity = (string) $request->validated('quantity');
|
||||
|
||||
$cart = DB::transaction(function () use ($user, $productId, $quantity) {
|
||||
$row = CartModel::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('product_id', $productId)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if ($row !== null) {
|
||||
$merged = bcadd((string) $row->quantity, $quantity, 2);
|
||||
if (bccomp($merged, self::MAX_QUANTITY, 2) > 0) {
|
||||
throw new RepositoryException('该商品在购物车中的数量已达上限');
|
||||
}
|
||||
$row->quantity = $merged;
|
||||
$row->save();
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
return CartModel::create([
|
||||
'user_id' => $user->id,
|
||||
'product_id' => $productId,
|
||||
'quantity' => $quantity,
|
||||
]);
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'id' => $cart->id,
|
||||
'quantity' => $cart->quantity,
|
||||
], '已加入购物车');
|
||||
}
|
||||
|
||||
/**
|
||||
* 购物车列表:当前用户全部项 + 实时等级价,逐项服务端 bcmul 算金额;
|
||||
* status=1 可购 / 0 商品下架、缺失或未设等级价;汇总只统计可购项
|
||||
*/
|
||||
#[GetRoute('/cart', authorize: true)]
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->currentUser($request);
|
||||
$store = $this->ensureStoreBound($user);
|
||||
|
||||
$rows = CartModel::query()
|
||||
->where('user_id', $user->id)
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
|
||||
if ($rows->isEmpty()) {
|
||||
return $this->success([
|
||||
'items' => [],
|
||||
'total_count' => 0,
|
||||
'total_quantity' => '0.00',
|
||||
'total_amount' => '0.00',
|
||||
]);
|
||||
}
|
||||
|
||||
$productIds = $rows->pluck('product_id')
|
||||
->map(static fn ($id) => (int) $id)
|
||||
->unique()->values()->all();
|
||||
|
||||
$products = ProductModel::withTrashed()->whereIn('id', $productIds)->get()->keyBy('id');
|
||||
$prices = ProductPriceModel::query()
|
||||
->where('level_id', $store->level_id)
|
||||
->whereIn('product_id', $productIds)
|
||||
->pluck('price', 'product_id');
|
||||
|
||||
// 图片一次查回(避免 ProductModel::$appends images_arr 的 N+1)。
|
||||
// 注意:image_ids 有 imageIds Attribute 访问器(get 返回数组),需取原始值
|
||||
$allFileIds = [];
|
||||
foreach ($products as $product) {
|
||||
foreach (explode(',', (string) $product->getRawOriginal('image_ids')) as $fileId) {
|
||||
if ($fileId !== '') {
|
||||
$allFileIds[] = (int) $fileId;
|
||||
}
|
||||
}
|
||||
}
|
||||
$fileMap = SysFileModel::query()
|
||||
->whereIn('id', $allFileIds)
|
||||
->get()->keyBy('id');
|
||||
|
||||
$items = [];
|
||||
$totalQuantity = '0.00';
|
||||
$totalAmount = '0.00';
|
||||
foreach ($rows as $row) {
|
||||
$product = $products->get($row->product_id);
|
||||
$productOn = $product !== null && $product->status === ProductModel::STATUS_ON;
|
||||
$price = $prices->get($row->product_id); // string|null
|
||||
$buyable = $productOn && $price !== null;
|
||||
$quantity = (string) $row->quantity;
|
||||
|
||||
$firstFileId = (int) (explode(',', (string) $product->getRawOriginal('image_ids'))[0] ?? 0);
|
||||
$firstFile = $firstFileId > 0 ? $fileMap->get($firstFileId) : null;
|
||||
|
||||
$item = [
|
||||
'id' => $row->id,
|
||||
'product_id' => $row->product_id,
|
||||
'name' => $product->name ?? '',
|
||||
'spec' => $product->spec ?? '',
|
||||
'unit' => $product->unit ?? '',
|
||||
'image' => $firstFile?->file_url ?? '',
|
||||
'price' => $price,
|
||||
'quantity' => $quantity,
|
||||
'amount' => $buyable ? bcmul($price, $quantity, 2) : null,
|
||||
'status' => $buyable ? 1 : 0,
|
||||
];
|
||||
$items[] = $item;
|
||||
|
||||
if ($buyable) {
|
||||
$totalQuantity = bcadd($totalQuantity, $quantity, 2);
|
||||
$totalAmount = bcadd($totalAmount, $item['amount'], 2);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'items' => $items,
|
||||
'total_count' => count($items),
|
||||
'total_quantity' => $totalQuantity,
|
||||
'total_amount' => $totalAmount,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改数量(校验归属)
|
||||
*/
|
||||
#[PutRoute('/cart/{id}', authorize: true, where: ['id' => '[0-9]+'])]
|
||||
public function update(int $id, MiniCartRequest $request): JsonResponse
|
||||
{
|
||||
$user = $this->currentUser($request);
|
||||
$this->ensureStoreBound($user);
|
||||
|
||||
$row = CartModel::query()
|
||||
->where('id', $id)
|
||||
->where('user_id', $user->id)
|
||||
->first();
|
||||
if ($row === null) {
|
||||
throw new RepositoryException('购物车项不存在');
|
||||
}
|
||||
|
||||
$row->quantity = (string) $request->validated('quantity');
|
||||
$row->save();
|
||||
|
||||
return $this->success(['id' => $row->id, 'quantity' => $row->quantity], '已修改数量');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单项(校验归属)
|
||||
*/
|
||||
#[DeleteRoute('/cart/{id}', authorize: true, where: ['id' => '[0-9]+'])]
|
||||
public function destroy(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->currentUser($request);
|
||||
$this->ensureStoreBound($user);
|
||||
|
||||
$deleted = CartModel::query()
|
||||
->where('id', $id)
|
||||
->where('user_id', $user->id)
|
||||
->delete();
|
||||
if ($deleted === 0) {
|
||||
throw new RepositoryException('购物车项不存在');
|
||||
}
|
||||
|
||||
return $this->success([], '已删除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空购物车(仅当前用户)
|
||||
*/
|
||||
#[DeleteRoute('/cart', authorize: true)]
|
||||
public function clear(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->currentUser($request);
|
||||
$this->ensureStoreBound($user);
|
||||
|
||||
CartModel::query()->where('user_id', $user->id)->delete();
|
||||
|
||||
return $this->success([], '购物车已清空');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user