确认订单
parent
70471f0c06
commit
a1c8622208
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace App\Endpoint\Api\Http\Controllers;
|
namespace App\Endpoint\Api\Http\Controllers;
|
||||||
|
|
||||||
use App\Endpoint\Api\Http\Resources\CouponResource;
|
use App\Endpoint\Api\Http\Resources\UserCouponResource;
|
||||||
use App\Helpers\Paginator as PaginatorHelper;
|
use App\Helpers\Paginator as PaginatorHelper;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
|
@ -22,6 +22,6 @@ class CouponController extends Controller
|
||||||
->filter($request->all())
|
->filter($request->all())
|
||||||
->simplePaginate(PaginatorHelper::resolvePerPage('per_page', 20, 50));
|
->simplePaginate(PaginatorHelper::resolvePerPage('per_page', 20, 50));
|
||||||
|
|
||||||
return CouponResource::collection($coupons);
|
return UserCouponResource::collection($coupons);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Endpoint\Api\Http\Controllers\Order;
|
||||||
|
|
||||||
|
use App\Endpoint\Api\Http\Controllers\Controller;
|
||||||
|
use App\Services\OrderService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class OrderVerifyController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 确认订单
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \App\Services\OrderService $orderService
|
||||||
|
* @return \Illuminate\Http\Response
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request, OrderService $orderService)
|
||||||
|
{
|
||||||
|
$rules = [
|
||||||
|
'coupon_id' => ['bail', 'nullable', 'int'],
|
||||||
|
'shipping_address_id' => ['bail', 'nullable', 'int'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 快速下单
|
||||||
|
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, [], [
|
||||||
|
'shopping_cart' => '购物车商品',
|
||||||
|
'product.sku_id' => '商品',
|
||||||
|
'product.quantity' => '数量',
|
||||||
|
'coupon_id' => '优惠券',
|
||||||
|
'shipping_address_id' => '收货地址',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
return response()->json(
|
||||||
|
$isQuick
|
||||||
|
? $orderService->verifyQuickOrder(
|
||||||
|
$user,
|
||||||
|
$request->input('product.sku_id'),
|
||||||
|
$request->input('product.quantity'),
|
||||||
|
$request->input('shipping_address_id'),
|
||||||
|
$request->input('coupon_id'),
|
||||||
|
) : $orderService->verifyShoppingCartOrder(
|
||||||
|
$user,
|
||||||
|
$request->input('shopping_cart'),
|
||||||
|
$request->input('shipping_address_id'),
|
||||||
|
$request->input('coupon_id'),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,9 @@ class ProductSkuSimpleResource extends JsonResource
|
||||||
'cover' => (string) $this->cover,
|
'cover' => (string) $this->cover,
|
||||||
'sell_price' => $this->sell_price,
|
'sell_price' => $this->sell_price,
|
||||||
'vip_price' => (string) $this->vip_price,
|
'vip_price' => (string) $this->vip_price,
|
||||||
|
'specs' => array_values((array) $this->specs),
|
||||||
|
'stock' => (int) $this->saleable_stock,
|
||||||
|
'is_online' => $this->isOnline(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ namespace App\Endpoint\Api\Http\Resources;
|
||||||
|
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
class CouponResource extends JsonResource
|
class UserCouponResource extends JsonResource
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Transform the resource into an array.
|
* Transform the resource into an array.
|
||||||
|
|
@ -20,8 +20,8 @@ class CouponResource extends JsonResource
|
||||||
'type' => $this->coupon_type,
|
'type' => $this->coupon_type,
|
||||||
'amount' => $this->coupon_amount_format,
|
'amount' => $this->coupon_amount_format,
|
||||||
'threshold' => $this->coupon_threshold_format,
|
'threshold' => $this->coupon_threshold_format,
|
||||||
'use_start_at' => $this->use_start_at,
|
'use_start_at' => $this->use_start_at->toDateTimeString(),
|
||||||
'use_end_at' => $this->use_end_at,
|
'use_end_at' => $this->use_end_at->toDateTimeString(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ use App\Endpoint\Api\Http\Controllers\ClickController;
|
||||||
use App\Endpoint\Api\Http\Controllers\CouponController;
|
use App\Endpoint\Api\Http\Controllers\CouponController;
|
||||||
use App\Endpoint\Api\Http\Controllers\MessageController;
|
use App\Endpoint\Api\Http\Controllers\MessageController;
|
||||||
use App\Endpoint\Api\Http\Controllers\Order\OrderController;
|
use App\Endpoint\Api\Http\Controllers\Order\OrderController;
|
||||||
|
use App\Endpoint\Api\Http\Controllers\Order\OrderVerifyController;
|
||||||
use App\Endpoint\Api\Http\Controllers\Product\HotController;
|
use App\Endpoint\Api\Http\Controllers\Product\HotController;
|
||||||
use App\Endpoint\Api\Http\Controllers\Product\ProductCategoryController;
|
use App\Endpoint\Api\Http\Controllers\Product\ProductCategoryController;
|
||||||
use App\Endpoint\Api\Http\Controllers\Product\ProductSkuController;
|
use App\Endpoint\Api\Http\Controllers\Product\ProductSkuController;
|
||||||
|
|
@ -107,5 +108,6 @@ Route::group([
|
||||||
|
|
||||||
// 订单
|
// 订单
|
||||||
Route::apiResource('order/orders', OrderController::class);
|
Route::apiResource('order/orders', OrderController::class);
|
||||||
|
Route::post('order/verify-order', OrderVerifyController::class);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exceptions;
|
||||||
|
|
||||||
|
class ShippingNotSupportedException extends BizException
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('所在地区不支持配送');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ class Coupon extends Model
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use HasDateTimeFormatter;
|
use HasDateTimeFormatter;
|
||||||
|
|
||||||
public const TYPE_VALUE = 1; // 抵扣券
|
public const TYPE_AMOUNT = 1; // 抵扣券
|
||||||
public const TYPE_DISCOUNT = 2; // 折扣券
|
public const TYPE_DISCOUNT = 2; // 折扣券
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
|
|
@ -22,8 +22,6 @@ class Coupon extends Model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 此优惠券的使用范围
|
* 此优惠券的使用范围
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
*/
|
||||||
public function ranges()
|
public function ranges()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class OrderProduct extends Model
|
||||||
*/
|
*/
|
||||||
public function getTotalAmountFormatAttribute()
|
public function getTotalAmountFormatAttribute()
|
||||||
{
|
{
|
||||||
return Numeric::trimZero(bcdiv($this->attributes['total_amount'], 100, 2));
|
return Numeric::trimTrailingZero(bcdiv($this->attributes['total_amount'], 100, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function packageProducts()
|
public function packageProducts()
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,21 @@ class ProductSku extends Model
|
||||||
return $this->release_at !== null;
|
return $this->release_at !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户来获取商品的真实价格
|
||||||
|
*
|
||||||
|
* @param \App\Models\User $user
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getRealPrice(User $user): int
|
||||||
|
{
|
||||||
|
if (! is_null($this->vip_price) && $user->isVip()) {
|
||||||
|
return $this->vip_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->sell_price;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取此商品的可售库存
|
* 获取此商品的可售库存
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ class User extends Model implements AuthorizableContract, AuthenticatableContrac
|
||||||
*/
|
*/
|
||||||
public function isVip(): bool
|
public function isVip(): bool
|
||||||
{
|
{
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -130,8 +130,6 @@ class User extends Model implements AuthorizableContract, AuthenticatableContrac
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 属于此用户的优惠券
|
* 属于此用户的优惠券
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
*/
|
||||||
public function coupons()
|
public function coupons()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ class UserCoupon extends Model
|
||||||
*/
|
*/
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'is_use' => 'bool',
|
'is_use' => 'bool',
|
||||||
|
'use_start_at' => 'datetime',
|
||||||
|
'use_end_at' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -87,6 +89,30 @@ class UserCoupon extends Model
|
||||||
return $this->coupon_type === Coupon::TYPE_DISCOUNT;
|
return $this->coupon_type === Coupon::TYPE_DISCOUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认此优惠券是否支持商品使用
|
||||||
|
*
|
||||||
|
* @param \App\Models\ProductSku $sku
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isSupport(ProductSku $sku): bool
|
||||||
|
{
|
||||||
|
if ($this->ranges->count() === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->ranges as $range) {
|
||||||
|
if (
|
||||||
|
($range->isTypeCategory() && in_array($sku->category_id, $range->rangeIds))
|
||||||
|
|| ($range->isTypeProduct() && in_array($sku->id, $range->rangeIds))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取此优惠券的面值
|
* 获取此优惠券的面值
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,7 @@
|
||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Models\ProductSku;
|
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\UserCoupon;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
class CouponService
|
class CouponService
|
||||||
{
|
{
|
||||||
|
|
@ -13,115 +10,44 @@ class CouponService
|
||||||
* 根据SKU商品获取可用优惠券
|
* 根据SKU商品获取可用优惠券
|
||||||
*
|
*
|
||||||
* @param User $user
|
* @param User $user
|
||||||
* @param array $skus 至少包含以下内容
|
* @param array $products 至少包含以下内容
|
||||||
* [
|
* [
|
||||||
* ['id', 'category_id', 'sell_price', 'vip_price', 'num']
|
* ['sku' => 商品SKU, 'quantity' => 10]
|
||||||
* ]
|
* ]
|
||||||
* @return collection
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function availableCouponsToUser(User $user, array $skus): collection
|
public function getAvailableCoupons(User $user, array $products): array
|
||||||
{
|
{
|
||||||
//获取用户当前所有可用券
|
$coupons = $user->coupons()->onlyUnuse()->get();
|
||||||
$coupons = $this->userValidCoupons($user);
|
|
||||||
|
$coupons->load(['ranges' => function ($query) {
|
||||||
|
$query->isEnable();
|
||||||
|
}]);
|
||||||
|
|
||||||
$couponSkus = [];
|
|
||||||
$availableCoupons = [];
|
$availableCoupons = [];
|
||||||
|
|
||||||
foreach ($coupons as $coupon) {
|
foreach ($coupons as $coupon) {
|
||||||
if (!isset($couponSkus[$coupon->coupon_id])) {
|
// 是否满足券使用规则
|
||||||
foreach ($skus as $sku) {
|
$passes = false;
|
||||||
//这个商品是否在这个券的可用范围
|
|
||||||
if ($this->isAvailableCouponToSku($sku, $coupon)) {
|
// 可用优惠券的商品的总额
|
||||||
//按coupon_id 分组商品
|
$amount = 0;
|
||||||
$couponSkus[$coupon->coupon_id][] = $sku;
|
|
||||||
}
|
foreach ($products as $product) {
|
||||||
|
$sku = $product['sku'];
|
||||||
|
|
||||||
|
if ($coupon->isSupport($sku)) {
|
||||||
|
$passes = true;
|
||||||
|
|
||||||
|
$amount += $sku->getRealPrice($user) * $product['quantity'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($coupons as $coupon) {
|
if ($passes && $amount >= $coupon->coupon_threshold) {
|
||||||
// 这个券是否有可用的商品
|
$availableCoupons[] = $coupon;
|
||||||
if (isset($couponSkus[$coupon->coupon_id])) {
|
|
||||||
//用户是否是vip
|
|
||||||
if ($user->isVip()) {
|
|
||||||
$totalAmount = collect($couponSkus[$coupon->coupon_id])->sum(function ($item) {
|
|
||||||
return bcmul($item['vip_price'], $item['num']);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$totalAmount = collect($couponSkus[$coupon->coupon_id])->sum(function ($item) {
|
|
||||||
return bcmul($item['sell_price'], $item['num']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//针对这个券,可用的商品合计价格是否满足使用门槛
|
|
||||||
if ($totalAmount >= $coupon->coupon_threshold) {
|
|
||||||
$availableCoupons[] = $coupon;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return collect($availableCoupons);
|
return $availableCoupons;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户有效的优惠券
|
|
||||||
*
|
|
||||||
* @param User $user
|
|
||||||
* @return collection
|
|
||||||
*/
|
|
||||||
public function userValidCoupons(User $user): collection
|
|
||||||
{
|
|
||||||
return UserCoupon::with('ranges')->where([
|
|
||||||
'user_id'=>$user->id,
|
|
||||||
])->onlyUnuse()->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 这批商品中,这个券是否可用
|
|
||||||
*
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
protected function isAvailableCouponToSomeSku(collection $skus, UserCoupon $coupon)
|
|
||||||
{
|
|
||||||
$res = false;
|
|
||||||
foreach ($skus as $sku) {
|
|
||||||
if ($sku instanceof ProductSku && $this->isAvailableCouponToSku($sku, $coupon)) {
|
|
||||||
$res = true;
|
|
||||||
break;//提前退出循环
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 这个商品是否可用这个券
|
|
||||||
*
|
|
||||||
* @param array $productSku ['id', 'category_id', 'sell_price', 'vip_price', 'num']
|
|
||||||
* @param Coupon $coupon
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
protected function isAvailableCouponToSku($sku, UserCoupon $coupon)
|
|
||||||
{
|
|
||||||
$res = false;
|
|
||||||
if ($coupon->ranges->count() == 0) {//如果没有规则则通用
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
foreach ($coupon->ranges as $range) {
|
|
||||||
switch ($range->type) {
|
|
||||||
case 1://指定商品分类
|
|
||||||
if (in_array($sku['category_id'], explode(',', $range->ranges))) {
|
|
||||||
$res = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 2://指定商品IDS
|
|
||||||
if (in_array($sku['id'], explode(',', $range->ranges))) {
|
|
||||||
$res = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if ($res) {//如果可用提前跳出循环;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $res;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,391 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Endpoint\Api\Http\Resources\ProductSku\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\Models\ProductSku;
|
||||||
|
use App\Models\ShippingAddress;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\UserCoupon;
|
||||||
|
|
||||||
|
class OrderService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 确认快速下单
|
||||||
|
*
|
||||||
|
* @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()->onlyUnuse()->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);
|
||||||
|
|
||||||
|
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 + $shippingFee, 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('优惠券不满足使用条件');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 商品的券折扣金额
|
||||||
|
$discountAmounts = [];
|
||||||
|
// 优惠券折扣总额
|
||||||
|
$discountTotalAmount = 0;
|
||||||
|
// 未计算优惠金额的商品总数
|
||||||
|
$lastCount = count($amounts);
|
||||||
|
|
||||||
|
foreach ($amounts as $skuId => $amount) {
|
||||||
|
if ($coupon->isDiscountCoupon()) {
|
||||||
|
/*
|
||||||
|
|----------------------------------------
|
||||||
|
| 计算折扣券的折扣金额
|
||||||
|
|----------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
$discountAmounts[$skuId] = (int) ($amount * $coupon->coupon_amount / 100);
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
|----------------------------------------
|
||||||
|
| 计算抵扣券的抵扣金额
|
||||||
|
|----------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
$couponAmount = $coupon->coupon_amount;
|
||||||
|
|
||||||
|
// 如果券的抵用金额大于商品的实际金额,则以商品实际金额作为抵扣金额
|
||||||
|
if ($couponAmount > $totalAmount) {
|
||||||
|
$couponAmount = $totalAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$discountAmounts[$skuId] = (int) ($couponAmount * $amount / $totalAmount);
|
||||||
|
$discountTotalAmount += $discountAmounts[$skuId];
|
||||||
|
|
||||||
|
$lastCount--;
|
||||||
|
|
||||||
|
// 将券的剩余抵扣金额给最后一个商品
|
||||||
|
if ($lastCount === 0) {
|
||||||
|
$discountAmounts[$skuId] += $couponAmount - $discountTotalAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $discountAmounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据购物车获取商品
|
||||||
|
*
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Exceptions\BizException;
|
use App\Exceptions\ShippingNotSupportedException;
|
||||||
use App\Models\ShippingRule;
|
use App\Models\ShippingRule;
|
||||||
|
|
||||||
class ShippingService
|
class ShippingService
|
||||||
|
|
@ -15,6 +15,8 @@ class ShippingService
|
||||||
* @param integer $templateId
|
* @param integer $templateId
|
||||||
* @param integer $zoneId
|
* @param integer $zoneId
|
||||||
* @return integer
|
* @return integer
|
||||||
|
*
|
||||||
|
* @throws \App\Exceptions\ShippingNotSupportedException
|
||||||
*/
|
*/
|
||||||
public function countShippingAmount(int $weight, int $totalAmount, int $templateId, int $zoneId): int
|
public function countShippingAmount(int $weight, int $totalAmount, int $templateId, int $zoneId): int
|
||||||
{
|
{
|
||||||
|
|
@ -54,7 +56,7 @@ class ShippingService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!$canShipping) {
|
if (!$canShipping) {
|
||||||
throw new BizException('当前选择的地址有部分商品不支持配送');
|
throw new ShippingNotSupportedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $shipping_amount;
|
return $shipping_amount;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use App\Models\ProductSku;
|
||||||
use App\Models\ProductSpu;
|
use App\Models\ProductSpu;
|
||||||
use App\Models\ShippingAddress;
|
use App\Models\ShippingAddress;
|
||||||
use App\Models\ShoppingCartItem;
|
use App\Models\ShoppingCartItem;
|
||||||
|
use App\Models\UserCoupon;
|
||||||
use App\Models\Zone;
|
use App\Models\Zone;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
@ -11,5 +12,6 @@ return [
|
||||||
ProductSpu::class => '商品',
|
ProductSpu::class => '商品',
|
||||||
ProductSku::class => '商品',
|
ProductSku::class => '商品',
|
||||||
ShoppingCartItem::class => '商品',
|
ShoppingCartItem::class => '商品',
|
||||||
|
UserCoupon::class => '优惠券',
|
||||||
Zone::class => '地区',
|
Zone::class => '地区',
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue