Files
xin-procurement/app/Http/Controllers/Mini/CartController.php
T
2026-08-29 13:54:06 +08:00

234 lines
7.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Mini;
use App\Exceptions\RepositoryException;
use App\Http\Requests\Mini\MiniCartRequest;
use App\Models\CartModel;
use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Services\CartService;
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;
use Throwable;
/**
* 小程序购物车
*/
#[RequestAttribute('/mini', 'mini', authGuard: 'users')]
class CartController extends BaseMiniController
{
/** decimal(10,2) 上限 */
private const string MAX_QUANTITY = '99999999.99';
/**
* 加购
* @throws Throwable
*/
#[PostRoute('/cart')]
public function store(MiniCartRequest $request): JsonResponse
{
$store = $this->currentStore($request);
if ($store->level_id <= 0 || $store->level === null) {
return $this->error('门店未设置客户等级,无法加购,请联系客服');
}
$productId = (int) $request->validated('product_id');
$product = ProductModel::where('status', ProductModel::STATUS_ON)->find($productId);
if ($product === null) {
return $this->error('商品不存在或已下架,请刷新后重试');
}
$quantity = (string) $request->validated('quantity');
$cart = DB::transaction(function () use ($store, $productId, $quantity) {
$row = CartModel::query()
->where('store_id', $store->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([
'store_id' => $store->id,
'product_id' => $productId,
'quantity' => $quantity,
]);
});
return $this->success([
'id' => $cart->id,
'quantity' => $cart->quantity,
], '已加入购物车');
}
/**
* 购物车列表
*/
#[GetRoute('/cart', authorize: true)]
public function index(Request $request): JsonResponse
{
$store = $this->currentStore($request);
$rows = CartModel::query()
->where('store_id', $store->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');
// 门店等级(售价 = 成本价 × (100 + 等级上浮比例) / 100
$level = $store->level_id > 0 ? $store->level : null;
// 图片一次查回(避免 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;
// 实际价(按门店等级上浮比例换算);未设等级为 null
$price = $level === null
? null
: CustomerLevelModel::calcLevelPrice((float) ($product?->cost_price ?? 0), $level->percent);
$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 ?? '',
'price_unit' => $product->price_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
{
$store = $this->currentStore($request);
$row = CartModel::query()
->where('id', $id)
->where('store_id', $store->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
{
$store = $this->currentStore($request);
$deleted = CartModel::query()
->where('id', $id)
->where('store_id', $store->id)
->delete();
if ($deleted === 0) {
throw new RepositoryException('购物车项不存在');
}
return $this->success([], '已删除');
}
/**
* 购物车悬浮球汇总(轻量接口):种数/总数量/总金额,供任意页面刷新右下角悬浮球
*/
#[GetRoute('/cart/summary', authorize: true)]
public function summary(Request $request): JsonResponse
{
return $this->success(app(CartService::class)->summary($this->currentStore($request)));
}
/**
* 清空购物车(仅当前用户)
*/
#[DeleteRoute('/cart', authorize: true)]
public function clear(Request $request): JsonResponse
{
$store = $this->currentStore($request);
CartModel::query()->where('store_id', $store->id)->delete();
return $this->success([], '购物车已清空');
}
}