mapItems($orderPreview->payload['items']); [ $productsTotalAmount, $discountReductionAmount, $paymentAmount, ] = $this->calculateFees($items); // 积分抵扣金额 $pointsDeductionAmount = $points; $paymentAmount -= $pointsDeductionAmount; // 优惠券抵扣 $couponDiscount = 0; $couponId = data_get($params, 'coupon_id'); if ($couponId) { $coupon = UserCoupon::where('user_id', $user->id)->onlyAvailable()->find($couponId); if (!$coupon) { throw new BizException("优惠券不可用"); } $couponDiscount = $coupon->getDiscountAmount($paymentAmount); $paymentAmount -= $couponDiscount; } if ($paymentAmount < 0) { $paymentAmount = 0; } do { $sn = serial_number(); } while(OfflineOrder::where('sn', $sn)->exists()); /** @var \App\Models\OfflineOrder */ $order = OfflineOrder::create([ 'user_id' => $user->id, 'store_id' => $orderPreview->store_id, 'staff_id' => $orderPreview->staff_id, 'sn' => $sn, 'products_total_amount' => $productsTotalAmount, 'discount_reduction_amount' => $discountReductionAmount, 'points_deduction_amount' => $pointsDeductionAmount, 'payment_amount' => $paymentAmount, 'status' => OfflineOrderStatus::Pending, 'orderable_type' => $orderPreview->getMorphClass(), 'orderable_id' => $orderPreview->id, 'user_remark' => data_get($params, 'user_remark'), 'staff_remark' => $orderPreview->remark, 'coupon_id' => $couponId, 'coupon_discount_amount' => $couponDiscount, ]); $this->insertOrderItems($order, $items); // 扣除积分 if ($points > 0) { (new PointService)->change($user, -$points, PointLogAction::Consumption, [ 'remark' => "线下订单{$order->sn}使用积分", 'loggable' => $order, 'store_id' => $orderPreview->store_id, ]); } if ($order->payment_amount === 0) { $this->pay($order, PayWay::None); $order->refresh(); } return $order; } public function check(User $user, OfflineOrderPreview $preview): array { if ($preview->order && $preview->order->status != OfflineOrderStatus::Pending) { throw new BizException('订单已支付, 请重新生成'); } $items = $preview->payload['items']; $productCategoryIds = collect($items)->pluck('product_category_id'); $productCategories = OfflineProductCategory::query() ->whereIn('id', $productCategoryIds->all()) ->get() ->keyBy('id'); if ($productCategories->count() != $productCategoryIds->count()) { throw new BizException('商品分类异常'); } $mapItems = $this->mapItems($items); [ $productsTotalAmount, $discountReductionAmount, $paymentAmount, ] = $this->calculateFees($mapItems); // 优惠券 $coupon = null; $couponDiscount = 0; if ($preview->coupon_id) { $coupon = UserCoupon::where('user_id', $user->id)->onlyAvailable()->find($preview->coupon_id); if ($coupon) { $couponDiscount = $coupon->getDiscountAmount($paymentAmount); } } $paymentAmount -= $couponDiscount; if ($paymentAmount < 0) { $paymentAmount = 0; } //--------------------------------------- // 积分当钱花 //--------------------------------------- $remainingPoints = $user->userInfo->points; // 用户剩余积分 $availablePoints = $paymentAmount > $remainingPoints ? $remainingPoints : $paymentAmount; // 可用积分 return [ 'items' => collect($mapItems)->map(fn($item) => [ 'product_category' => $productCategories->get($item['product_category_id']), 'discount_reduction_amount' => bcdiv($item['discount_reduction_amount'], 100, 2), 'products_total_amount' => bcdiv($item['products_total_amount'], 100, 2), 'payment_amount' => bcdiv($item['payment_amount'], 100, 2), ]), 'products_total_amount' => bcdiv($productsTotalAmount, 100, 2), 'discount_reduction_amount' => bcdiv($discountReductionAmount, 100, 2), 'payment_amount' => bcdiv($paymentAmount, 100, 2), 'remaining_points' => bcdiv($remainingPoints, 100, 2), // 剩余积分 'available_points' => bcdiv($availablePoints, 100, 2), // 可用积分 'points_discount_amount' => bcdiv($availablePoints, 100, 2), // 积分抵扣金额 // 优惠券 'coupon' => ['id' => $coupon?->id, 'name' => $coupon?->coupon_name, 'discount' => bcdiv($couponDiscount, 100, 2)], ]; } public function pay(OfflineOrder $order, PayWay $payWay) { if (! $order->isPending()) { throw new BizException('订单状态不是待付款'); } $payLog = $order->payLogs()->create([ 'pay_way' => $payWay, ]); $data = null; if ($order->payment_amount === 0) { (new PayService())->handleSuccess($payLog, [ 'pay_at' => now(), ]); } elseif ($payLog->isWxpay()) { if (is_null($tradeType = WxpayTradeType::tryFromPayWay($payLog->pay_way))) { throw new BizException('支付方式 非法'); } $params = [ 'body' => app_settings('app.app_name').'-线下订单', 'out_trade_no' => $payLog->pay_sn, 'total_fee' => $order->payment_amount, 'trade_type' => $tradeType->value, ]; if ($payLog->pay_way === PayWay::WxpayMiniProgram) { $socialite = 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', }); } return [ 'pay_way' => $payLog->pay_way, 'data' => $data, ]; } public function revoke(OfflineOrder $order) { if ($order->isPaid()) { throw new BizException('订单已付款'); } if ($order->isRevoked()) { throw new BizException('订单已取消'); } if ($order->points_deduction_amount > 0) { (new PointService())->change($order->user, $order->points_deduction_amount, PointLogAction::Refund, [ 'remark' => "线下订单{$order->sn}退还积分", 'loggable' => $order, 'store_id' => $order->store_id, ]); } $order->update([ 'status' => OfflineOrderStatus::Revoked, 'revoked_at' => now(), ]); } protected function insertOrderItems(OfflineOrder $order, array $items) { $remainingPointDiscountAmount = $order->points_deduction_amount; $remainCoupon = $order->coupon_discount_amount; OfflineOrderItem::insert( collect($items)->map(function ($item) use ($order, &$remainingPointDiscountAmount, &$remainCoupon) { $paymentAmount = $item['payment_amount']; // 优惠券 $couponDiscount = $paymentAmount; if ($paymentAmount > $remainCoupon) { $couponDiscount = $remainCoupon; } $remainCoupon -= $couponDiscount; $paymentAmount -= $couponDiscount; $pointsDeductionAmount = $paymentAmount; if ($item['payment_amount'] > $remainingPointDiscountAmount) { $pointsDeductionAmount = $remainingPointDiscountAmount; } $remainingPointDiscountAmount -= $pointsDeductionAmount; $paymentAmount -= $pointsDeductionAmount; return [ 'order_id' => $order->id, 'product_category_id' => $item['product_category_id'], 'products_total_amount' => $item['products_total_amount'], 'discount_reduction_amount' => $item['discount_reduction_amount'], 'points_deduction_amount' => $pointsDeductionAmount, 'coupon_discount_amount' => $couponDiscount, 'payment_amount' => $paymentAmount, 'created_at' => $order->created_at, 'updated_at' => $order->updated_at, ]; })->all() ); } protected function mapItems(array $items): array { return collect($items)->map(function ($item) { $productsTotalAmount = bcmul($item['products_total_amount'], 100); if ($productsTotalAmount < 0) { throw new BizException('商品总额不能小于0'); } $discountReductionAmount = 0; if (is_numeric($item['discount'])) { if ($item['discount'] <= 0) { throw new BizException('折扣必须大于0'); } elseif ($item['discount'] >= 10) { throw new BizException('折扣必须小于10'); } $discount = bcdiv($item['discount'], 10, 3); $discountReductionAmount = bcsub($productsTotalAmount, round(bcmul($productsTotalAmount, $discount, 2))); } return [ 'product_category_id' => $item['product_category_id'], 'products_total_amount' => (int) $productsTotalAmount, 'discount_reduction_amount' => (int) $discountReductionAmount, 'payment_amount' => (int) ($productsTotalAmount - $discountReductionAmount), ]; })->all(); } protected function calculateFees(array $items) { $totalProductsTotalAmount = 0; $totalDiscountReductionAmount = 0; $totalPaymentAmount = 0; foreach ($items as $item) { $totalProductsTotalAmount += $item['products_total_amount']; $totalDiscountReductionAmount += $item['discount_reduction_amount']; $totalPaymentAmount += $item['payment_amount']; } return [$totalProductsTotalAmount, $totalDiscountReductionAmount, $totalPaymentAmount]; } /** * 订单支付成功 * @param $data [payment_sn:商户订单号, payment_method:支付方式, payment_time: 支付时间, out_trade_no: 流水号] */ public function paySuccess(OfflineOrder $order, array $data) { $data['status'] = OfflineOrderStatus::Paid; $order->update($data); // 更新优惠券 if ($order->coupon_id) { $coupon = UserCoupon::find($order->coupon_id); if ($coupon) { $coupon->markAsUse(); } } if (config('app.env') != "local") { try { $this->orderShip($order); } catch (BizException $e) { logger()->error("线下订单-自动发货失败: " . $e->getMessage(), ['id' => $order->id, 'sn' => $order->sn]); } } } /** * 订单发货 */ public function orderShip(OfflineOrder $order) { $now = now(); if (!$order->isPaid()) { throw new BizException("订单未支付"); } $userId = $order->user_id; $socialite = SocialiteUser::where('user_id', $userId)->where('type', SocialiteType::WechatMiniProgram)->first(); if (!$socialite) { throw new BizException("未找到用户的openid"); } $openid = $socialite->socialite_id; $items = $order->items()->with(['productCategory'])->get(); $desc = []; foreach ($items as $item) { $name = data_get($item, "productCategory.name", "定制服务"); array_push($desc, $name); } $data = [ "order_key" => [ "order_number_type" => 1, "out_trade_no" => $order->payment_sn, ], "logistics_type" => 4, "delivery_mode" => 1, "shipping_list" => [ "item_desc" => implode(";", $desc) ], "upload_time" => $now, "payer" => [ "openid" => $openid ] ]; WxMpService::make()->uploadShippingInfo($data); $order->ship_time = $now; $order->save(); } }