feat: multiple improvements and bug fixes
- Add gift card redemption feature - Resolve custom range selection issue in overview - Allow log page size to be modified - Add subscription path change notification - Improve dynamic node rate feature - Support markdown documentation display for plugins - Reduce power reset service logging - Fix backend version number not updating after update
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Resources\PlanResource;
|
||||
use App\Models\GiftCardCode;
|
||||
use App\Models\GiftCardTemplate;
|
||||
use App\Models\GiftCardUsage;
|
||||
use App\Models\Plan;
|
||||
use App\Models\TrafficResetLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GiftCardService
|
||||
{
|
||||
protected readonly GiftCardCode $code;
|
||||
protected readonly GiftCardTemplate $template;
|
||||
protected ?User $user = null;
|
||||
|
||||
public function __construct(string $code)
|
||||
{
|
||||
$this->code = GiftCardCode::where('code', $code)->first()
|
||||
?? throw new ApiException('兑换码不存在');
|
||||
|
||||
$this->template = $this->code->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置使用用户
|
||||
*/
|
||||
public function setUser(User $user): self
|
||||
{
|
||||
$this->user = $user;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证兑换码
|
||||
*/
|
||||
public function validate(): self
|
||||
{
|
||||
$this->validateIsActive();
|
||||
|
||||
$eligibility = $this->checkUserEligibility();
|
||||
if (!$eligibility['can_redeem']) {
|
||||
throw new ApiException($eligibility['reason']);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证礼品卡本身是否可用 (不检查用户条件)
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function validateIsActive(): self
|
||||
{
|
||||
if (!$this->template->isAvailable()) {
|
||||
throw new ApiException('该礼品卡类型已停用');
|
||||
}
|
||||
|
||||
if (!$this->code->isAvailable()) {
|
||||
throw new ApiException('兑换码不可用:' . $this->code->status_name);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否满足兑换条件 (不抛出异常)
|
||||
*/
|
||||
public function checkUserEligibility(): array
|
||||
{
|
||||
if (!$this->user) {
|
||||
return [
|
||||
'can_redeem' => false,
|
||||
'reason' => '用户信息未提供'
|
||||
];
|
||||
}
|
||||
|
||||
if (!$this->template->checkUserConditions($this->user)) {
|
||||
return [
|
||||
'can_redeem' => false,
|
||||
'reason' => '您不满足此礼品卡的使用条件'
|
||||
];
|
||||
}
|
||||
|
||||
if (!$this->template->checkUsageLimit($this->user)) {
|
||||
return [
|
||||
'can_redeem' => false,
|
||||
'reason' => '您已达到此礼品卡的使用限制'
|
||||
];
|
||||
}
|
||||
|
||||
return ['can_redeem' => true, 'reason' => null];
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用礼品卡
|
||||
*/
|
||||
public function redeem(array $options = []): array
|
||||
{
|
||||
if (!$this->user) {
|
||||
throw new ApiException('未设置使用用户');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($options) {
|
||||
$actualRewards = $this->template->calculateActualRewards($this->user);
|
||||
|
||||
if ($this->template->type === GiftCardTemplate::TYPE_MYSTERY) {
|
||||
$this->code->setActualRewards($actualRewards);
|
||||
}
|
||||
|
||||
$this->giveRewards($actualRewards);
|
||||
|
||||
$inviteRewards = null;
|
||||
if ($this->user->invite_user_id && isset($actualRewards['invite_reward_rate'])) {
|
||||
$inviteRewards = $this->giveInviteRewards($actualRewards);
|
||||
}
|
||||
|
||||
$this->code->markAsUsed($this->user);
|
||||
|
||||
GiftCardUsage::createRecord(
|
||||
$this->code,
|
||||
$this->user,
|
||||
$actualRewards,
|
||||
array_merge($options, [
|
||||
'invite_rewards' => $inviteRewards,
|
||||
'multiplier' => $this->calculateMultiplier(),
|
||||
])
|
||||
);
|
||||
|
||||
return [
|
||||
'rewards' => $actualRewards,
|
||||
'invite_rewards' => $inviteRewards,
|
||||
'code' => $this->code->code,
|
||||
'template_name' => $this->template->name,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发放奖励
|
||||
*/
|
||||
protected function giveRewards(array $rewards): void
|
||||
{
|
||||
$userService = app(UserService::class);
|
||||
|
||||
if (isset($rewards['balance']) && $rewards['balance'] > 0) {
|
||||
if (!$userService->addBalance($this->user->id, $rewards['balance'])) {
|
||||
throw new ApiException('余额发放失败');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($rewards['transfer_enable']) && $rewards['transfer_enable'] > 0) {
|
||||
$this->user->transfer_enable = ($this->user->transfer_enable ?? 0) + $rewards['transfer_enable'];
|
||||
}
|
||||
|
||||
if (isset($rewards['device_limit']) && $rewards['device_limit'] > 0) {
|
||||
$this->user->device_limit = ($this->user->device_limit ?? 0) + $rewards['device_limit'];
|
||||
}
|
||||
|
||||
if (isset($rewards['reset_package']) && $rewards['reset_package']) {
|
||||
if ($this->user->plan_id) {
|
||||
app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_GIFT_CARD);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($rewards['plan_id'])) {
|
||||
$plan = Plan::find($rewards['plan_id']);
|
||||
if ($plan) {
|
||||
$userService->assignPlan(
|
||||
$this->user,
|
||||
$plan,
|
||||
$rewards['plan_validity_days'] ?? null
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 只有在不是套餐卡的情况下,才处理独立的有效期奖励
|
||||
if (isset($rewards['expire_days']) && $rewards['expire_days'] > 0) {
|
||||
$userService->extendSubscription($this->user, $rewards['expire_days']);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户更改
|
||||
if (!$this->user->save()) {
|
||||
throw new ApiException('用户信息更新失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发放邀请人奖励
|
||||
*/
|
||||
protected function giveInviteRewards(array $rewards): ?array
|
||||
{
|
||||
if (!$this->user->invite_user_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$inviteUser = User::find($this->user->invite_user_id);
|
||||
if (!$inviteUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$rate = $rewards['invite_reward_rate'] ?? 0.2;
|
||||
$inviteRewards = [];
|
||||
|
||||
$userService = app(UserService::class);
|
||||
|
||||
// 邀请人余额奖励
|
||||
if (isset($rewards['balance']) && $rewards['balance'] > 0) {
|
||||
$inviteBalance = intval($rewards['balance'] * $rate);
|
||||
if ($inviteBalance > 0) {
|
||||
$userService->addBalance($inviteUser->id, $inviteBalance);
|
||||
$inviteRewards['balance'] = $inviteBalance;
|
||||
}
|
||||
}
|
||||
|
||||
// 邀请人流量奖励
|
||||
if (isset($rewards['transfer_enable']) && $rewards['transfer_enable'] > 0) {
|
||||
$inviteTransfer = intval($rewards['transfer_enable'] * $rate);
|
||||
if ($inviteTransfer > 0) {
|
||||
$inviteUser->transfer_enable = ($inviteUser->transfer_enable ?? 0) + $inviteTransfer;
|
||||
$inviteUser->save();
|
||||
$inviteRewards['transfer_enable'] = $inviteTransfer;
|
||||
}
|
||||
}
|
||||
|
||||
return $inviteRewards;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算倍率
|
||||
*/
|
||||
protected function calculateMultiplier(): float
|
||||
{
|
||||
return $this->getFestivalBonus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节日加成倍率
|
||||
*/
|
||||
private function getFestivalBonus(): float
|
||||
{
|
||||
$festivalConfig = $this->template->special_config ?? [];
|
||||
$now = time();
|
||||
|
||||
if (
|
||||
isset($festivalConfig['start_time'], $festivalConfig['end_time']) &&
|
||||
$now >= $festivalConfig['start_time'] &&
|
||||
$now <= $festivalConfig['end_time']
|
||||
) {
|
||||
return $festivalConfig['festival_bonus'] ?? 1.0;
|
||||
}
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取兑换码信息(不包含敏感信息)
|
||||
*/
|
||||
public function getCodeInfo(): array
|
||||
{
|
||||
$info = [
|
||||
'code' => $this->code->code,
|
||||
'template' => [
|
||||
'name' => $this->template->name,
|
||||
'description' => $this->template->description,
|
||||
'type' => $this->template->type,
|
||||
'type_name' => $this->template->type_name,
|
||||
'icon' => $this->template->icon,
|
||||
'background_image' => $this->template->background_image,
|
||||
'theme_color' => $this->template->theme_color,
|
||||
],
|
||||
'status' => $this->code->status,
|
||||
'status_name' => $this->code->status_name,
|
||||
'expires_at' => $this->code->expires_at,
|
||||
'usage_count' => $this->code->usage_count,
|
||||
'max_usage' => $this->code->max_usage,
|
||||
];
|
||||
if ($this->template->type === GiftCardTemplate::TYPE_PLAN) {
|
||||
$plan = Plan::find($this->code->template->rewards['plan_id']);
|
||||
if ($plan) {
|
||||
$info['plan_info'] = PlanResource::make($plan)->toArray(request());
|
||||
}
|
||||
}
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览奖励(不实际发放)
|
||||
*/
|
||||
public function previewRewards(): array
|
||||
{
|
||||
if (!$this->user) {
|
||||
throw new ApiException('未设置使用用户');
|
||||
}
|
||||
|
||||
return $this->template->calculateActualRewards($this->user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取兑换码
|
||||
*/
|
||||
public function getCode(): GiftCardCode
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板
|
||||
*/
|
||||
public function getTemplate(): GiftCardTemplate
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志
|
||||
*/
|
||||
protected function logUsage(string $action, array $data = []): void
|
||||
{
|
||||
Log::info('礼品卡使用记录', [
|
||||
'action' => $action,
|
||||
'code' => $this->code->code,
|
||||
'template_id' => $this->template->id,
|
||||
'user_id' => $this->user?->id,
|
||||
'data' => $data,
|
||||
'ip' => request()->ip(),
|
||||
'user_agent' => request()->userAgent(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Exceptions\ApiException;
|
||||
use App\Jobs\OrderHandleJob;
|
||||
use App\Models\Order;
|
||||
use App\Models\Plan;
|
||||
use App\Models\TrafficResetLog;
|
||||
use App\Models\User;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Utils\Helper;
|
||||
@@ -37,6 +38,7 @@ class OrderService
|
||||
* @param Plan $plan
|
||||
* @param string $period
|
||||
* @param string|null $couponCode
|
||||
* @param array|null $telegramMessageIds
|
||||
* @return Order
|
||||
* @throws ApiException
|
||||
*/
|
||||
@@ -106,7 +108,7 @@ class OrderService
|
||||
$this->buyByOneTime($plan);
|
||||
break;
|
||||
case Plan::PERIOD_RESET_TRAFFIC:
|
||||
$this->buyByResetTraffic();
|
||||
app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
|
||||
break;
|
||||
default:
|
||||
$this->buyByPeriod($order, $plan);
|
||||
@@ -321,7 +323,7 @@ class OrderService
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
\Log::error($e);
|
||||
Log::error($e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -336,12 +338,6 @@ class OrderService
|
||||
$this->user->device_limit = $deviceLimit;
|
||||
}
|
||||
|
||||
private function buyByResetTraffic()
|
||||
{
|
||||
$this->user->u = 0;
|
||||
$this->user->d = 0;
|
||||
}
|
||||
|
||||
private function buyByPeriod(Order $order, Plan $plan)
|
||||
{
|
||||
// change plan process
|
||||
@@ -351,10 +347,10 @@ class OrderService
|
||||
$this->user->transfer_enable = $plan->transfer_enable * 1073741824;
|
||||
// 从一次性转换到循环
|
||||
if ($this->user->expired_at === NULL)
|
||||
$this->buyByResetTraffic();
|
||||
app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
|
||||
// 新购
|
||||
if ($order->type === Order::TYPE_NEW_PURCHASE)
|
||||
$this->buyByResetTraffic();
|
||||
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);
|
||||
@@ -362,7 +358,7 @@ class OrderService
|
||||
|
||||
private function buyByOneTime(Plan $plan)
|
||||
{
|
||||
$this->buyByResetTraffic();
|
||||
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;
|
||||
@@ -397,7 +393,7 @@ class OrderService
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
$this->buyByResetTraffic();
|
||||
app(TrafficResetService::class)->performReset($this->user, TrafficResetLog::SOURCE_ORDER);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,14 +60,6 @@ class TrafficResetService
|
||||
]);
|
||||
|
||||
$this->clearUserCache($user);
|
||||
|
||||
Log::info(__('traffic_reset.reset_success'), [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'old_traffic' => $oldTotal,
|
||||
'trigger_source' => $triggerSource,
|
||||
]);
|
||||
|
||||
return true;
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
@@ -283,11 +275,6 @@ class TrafficResetService
|
||||
$errors = [];
|
||||
$lastProcessedId = 0;
|
||||
|
||||
Log::info('Starting batch traffic reset task.', [
|
||||
'batch_size' => $batchSize,
|
||||
'start_time' => now()->toDateTimeString(),
|
||||
]);
|
||||
|
||||
try {
|
||||
do {
|
||||
$users = User::where('next_reset_at', '<=', time())
|
||||
@@ -307,9 +294,7 @@ class TrafficResetService
|
||||
break;
|
||||
}
|
||||
|
||||
$batchStartTime = microtime(true);
|
||||
$batchResetCount = 0;
|
||||
$batchErrors = [];
|
||||
|
||||
if ($progressCallback) {
|
||||
$progressCallback([
|
||||
@@ -319,13 +304,6 @@ class TrafficResetService
|
||||
]);
|
||||
}
|
||||
|
||||
Log::info("Processing batch #{$batchNumber}", [
|
||||
'batch_number' => $batchNumber,
|
||||
'batch_size' => $users->count(),
|
||||
'total_processed' => $totalProcessedCount,
|
||||
'id_range' => $users->first()->id . '-' . $users->last()->id,
|
||||
]);
|
||||
|
||||
foreach ($users as $user) {
|
||||
try {
|
||||
if ($this->checkAndReset($user, TrafficResetLog::SOURCE_CRON)) {
|
||||
@@ -352,17 +330,6 @@ class TrafficResetService
|
||||
}
|
||||
}
|
||||
|
||||
$batchDuration = round(microtime(true) - $batchStartTime, 2);
|
||||
|
||||
Log::info("Batch #{$batchNumber} processing complete", [
|
||||
'batch_number' => $batchNumber,
|
||||
'processed_count' => $users->count(),
|
||||
'reset_count' => $batchResetCount,
|
||||
'error_count' => count($batchErrors),
|
||||
'duration' => $batchDuration,
|
||||
'last_processed_id' => $lastProcessedId,
|
||||
]);
|
||||
|
||||
$batchNumber++;
|
||||
|
||||
if ($batchNumber % 10 === 0) {
|
||||
@@ -407,8 +374,6 @@ class TrafficResetService
|
||||
'completed_at' => now()->toDateTimeString(),
|
||||
];
|
||||
|
||||
Log::info('Batch traffic reset task completed', $result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Jobs\StatUserJob;
|
||||
use App\Jobs\TrafficFetchJob;
|
||||
use App\Models\Order;
|
||||
use App\Models\Plan;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Services\TrafficResetService;
|
||||
@@ -113,8 +114,11 @@ class UserService
|
||||
return true;
|
||||
}
|
||||
|
||||
public function trafficFetch(array $server, string $protocol, array $data)
|
||||
public function trafficFetch(Server $server, string $protocol, array $data)
|
||||
{
|
||||
$server->rate = $server->getCurrentRate();
|
||||
$server = $server->toArray();
|
||||
|
||||
list($server, $protocol, $data) = HookManager::filter('traffic.before_process', [
|
||||
$server,
|
||||
$protocol,
|
||||
@@ -227,6 +231,44 @@ class UserService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户分配一个新套餐或续费现有套餐
|
||||
*
|
||||
* @param User $user 用户模型
|
||||
* @param Plan $plan 套餐模型
|
||||
* @param int $validityDays 购买天数
|
||||
* @return User 更新后的用户模型
|
||||
*/
|
||||
public function assignPlan(User $user, Plan $plan, int $validityDays): User
|
||||
{
|
||||
$user->plan_id = $plan->id;
|
||||
$user->group_id = $plan->group_id;
|
||||
$user->transfer_enable = $plan->transfer_enable * 1073741824;
|
||||
$user->speed_limit = $plan->speed_limit;
|
||||
|
||||
if ($validityDays > 0) {
|
||||
$user = $this->extendSubscription($user, $validityDays);
|
||||
}
|
||||
|
||||
$user->save();
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 延长用户的订阅有效期
|
||||
*
|
||||
* @param User $user 用户模型
|
||||
* @param int $days 延长天数
|
||||
* @return User 更新后的用户模型
|
||||
*/
|
||||
public function extendSubscription(User $user, int $days): User
|
||||
{
|
||||
$currentExpired = $user->expired_at ?? time();
|
||||
$user->expired_at = max($currentExpired, time()) + ($days * 86400);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置试用计划
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user