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); HookManager::call('order.create.before', [$user, $plan, $period, $couponCode]); 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: $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.create.after', $order); // 兼容旧钩子 HookManager::call('order.after_create', $order); return $order; }); } public function open(): void { $order = $this->order; $plan = Plan::find($order->plan_id); HookManager::call('order.open.before', $order); DB::transaction(function () use ($order, $plan) { $this->user = User::lockForUpdate()->find($order->user_id); if ($order->refund_amount) { $this->user->balance += $order->refund_amount; } if ($order->surplus_order_ids) { Order::whereIn('id', $order->surplus_order_ids) ->update(['status' => Order::STATUS_DISCOUNTED]); } match ((string) $order->period) { Plan::PERIOD_ONETIME => $this->buyByOneTime($plan), Plan::PERIOD_RESET_TRAFFIC => app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER), default => $this->buyByPeriod($order, $plan), }; $this->setSpeedLimit($plan->speed_limit); $this->setDeviceLimit($plan->device_limit); if (!$this->user->save()) { throw new \RuntimeException('用户信息保存失败'); } $order->status = Order::STATUS_COMPLETED; if (!$order->save()) { throw new \RuntimeException('订单信息保存失败'); } }); $eventId = match ((int) $order->type) { Order::STATUS_PROCESSING => admin_setting('new_order_event_id', 0), Order::TYPE_RENEWAL => admin_setting('renew_order_event_id', 0), Order::TYPE_UPGRADE => admin_setting('change_order_event_id', 0), default => 0, }; if ($eventId) { $this->openEvent($eventId); } HookManager::call('order.open.after', $order); } 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 === null || $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', [Order::STATUS_PENDING, Order::STATUS_CANCELLED]) ->first(); } private function getSurplusValue(User $user, Order $order) { if ($user->expired_at === NULL) { $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 = Helper::transferToGB($user->transfer_enable); if (!$nowUserTraffic) return; $paidTotalAmount = ($lastOneTimeOrder->total_amount + $lastOneTimeOrder->balance_amount); if (!$paidTotalAmount) return; $trafficUnitPrice = $paidTotalAmount / $nowUserTraffic; $notUsedTraffic = $nowUserTraffic - Helper::transferToGB($user->u + $user->d); $result = $trafficUnitPrice * $notUsedTraffic; $order->surplus_amount = (int) ($result > 0 ? $result : 0); $order->surplus_order_ids = Order::where('user_id', $user->id) ->where('period', '!=', Plan::PERIOD_RESET_TRAFFIC) ->where('status', Order::STATUS_COMPLETED) ->pluck('id') ->all(); } else { $orders = Order::query() ->where('user_id', $user->id) ->whereNotIn('period', [Plan::PERIOD_RESET_TRAFFIC, Plan::PERIOD_ONETIME]) ->where('status', Order::STATUS_COMPLETED) ->get(); if ($orders->isEmpty()) { $order->surplus_amount = 0; $order->surplus_order_ids = []; return; } $orderAmountSum = $orders->sum(fn($item) => $item->total_amount + $item->balance_amount + $item->surplus_amount - $item->refund_amount); $orderMonthSum = $orders->sum(fn($item) => self::STR_TO_TIME[PlanService::getPeriodKey($item->period)] ?? 0); $firstOrderAt = $orders->min('created_at'); $expiredAt = Carbon::createFromTimestamp($firstOrderAt)->addMonths($orderMonthSum); $now = now(); $totalSeconds = $expiredAt->timestamp - $firstOrderAt; $remainSeconds = max(0, $expiredAt->timestamp - $now->timestamp); $cycleRatio = $totalSeconds > 0 ? $remainSeconds / $totalSeconds : 0; $plan = Plan::find($user->plan_id); $totalTraffic = $plan?->transfer_enable * $orderMonthSum; $usedTraffic = Helper::transferToGB($user->u + $user->d); $remainTraffic = max(0, $totalTraffic - $usedTraffic); $trafficRatio = $totalTraffic > 0 ? $remainTraffic / $totalTraffic : 0; $ratio = $cycleRatio; if (admin_setting('change_order_event_id', 0) == 1) { $ratio = min($cycleRatio, $trafficRatio); } $order->surplus_amount = (int) max(0, $orderAmountSum * $ratio); $order->surplus_order_ids = $orders->pluck('id')->all(); } } 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; HookManager::call('order.cancel.before', $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(); HookManager::call('order.cancel.after', $order); 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 || $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; } /** * 计算套餐到期时间 * @param string $periodKey * @param int $timestamp * @return int * @throws ApiException */ private function getTime(string $periodKey, ?int $timestamp = null): int { $timestamp = $timestamp < time() ? time() : $timestamp; $periodKey = PlanService::getPeriodKey($periodKey); if (isset(self::STR_TO_TIME[$periodKey])) { $months = self::STR_TO_TIME[$periodKey]; return Carbon::createFromTimestamp($timestamp)->addMonths($months)->timestamp; } throw new ApiException('无效的套餐周期'); } 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(); } /** * Summary of handleUserBalance * @param User $user * @param UserService $userService * @return void */ 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; } } }