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

1500 lines
48 KiB
PHP
Raw Permalink 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\Enums\PayWay;
use App\Enums\PointLogAction;
use App\Enums\SocialiteType;
use App\Enums\WxpayTradeType;
use App\Exceptions\BizException;
use App\Exceptions\ShippingNotSupportedException;
use App\Models\ActivityProductPart;
use App\Models\BargainOrder;
use App\Models\Order;
use App\Models\OrderActivity;
use App\Models\OrderProduct;
use App\Models\ProductGift;
use App\Models\ProductPartSku;
use App\Models\ProductSku;
use App\Models\ShippingAddress;
use App\Models\SocialiteUser;
use App\Models\Store\{Store, Desk, DeviceRecord};
use App\Models\UserCoupon;
use App\Models\{Agent, User, OrderPre, Tag};
use App\Services\Payment\WxpayService;
use Illuminate\Database\Eloquent\Builder;
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
* @param BargainOrder|null $bargainOrder
* @return \App\Models\Order
*/
public function createQuickOrder(
User $user,
int $skuId,
int $quantity,
?int $shippingAddressId,
?int $couponId = null,
?string $note = null,
?int $points = null,
?BargainOrder $bargainOrder = null,
): Order {
$sku = ProductSku::online()->findOrFail($skuId);
$product = [
'sku' => $sku,
'quantity' => $quantity,
];
return $this->createOrder($user, [$product], $shippingAddressId, $couponId, $note, $points, $bargainOrder);
}
/**
* 购物车下单
*
* @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,
?int $points = null,
): Order {
$order = $this->createOrder(
$user,
$this->getProductsByShoppingCart($user, $shoppingCartItemIds),
$shippingAddressId,
$couponId,
$note,
$points,
);
$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
* @param BargainOrder|null $bargainOrder
* @return \App\Models\Order
*/
public function createOrder(
User $user,
array $products,
?int $shippingAddressId,
?int $couponId = null,
?string $note = null,
?int $points = null,
?BargainOrder $bargainOrder = null,
): Order {
foreach ($products as $product) {
$sku = $product['sku'];
// 商品开启预售, 则不验证库存
$is_pre_sale = $sku->spu->is_pre_sale || $sku->is_pre_sale;
if (!$is_pre_sale && $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, $bargainOrder);
// 计算运费
$shippingFee = $this->calculateShippingFee($mapProducts, $shippingAddress);
list(
$productsTotalAmount,
$vipDiscountAmount,
$couponDiscountAmount,
$salesValue
) = $this->calculateFees($mapProducts);
$order = $this->storeOrder(
$user,
$productsTotalAmount,
$couponDiscountAmount,
$vipDiscountAmount,
$shippingFee,
$points,
$salesValue,
$shippingAddress,
$note,
$coupon,
$bargainOrder,//添加砍价订单逻辑
$mapProducts,
);
$this->storeOrderProducts($order, $mapProducts);
// 将优惠券标记为已使用
$coupon?->markAsUse();
if ($order->total_amount === 0) {
$this->pay($order, PayWay::None);
$order->refresh();
}
return $order;
}
/**
* 预订单, 扫码下单
*/
public function createOrderByPre(User $user, OrderPre $order_pre, $coupon_id = null, $note = null, int $points = 0)
{
$products = [];
foreach($order_pre->products as $item) {
array_push($products, [
'sku' => ProductSku::findOrFail($item['sku_id']),
'quantity' => $item['quantity']
]);
}
$coupon_id = $coupon_id ?: data_get($order_pre, 'others.coupon_id');
$note = $note ?: data_get($order_pre, 'others.note');
// 优惠券
$coupon = null;
if ($coupon_id) {
$coupon = $user->coupons()->onlyAvailable()->lockForUpdate()->findOrFail($coupon_id);
}
$mapProducts = $this->mapProducts($user, $products, $coupon);
list(
$productsTotalAmount,
$vipDiscountAmount,
$couponDiscountAmount,
$salesValue
) = $this->calculateFees($mapProducts);
$order = $this->storeOrder(
$user,
$productsTotalAmount,
$couponDiscountAmount,
$vipDiscountAmount,
0,
$points,
$salesValue,
null,
$note,
$coupon,
null,
$mapProducts
);
$order->update([
'store_id' => $order_pre->store_id,
'inviter_id' => $order_pre->user_id,
'source_type' => OrderPre::class,
'source_id' => $order_pre->id,
]);
$this->storeOrderProducts($order, $mapProducts);
// 将优惠券标记为已使用
$coupon?->markAsUse();
if ($order->total_amount === 0) {
$this->pay($order, PayWay::None);
$order->refresh();
}
return $order;
}
/**
* 门店直接下单
*
* @param User $user
* @param int $storeId
* @param array $products 商品信息 [{sku_id, quantity}]
* @param array $params {coupon_id: 优惠券id, desk: 桌号id}
*
* @return Order
* @throws BizException
*/
public function createOrderByStore(User $user, $storeId, array $products, array $params = [])
{
$store = Store::find($storeId);
if (!$store) {
throw new BizException('门店不存在');
}
if (!$store->status) {
throw new BizException('门店: '.$store->title.' 已关闭');
}
$coupon_id = data_get($params, 'coupon_id');
$note = data_get($params, 'note');
$sourceType = Store::class;
$sourceId = $store->id;
// 桌号下单
if ($deskId = data_get($params, 'desk')) {
$desk = $store->desks()->find($deskId);
if (!$desk) {
throw new BizException('门店桌号不存在');
}
if (!$desk->status) {
throw new BizException('门店桌号: '.$desk->name.' 已关闭');
}
$sourceType = Desk::class;
$sourceId = $desk->id;
}
// 检查门店商品库存
$skuList = $store->productSkus;
foreach($products as &$item) {
$sku = $skuList->firstWhere('id', $item['sku_id']);
if (!$sku) {
throw new BizException('门店商品: '.$item->name.' 不存在');
}
if ($sku->pivot->amount < $item['quantity']) {
throw new BizException('门店商品: '.$item->name.' 库存不足');
}
$item['sku'] = $sku;
}
// 优惠券
$coupon = null;
if ($coupon_id) {
$coupon = $user->coupons()->onlyAvailable()->lockForUpdate()->findOrFail($coupon_id);
}
$mapProducts = $this->mapProducts($user, $products, $coupon);
list($productsTotalAmount, $vipDiscountAmount, $couponDiscountAmount, $salesValue) = $this->calculateFees($mapProducts);
$order = $this->storeOrder(
$user,
$productsTotalAmount,
$couponDiscountAmount,
$vipDiscountAmount,
0,
$params['points'] ?? 0,
$salesValue,
null,
$note,
$coupon,
null,
$mapProducts
);
$order->update([
'store_id' => $store->id,
'source_type' => $sourceType,
'source_id' => $sourceId,
]);
$this->storeOrderProducts($order, $mapProducts);
// 将优惠券标记为已使用
$coupon?->markAsUse();
if ($order->total_amount === 0) {
$this->pay($order, PayWay::None);
$order->refresh();
}
return $order;
}
/**
* 保存订单
*
* @param \App\Models\User $user 下单用户
* @param int $productsTotalAmount 商品售价总额
* @param int $couponDiscountAmount 优惠券抵扣金额
* @param int $vipDiscountAmount 会员抵扣金额
* @param int $shippingFee 配送费
* @param float $salesValue 成长值
* @param \App\Models\ShippingAddress $shippingAddress 配送地址
* @param string $note 用户订单备注
* @param \App\Models\UserCoupon $coupon 使用的优惠券
* @param \App\Models\BargainOrder 砍价订单
* @param array 商品列表
*
* @return \App\Models\Order
*/
protected function storeOrder(
User $user,
int $productsTotalAmount,
int $couponDiscountAmount,
int $vipDiscountAmount,
int $shippingFee,
int $points,
$salesValue,
?ShippingAddress $shippingAddress,
?string $note = null,
?UserCoupon $coupon = null,
?BargainOrder $bargainOrder = null,
?array $mapProducts = null,
): Order {
// 积分抵扣金额(1积分等于1分钱)
$pointDiscountAmount = $points;
// 订单支付金额=商品总额-券折扣金额-会员折扣金额+邮费-砍价金额
$totalAmount = $productsTotalAmount - $couponDiscountAmount - $vipDiscountAmount;
//如果有砍价优惠
if ($bargainOrder) {
$totalAmount -= $bargainOrder->bargain_price;
}
if ($totalAmount < 0) {
$totalAmount = 0;
}
$totalAmount += $shippingFee;
$totalAmount -= $pointDiscountAmount;
// 生成不重复的订单号
do {
$sn = serial_number();
} while(Order::where('sn', $sn)->exists());
// 计算商品市场价和成本价之和
$market_price = 0;
$cost_price = 0;
foreach($mapProducts as $item) {
$sku = $item['sku'];
$market_price += $sku->market_price * $item['quantity'];
$cost_price += $sku->cost_price * $item['quantity'];
}
$attrs = [
'sn' => $sn,
'user_coupon_id' => $coupon?->id,
'coupon_discount_amount' => $couponDiscountAmount,
'vip_discount_amount' => $vipDiscountAmount,
'shipping_fee' => $shippingFee,
'products_total_amount' => $productsTotalAmount,
'total_amount' => $totalAmount, // 商品总额-券折扣金额-会员折扣金额+邮费
'sales_value' => $salesValue, // 订单总销售值
'note' => $note,
// 收货地址
'consignee_name' => $shippingAddress->consignee??'',
'consignee_telephone' => $shippingAddress->telephone??'',
'consignee_zone' => $shippingAddress->zone??'',
'consignee_address' => $shippingAddress->address??'',
//砍价订单金额
'bargain_amount'=>$bargainOrder?->bargain_price ?? 0,
'market_price' => $market_price,
'cost_price' => $cost_price,
// 积分抵扣金额
'point_discount_amount' => $pointDiscountAmount,
];
$order = $user->orders()->create($attrs);
// 扣除积分
if ($points > 0) {
(new PointService)->change($user, -$points, PointLogAction::Consumption, "订单{$order->sn}使用积分", $order);
}
return $order;
}
/**
* 保存订单商品
*
* @param \App\Models\Order $order
* @param array $mapProducts
* @return void
*/
public function storeOrderProducts(Order $order, array $mapProducts)
{
// 积分抵扣金额
$remainingPointDiscountAmount = $order->point_discount_amount;
$orderProducts = [];
foreach ($mapProducts as $product) {
$sku = $product['sku'];
$qty = $product['quantity'];
// 支付金额 = 商品总额 - 优惠券折扣金额 - 会员折扣金额 - 砍价金额
$totalAmount = $product['total_amount'] - $product['coupon_discount_amount'] - $product['vip_discount_amount'] - $product['bargain_amount'];
// 积分抵扣金额
$pointDiscountAmount = $totalAmount > $remainingPointDiscountAmount ? $remainingPointDiscountAmount : $totalAmount;
$remainingPointDiscountAmount -= $pointDiscountAmount;
$totalAmount -= $pointDiscountAmount;
$orderProducts[] = [
'gift_for_sku_id' => null,
'activity_id'=>null,
'is_gift'=> false,
'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' => $product['total_sales_value'],
'market_price' => $sku->cost_price,
'cost_price' => $sku->cost_price,
'quantity' => $qty,
'remain_quantity' => $qty, // 剩余发货数量
'coupon_discount_amount' => $product['coupon_discount_amount'],
'vip_discount_amount' => $product['vip_discount_amount'],
'point_discount_amount' => $pointDiscountAmount,
'total_amount' => $totalAmount,
'bargain_amount'=> $product['bargain_amount'],
'created_at' => $order->created_at,
'updated_at' => $order->updated_at,
];
// 扣除商品库存
if (!$order->store_id) {
$this->deductProduct($sku, $qty);
}
}
//根据订单参加的活动添加赠品;
$gifts = $this->activityGifts($order, $orderProducts);
$orderProducts = array_merge($orderProducts, $gifts);
OrderProduct::insert($orderProducts);
}
/**
* 扣商品的库存
* 03-04取消根据商品赠送赠品
*
* @param \App\Models\ProductSku $sku
* @param int $qty
* @return array
*/
protected function deductProduct(ProductSku $sku, int $qty)
{
$sku->update([
'sales' => DB::Raw("sales + {$qty}")
]);
// 未开启预售, 扣除商品库存
$is_pre_sale = $sku->spu->is_pre_sale || $sku->is_pre_sale;
if (!$is_pre_sale) {
$sku->update([
'stock' => DB::raw("stock - {$qty}")
]);
}
$sku->spu?->increment('sales', $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);
}
/**
* 从门店中扣除商品库存
*/
protected function deductProductFromStore(Store $store, ProductSku $sku, int $amount)
{
$sku = $store->productSkus()->findOrFail($sku->id);
// 添加出库记录
$tag = Tag::firstOrCreate([
'type' => Tag::TYPE_STORE_STOCK,
'name' => '下单出库'
]);
$store->stockLogs()->create([
'amount' => 0-$amount,
'product_sku_id' => $sku->id,
'remarks' => '购买',
'tag_id' => $tag->id
]);
$store->productSkus()->updateExistingPivot($sku->id, [
'amount' => $sku->pivot->amount - $amount
]);
}
/**
* 扣出商品的赠品
*
* @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()) {
if ($gift->remaining === 0) {
continue;
}
if ($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;
}
/**
* Undocumented function
*
* @param Order $order
* @param [type] $products
* @return array $gifts
*/
protected function activityGifts(Order $order, $products)
{
$_products = array_column($products, 'total_amount', 'sku_id');
$inValidParts = [];
// 整理订单商品的分区
$partSkus = ProductPartSku::with('part')->whereIn('sku_id', array_keys($_products))->get();
foreach ($partSkus as $partSku) {
if ($partSku->part?->is_show) {
$inValidParts[$partSku->part_id][$partSku->sku_id] = $_products[$partSku->sku_id] ?? 0;
// if (isset($inValidParts[$partSku->part_id])) {
// $inValidParts[$partSku->part_id] += $_products[$partSku->sku_id] ?? 0;
// } else {
// $inValidParts[$partSku->part_id] = $_products[$partSku->sku_id] ?? 0;
// }
}
}
// dd($inValidParts);
//根据分区获取活动
$partActivities = ActivityProductPart::with(['activity', 'activity.gifts'])->whereHas('activity', function (Builder $query) {
return $query->where('is_use', true)->where('started_at', '<', now())->where('ended_at', '>=', now());
})->whereIn('part_id', array_keys($inValidParts))->get();
$activityArr = [];
$partActivities->each(function ($item) use (&$activityArr) {
$activityArr[$item->activity_id][] = $item->part_id;
});
$giveGifts = [];
$sendedActivities = [];
foreach ($partActivities as $partActivity) {
//判断该活动是否已处理
if (in_array($partActivity->activity_id, $sendedActivities)) {
continue;
}
$sendedActivities[] = $partActivity->activity_id;
//获取活动的赠品赠送规则
$_giftsRule = $partActivity->activity?->gifts_rule;
//判断是否首单times=0为仅首单赠送, 1为不限
if ($_giftsRule['times'] == 0 && OrderProduct::where([
'activity_id' => $partActivity->activity_id,
'user_id' => $order->user_id,
])->whereHas('order', function ($query) {//除取消外的订单
return $query->where('status', '<>', Order::STATUS_CANCELLED);
})->exists()) {
continue;//提前结束本次循环
}
//判断是否满足门槛
$inValidGoods = [];
foreach ($inValidParts as $key => $part) {
if (in_array($key, $activityArr[$partActivity->activity_id])) {
$inValidGoods = array_merge($inValidGoods, $part);
}
}
if (bcmul($_giftsRule['value'], 100) > array_sum($inValidGoods)) {
continue;//提前结束本次循环
}
//返回赠品
$_gifts = $partActivity->activity->gifts;
$_num = 0;
foreach ($_gifts as $_gift) {
if (($_gift->stock - $_gift->pivot->qty) >= 0) {
$_num += $_gift->pivot->qty;
$giveGifts[] = [
'gift_for_sku_id'=> null,
'user_id' => $order->user_id,
'order_id' => $order->id,
'spu_id' => $_gift->spu_id,
'sku_id' => $_gift->id,
'category_id' => $_gift->category_id,
'name' => $_gift->name,
'specs' => json_encode($_gift->specs),
'cover' => $_gift->cover,
'weight' => $_gift->weight,
'sell_price' => $_gift->sell_price,
'vip_price' => $_gift->vip_price,
'sales_value' => 0, // 赠品不算销售值
'quantity' => $_gift->pivot->qty,
'remain_quantity' => $_gift->pivot->qty, // 剩余发货数量
'coupon_discount_amount' => 0,
'vip_discount_amount' => 0,
'total_amount' => 0,
'bargain_amount'=> 0,
'created_at' => $order->created_at,
'updated_at' => $order->updated_at,
'is_gift'=> true,
'activity_id'=>$partActivity->activity_id,
];
// 扣除商品库存
$this->deductProduct($_gift, $_gift->pivot->qty);
}
}
if ($_num > 0) {
//记录订单参与活动信息
OrderActivity::firstOrCreate([
'order_id'=>$order->id,
'activity_id'=>$partActivity->activity_id,
]);
}
}
return $giveGifts;
}
/**
* 确认快速下单
*
* @param \App\Models\User $user
* @param int $skuId
* @param int $quantity
* @param int|null $shippingAddressId
* @param int|null $couponId
* @param int|null $bargainOrderId
* @return array
*/
public function verifyQuickOrder(User $user, int $skuId, int $quantity, ?int $shippingAddressId = null, ?int $couponId = null, ?int $bargainOrderId = null)
{
$sku = ProductSku::online()->findOrFail($skuId);
$product = [
'sku' => $sku,
'quantity' => $quantity,
];
return $this->verifyOrder($user, [$product], $shippingAddressId, $couponId, $bargainOrderId);
}
/**
* 确认购物车订单
*
* @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
* @param int|null $bargainOrderId
* @return array
*/
public function verifyOrder(
User $user,
array $products,
?int $shippingAddressId = null,
?int $couponId = null,
?int $bargainOrderId = null,
bool $ignoreShippingFee = false,
): array {
// 获取收货地址
$shippingAddress = $this->getShippingAddress($user, $shippingAddressId);
// 优惠券
$coupon = null;
if ($couponId) {
$coupon = $user->coupons()->onlyAvailable()->findOrFail($couponId);
}
$bargainOrder = null;
if ($bargainOrderId) {
$bargainOrder = $user->bargainOrders()->findOrFail($bargainOrderId);
}
$mapProducts = $this->mapProducts($user, $products, $coupon, $bargainOrder);
// 是否支持配送
$shippingSupported = true;
// 运费
$shippingFee = 0;
if (! $ignoreShippingFee && $shippingAddress) {
try {
$shippingFee = $this->calculateShippingFee($mapProducts, $shippingAddress);
} catch (ShippingNotSupportedException $e) {
$shippingFee = 0;
$shippingSupported = false;
}
}
list(
$productsTotalAmount,
$vipDiscountAmount,
$couponDiscountAmount,
$salesValue,
$bargainAmount,
) = $this->calculateFees($mapProducts);
$totalAmount = $productsTotalAmount - $couponDiscountAmount - $vipDiscountAmount - $bargainAmount;
if ($totalAmount < 0) {
$totalAmount = 0;
}
$totalAmount += $shippingFee;
//---------------------------------------
// 积分当钱花
//---------------------------------------
$remainingPoints = $user->userInfo->points; // 用户剩余积分
$availablePoints = $totalAmount > $remainingPoints ? $remainingPoints : $totalAmount; // 可用积分
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)), // 实付金额
'bargain_amount' => trim_trailing_zeros(bcdiv($bargainAmount, 100, 2)), //砍价金额
'remaining_points' => trim_trailing_zeros(bcdiv($remainingPoints, 100, 2)), // 剩余积分
'available_points' => trim_trailing_zeros(bcdiv($availablePoints, 100, 2)), // 可用积分
'point_discount_amount' => trim_trailing_zeros(bcdiv($availablePoints, 100, 2)), // 积分抵扣金额
];
}
/**
* 计算费用
*
* @param array $products
* @return array
*/
protected function calculateFees(array $products)
{
$productsTotalAmount = 0;
$vipDiscountAmount = 0;
$couponDiscountAmount = 0;
$salesValue = 0;
$bargainAmount = 0;
foreach ($products as $product) {
$productsTotalAmount += $product['total_amount'];
$vipDiscountAmount += $product['vip_discount_amount'];
$couponDiscountAmount += $product['coupon_discount_amount'];
$salesValue = bcadd($salesValue, $product['total_sales_value'], 2);
$bargainAmount += $product['bargain_amount'];
}
return [
$productsTotalAmount,
$vipDiscountAmount,
$couponDiscountAmount,
$salesValue,
$bargainAmount,
];
}
/**
* 计算运费
*
* @param array $products
* @param \App\Models\ShippingAddress $shippingAddress
* @return int
*/
protected function calculateShippingFee(array $products, ?ShippingAddress $shippingAddress): int
{
// 运费
$shippingFee = 0;
if (!$shippingAddress) {
return $shippingFee;
}
$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 {sku: App\Models\ProductSku, quantity: 数量}
* @param \App\Models\UserCoupon|null $coupon
* @return array
*
* @throws \App\Exceptions\BizException
*/
protected function mapProducts(User $user, array $products, ?UserCoupon $coupon, ?BargainOrder $bargainOrder = null): 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'],
// 总销售值
'total_sales_value' => bcmul($sku->sales_value, $item['quantity'], 2),
//砍价金额
'bargain_amount' => 0,
]);
});
//处理砍价金额
if ($bargainOrder) {
$bargainDiscountAmount = $this->getBargainDiscountAmount($bargainOrder, $_products->all());
$_products = $_products->map(function ($item) use ($bargainDiscountAmount) {
$item['bargain_amount'] = $bargainDiscountAmount[$item['sku']->id] ?? 0;
return $item;
});
}
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 = [];
$pass = false;
foreach ($products as $item) {
$sku = $item['sku'];
$support = $coupon->isSupport($sku);
if ($support) {
$pass = true;
}
$amount = $item['total_amount'] - $item['vip_discount_amount'];
// 仅保留商品真实总额大于0的商品
if ($amount > 0 && $support) {
$amounts[$sku->id] = $amount;
}
}
if (!$pass) {
throw new BizException('优惠券不满足使用条件');
}
// 全部商品总额
$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--;
$originalAmount = $amount;
if ($i > 0) {
$amount = (int) bcdiv(bcmul($couponAmount, $amount, 0), $totalAmount, 2);
$couponAmount -= $amount;
$totalAmount -= $originalAmount;
} else {
$amount = $couponAmount;
}
unset($amount);
}
return $amounts;
}
/**
*
*/
protected function getBargainDiscountAmount(BargainOrder $bargainOrder, array $products): array
{
// 砍价的商品
$amounts = [];
foreach ($products as $item) {
$sku = $item['sku'];
$amount = $item['total_amount'] - $item['vip_discount_amount'];
// 仅保留商品真实总额大于0的商品
if ($amount > 0) {
$amounts[$sku->id] = $amount;
}
}
// 全部商品总额
$totalAmount = array_sum($amounts);
$bargainAmount = $bargainOrder->bargain_price ?? 0;
// 待计算优惠的商品总数
$i = count($amounts);
foreach ($amounts as &$amount) {
$i--;
$originalAmount = $amount;
if ($i > 0) {
$amount = (int) bcdiv(bcmul($bargainAmount, $amount, 0), $totalAmount, 2);
$bargainAmount -= $amount;
$totalAmount -= $originalAmount;
} else {
$amount = $bargainAmount;
}
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.spu');
$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 \App\Enums\PayWay $payWay
* @return mixed
*
* @throws \App\Exceptions\WeChatPayException
*/
public function pay(Order $order, PayWay $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);
$data = [
'pay_sn' => $payLog->pay_sn,
];
// 如果支付方式为线下支付,或支付金额为 0则按支付完成处理
if ($order->total_amount === 0 || $payLog->isOffline()) {
(new PayService())->handleSuccess($payLog, [
'pay_at' => now(),
]);
$data = null;
} elseif ($payLog->isWxpay()) {
if (is_null($tradeType = WxpayTradeType::tryFromPayWay($payLog->pay_way))) {
throw new BizException('支付方式 非法');
}
// 是否开启分账
$share = 'Y';
$user = $order->user;
$agentIds = User::whereIn('id', $user->userInfo->parent_ids)->pluck('agent_id');
$ratio = Agent::whereIn('id', $agentIds)->sum('ratio');
$profit = $ratio * $order->sales_value;
// 没有提成
if ($profit == 0) {
$share = 'N';
}
$params = [
'body' => app_settings('app.app_name').'-商城订单',
'out_trade_no' => $payLog->pay_sn,
'total_fee' => $order->total_amount,
'trade_type' => $tradeType->value,
'profit_sharing' => $share,
];
if ($tradeType === WxpayTradeType::JSAPI) {
$socialite = match ($payLog->pay_way) {
PayWay::WxpayMiniProgram => SocialiteUser::where([
'user_id' => $order->user_id,
'socialite_type' => SocialiteType::WechatMiniProgram,
])->first(),
};
if ($socialite === null) {
throw new BizException('未绑定微信小程序');
}
$params['openid'] = $socialite->socialite_id;
}
$data = (new WxpayService())->pay($params, match ($payLog->pay_way) {
PayWay::WxpayMiniProgram => 'mini_program',
default => 'default',
});
// 更新订单冻结状态
$attributes = ['status' => $share];
$order->update(['wx_share' => $order->wx_share ? array_merge($order->wx_share, $attributes) : $attributes]);
} elseif ($payLog->isAlipay()) {
$params = [
'subject' => app_settings('app.app_name').'-商城订单',
'out_trade_no' => $payLog->pay_sn,
'total_amount' => bcdiv($order->total_amount, 100, 2),
];
$data = app(AlipayService::class)->pay($params);
}
return [
'pay_way' => $payLog->pay_way,
'data' => $data,
];
}
/**
* 确认订单
*
* @param \App\Models\Order $order
* @param bool $isSettlable
* @return void
*/
public function confirm(Order $order, $isSettlable = false)
{
if (! $order->isShipped()) {
throw new BizException('订单包裹未发完');
}
if ($isSettlable && $order->afterSales()->processing()->count() > 0) {
throw new BizException('订单商品售后中,不能完成此订单');
}
$orderPackageService = new OrderPackageService();
$order->loadMissing('packages');
$packages = $order->packages->filter(function ($item) {
return ! $item->is_failed && ! $item->isChecked();
});
foreach ($packages as $package) {
$orderPackageService->checkPackage($package, true);
}
$order->update([
'status' => Order::STATUS_COMPLETED,
'completed_at' => now(),
]);
}
/**
* 取消订单
*
* @param \App\Models\Order $order
* @return void
*/
public function cancel(Order $order)
{
if (! $order->isPending() && ! $order->isWaitShipping()) {
throw new BizException('订单状态不是待付款或待发货');
}
if ($order->isWaitShipping()) {
$order->refundLogs()->create([
'sn' => serial_number(),
'amount' => $order->total_amount,
'reason' => '取消订单',
]);
}
// 门店订单
if ($order->store_id) {
// 返还店铺库存
} else {
$products = $order->products()->get();
foreach ($products->load(['sku', 'spu']) as $product) {
if ($product->sku === null) {
continue;
}
// 如果商品不是赠品,则直接增加商品库存
if (! $product->isGift()) {
$product->spu?->increment('sales', -$product->quantity);
$product->sku?->update([
'sales' => DB::Raw("sales - {$product->quantity}"),
]);
// 商品未开启预售,返还库存
$is_pre_sale = $product->spu->is_pre_sale || $product->sku->is_pre_sale;
if (!$is_pre_sale) {
$product->sku?->update([
'stock' => DB::Raw("stock + {$product->quantity}"),
]);
}
continue;
}
//原赠品
if ($product->gift_for_sku_id) {
$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);
}
continue;
} else {//新赠品, 则直接增加商品库存
$product->spu?->increment('sales', -$product->quantity);
$product->sku?->update([
'stock' => DB::Raw("stock + {$product->quantity}"),
'sales' => DB::Raw("sales - {$product->quantity}"),
]);
continue;
}
}
}
// 取消订单,退券
if ($order->user_coupon_id) {
UserCoupon::where('user_id', $order->user_id)->where('id', $order->user_coupon_id)->update([
'is_use'=>false,
]);
}
if ($order->point_discount_amount > 0) {
(new PointService())->change($order->user, $order->point_discount_amount, PointLogAction::Refund, "订单{$order->sn}退还积分", $order);
}
$order->update([
'status' => Order::STATUS_CANCELLED,
]);
}
/**
* 打印订单小票
*
* @param Order $order 订单
* @return array 打印结果 [{code: 0(成功), msg: ''}, ...]
* @throws BizException
*/
public function print(Order $order)
{
if (!$order->store_id) {
throw new BizException('不是门店订单');
}
$store = $order->store;
if (!$store) {
throw new BizException('未找到门店');
}
$desk = '';
if ($order->source_type === Desk::class) {
$desk = $order->source;
$desk = data_get($order->source, 'name', '');
}
$devices = $store->devices()->where('status', 1)->get();
if ($devices->count() == 0) {
throw new BizException('门店未找到可用的设备');
}
$templateId = data_get($store->extra, 'order_desk_print_template');
if (!$templateId) {
throw new BizException('门店未配置打印模板');
}
$service = PrintService::make();
$products = [];
foreach($order->products as $item) {
array_push($products, [
'name' => $item->name,
'price' => round($item->sell_price / 100, 2, PHP_ROUND_HALF_DOWN),
'amount' => $item->quantity,
'money' => round($item->total_amount / 100, 2, PHP_ROUND_HALF_DOWN),
]);
}
$data = [
'name' => $store->title,
'sn' => $order->sn,
'time' => $order->created_at->format('Y-m-d H:i:s'),
'desk' => $desk,
'products' => $products,
'total' => round($order->total_amount / 100, 2, PHP_ROUND_HALF_DOWN),
'remarks' => $order->note ?: '',
];
$results = [];
foreach($devices as $item) {
$result = $service->template($item->device_no, $templateId, $data);
$status = data_get($result, 'code') == 0 ? DeviceRecord::STATUS_SUCCESS : DeviceRecord::STATUS_FAIL;
array_push($results, $result);
// 添加打印的日志记录
$item->records()->create([
'data' => $data,
'device_id' => $item->id,
'result' => $result,
'resource_id' => $order->id,
'resource_type' => $order->getMorphClass(),
'status' => $status,
'store_id' => $store->id
]);
}
return $results;
}
}