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

744 lines
23 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 string $sn
* @param array $params
* @return void
*/
public function paySuccess(string $sn, array $params = []): void
{
$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 处理预收益
}
/**
* 更新包裹状态
*
* @param Collection|null $packages
* @param integer $status
* @return void
*/
public function updatePackageStatus(OrderPackage $package, int $status)
{
//如果包裹已签收则不再更新包裹状态
if ($package->status == OrderPackage::STATUS_CHECK || $package->status == OrderPackage::STATUS_AUTOCHECK) {
return;
}
//如果签收状态
if ($status == OrderPackage::STATUS_CHECK || $status == OrderPackage::STATUS_AUTOCHECK) {
$this->checkOrderPackage($package, $status);
} else {
$package->update([
'status'=>$status,
]);
}
}
/**
* 订单确认收货
*
* @return void
*/
public function checkOrder(Order $order)
{
//获取订单的未作废未签收包裹
$packages = $order->packages()->where('is_failed', false)->whereNotIn('status', [OrderPackage::STATUS_CHECK, OrderPackage::STATUS_AUTOCHECK])->get();
foreach ($packages as $package) {
$this->checkPackage($package, OrderPackage::STATUS_AUTOCHECK);
}
if ($order->isShipped()
&& OrderPackage::where('order_id', $order->id)->whereNotIn('status', [OrderPackage::STATUS_CHECK, OrderPackage::STATUS_AUTOCHECK])->doesntExist()
) {
//更新订单完成
$order->update([
'status' => Order::STATUS_COMPLETED,
'completed_at' => now(),
]);
}
}
/**
* 签收订单包裹:会更新订单状态
*
* @param OrderPackage $package
* @param integer $status
* @param Carbon|null $time
* @return void
*/
public function checkOrderPackage(OrderPackage $package, int $status, ?Carbon $time = null)
{
$this->checkPackage($package, $status);
//如果订单是发货完成状态,且所有包裹都签收了
if ($package->order->isShipped()
&& OrderPackage::where('order_id', $package->order_id)->whereNotIn('status', [OrderPackage::STATUS_CHECK, OrderPackage::STATUS_AUTOCHECK])->doesntExist()
) {
//更新订单完成
$package->order->update([
'status' => Order::STATUS_COMPLETED,
'completed_at' => now(),
]);
}
}
/**
* 签收包裹
*
* status 区分3签收物流推送签收和11自动签收属于用户手动点或者时间到了
* @return void
*/
protected function checkPackage(OrderPackage $package, int $status, ?Carbon $time = null)
{
//如果不是签收状态,则直接退出
if (!in_array($status, [OrderPackage::STATUS_CHECK, OrderPackage::STATUS_AUTOCHECK])) {
return;
}
$checkTime = $time ?? now();//没指定签收时间,则默认当前时间
//物流签收,更新包裹状态。
// if ($status == OrderPackage::STATUS_CHECK) { //自动签收不更新包裹状态
$package->update([
'status'=> $status,
'checked_at' => $checkTime,
]);
// }
//如果这个包裹的订单状态是发货中,发货完成
if ($package->order->isShipped() || $package->order->isShipping()) {
//处理这个包裹的商品的售后过期时间
$package->orderProducts()->update([
'after_expire_at'=>$checkTime->addDays(config('settings.sale_after_expire_days', 7)),
]);
}
}
}