提交订单
parent
9733f929dc
commit
8b354a5593
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; // 已取消
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -121,6 +121,16 @@ class UserCoupon extends Model
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将此优惠券标记为已使用
|
||||
*/
|
||||
public function markAsUse()
|
||||
{
|
||||
$this->forceFill([
|
||||
'is_use' => true,
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取此优惠券的面值
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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)), // 实付金额
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue