6
0
Fork 0
jiqu-library-server/app/Services/OrderService.php

830 lines
25 KiB
PHP

<?php
namespace App\Services;
use App\Constants\PayWay;
use App\Endpoint\Api\Http\Resources\ProductSkuSimpleResource;
use App\Endpoint\Api\Http\Resources\ShippingAddressResource;
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\ProductGift;
use App\Models\ProductSku;
use App\Models\ShippingAddress;
use App\Models\User;
use App\Models\UserCoupon;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
class OrderService
{
/**
* @var array
*/
protected $wxpayWays = [
PayWay::WXPAY_APP,
PayWay::WXPAY_JSAPI,
PayWay::WXPAY_MINI,
PayWay::WXPAY_H5,
];
/**
* 快速下单
*
* @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;
}
/**
* 创建订单
*
* @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;
$orderAttrs = [
'sn' => OrderHelper::serialNumber(),
'user_coupon_id' => $coupon?->id,
'coupon_discount_amount' => $couponDiscountAmount,
'vip_discount_amount' => $vipDiscountAmount,
'shipping_fee' => $shippingFee,
'products_total_amount' => $productsTotalAmount,
'total_amount' => $totalAmount,
'note' => $note,
// 收货地址
'consignee_name' => $shippingAddress->consignee,
'consignee_telephone' => $shippingAddress->telephone,
'consignee_zone' => $shippingAddress->zone,
'consignee_address' => $shippingAddress->address,
];
if ($totalAmount === 0) {
$orderAttrs = array_merge([
'status' => Order::STATUS_PAID,
'pay_sn' => OrderHelper::serialNumber(),
'pay_way' => Order::PAY_WAY_NONE,
'pay_at' => now(),
]);
}
$order = $user->orders()->create($orderAttrs);
$orderProducts = [];
foreach ($mapProducts as $product) {
$sku = $product['sku'];
$qty = $product['quantity'];
// 支付金额 = 商品总额 - 优惠券折扣金额- 会员折扣金额
$totalAmount = $product['total_amount'] - $product['coupon_discount_amount'] - $product['vip_discount_amount'];
$orderProducts[] = [
'gift_for_sku_id' => null,
'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' => $qty,
'remain_quantity' => $qty, // 剩余发货数量
'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,
];
// 将赠品加入订单中
$gifts = $this->deductProduct($sku, $qty);
foreach ($gifts as $gift) {
$giftSku = $gift['sku'];
$orderProducts[] = [
'gift_for_sku_id' => $sku->id,
'user_id' => $order->user_id,
'order_id' => $order->id,
'spu_id' => $giftSku->spu_id,
'sku_id' => $giftSku->id,
'category_id' => $giftSku->category_id,
'name' => $giftSku->name,
'specs' => json_encode($giftSku->specs),
'cover' => $giftSku->cover,
'weight' => $giftSku->weight,
'sell_price' => $giftSku->sell_price,
'vip_price' => $giftSku->vip_price,
'quantity' => $gift['num'],
'remain_quantity' => $gift['num'], // 剩余发货数量
'coupon_discount_amount' => 0,
'vip_discount_amount' => 0,
'total_amount' => 0,
'created_at' => $order->created_at,
'updated_at' => $order->updated_at,
];
}
}
// 处理赠品
OrderProduct::insert($orderProducts);
// 将优惠券标记为已使用
$coupon?->markAsUse();
return $order;
}
/**
* 扣商品的库存和赠品数量
*
* @param \App\Models\ProductSku $sku
* @param int $qty
* @return array
*/
protected function deductProduct(ProductSku $sku, int $qty)
{
// 扣商品库存
$sku->update([
'stock' => DB::raw("stock - {$qty}"), // 库存
]);
try {
return retry(5, function () use ($sku, $qty) {
return $this->deductGifts($sku, $qty);
}, 0, function ($e) {
// 如果赠品库存不足时,需重试
return $e instanceof QueryException
&& strpos($e->getMessage(), 'Numeric value out of range') !== false;
});
} catch (QueryException $e) {
// 赠品库存不足
if (strpos($e->getMessage(), 'Numeric value out of range') !== false) {
$e = new BizException('下单人数过多,请稍后再试!');
}
throw $e;
}
}
/**
* 扣出商品的赠品
*
* @param \App\Models\ProductSku $sku
* @param int $qty
* @return array
*/
protected function deductGifts(ProductSku $sku, int $qty)
{
// 赠品
$gifts = [];
if ($qty < 1) {
return $gifts;
}
$sku->gifts->loadMissing('giftSku');
foreach ($sku->gifts as $gift) {
// 如果未找到赠品,则不赠送
if ($gift->giftSku === null) {
continue;
}
// 需赠送礼品的总数
$num = $gift->num * $qty;
// 如果赠品有限,且剩余数量不足时,直接赠送剩余赠品
if ($gift->isLimit() && $num > $gift->remaining) {
// 计算剩余可赠送的份数
$remainingQty = (int) ($gift->remaining / $gift->num);
$num = $gift->num * $remainingQty;
}
if ($gift->isLimit()) {
$gift->update([
'remaining' => DB::raw("remaining-{$num}"),
'sent' => DB::raw("sent+{$num}"),
]);
} else {
$gift->increment('sent', $num);
}
$gifts[] = [
'sku' => $gift->giftSku,
'num' => $num, // 赠送商品总数
];
}
return $gifts;
}
/**
* 确认快速下单
*
* @param \App\Models\User $user
* @param int $skuId
* @param int $quantity
* @param int|null $shippingAddressId
* @param int|null $couponId
* @return array
*/
public function verifyQuickOrder(User $user, int $skuId, int $quantity, ?int $shippingAddressId = null, ?int $couponId = null)
{
$sku = ProductSku::online()->findOrFail($skuId);
$product = [
'sku' => $sku,
'quantity' => $quantity,
];
return $this->verifyOrder($user, [$product], $shippingAddressId, $couponId);
}
/**
* 确认购物车订单
*
* @param \App\Models\User $user
* @param array $shoppingCartItemIds
* @param int|null $shippingAddressId
* @param int|null $couponId
* @return array
*/
public function verifyShoppingCartOrder(User $user, array $shoppingCartItemIds, ?int $shippingAddressId = null, ?int $couponId = null)
{
// 获取购买商品
$products = $this->getProductsByShoppingCart($user, $shoppingCartItemIds);
return $this->verifyOrder($user, $products, $shippingAddressId, $couponId);
}
/**
* 确认订单
*
* @param \App\Models\User $user
* @param array $products
* @param int|null $shippingAddressId
* @param int|null $couponId
* @return array
*/
protected function verifyOrder(User $user, array $products, ?int $shippingAddressId = null, ?int $couponId = null): array
{
// 获取收货地址
$shippingAddress = $this->getShippingAddress($user, $shippingAddressId);
// 优惠券
$coupon = null;
if ($couponId) {
$coupon = $user->coupons()->onlyAvailable()->findOrFail($couponId);
}
$mapProducts = $this->mapProducts($user, $products, $coupon);
// 是否支持配送
$shippingSupported = true;
// 运费
$shippingFee = 0;
if ($shippingAddress) {
try {
$shippingFee = $this->calculateShippingFee($mapProducts, $shippingAddress);
} catch (ShippingNotSupportedException $e) {
$shippingFee = 0;
$shippingSupported = false;
}
}
list(
$productsTotalAmount,
$vipDiscountAmount,
$couponDiscountAmount,
$totalAmount,
) = $this->calculateFees($mapProducts);
// 订单总费用
$totalAmount += $shippingFee;
return [
'products' => collect($mapProducts)->map(function ($item) {
return [
'sku' => ProductSkuSimpleResource::make($item['sku']),
'quantity' => $item['quantity'],
];
}),
'coupons' => UserCouponResource::collection((new CouponService())->getAvailableCoupons($user, $products)),
'shipping_address' => $shippingAddress ? ShippingAddressResource::make($shippingAddress) : null, // 收货地址
'shipping_supported' => $shippingSupported,
'shipping_fee' => Numeric::trimTrailingZero(bcdiv($shippingFee, 100, 2)), // 运费
'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, 100, 2)), // 实付金额
];
}
/**
* 计算费用
*
* @param array $products
* @return array
*/
protected function calculateFees(array $products)
{
$productsTotalAmount = 0;
$vipDiscountAmount = 0;
$couponDiscountAmount = 0;
foreach ($products as $product) {
$productsTotalAmount += $product['total_amount'];
$vipDiscountAmount += $product['vip_discount_amount'];
$couponDiscountAmount += $product['coupon_discount_amount'];
}
// 订单总额
$totalAmount = $productsTotalAmount - $vipDiscountAmount - $couponDiscountAmount;
return [
$productsTotalAmount,
$vipDiscountAmount,
$couponDiscountAmount,
$totalAmount,
];
}
/**
* 计算运费
*
* @param array $products
* @param \App\Models\ShippingAddress $shippingAddress
* @return int
*/
protected function calculateShippingFee(array $products, ShippingAddress $shippingAddress): int
{
// 运费
$shippingFee = 0;
$shippings = [];
foreach ($products as $product) {
$sku = $product['sku'];
if (is_null($sku->shipping_template_id)) {
continue;
}
$shipping = $shippings[$sku->shipping_template_id] ?? [
'total_weight' => 0,
'total_amount' => 0,
];
$shipping['total_weight'] += $sku->weight * $product['quantity'];
$shipping['total_amount'] += $product['total_amount'] - $product['vip_discount_amount'] - $product['coupon_discount_amount'];
$shippings[$sku->shipping_template_id] = $shipping;
}
$shippingService = new ShippingService();
foreach ($shippings as $templateId => $shipping) {
$shippingFee += $shippingService->countShippingAmount(
$shipping['total_weight'],
$shipping['total_amount'],
$templateId,
$shippingAddress->zone_id
);
}
return $shippingFee;
}
/**
* 准备商品信息
*
* @param \App\Models\User $user
* @param array $products
* @param \App\Models\UserCoupon|null $coupon
* @return array
*
* @throws \App\Exceptions\BizException
*/
protected function mapProducts(User $user, array $products, ?UserCoupon $coupon): array
{
$_products = collect($products)->map(function ($item) use ($user) {
$sku = $item['sku'];
return array_merge($item, [
// 优惠券折扣金额
'coupon_discount_amount' => 0,
// 会员折扣金额
'vip_discount_amount' => $this->calculateVipDiscountAmount(
$user, $sku, $item['quantity']
),
// 总金额
'total_amount' => $sku->sell_price * $item['quantity'],
]);
});
if ($coupon === null) {
return $_products->all();
}
$couponDiscountAmounts = $this->getCouponDiscountAmounts($coupon, $_products->all());
return $_products->map(function ($item) use ($couponDiscountAmounts) {
$item['coupon_discount_amount'] = $couponDiscountAmounts[$item['sku']->id] ?? 0;
return $item;
})->all();
}
/**
* 计算会员折扣金额
*
* @param \App\Models\User $user
* @param \App\Models\ProductSku $sku
* @param int $quantity
* @return int
*/
protected function calculateVipDiscountAmount(User $user, ProductSku $sku, int $quantity)
{
$price = $sku->getRealPrice($user);
if ($price > $sku->sell_price) {
return 0;
}
return ($sku->sell_price - $price) * $quantity;
}
/**
* 获取商品的优惠券折扣金额
*
* @param \App\Models\UserCoupon $coupon
* @param array $products
* @return array
*
* @throws \App\Exceptions\BizException
*/
protected function getCouponDiscountAmounts(UserCoupon $coupon, array $products): array
{
// 启用的券的使用规则
$coupon->loadMissing(['ranges' => function ($query) {
$query->isEnable();
}]);
// 可使用优惠券的商品总额
$amounts = [];
foreach ($products as $item) {
$sku = $item['sku'];
if (! $coupon->isSupport($sku)) {
throw new BizException('优惠券不满足使用条件');
}
$amount = $item['total_amount'] - $item['vip_discount_amount'];
// 仅保留商品真实总额大于0的商品
if ($amount > 0) {
$amounts[$sku->id] = $amount;
}
}
// 全部商品总额
$totalAmount = array_sum($amounts);
if ($coupon->coupon_threshold > $totalAmount) {
throw new BizException('优惠券不满足使用条件');
}
// 优惠券折扣金额
$couponAmount = value(function ($coupon, $totalAmount) {
// 如果优惠券是折扣券,则需算出优惠总额
if ($coupon->isDiscountCoupon()) {
return (int) bcmul($totalAmount, bcdiv(100 - $coupon->coupon_amount, 100, 2));
}
// 如果优惠券的折扣金额超过商品总额,则优惠金额为商品总额
if ($coupon->coupon_amount > $totalAmount) {
return $totalAmount;
}
return $coupon->coupon_amount;
}, $coupon, $totalAmount);
// 待计算优惠的商品总数
$i = count($amounts);
foreach ($amounts as &$amount) {
$i--;
if ($i > 0) {
$amount = (int) bcdiv(bcmul($couponAmount, $amount, 0), $totalAmount, 2);
$couponAmount -= $amount;
} else {
$amount = $couponAmount;
}
unset($amount);
}
return $amounts;
}
/**
* 根据购物车获取商品
*
* @param \App\Models\User $user
* @param array $shoppingCartItemIds
* @return array
*
* @throws \App\Exceptions\BizException
*/
protected function getProductsByShoppingCart(User $user, array $shoppingCartItemIds): array
{
$shoppingCartItems = $user->shoppingCartItems()->findMany($shoppingCartItemIds);
if ($shoppingCartItems->count() !== count($shoppingCartItemIds)) {
throw new BizException('购物车商品已丢失');
}
$shoppingCartItems->load('sku');
$lostShoppingCartItems = $shoppingCartItems->filter(function ($item) {
return $item->sku === null;
});
if ($lostShoppingCartItems->count() > 0) {
throw new BizException('购物车商品已失效');
}
return $shoppingCartItems->map(function ($item) {
return [
'sku' => $item->sku,
'quantity' => $item->quantity,
];
})->all();
}
/**
* 获取收货地址
*
* @param \App\Models\User $user
* @param int|null $shippingAddressId
* @return \App\Models\ShippingAddress|null
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*/
protected function getShippingAddress(User $user, ?int $shippingAddressId = null): ?ShippingAddress
{
if ($shippingAddressId) {
return $user->shippingAddresses()->findOrFail($shippingAddressId);
}
return $user->shippingAddresses()->where('is_default', true)->first();
}
/**
* 订单付款
*
* @param \App\Models\Order $order
* @param string $payWay
* @return mixed
*
* @throws \App\Exceptions\WeChatPayException
*/
public function pay(Order $order, string $payWay)
{
if (! $order->isPending()) {
throw new BizException('订单状态不是待付款');
}
if (in_array($payWay, $this->wxpayWays)) {
return (new WeChatPayService())->pay([
'attach' => json_encode([
'pay_way' => $payWay,
]),
'body' => config('settings.app_name').'-商城订单',
'out_trade_no' => $order->sn,
'total_fee' => $order->total_amount,
'notify_url' => url(route('wxpay.order_paid_notify', [], false), [], true),
'trade_type' => PayWay::$wxpayTradeTypes[$payWay],
]);
}
throw new BizException('支付方式不支持');
}
/**
* 支付成功
*
* @param string $sn
* @param array $params
* @return \App\Models\Order
*/
public function paySuccess(string $sn, array $params = []): Order
{
$order = Order::where('sn', $sn)->firstOrFail();
if (! $order->isPending()) {
throw new BizException('订单状态不是待支付');
}
$order->update([
'pay_sn' => $params['pay_sn'],
'pay_way' => $params['pay_way'],
'pay_at' => $params['pay_at'],
'status' => Order::STATUS_PAID,
]);
// todo 处理预收益
return $order;
}
/**
* 确认订单
*
* @param \App\Models\Order $order
* @return void
*/
public function confirm(Order $order)
{
if (! $order->isShipped()) {
throw new BizException('订单包裹未发完');
}
$orderPackageService = new OrderPackageService();
// 获取订单的未作废未签收包裹
$packages = $order->packages()->unchecked()->where('is_failed', false)->get();
foreach ($packages as $package) {
$orderPackageService->checkPackage($package, true);
}
$order->markAsCompleted();
}
/**
* 取消订单
*
* @param \App\Models\Order $order
* @return void
*/
public function cancel(Order $order)
{
if (! $order->isPending() && ! $order->isWaitShipping()) {
throw new BizException('订单状态不是待付款或待发货');
}
if ($order->isWaitShipping()) {
$order->refundTasks()->create([
'sn' => OrderHelper::serialNumber(),
'amount' => $order->total_amount,
'reason' => '取消订单',
]);
}
$products = $order->products()->get();
foreach ($products->load('sku') as $product) {
if ($product->sku === null) {
continue;
}
// 如果商品不是赠品,则直接增加商品库存
if (! $product->isGift()) {
$product->sku->increment('stock', $product->quantity);
continue;
}
$gift = ProductGift::where('sku_id', $product->gift_for_sku_id)
->where('gift_sku_id', $product->sku_id)
->first();
if ($gift === null) {
continue;
}
if ($gift->isLimit()) {
$gift->update([
'remaining' => DB::raw("remaining+{$product->quantity}"),
'sent' => DB::raw("sent-{$product->quantity}"),
]);
} else {
$gift->decrement('sent', $product->quantity);
}
}
$order->update([
'status' => Order::STATUS_CANCELLED,
]);
}
}