diff --git a/app/Endpoint/Api/Http/Controllers/Order/OrderController.php b/app/Endpoint/Api/Http/Controllers/Order/OrderController.php index 64b6f682..46a591d4 100644 --- a/app/Endpoint/Api/Http/Controllers/Order/OrderController.php +++ b/app/Endpoint/Api/Http/Controllers/Order/OrderController.php @@ -4,8 +4,13 @@ namespace App\Endpoint\Api\Http\Controllers\Order; use App\Endpoint\Api\Http\Controllers\Controller; use App\Endpoint\Api\Http\Resources\OrderSimpleResource; +use App\Exceptions\BizException; use App\Helpers\Paginator as PaginatorHelper; +use App\Services\OrderService; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; +use Throwable; class OrderController extends Controller { @@ -27,4 +32,76 @@ class OrderController extends Controller return OrderSimpleResource::collection($orders); } + + /** + * 创建订单 + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $rules = [ + 'shipping_address_id' => ['bail', 'required', 'int'], + 'coupon_id' => ['bail', 'nullable', 'int'], + 'note' => ['bail', 'nullable', 'string', 'max:255'], + ]; + + if ($isQuick = $request->filled('product')) { + $rules = array_merge($rules, [ + 'product.sku_id' => ['bail', 'required', 'int'], + 'product.quantity' => ['bail', 'required', 'int', 'min:1'], + ]); + } else { + $rules = array_merge($rules, [ + 'shopping_cart' => ['bail', 'required', 'array'], + ]); + } + + $request->validate($rules, [], [ + 'product.sku_id' => '商品', + 'product.quantity' => '数量', + 'shopping_cart' => '购物车商品', + 'shipping_address_id' => '收货地址', + 'coupon_id' => '优惠券', + 'note' => '订单备注', + ]); + + $user = $request->user(); + + try { + $order = retry(3, function () use ($isQuick, $user, $request) { + return DB::transaction(function () use ($isQuick, $user, $request) { + $orderService = new OrderService(); + + if ($isQuick) { + return $orderService->createQuickOrder( + $user, + $request->input('product.sku_id'), + $request->input('product.quantity'), + $request->input('shipping_address_id'), + $request->input('coupon_id'), + $request->input('note'), + ); + } + + return $orderService->createShoppingCartOrder( + $user, + $request->input('shopping_cart'), + $request->input('shipping_address_id'), + $request->input('coupon_id'), + $request->input('note'), + ); + }); + }); + } catch (BizException | ModelNotFoundException $e) { + throw $e; + } catch (Throwable $e) { + report($e); + + throw new BizException('系统繁忙,请稍后再试'); + } + + return OrderSimpleResource::make($order); + } } diff --git a/app/Endpoint/Api/Http/Resources/OrderProductResource.php b/app/Endpoint/Api/Http/Resources/OrderProductResource.php index f4d7a495..969070eb 100644 --- a/app/Endpoint/Api/Http/Resources/OrderProductResource.php +++ b/app/Endpoint/Api/Http/Resources/OrderProductResource.php @@ -20,8 +20,9 @@ class OrderProductResource extends JsonResource 'name' => $this->name, 'cover' => $this->cover, 'specs' => array_values((array) $this->specs), + 'sell_price' => $this->sell_price_format, + 'vip_price' => $this->vip_price_format, 'quantity' => $this->quantity, - 'total_amount' => $this->total_amount_format, ]; } } diff --git a/app/Endpoint/Api/Http/Resources/OrderSimpleResource.php b/app/Endpoint/Api/Http/Resources/OrderSimpleResource.php index f006f3bf..05483733 100644 --- a/app/Endpoint/Api/Http/Resources/OrderSimpleResource.php +++ b/app/Endpoint/Api/Http/Resources/OrderSimpleResource.php @@ -19,8 +19,8 @@ class OrderSimpleResource extends JsonResource 'sn' => $this->sn, 'total_amount' => $this->total_amount_format, 'status' => $this->status, - 'products' => OrderProductResource::collection($this->whenLoaded('products')), 'created_date' => $this->created_at->toDateString(), + 'products' => OrderProductResource::collection($this->whenLoaded('products')), ]; } } diff --git a/app/Models/Order.php b/app/Models/Order.php index e1be9247..89a083fe 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -11,6 +11,7 @@ class Order extends Model use Filterable; public const STATUS_PENDING = 0; // 待付款 + public const STATUS_PAID = 1; // 已付款 public const STATUS_COMPLETED = 9; // 已完成 public const STATUS_CANCELLED = 10; // 已取消 diff --git a/app/Models/OrderProduct.php b/app/Models/OrderProduct.php index bc6bd4c2..d9f33a6c 100644 --- a/app/Models/OrderProduct.php +++ b/app/Models/OrderProduct.php @@ -37,13 +37,27 @@ class OrderProduct extends Model ]; /** - * 获取订单支付金额 + * 获取订单商品真实价格 * * @return string */ - public function getTotalAmountFormatAttribute() + public function getSellPriceFormatAttribute() { - return Numeric::trimTrailingZero(bcdiv($this->attributes['total_amount'], 100, 2)); + return Numeric::trimTrailingZero(bcdiv($this->attributes['sell_price'], 100, 2)); + } + + /** + * 获取订单商品真实价格 + * + * @return string + */ + public function getVipPriceFormatAttribute() + { + if (is_null($price = $this->attributes['vip_price'])) { + return ''; + } + + return Numeric::trimTrailingZero(bcdiv($price, 100, 2)); } public function packageProducts() diff --git a/app/Models/UserCoupon.php b/app/Models/UserCoupon.php index 8bb91fff..70106c8b 100644 --- a/app/Models/UserCoupon.php +++ b/app/Models/UserCoupon.php @@ -121,6 +121,16 @@ class UserCoupon extends Model return false; } + /** + * 将此优惠券标记为已使用 + */ + public function markAsUse() + { + $this->forceFill([ + 'is_use' => true, + ])->save(); + } + /** * 获取此优惠券的面值 * diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php index dae69620..195ca019 100644 --- a/app/Services/OrderService.php +++ b/app/Services/OrderService.php @@ -8,13 +8,77 @@ use App\Endpoint\Api\Http\Resources\UserCouponResource; use App\Exceptions\BizException; use App\Exceptions\ShippingNotSupportedException; use App\Helpers\Numeric; +use App\Helpers\Order as OrderHelper; +use App\Models\Order; +use App\Models\OrderProduct; use App\Models\ProductSku; use App\Models\ShippingAddress; use App\Models\User; use App\Models\UserCoupon; +use Illuminate\Support\Facades\DB; class OrderService { + /** + * 快速下单 + * + * @param \App\Models\User $user + * @param int $skuId + * @param int $quantity + * @param int $shippingAddressId + * @param int|null $couponId + * @param string|null $note + * @return \App\Models\Order + */ + public function createQuickOrder( + User $user, + int $skuId, + int $quantity, + int $shippingAddressId, + ?int $couponId = null, + ?string $note = null + ): Order { + $sku = ProductSku::online()->findOrFail($skuId); + + $product = [ + 'sku' => $sku, + 'quantity' => $quantity, + ]; + + return $this->createOrder($user, [$product], $shippingAddressId, $couponId, $note); + } + + /** + * 购物车下单 + * + * @param \App\Models\User $user + * @param int $skuId + * @param int $quantity + * @param int $shippingAddressId + * @param int|null $couponId + * @param string|null $note + * @return \App\Models\Order + */ + public function createShoppingCartOrder( + User $user, + array $shoppingCartItemIds, + int $shippingAddressId, + ?int $couponId = null, + ?string $note = null, + ): Order { + $order = $this->createOrder( + $user, + $this->getProductsByShoppingCart($user, $shoppingCartItemIds), + $shippingAddressId, + $couponId, + $note + ); + + $user->shoppingCartItems()->whereIn('id', $shoppingCartItemIds)->delete(); + + return $order; + } + /** * 确认快速下单 * @@ -54,6 +118,129 @@ class OrderService return $this->verifyOrder($user, $products, $shippingAddressId, $couponId); } + /** + * 创建订单 + * + * @param \App\Models\User $user + * @param array $products + * @param int $shippingAddressId + * @param int|null $couponId + * @param string|null $note + * @return \App\Models\Order + */ + protected function createOrder( + User $user, + array $products, + int $shippingAddressId, + ?int $couponId = null, + ?string $note = null, + ): Order { + foreach ($products as $product) { + $sku = $product['sku']; + + if ($product['quantity'] > $sku->saleable_stock) { + throw new BizException('商品库存不足'); + } + } + + $shippingAddress = $this->getShippingAddress($user, $shippingAddressId); + + // 优惠券 + $coupon = null; + + if ($couponId) { + $coupon = $user->coupons()->onlyAvailable()->lockForUpdate()->findOrFail($couponId); + } + + $mapProducts = $this->mapProducts($user, $products, $coupon); + + // 计算运费 + $shippingFee = $this->calculateShippingFee($mapProducts, $shippingAddress); + + list( + $productsTotalAmount, + $vipDiscountAmount, + $couponDiscountAmount, + $totalAmount, + ) = $this->calculateFees($mapProducts); + + // 订单总费用 + $totalAmount += $shippingFee; + + $order = $user->orders()->create([ + 'sn' => OrderHelper::serialNumber(), + 'coupon_discount_amount' => $couponDiscountAmount, + 'vip_discount_amount' => $vipDiscountAmount, + 'shipping_fee' => $shippingFee, + 'products_total_amount' => $productsTotalAmount, + 'total_amount' => $totalAmount, + 'status' => $totalAmount > 0 ? Order::STATUS_PENDING : Order::STATUS_PAID, + 'note' => $note, + // 收货地址 + 'consignee_name' => $shippingAddress->consignee, + 'consignee_telephone' => $shippingAddress->telephone, + 'consignee_zone' => $shippingAddress->zone, + 'consignee_address' => $shippingAddress->address, + ]); + + $data = []; + + foreach ($mapProducts as $product) { + $sku = $product['sku']; + + // 支付金额 = 商品总额 - 优惠券折扣金额- 会员折扣金额 + $totalAmount = $product['total_amount'] - $product['coupon_discount_amount'] - $product['vip_discount_amount']; + + $data[] = [ + 'user_id' => $order->user_id, + 'order_id' => $order->id, + 'spu_id' => $sku->spu_id, + 'sku_id' => $sku->id, + 'category_id' => $sku->category_id, + 'name' => $sku->name, + 'specs' => json_encode($sku->specs), + 'cover' => $sku->cover, + 'weight' => $sku->weight, + 'sell_price' => $sku->sell_price, + 'vip_price' => $sku->vip_price, + 'quantity' => $product['quantity'], + 'coupon_discount_amount' => $product['coupon_discount_amount'], + 'vip_discount_amount' => $product['vip_discount_amount'], + 'total_amount' => $totalAmount, + 'created_at' => $order->created_at, + 'updated_at' => $order->updated_at, + ]; + + $this->deductSkuStock($sku, $product['quantity']); + } + + OrderProduct::insert($data); + + // 将优惠券标记为已使用 + $coupon?->markAsUse(); + + return $order; + } + + /** + * 扣除商品库存 + * + * @param \App\Models\ProductSku $sku + * @param int $quantity + * @return void + */ + protected function deductSkuStock(ProductSku $sku, int $quantity): void + { + $sku->update([ + 'stock' => DB::raw("stock + {$quantity}"), // 库存 + 'sales' => DB::raw("sales + {$quantity}"), // 销量 + ]); + + $sku->spu->update([ + 'sales' => DB::raw("sales + {$quantity}"), // 销量 + ]); + } + /** * 确认订单 * @@ -98,6 +285,9 @@ class OrderService $totalAmount, ) = $this->calculateFees($mapProducts); + // 订单总费用 + $totalAmount += $shippingFee; + return [ 'products' => collect($mapProducts)->map(function ($item) { return [ @@ -112,7 +302,7 @@ class OrderService 'products_total_amount' => Numeric::trimTrailingZero(bcdiv($productsTotalAmount, 100, 2)), // 商品总额 'vip_discount_amount' => Numeric::trimTrailingZero(bcdiv($vipDiscountAmount, 100, 2)), // 会员折扣金额 'coupon_discount_amount' => Numeric::trimTrailingZero(bcdiv($couponDiscountAmount, 100, 2)), // 优惠券折扣金额 - 'total_amount' => Numeric::trimTrailingZero(bcdiv($totalAmount + $shippingFee, 100, 2)), // 实付金额 + 'total_amount' => Numeric::trimTrailingZero(bcdiv($totalAmount, 100, 2)), // 实付金额 ]; }