1, Plan::PERIOD_QUARTERLY => 3, Plan::PERIOD_HALF_YEARLY => 6, Plan::PERIOD_YEARLY => 12, Plan::PERIOD_TWO_YEARLY => 24, Plan::PERIOD_THREE_YEARLY => 36 ]; public $order; public $user; public function __construct(Order $order) { $this->order = $order; } /** * Create an order from a request. * * @param User $user * @param Plan $plan * @param string $period * @param string|null $couponCode * @return Order * @throws ApiException */ public static function createFromRequest( User $user, Plan $plan, string $period, ?string $couponCode = null, ): Order { $userService = app(UserService::class); $planService = new PlanService($plan); $planService->validatePurchase($user, $period); return DB::transaction(function () use ($user, $plan, $period, $couponCode, $userService) { $newPeriod = PlanService::getPeriodKey($period); $order = new Order([ 'user_id' => $user->id, 'plan_id' => $plan->id, 'period' => $newPeriod, 'trade_no' => Helper::generateOrderNo(), 'total_amount' => (int) (optional($plan->prices)[$newPeriod] * 100), ]); $orderService = new self($order); if ($couponCode) { $orderService->applyCoupon($couponCode); } $orderService->setVipDiscount($user); $orderService->setOrderType($user); $orderService->setInvite($user); if ($user->balance && $order->total_amount > 0) { $orderService->handleUserBalance($user, $userService); } if (!$order->save()) { throw new ApiException(__('Failed to create order')); } HookManager::call('order.after_create', $order); return $order; }); } public function open() { $order = $this->order; $this->user = User::find($order->user_id); $plan = Plan::find($order->plan_id); if ($order->refund_amount) { $this->user->balance = $this->user->balance + $order->refund_amount; } try { DB::beginTransaction(); if ($order->surplus_order_ids) { Order::whereIn('id', $order->surplus_order_ids)->update([ 'status' => Order::STATUS_DISCOUNTED ]); } switch ((string) $order->period) { case Plan::PERIOD_ONETIME: $this->buyByOneTime($plan); break; case Plan::PERIOD_RESET_TRAFFIC: app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER); break; default: $this->buyByPeriod($order, $plan); } switch ((int) $order->type) { case Order::STATUS_PROCESSING: $this->openEvent(admin_setting('new_order_event_id', 0)); break; case Order::TYPE_RENEWAL: $this->openEvent(admin_setting('renew_order_event_id', 0)); break; case Order::TYPE_UPGRADE: $this->openEvent(admin_setting('change_order_event_id', 0)); break; } $this->setSpeedLimit($plan->speed_limit); $this->setDeviceLimit($plan->device_limit); if (!$this->user->save()) { throw new \Exception('用户信息保存失败'); } $order->status = Order::STATUS_COMPLETED; if (!$order->save()) { throw new \Exception('订单信息保存失败'); } DB::commit(); } catch (\Exception $e) { DB::rollBack(); Log::error($e); throw new ApiException('开通失败'); } } public function setOrderType(User $user) { $order = $this->order; if ($order->period === Plan::PERIOD_RESET_TRAFFIC) { $order->type = Order::TYPE_RESET_TRAFFIC; } else if ($user->plan_id !== NULL && $order->plan_id !== $user->plan_id && ($user->expired_at > time() || $user->expired_at === NULL)) { if (!(int) admin_setting('plan_change_enable', 1)) throw new ApiException('目前不允许更改订阅,请联系客服或提交工单操作'); $order->type = Order::TYPE_UPGRADE; if ((int) admin_setting('surplus_enable', 1)) $this->getSurplusValue($user, $order); if ($order->surplus_amount >= $order->total_amount) { $order->refund_amount = (int) ($order->surplus_amount - $order->total_amount); $order->total_amount = 0; } else { $order->total_amount = (int) ($order->total_amount - $order->surplus_amount); } } else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) { // 用户订阅未过期且购买订阅与当前订阅相同 === 续费 $order->type = Order::TYPE_RENEWAL; } else { // 新购 $order->type = Order::TYPE_NEW_PURCHASE; } } public function setVipDiscount(User $user) { $order = $this->order; if ($user->discount) { $order->discount_amount = $order->discount_amount + ($order->total_amount * ($user->discount / 100)); } $order->total_amount = $order->total_amount - $order->discount_amount; } public function setInvite(User $user): void { $order = $this->order; if ($user->invite_user_id && ($order->total_amount <= 0)) return; $order->invite_user_id = $user->invite_user_id; $inviter = User::find($user->invite_user_id); if (!$inviter) return; $commissionType = (int) $inviter->commission_type; if ($commissionType === User::COMMISSION_TYPE_SYSTEM) { $commissionType = (bool) admin_setting('commission_first_time_enable', true) ? User::COMMISSION_TYPE_ONETIME : User::COMMISSION_TYPE_PERIOD; } $isCommission = false; switch ($commissionType) { case User::COMMISSION_TYPE_PERIOD: $isCommission = true; break; case User::COMMISSION_TYPE_ONETIME: $isCommission = !$this->haveValidOrder($user); break; } if (!$isCommission) return; if ($inviter->commission_rate) { $order->commission_balance = $order->total_amount * ($inviter->commission_rate / 100); } else { $order->commission_balance = $order->total_amount * (admin_setting('invite_commission', 10) / 100); } } private function haveValidOrder(User $user): Order|null { return Order::where('user_id', $user->id) ->whereNotIn('status', [0, 2]) ->first(); } private function getSurplusValue(User $user, Order $order) { if ($user->expired_at === NULL) { $this->getSurplusValueByOneTime($user, $order); } else { $this->getSurplusValueByPeriod($user, $order); } } private function getSurplusValueByOneTime(User $user, Order $order) { $lastOneTimeOrder = Order::where('user_id', $user->id) ->where('period', Plan::PERIOD_ONETIME) ->where('status', Order::STATUS_COMPLETED) ->orderBy('id', 'DESC') ->first(); if (!$lastOneTimeOrder) return; $nowUserTraffic = $user->transfer_enable / 1073741824; if (!$nowUserTraffic) return; $paidTotalAmount = ($lastOneTimeOrder->total_amount + $lastOneTimeOrder->balance_amount); if (!$paidTotalAmount) return; $trafficUnitPrice = $paidTotalAmount / $nowUserTraffic; $notUsedTraffic = $nowUserTraffic - (($user->u + $user->d) / 1073741824); $result = $trafficUnitPrice * $notUsedTraffic; $orderModel = Order::where('user_id', $user->id)->where('period', '!=', Plan::PERIOD_RESET_TRAFFIC)->where('status', Order::STATUS_COMPLETED); $order->surplus_amount = (int) ($result > 0 ? $result : 0); $order->surplus_order_ids = array_column($orderModel->get()->toArray(), 'id'); } private function getSurplusValueByPeriod(User $user, Order $order) { $orders = Order::where('user_id', $user->id) ->whereNotIn('period', [Plan::PERIOD_RESET_TRAFFIC, Plan::PERIOD_ONETIME]) ->where('status', Order::STATUS_COMPLETED) ->get() ->toArray(); if (!$orders) return; $orderAmountSum = 0; $orderMonthSum = 0; $lastValidateAt = 0; foreach ($orders as $item) { $period = self::STR_TO_TIME[PlanService::getPeriodKey($item['period'])]; if (strtotime("+{$period} month", $item['created_at']) < time()) continue; $lastValidateAt = $item['created_at']; $orderMonthSum = $period + $orderMonthSum; $orderAmountSum = $orderAmountSum + ($item['total_amount'] + $item['balance_amount'] + $item['surplus_amount'] - $item['refund_amount']); } if (!$lastValidateAt) return; $expiredAtByOrder = strtotime("+{$orderMonthSum} month", $lastValidateAt); if ($expiredAtByOrder < time()) return; $orderSurplusSecond = $expiredAtByOrder - time(); $orderRangeSecond = $expiredAtByOrder - $lastValidateAt; $avgPrice = $orderAmountSum / $orderRangeSecond; $orderSurplusAmount = $avgPrice * $orderSurplusSecond; if (!$orderSurplusSecond || !$orderSurplusAmount) return; $order->surplus_amount = (int) ($orderSurplusAmount > 0 ? $orderSurplusAmount : 0); $order->surplus_order_ids = array_column($orders, 'id'); } public function paid(string $callbackNo) { $order = $this->order; if ($order->status !== Order::STATUS_PENDING) return true; $order->status = Order::STATUS_PROCESSING; $order->paid_at = time(); $order->callback_no = $callbackNo; if (!$order->save()) return false; try { OrderHandleJob::dispatchSync($order->trade_no); } catch (\Exception $e) { Log::error($e); return false; } return true; } public function cancel(): bool { $order = $this->order; try { DB::beginTransaction(); $order->status = Order::STATUS_CANCELLED; if (!$order->save()) { throw new \Exception('Failed to save order status.'); } if ($order->balance_amount) { $userService = new UserService(); if (!$userService->addBalance($order->user_id, $order->balance_amount)) { throw new \Exception('Failed to add balance.'); } } DB::commit(); return true; } catch (\Exception $e) { DB::rollBack(); Log::error($e); return false; } } private function setSpeedLimit($speedLimit) { $this->user->speed_limit = $speedLimit; } private function setDeviceLimit($deviceLimit) { $this->user->device_limit = $deviceLimit; } private function buyByPeriod(Order $order, Plan $plan) { // change plan process if ((int) $order->type === Order::TYPE_UPGRADE) { $this->user->expired_at = time(); } $this->user->transfer_enable = $plan->transfer_enable * 1073741824; // 从一次性转换到循环 if ($this->user->expired_at === NULL) app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER); // 新购 if ($order->type === Order::TYPE_NEW_PURCHASE) app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER); $this->user->plan_id = $plan->id; $this->user->group_id = $plan->group_id; $this->user->expired_at = $this->getTime($order->period, $this->user->expired_at); } private function buyByOneTime(Plan $plan) { app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER); $this->user->transfer_enable = $plan->transfer_enable * 1073741824; $this->user->plan_id = $plan->id; $this->user->group_id = $plan->group_id; $this->user->expired_at = NULL; } private function getTime($str, $timestamp) { if ($timestamp < time()) { $timestamp = time(); } $str = PlanService::getPeriodKey($str); switch ($str) { case Plan::PERIOD_MONTHLY: return strtotime('+1 month', $timestamp); case Plan::PERIOD_QUARTERLY: return strtotime('+3 month', $timestamp); case Plan::PERIOD_HALF_YEARLY: return strtotime('+6 month', $timestamp); case Plan::PERIOD_YEARLY: return strtotime('+12 month', $timestamp); case Plan::PERIOD_TWO_YEARLY: return strtotime('+24 month', $timestamp); case Plan::PERIOD_THREE_YEARLY: return strtotime('+36 month', $timestamp); } } private function openEvent($eventId) { switch ((int) $eventId) { case 0: break; case 1: app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER); break; } } protected function applyCoupon(string $couponCode): void { $couponService = new CouponService($couponCode); if (!$couponService->use($this->order)) { throw new ApiException(__('Coupon failed')); } $this->order->coupon_id = $couponService->getId(); } protected function handleUserBalance(User $user, UserService $userService): void { $remainingBalance = $user->balance - $this->order->total_amount; if ($remainingBalance >= 0) { if (!$userService->addBalance($this->order->user_id, -$this->order->total_amount)) { throw new ApiException(__('Insufficient balance')); } $this->order->balance_amount = $this->order->total_amount; $this->order->total_amount = 0; } else { if (!$userService->addBalance($this->order->user_id, -$user->balance)) { throw new ApiException(__('Insufficient balance')); } $this->order->balance_amount = $user->balance; $this->order->total_amount = $this->order->total_amount - $user->balance; } } }