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(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)), ]); } } /** * 支付成功 * * @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 处理预收益 } }