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

700 lines
22 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\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\OrderPackage;
use App\Models\OrderProduct;
use App\Models\ProductSku;
use App\Models\ShippingAddress;
use App\Models\User;
use App\Models\UserCoupon;
use Carbon\Carbon;
use Illuminate\Support\Collection;
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 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 $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(),
'user_coupon_id' => $coupon?->id,
'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'],
'remain_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}"), // 销量
]);
}
/**
* 确认订单
*
* @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_target' => 'order',
'pay_way' => $payWay,
]),
'body' => config('settings.app_name').'-商城订单',
'out_trade_no' => $order->sn,
'total_fee' => $order->total_amount,
'notify_url' => url(route('wxpay.paid_notify', [], false), [], true),
'trade_type' => PayWay::$wxpayTradeTypes[$payWay],
]);
}
throw new BizException('支付方式不支持');
}
/**
* 更新包裹状态
*
* @param Collection|null $packages
* @param integer $status
* @return void
*/
public function updatePackageStatus(?Collection $packages, int $status)
{
if ($packages instanceof OrderPackage) {
$packages[] = $packages;
$packages = collect($packages);
}
//如果签收状态
if ($status == OrderPackage::STATUS_CHECK || $status == OrderPackage::STATUS_AUTOCHECK) {
$this->checkOrderPackage($packages, $status, now());
} else {
OrderPackage::whereIn('id', $packages->pluck('id')->toArray())->update([
'status'=>$status,
]);
}
}
/**
* 签收订单包裹
*
* status 区分3签收物流推送签收和11自动签收属于用户手动点或者时间到了
* @return void
*/
public function checkOrderPackage(?Collection $packages, int $status, ?Carbon $time = null)
{
$checkTime = $time ?? now();//没指定签收时间,则默认当前时间
if ($packages instanceof OrderPackage) {
$packages[] = $packages;
$packages = collect($packages);
}
//物流签收,更新包裹状态。自动签收不更新包裹状态
if ($status == OrderPackage::STATUS_CHECK) {
OrderPackage::whereIn('id', $packages->pluck('id')->toArray())->update([
'status'=> $status,
'checked_at' => $checkTime,
]);
}
//按订单ID分组包裹
$packages = $packages->groupBy('order_id');
//获取所有订单
$orders = Order::whereIn('id', $packages->keys()->toArray())->get()->keyBy('id');
$checkedOrder = [];//已执行过订单状态更新的订单IDS
//遍历包裹-处理签收
foreach ($packages as $orderId => $orderPackages) {
$_order = $orders[$orderId];
//如果订单已完成,则不更新
if ($_order->isCompleted()) {
continue;
}
//处理包裹下订单商品售后过期时间
foreach ($orderPackages as $package) {
$package->orderProducts()->update([
'after_expire_at'=>$checkTime->addDays(7),
]);
}
//处理订单已完成发货,且包裹已全部签收,则更新为完成状态、记录完成时间;
if (!in_array($_order->id, $checkedOrder)
&& $_order->isShipped()
&& $_order->packages()->where('is_failed', false)->whereNotIn('status', [OrderPackage::STATUS_CHECK, OrderPackage::STATUS_AUTOCHECK])->doesntExist()
) {
Order::where('id', $_order->id)->update([
'status' => Order::STATUS_COMPLETED,
'completed_at' => now(),
]);
$checkedOrder[] = $_order->id;//每个订单只更新执行一次
}
}
}
}