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

883 lines
27 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Services;
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\Models\Coupon;
use App\Models\DistributionPreIncomeJob;
use App\Models\Order;
use App\Models\OrderProduct;
use App\Models\PayLog;
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
{
/**
* 快速下单
*
* @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,
) = $this->calculateFees($mapProducts);
$order = $this->storeOrder(
$user,
$shippingAddress,
$productsTotalAmount,
$couponDiscountAmount,
$vipDiscountAmount,
$shippingFee,
$note,
$coupon
);
$this->storeOrderProducts($order, $mapProducts);
// 将优惠券标记为已使用
$coupon?->markAsUse();
if ($order->total_amount === 0) {
$this->pay($order, PayLog::PAY_WAY_BALANCE);
$order->refresh();
}
return $order;
}
/**
* 保存订单
*
* @param \App\Models\User $user
* @param \App\Models\ShippingAddress $shippingAddress
* @param int $productsTotalAmount
* @param int $couponDiscountAmount
* @param int $vipDiscountAmount
* @param int $shippingFee
* @param string|null $note
* @param \App\Models\Coupon|null $coupon
* @return \App\Models\Order
*/
protected function storeOrder(
User $user,
ShippingAddress $shippingAddress,
int $productsTotalAmount,
int $couponDiscountAmount,
int $vipDiscountAmount,
int $shippingFee,
?string $note = null,
?Coupon $coupon = null,
): Order {
// 订单支付金额=商品总额-券折扣金额-会员折扣金额+邮费
$totalAmount = $productsTotalAmount - $couponDiscountAmount - $vipDiscountAmount;
if ($totalAmount < 0) {
$totalAmount = 0;
}
$totalAmount += $shippingFee;
do {
// 如果订单号重复,则直接重试
try {
$attrs = [
'sn' => serial_number(),
'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,
];
return $user->orders()->create($attrs);
} catch (QueryException $e) {
if (strpos($e->getMessage(), 'Duplicate entry') === false) {
throw $e;
}
}
} while (true);
}
/**
* 保存订单商品
*
* @param \App\Models\Order $order
* @param array $mapProducts
* @return void
*/
public function storeOrderProducts(Order $order, array $mapProducts)
{
$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,
'sales_value' => $sku->sales_value,
'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,
'sales_value' => 0, // 赠品不算销售值
'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);
}
/**
* 扣商品的库存和赠品数量
*
* @param \App\Models\ProductSku $sku
* @param int $qty
* @return array
*/
protected function deductProduct(ProductSku $sku, int $qty)
{
// 扣商品库存
$sku->update([
'stock' => DB::raw("stock - {$qty}"), // 库存
]);
// 如果是因为赠品库存不足引起的异常,则需重试
do {
try {
return $this->deductGifts($sku, $qty);
} catch (QueryException $e) {
if (strpos($e->getMessage(), 'Numeric value out of range') === false) {
throw $e;
}
}
} while (true);
}
/**
* 扣出商品的赠品
*
* @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,
) = $this->calculateFees($mapProducts);
$totalAmount = $productsTotalAmount - $couponDiscountAmount - $vipDiscountAmount;
if ($totalAmount < 0) {
$totalAmount = 0;
}
$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' => trim_trailing_zeros(bcdiv($shippingFee, 100, 2)), // 运费
'products_total_amount' => trim_trailing_zeros(bcdiv($productsTotalAmount, 100, 2)), // 商品总额
'vip_discount_amount' => trim_trailing_zeros(bcdiv($vipDiscountAmount, 100, 2)), // 会员折扣金额
'coupon_discount_amount' => trim_trailing_zeros(bcdiv($couponDiscountAmount, 100, 2)), // 优惠券折扣金额
'total_amount' => trim_trailing_zeros(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'];
}
return [
$productsTotalAmount,
$vipDiscountAmount,
$couponDiscountAmount,
];
}
/**
* 计算运费
*
* @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('订单状态不是待付款');
}
do {
$payLog = null;
try {
$payLog = $order->payLogs()->create([
'pay_sn' => serial_number(),
'pay_way' => $payWay,
]);
} catch (QueryException $e) {
if (strpos($e->getMessage(), 'Duplicate entry') === false) {
throw $e;
}
}
} while ($payLog === null);
// 如果支付方式为线下支付,或支付金额为 0则按支付完成处理
if ($payLog->isOffline() || $order->total_amount === 0) {
return (new PayService())->handleSuccess($payLog, [
'pay_at' => now(),
]);
}
if ($payLog->isWxpay()) {
if (! isset(WeChatPayService::$tradeTypes[$payLog->pay_way])) {
throw new BizException('支付方式不支持');
}
return (new WeChatPayService())->pay([
'body' => app_settings('app_name').'-商城订单',
'out_trade_no' => $payLog->pay_sn,
'total_fee' => $order->total_amount,
'trade_type' => WeChatPayService::$tradeTypes[$payLog->pay_way],
]);
}
return [
'pay_sn' => $payLog->pay_sn,
'pay_way' => $payLog->pay_way,
];
}
/**
* 确认订单
*
* @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()) {
$refundLog = $order->refundLogs()->create([
'sn' => serial_number(),
'amount' => $order->total_amount,
'reason' => '取消订单',
]);
DistributionPreIncomeJob::create([
'jobable_id' => $refundLog->id,
'jobable_type' => $refundLog->getMorphClass(),
'remarks' => '取消订单',
]);
}
$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);
}
}
// 取消订单,退券
if ($order->user_coupon_id) {
UserCoupon::where('user_id', $order->user_id)->where('id', $order->user_coupon_id)->update([
'is_use'=>false,
]);
}
$order->update([
'status' => Order::STATUS_CANCELLED,
]);
}
}